From 2d33a757bf4bc3cc4c379517b02141d702580033 Mon Sep 17 00:00:00 2001 From: Fr4nzD13trich Date: Thu, 20 Nov 2025 14:05:57 +0100 Subject: [PATCH] Repo created --- AUTHORS | 7 + CONTRIBUTING.md | 117 + LICENSE | 674 ++ README.md | 46 +- SECURITY.md | 5 + app/.gitignore | 2 + app/build.gradle.kts | 227 + app/lint.xml | 13 + app/proguard-rules-release.pro | 31 + .../10.json | 398 + .../11.json | 536 ++ .../12.json | 615 ++ .../13.json | 640 ++ .../14.json | 669 ++ .../15.json | 675 ++ .../16.json | 675 ++ .../17.json | 648 ++ .../18.json | 648 ++ .../at.bitfire.davdroid.db.AppDatabase/8.json | 298 + .../at.bitfire.davdroid.db.AppDatabase/9.json | 366 + app/src/.gitignore | 1 + app/src/androidTest/AndroidManifest.xml | 10 + .../bitfire/davdroid/ExternalLibrariesTest.kt | 30 + .../at/bitfire/davdroid/HiltTestRunner.kt | 42 + .../kotlin/at/bitfire/davdroid/TestUtils.kt | 58 + .../at/bitfire/davdroid/db/AppDatabaseTest.kt | 79 + .../at/bitfire/davdroid/db/CollectionTest.kt | 207 + .../at/bitfire/davdroid/db/HomeSetDaoTest.kt | 97 + .../at/bitfire/davdroid/db/MemoryDbModule.kt | 41 + .../bitfire/davdroid/db/PrincipalDaoTest.kt | 96 + .../bitfire/davdroid/db/SyncStatsDaoTest.kt | 77 + .../davdroid/db/WebDavDocumentDaoTest.kt | 80 + .../db/migration/AutoMigration16Test.kt | 83 + .../db/migration/AutoMigration18Test.kt | 79 + .../db/migration/DatabaseMigrationTest.kt | 86 + .../davdroid/di/FakeSyncAdapterModule.kt | 20 + .../di/TestCoroutineDispatchersModule.kt | 59 + .../davdroid/di/TestTasksAppWatcherModule.kt | 24 + .../davdroid/network/Android10ResolverTest.kt | 35 + .../davdroid/network/DnsRecordResolverTest.kt | 122 + .../davdroid/network/HttpClientTest.kt | 88 + .../davdroid/network/OkhttpClientTest.kt | 46 + .../davdroid/push/PushMessageHandlerTest.kt | 46 + .../davdroid/push/UnifiedPushServiceTest.kt | 102 + .../repository/AccountRepositoryTest.kt | 213 + .../resource/LocalAddressBookStoreTest.kt | 225 + .../davdroid/resource/LocalAddressBookTest.kt | 177 + .../davdroid/resource/LocalCalendarTest.kt | 236 + .../davdroid/resource/LocalEventTest.kt | 265 + .../davdroid/resource/LocalGroupTest.kt | 278 + .../davdroid/resource/LocalTestAddressBook.kt | 59 + .../resource/LocalTestAddressBookProvider.kt | 72 + .../CachedGroupMembershipHandlerTest.kt | 90 + .../contactrow/GroupMembershipBuilderTest.kt | 103 + .../contactrow/GroupMembershipHandlerTest.kt | 110 + .../UnknownPropertiesBuilderTest.kt | 32 + .../UnknownPropertiesHandlerTest.kt | 33 + .../CollectionsWithoutHomeSetRefresherTest.kt | 222 + .../servicedetection/DavResourceFinderTest.kt | 230 + .../servicedetection/HomeSetRefresherTest.kt | 473 ++ .../PrincipalsRefresherTest.kt | 236 + .../servicedetection/ServiceRefresherTest.kt | 163 + .../davdroid/settings/AccountSettingsTest.kt | 60 + .../davdroid/settings/SettingsManagerTest.kt | 93 + .../AccountSettingsMigration17Test.kt | 102 + .../AccountSettingsMigration18Test.kt | 122 + .../AccountSettingsMigration19Test.kt | 83 + .../AccountSettingsMigration20Test.kt | 144 + .../davdroid/sync/AndroidSyncFrameworkTest.kt | 238 + .../bitfire/davdroid/sync/FakeSyncAdapter.kt | 51 + .../davdroid/sync/JtxSyncManagerTest.kt | 187 + .../davdroid/sync/LocalTestCollection.kt | 47 + .../davdroid/sync/LocalTestResource.kt | 44 + .../davdroid/sync/SyncAdapterImplTest.kt | 158 + .../bitfire/davdroid/sync/SyncManagerTest.kt | 503 ++ .../at/bitfire/davdroid/sync/SyncerTest.kt | 228 + .../bitfire/davdroid/sync/TestSyncManager.kt | 123 + .../sync/account/AccountsCleanupWorkerTest.kt | 163 + .../sync/account/SystemAccountUtilsTest.kt | 60 + .../davdroid/sync/account/TestAccount.kt | 53 + .../sync/worker/PeriodicSyncWorkerTest.kt | 95 + .../sync/worker/SyncConditionsTest.kt | 280 + .../sync/worker/SyncWorkerManagerTest.kt | 90 + .../ui/CollectionSelectedUseCaseTest.kt | 94 + .../davdroid/ui/DebugInfoActivityTest.kt | 37 + .../davdroid/ui/setup/LoginActivityTest.kt | 67 + .../davdroid/webdav/CredentialsStoreTest.kt | 41 + .../webdav/WebDavMountRepositoryTest.kt | 66 + .../QueryChildDocumentsOperationTest.kt | 247 + .../res/drawable-hdpi/ic_launcher.png | Bin 0 -> 9397 bytes .../res/drawable-ldpi/ic_launcher.png | Bin 0 -> 2729 bytes .../res/drawable-mdpi/ic_launcher.png | Bin 0 -> 5237 bytes .../res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 14383 bytes app/src/androidTest/res/values/strings.xml | 6 + app/src/main/AndroidManifest.xml | 353 + app/src/main/assets/gplv3.html | 628 ++ app/src/main/assets/logging.properties | 4 + app/src/main/assets/translators.json | 1 + app/src/main/ic_launcher-web.png | Bin 0 -> 23286 bytes .../main/kotlin/at/bitfire/davdroid/App.kt | 83 + .../kotlin/at/bitfire/davdroid/Constants.kt | 23 + .../kotlin/at/bitfire/davdroid/TextTable.kt | 86 + .../at/bitfire/davdroid/db/AppDatabase.kt | 160 + .../at/bitfire/davdroid/db/Collection.kt | 266 + .../at/bitfire/davdroid/db/CollectionDao.kt | 132 + .../at/bitfire/davdroid/db/Converters.kt | 31 + .../kotlin/at/bitfire/davdroid/db/HomeSet.kt | 43 + .../at/bitfire/davdroid/db/HomeSetDao.kt | 60 + .../at/bitfire/davdroid/db/Principal.kt | 70 + .../at/bitfire/davdroid/db/PrincipalDao.kt | 67 + .../kotlin/at/bitfire/davdroid/db/Service.kt | 44 + .../at/bitfire/davdroid/db/ServiceDao.kt | 49 + .../at/bitfire/davdroid/db/SyncStats.kt | 28 + .../at/bitfire/davdroid/db/SyncStatsDao.kt | 22 + .../at/bitfire/davdroid/db/WebDavDocument.kt | 140 + .../bitfire/davdroid/db/WebDavDocumentDao.kt | 107 + .../at/bitfire/davdroid/db/WebDavMount.kt | 24 + .../at/bitfire/davdroid/db/WebDavMountDao.kt | 42 + .../db/WebDavMountWithRootDocument.kt | 18 + .../davdroid/db/migration/AutoMigration12.kt | 47 + .../davdroid/db/migration/AutoMigration16.kt | 47 + .../davdroid/db/migration/AutoMigration18.kt | 81 + .../davdroid/db/migration/Migration2.kt | 29 + .../davdroid/db/migration/Migration3.kt | 26 + .../davdroid/db/migration/Migration4.kt | 23 + .../davdroid/db/migration/Migration5.kt | 29 + .../davdroid/db/migration/Migration6.kt | 71 + .../davdroid/db/migration/Migration7.kt | 24 + .../davdroid/db/migration/Migration8.kt | 26 + .../davdroid/db/migration/Migration9.kt | 30 + .../davdroid/di/CoroutineDispatchersModule.kt | 61 + .../davdroid/di/CoroutineScopesModule.kt | 30 + .../at/bitfire/davdroid/di/LoggerModule.kt | 20 + .../at/bitfire/davdroid/log/LogFileHandler.kt | 177 + .../at/bitfire/davdroid/log/LogManager.kt | 90 + .../at/bitfire/davdroid/log/StringHandler.kt | 57 + .../davdroid/network/Android10Resolver.kt | 73 + .../davdroid/network/ClientCertKeyManager.kt | 47 + .../davdroid/network/DnsRecordResolver.kt | 166 + .../at/bitfire/davdroid/network/HttpClient.kt | 315 + .../davdroid/network/MemoryCookieStore.kt | 81 + .../davdroid/network/NextcloudLoginFlow.kt | 139 + .../bitfire/davdroid/network/OAuthFastmail.kt | 49 + .../bitfire/davdroid/network/OAuthGoogle.kt | 53 + .../davdroid/network/OAuthIntegration.kt | 63 + .../davdroid/network/OAuthInterceptor.kt | 106 + .../bitfire/davdroid/network/OAuthModule.kt | 40 + .../davdroid/network/UserAgentInterceptor.kt | 33 + .../davdroid/push/PushMessageHandler.kt | 119 + .../davdroid/push/PushNotificationManager.kt | 70 + .../davdroid/push/PushRegistrationManager.kt | 375 + .../davdroid/push/PushRegistrationWorker.kt | 38 + .../davdroid/push/UnifiedPushService.kt | 80 + .../davdroid/repository/AccountRepository.kt | 269 + .../repository/DavCollectionRepository.kt | 425 + .../repository/DavHomeSetRepository.kt | 36 + .../repository/DavServiceRepository.kt | 52 + .../repository/DavSyncStatsRepository.kt | 50 + .../repository/PreferenceRepository.kt | 75 + .../repository/PrincipalRepository.kt | 19 + .../bitfire/davdroid/resource/LocalAddress.kt | 9 + .../davdroid/resource/LocalAddressBook.kt | 360 + .../resource/LocalAddressBookStore.kt | 269 + .../davdroid/resource/LocalCalendar.kt | 235 + .../davdroid/resource/LocalCalendarStore.kt | 154 + .../davdroid/resource/LocalCollection.kt | 76 + .../bitfire/davdroid/resource/LocalContact.kt | 239 + .../davdroid/resource/LocalDataStore.kt | 82 + .../bitfire/davdroid/resource/LocalEvent.kt | 197 + .../bitfire/davdroid/resource/LocalGroup.kt | 313 + .../davdroid/resource/LocalJtxCollection.kt | 84 + .../resource/LocalJtxCollectionStore.kt | 118 + .../davdroid/resource/LocalJtxICalObject.kt | 88 + .../davdroid/resource/LocalResource.kt | 115 + .../at/bitfire/davdroid/resource/LocalTask.kt | 159 + .../davdroid/resource/LocalTaskList.kt | 129 + .../davdroid/resource/LocalTaskListStore.kt | 124 + .../at/bitfire/davdroid/resource/SyncState.kt | 59 + .../CachedGroupMembershipHandler.kt | 28 + .../contactrow/GroupMembershipBuilder.kt | 43 + .../contactrow/GroupMembershipHandler.kt | 39 + .../resource/contactrow/UnknownProperties.kt | 17 + .../contactrow/UnknownPropertiesBuilder.kt | 31 + .../contactrow/UnknownPropertiesHandler.kt | 21 + .../workaround/Android7DirtyVerifier.kt | 161 + .../workaround/ContactDirtyVerifier.kt | 54 + .../CollectionsWithoutHomeSetRefresher.kt | 73 + .../servicedetection/DavResourceFinder.kt | 506 ++ .../servicedetection/HomeSetRefresher.kt | 162 + .../servicedetection/PrincipalsRefresher.kt | 73 + .../RefreshCollectionsWorker.kt | 251 + .../servicedetection/ServiceDetectionUtils.kt | 66 + .../servicedetection/ServiceRefresher.kt | 178 + .../davdroid/settings/AccountSettings.kt | 443 + .../bitfire/davdroid/settings/Credentials.kt | 46 + .../davdroid/settings/DefaultsProvider.kt | 98 + .../at/bitfire/davdroid/settings/Settings.kt | 69 + .../davdroid/settings/SettingsManager.kt | 213 + .../davdroid/settings/SettingsProvider.kt | 71 + .../settings/SharedPreferencesProvider.kt | 155 + .../migration/AccountSettingsMigration.kt | 25 + .../migration/AccountSettingsMigration10.kt | 69 + .../migration/AccountSettingsMigration11.kt | 61 + .../migration/AccountSettingsMigration12.kt | 126 + .../migration/AccountSettingsMigration13.kt | 71 + .../migration/AccountSettingsMigration14.kt | 96 + .../migration/AccountSettingsMigration15.kt | 40 + .../migration/AccountSettingsMigration16.kt | 67 + .../migration/AccountSettingsMigration17.kt | 96 + .../migration/AccountSettingsMigration18.kt | 77 + .../migration/AccountSettingsMigration19.kt | 59 + .../migration/AccountSettingsMigration20.kt | 133 + .../migration/AccountSettingsMigration7.kt | 50 + .../migration/AccountSettingsMigration8.kt | 71 + .../migration/AccountSettingsMigration9.kt | 48 + .../davdroid/startup/CrashHandlerSetup.kt | 90 + .../bitfire/davdroid/startup/StartupPlugin.kt | 44 + .../davdroid/startup/TasksAppWatcher.kt | 74 + .../davdroid/sync/AddressBookSyncer.kt | 132 + .../davdroid/sync/AutomaticSyncManager.kt | 154 + .../davdroid/sync/CalendarSyncManager.kt | 319 + .../bitfire/davdroid/sync/CalendarSyncer.kt | 74 + .../davdroid/sync/ContactsSyncManager.kt | 506 ++ .../bitfire/davdroid/sync/JtxSyncManager.kt | 217 + .../at/bitfire/davdroid/sync/JtxSyncer.kt | 85 + .../at/bitfire/davdroid/sync/ResyncType.kt | 34 + .../bitfire/davdroid/sync/SyncConditions.kt | 165 + .../at/bitfire/davdroid/sync/SyncDataType.kt | 78 + .../at/bitfire/davdroid/sync/SyncException.kt | 94 + .../at/bitfire/davdroid/sync/SyncManager.kt | 813 ++ .../davdroid/sync/SyncNotificationManager.kt | 246 + .../at/bitfire/davdroid/sync/SyncResult.kt | 55 + .../kotlin/at/bitfire/davdroid/sync/Syncer.kt | 283 + .../at/bitfire/davdroid/sync/TaskSyncer.kt | 86 + .../bitfire/davdroid/sync/TasksAppManager.kt | 149 + .../bitfire/davdroid/sync/TasksSyncManager.kt | 195 + .../account/AccountAuthenticatorService.kt | 53 + .../sync/account/AccountsCleanupWorker.kt | 125 + .../AddressBookAuthenticatorService.kt | 48 + .../sync/account/InvalidAccountException.kt | 12 + .../sync/account/SystemAccountUtils.kt | 71 + .../davdroid/sync/adapter/SyncAdapter.kt | 19 + .../davdroid/sync/adapter/SyncAdapterImpl.kt | 186 + .../sync/adapter/SyncAdapterServices.kt | 41 + .../sync/adapter/SyncFrameworkIntegration.kt | 270 + .../sync/groups/CategoriesStrategy.kt | 46 + .../sync/groups/ContactGroupStrategy.kt | 15 + .../davdroid/sync/groups/VCard4Strategy.kt | 54 + .../davdroid/sync/worker/BaseSyncWorker.kt | 284 + .../davdroid/sync/worker/OneTimeSyncWorker.kt | 68 + .../sync/worker/PeriodicSyncWorker.kt | 63 + .../davdroid/sync/worker/SyncWorkerManager.kt | 309 + .../at/bitfire/davdroid/ui/AboutActivity.kt | 360 + .../bitfire/davdroid/ui/AccountsActivity.kt | 58 + .../davdroid/ui/AccountsDrawerHandler.kt | 304 + .../at/bitfire/davdroid/ui/AccountsModel.kt | 299 + .../at/bitfire/davdroid/ui/AccountsScreen.kt | 595 ++ .../davdroid/ui/AppSettingsActivity.kt | 60 + .../bitfire/davdroid/ui/AppSettingsModel.kt | 185 + .../bitfire/davdroid/ui/AppSettingsScreen.kt | 759 ++ .../kotlin/at/bitfire/davdroid/ui/AppTheme.kt | 70 + .../davdroid/ui/CollectionSelectedUseCase.kt | 87 + .../bitfire/davdroid/ui/DebugInfoActivity.kt | 272 + .../bitfire/davdroid/ui/DebugInfoGenerator.kt | 561 ++ .../at/bitfire/davdroid/ui/DebugInfoModel.kt | 196 + .../at/bitfire/davdroid/ui/DebugInfoScreen.kt | 361 + .../at/bitfire/davdroid/ui/ExternalUris.kt | 95 + .../bitfire/davdroid/ui/ForegroundTracker.kt | 40 + .../davdroid/ui/NotificationRegistry.kt | 182 + .../davdroid/ui/OseAccountsDrawerHandler.kt | 148 + .../davdroid/ui/PermissionsActivity.kt | 25 + .../bitfire/davdroid/ui/PermissionsModel.kt | 57 + .../bitfire/davdroid/ui/PermissionsScreen.kt | 257 + .../at/bitfire/davdroid/ui/TasksActivity.kt | 24 + .../at/bitfire/davdroid/ui/TasksModel.kt | 84 + .../at/bitfire/davdroid/ui/TasksScreen.kt | 257 + .../kotlin/at/bitfire/davdroid/ui/UiUtils.kt | 141 + .../davdroid/ui/account/AccountActivity.kt | 81 + .../davdroid/ui/account/AccountProgress.kt | 32 + .../ui/account/AccountProgressUseCase.kt | 86 + .../davdroid/ui/account/AccountScreen.kt | 774 ++ .../davdroid/ui/account/AccountScreenModel.kt | 209 + .../ui/account/AccountSettingsActivity.kt | 51 + .../ui/account/AccountSettingsModel.kt | 290 + .../ui/account/AccountSettingsScreen.kt | 804 ++ .../davdroid/ui/account/CollectionActivity.kt | 45 + .../davdroid/ui/account/CollectionScreen.kt | 418 + .../ui/account/CollectionScreenModel.kt | 144 + .../ui/account/CollectionSelectedUseCase.kt | 89 + .../davdroid/ui/account/CollectionsList.kt | 275 + .../ui/account/CreateAddressBookActivity.kt | 45 + .../ui/account/CreateAddressBookModel.kt | 98 + .../ui/account/CreateAddressBookScreen.kt | 203 + .../ui/account/CreateCalendarActivity.kt | 48 + .../ui/account/CreateCalendarModel.kt | 158 + .../ui/account/CreateCalendarScreen.kt | 381 + .../GetBindableHomeSetsFromServiceUseCase.kt | 29 + .../GetServiceCollectionPagerUseCase.kt | 72 + .../davdroid/ui/account/HomeSetSelection.kt | 123 + .../ui/account/RenameAccountDialog.kt | 98 + .../ui/account/WifiPermissionsActivity.kt | 54 + .../ui/account/WifiPermissionsModel.kt | 29 + .../ui/account/WifiPermissionsScreen.kt | 266 + .../davdroid/ui/composable/ActionCard.kt | 80 + .../davdroid/ui/composable/Assistant.kt | 73 + .../bitfire/davdroid/ui/composable/Boxes.kt | 37 + .../davdroid/ui/composable/CardWithImage.kt | 146 + .../ui/composable/ExceptionInfoDialog.kt | 100 + .../davdroid/ui/composable/InputDialogs.kt | 197 + .../ui/composable/PasswordTextField.kt | 136 + .../ui/composable/PermissionSwitchRow.kt | 144 + .../davdroid/ui/composable/ProgressBar.kt | 46 + .../davdroid/ui/composable/RadioButtons.kt | 97 + .../davdroid/ui/composable/RadioWithSwitch.kt | 86 + .../ui/composable/SafeAndroidUriHandler.kt | 29 + .../composable/SelectClientCertificateCard.kt | 97 + .../davdroid/ui/composable/Settings.kt | 192 + .../davdroid/ui/icon/CalendarImport.kt | 67 + .../ui/intro/BatteryOptimizationsPage.kt | 58 + .../intro/BatteryOptimizationsPageContent.kt | 225 + .../ui/intro/BatteryOptimizationsPageModel.kt | 118 + .../davdroid/ui/intro/IntroActivity.kt | 72 + .../bitfire/davdroid/ui/intro/IntroModel.kt | 50 + .../at/bitfire/davdroid/ui/intro/IntroPage.kt | 43 + .../davdroid/ui/intro/IntroPageFactory.kt | 11 + .../bitfire/davdroid/ui/intro/IntroScreen.kt | 286 + .../davdroid/ui/intro/OpenSourcePage.kt | 159 + .../davdroid/ui/intro/PermissionsIntroPage.kt | 41 + .../davdroid/ui/intro/TasksIntroPage.kt | 31 + .../bitfire/davdroid/ui/intro/WelcomePage.kt | 170 + .../davdroid/ui/setup/AccountDetailsPage.kt | 236 + .../davdroid/ui/setup/AdvancedLogin.kt | 213 + .../davdroid/ui/setup/AdvancedLoginModel.kt | 82 + .../davdroid/ui/setup/DetectResourcesPage.kt | 184 + .../bitfire/davdroid/ui/setup/EmailLogin.kt | 162 + .../davdroid/ui/setup/EmailLoginModel.kt | 64 + .../davdroid/ui/setup/FastmailLogin.kt | 184 + .../davdroid/ui/setup/FastmailLoginModel.kt | 114 + .../bitfire/davdroid/ui/setup/GoogleLogin.kt | 254 + .../davdroid/ui/setup/GoogleLoginModel.kt | 130 + .../davdroid/ui/setup/LoginActivity.kt | 150 + .../davdroid/ui/setup/LoginDetailsPage.kt | 25 + .../at/bitfire/davdroid/ui/setup/LoginInfo.kt | 19 + .../bitfire/davdroid/ui/setup/LoginScreen.kt | 136 + .../davdroid/ui/setup/LoginScreenModel.kt | 326 + .../at/bitfire/davdroid/ui/setup/LoginType.kt | 25 + .../davdroid/ui/setup/LoginTypePage.kt | 32 + .../davdroid/ui/setup/LoginTypesProvider.kt | 39 + .../davdroid/ui/setup/NextcloudLogin.kt | 240 + .../davdroid/ui/setup/NextcloudLoginModel.kt | 180 + .../at/bitfire/davdroid/ui/setup/UrlLogin.kt | 184 + .../davdroid/ui/setup/UrlLoginModel.kt | 77 + .../ui/webdav/AddWebdavMountActivity.kt | 26 + .../davdroid/ui/webdav/AddWebdavMountModel.kt | 108 + .../ui/webdav/AddWebdavMountScreen.kt | 266 + .../ui/webdav/WebdavMountsActivity.kt | 29 + .../davdroid/ui/webdav/WebdavMountsModel.kt | 62 + .../davdroid/ui/webdav/WebdavMountsScreen.kt | 423 + .../ui/widget/CalendarColorPickerDialog.kt | 62 + .../ui/widget/IconSyncButtonWidget.kt | 84 + .../ui/widget/IconSyncButtonWidgetReceiver.kt | 12 + .../ui/widget/LabeledSyncButtonWidget.kt | 100 + .../widget/LabeledSyncButtonWidgetReceiver.kt | 12 + .../davdroid/ui/widget/SyncWidgetModel.kt | 28 + .../davdroid/util/BroadcastReceiverFlow.kt | 80 + .../at/bitfire/davdroid/util/DavUtils.kt | 93 + .../bitfire/davdroid/util/PermissionUtils.kt | 158 + .../bitfire/davdroid/util/SensitiveString.kt | 69 + .../at/bitfire/davdroid/util/StringUtils.kt | 15 + .../davdroid/webdav/CredentialsStore.kt | 75 + .../davdroid/webdav/DavDocumentsProvider.kt | 106 + .../davdroid/webdav/DavHttpClientBuilder.kt | 49 + .../davdroid/webdav/DocumentProviderUtils.kt | 90 + .../davdroid/webdav/DocumentSortByMapper.kt | 81 + .../bitfire/davdroid/webdav/DocumentState.kt | 25 + .../davdroid/webdav/DocumentsCursor.kt | 40 + .../bitfire/davdroid/webdav/HeadResponse.kt | 67 + .../bitfire/davdroid/webdav/PagingReader.kt | 131 + .../davdroid/webdav/RandomAccessCallback.kt | 229 + .../webdav/RandomAccessCallbackWrapper.kt | 88 + .../webdav/StreamingFileDescriptor.kt | 137 + .../davdroid/webdav/WebDavMountRepository.kt | 151 + .../davdroid/webdav/cache/DiskCache.kt | 106 + .../davdroid/webdav/cache/ThumbnailCache.kt | 72 + .../webdav/operation/CopyDocumentOperation.kt | 77 + .../operation/CreateDocumentOperation.kt | 89 + .../operation/DeleteDocumentOperation.kt | 55 + .../operation/IsChildDocumentOperation.kt | 38 + .../webdav/operation/MoveDocumentOperation.kt | 66 + .../webdav/operation/OpenDocumentOperation.kt | 115 + .../OpenDocumentThumbnailOperation.kt | 126 + .../operation/QueryChildDocumentsOperation.kt | 214 + .../operation/QueryDocumentOperation.kt | 55 + .../webdav/operation/QueryRootsOperation.kt | 65 + .../operation/RenameDocumentOperation.kt | 67 + .../res/drawable-anydpi/ic_storage_notify.xml | 15 + .../res/drawable-hdpi/ic_storage_notify.png | Bin 0 -> 272 bytes .../res/drawable-mdpi/ic_storage_notify.png | Bin 0 -> 197 bytes .../res/drawable-xhdpi/ic_storage_notify.png | Bin 0 -> 291 bytes .../res/drawable-xxhdpi/ic_storage_notify.png | Bin 0 -> 467 bytes .../main/res/drawable/accounts_background.xml | 189 + app/src/main/res/drawable/google_g_logo.xml | 18 + app/src/main/res/drawable/ic_database_off.xml | 9 + .../res/drawable/ic_foreground_notify.xml | 11 + .../res/drawable/ic_launcher_foreground.xml | 36 + .../main/res/drawable/ic_sd_card_notify.xml | 13 + app/src/main/res/drawable/ic_settings.xml | 10 + app/src/main/res/drawable/ic_share.xml | 10 + app/src/main/res/drawable/ic_sync.xml | 10 + .../res/drawable/ic_sync_problem_notify.xml | 13 + .../main/res/drawable/ic_sync_shortcut.xml | 9 + .../main/res/drawable/ic_warning_notify.xml | 12 + .../main/res/drawable/intro_open_source.xml | 222 + .../main/res/drawable/intro_permissions.xml | 347 + app/src/main/res/drawable/intro_tasks.xml | 371 + app/src/main/res/drawable/mastodon.xml | 8 + ...ct_logomark_cloud_messaging_full_color.xml | 25 + .../res/drawable/shape_rounded_primary.xml | 9 + .../main/res/drawable/undraw_server_down.xml | 185 + .../widget_preview_icon_sync_button.xml | 21 + .../widget_preview_labeled_sync_button.xml | 28 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 7 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3410 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2239 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4391 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6521 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 8597 bytes app/src/main/res/resources.properties | 2 + app/src/main/res/values-ar/strings.xml | 212 + app/src/main/res/values-bg/strings.xml | 481 ++ app/src/main/res/values-ca/strings.xml | 479 ++ app/src/main/res/values-cs/strings.xml | 414 + app/src/main/res/values-da/strings.xml | 416 + app/src/main/res/values-de/strings.xml | 481 ++ app/src/main/res/values-el/strings.xml | 414 + app/src/main/res/values-en-rGB/strings.xml | 359 + app/src/main/res/values-es/strings.xml | 418 + app/src/main/res/values-et/strings.xml | 480 ++ app/src/main/res/values-eu/strings.xml | 477 ++ app/src/main/res/values-fa/strings.xml | 329 + app/src/main/res/values-fi/strings.xml | 42 + app/src/main/res/values-fr/strings.xml | 437 + app/src/main/res/values-gl/strings.xml | 450 ++ app/src/main/res/values-hr/strings.xml | 281 + app/src/main/res/values-hu/strings.xml | 450 ++ app/src/main/res/values-it/strings.xml | 406 + app/src/main/res/values-ja/strings.xml | 478 ++ app/src/main/res/values-ka/strings.xml | 413 + app/src/main/res/values-ko/strings.xml | 462 ++ app/src/main/res/values-nb/strings.xml | 171 + app/src/main/res/values-nl/strings.xml | 483 ++ app/src/main/res/values-pl/strings.xml | 489 ++ app/src/main/res/values-pt-rBR/strings.xml | 482 ++ app/src/main/res/values-pt/strings.xml | 452 ++ app/src/main/res/values-ro/strings.xml | 478 ++ app/src/main/res/values-ru/strings.xml | 489 ++ app/src/main/res/values-sk/strings.xml | 194 + app/src/main/res/values-sl/strings.xml | 211 + app/src/main/res/values-sr/strings.xml | 282 + app/src/main/res/values-sv/strings.xml | 465 ++ app/src/main/res/values-szl/strings.xml | 207 + app/src/main/res/values-tr/strings.xml | 113 + app/src/main/res/values-uk/strings.xml | 263 + app/src/main/res/values-vi/strings.xml | 328 + app/src/main/res/values-zh-rTW/strings.xml | 476 ++ app/src/main/res/values-zh/strings.xml | 473 ++ app/src/main/res/values/colors.xml | 6 + app/src/main/res/values/strings.xml | 544 ++ .../main/res/xml/account_authenticator.xml | 6 + .../account_authenticator_address_book.xml | 6 + app/src/main/res/xml/contacts.xml | 108 + app/src/main/res/xml/debug_paths.xml | 4 + .../main/res/xml/network_security_config.xml | 9 + app/src/main/res/xml/sync_calendars.xml | 17 + app/src/main/res/xml/sync_contacts.xml | 9 + app/src/main/res/xml/sync_notes.xml | 9 + app/src/main/res/xml/sync_opentasks.xml | 9 + app/src/main/res/xml/sync_prefs.xml | 6 + app/src/main/res/xml/sync_tasks_org.xml | 9 + .../res/xml/widget_info_icon_sync_button.xml | 17 + .../xml/widget_info_labeled_sync_button.xml | 18 + app/src/ose/AndroidManifest.xml | 27 + .../bitfire/davdroid/DebugInfoCrashHandler.kt | 50 + .../at/bitfire/davdroid/di/OseFlavorModule.kt | 52 + .../ui/OpenSourceLicenseInfoProvider.kt | 70 + .../at/bitfire/davdroid/ui/ThemeColors.kt | 167 + .../davdroid/ui/intro/OseIntroPageFactory.kt | 24 + .../ui/setup/StandardLoginTypePage.kt | 121 + .../ui/setup/StandardLoginTypesProvider.kt | 66 + .../at/bitfire/davdroid/DavUtilsTest.kt | 56 + .../bitfire/davdroid/log/StringHandlerTest.kt | 42 + .../davdroid/network/MemoryCookieStoreTest.kt | 84 + .../davdroid/sync/SyncExceptionTest.kt | 206 + .../davdroid/util/SensitiveStringTest.kt | 49 + .../bitfire/davdroid/util/StringUtilsTest.kt | 44 + .../bitfire/davdroid/webdav/DiskCacheTest.kt | 114 + .../webdav/DocumentSortByMapperTest.kt | 52 + .../davdroid/webdav/PagingReaderTest.kt | 209 + build.gradle.kts | 13 + doc/.gitignore | 1 + doc/DAVdroid-Linuxwochen-2016.odp | Bin 0 -> 14791 bytes doc/NIST.SP.800-52r1.pdf | Bin 0 -> 585486 bytes doc/caldav-proxy.txt | 560 ++ doc/how_davx5_works.svgz | Bin 0 -> 1971248 bytes ...rfc3744-webdav-access-control-protocol.txt | 4035 ++++++++++ doc/rfc4791-caldav.txt | 5995 ++++++++++++++ doc/rfc4918-webdav.txt | 7115 +++++++++++++++++ ...397-webdav-current-principal-extension.txt | 281 + doc/rfc5785-well-known-uris.txt | 451 ++ doc/rfc6352-carddav.txt | 2691 +++++++ ...fc6638-scheduling-extensions-to-caldav.txt | 4371 ++++++++++ ...c6764-caldav-carddav-service-discovery.txt | 787 ++ doc/undraw-license.pdf | Bin 0 -> 30071 bytes .../metadata/android/ar/full_description.txt | 5 + .../metadata/android/ar/short_description.txt | 1 + .../metadata/android/bg/full_description.txt | 5 + .../metadata/android/bg/short_description.txt | 1 + .../metadata/android/ca/full_description.txt | 5 + .../metadata/android/ca/short_description.txt | 1 + .../metadata/android/cs/full_description.txt | 5 + .../metadata/android/cs/short_description.txt | 1 + .../metadata/android/da/full_description.txt | 5 + .../metadata/android/da/short_description.txt | 1 + .../metadata/android/de/full_description.txt | 5 + .../metadata/android/de/short_description.txt | 1 + .../metadata/android/el/full_description.txt | 5 + .../metadata/android/el/short_description.txt | 1 + .../android/en-GB/full_description.txt | 5 + .../android/en-GB/short_description.txt | 1 + .../android/en-US/changelogs/300000003.txt | 6 + .../android/en-US/changelogs/301000012.txt | 6 + .../android/en-US/changelogs/302000003.txt | 9 + .../android/en-US/changelogs/303000006.txt | 8 + .../android/en-US/changelogs/303010001.txt | 1 + .../android/en-US/changelogs/303020005.txt | 2 + .../android/en-US/changelogs/303030005.txt | 7 + .../android/en-US/changelogs/303060003.txt | 6 + .../android/en-US/changelogs/303070004.txt | 5 + .../android/en-US/changelogs/303100003.txt | 4 + .../android/en-US/changelogs/303110004.txt | 6 + .../android/en-US/changelogs/303120005.txt | 4 + .../metadata/android/en-US/changelogs/328.txt | 5 + .../metadata/android/en-US/changelogs/329.txt | 1 + .../metadata/android/en-US/changelogs/331.txt | 4 + .../android/en-US/changelogs/400000005.txt | 1 + .../android/en-US/changelogs/401000005.txt | 5 + .../android/en-US/changelogs/401010002.txt | 5 + .../android/en-US/changelogs/402000006.txt | 3 + .../android/en-US/changelogs/402020000.txt | 2 + .../android/en-US/changelogs/402030001.txt | 3 + .../android/en-US/changelogs/402040002.txt | 13 + .../android/en-US/changelogs/402050001.txt | 3 + .../android/en-US/changelogs/403000005.txt | 7 + .../android/en-US/changelogs/405000000.txt | 3 + .../android/en-US/full_description.txt | 5 + .../android/en-US/images/featureGraphic.png | Bin 0 -> 500709 bytes .../metadata/android/en-US/images/icon.png | Bin 0 -> 23228 bytes .../en-US/images/phoneScreenshots/1.png | Bin 0 -> 644395 bytes .../en-US/images/phoneScreenshots/2.png | Bin 0 -> 826339 bytes .../en-US/images/phoneScreenshots/3.png | Bin 0 -> 589517 bytes .../en-US/images/phoneScreenshots/4.png | Bin 0 -> 940871 bytes .../en-US/images/phoneScreenshots/5.png | Bin 0 -> 671584 bytes .../en-US/images/phoneScreenshots/6.png | Bin 0 -> 867407 bytes .../android/en-US/short_description.txt | 1 + fastlane/metadata/android/en-US/title.txt | 1 + fastlane/metadata/android/en-US/video.txt | 1 + .../android/en-rGB/full_description.txt | 5 + .../android/en-rGB/short_description.txt | 1 + .../metadata/android/es/full_description.txt | 5 + .../metadata/android/es/short_description.txt | 1 + .../metadata/android/et/full_description.txt | 5 + .../metadata/android/et/short_description.txt | 1 + .../metadata/android/eu/full_description.txt | 5 + .../metadata/android/eu/short_description.txt | 1 + .../metadata/android/fa/full_description.txt | 5 + .../metadata/android/fi/full_description.txt | 5 + .../metadata/android/fi/short_description.txt | 1 + .../android/fr-FR/full_description.txt | 5 + .../android/fr-FR/short_description.txt | 1 + .../android/fr-rFR/full_description.txt | 5 + .../android/fr-rFR/short_description.txt | 1 + .../metadata/android/fr/full_description.txt | 5 + .../metadata/android/fr/short_description.txt | 1 + .../metadata/android/gl/full_description.txt | 5 + .../metadata/android/gl/short_description.txt | 1 + .../metadata/android/hr/full_description.txt | 5 + .../metadata/android/hr/short_description.txt | 1 + .../metadata/android/hu/full_description.txt | 5 + .../metadata/android/hu/short_description.txt | 1 + .../metadata/android/id/full_description.txt | 5 + .../metadata/android/id/short_description.txt | 1 + .../metadata/android/it/full_description.txt | 5 + .../metadata/android/it/short_description.txt | 1 + .../metadata/android/ja/full_description.txt | 5 + .../metadata/android/ja/short_description.txt | 1 + .../metadata/android/ka/full_description.txt | 5 + .../metadata/android/ka/short_description.txt | 1 + .../metadata/android/ko/full_description.txt | 5 + .../metadata/android/ko/short_description.txt | 1 + .../metadata/android/nl/full_description.txt | 5 + .../metadata/android/nl/short_description.txt | 1 + .../metadata/android/pl/full_description.txt | 5 + .../metadata/android/pl/short_description.txt | 1 + .../android/pt-rBR/full_description.txt | 5 + .../android/pt-rBR/short_description.txt | 1 + .../metadata/android/pt/full_description.txt | 5 + .../metadata/android/pt/short_description.txt | 1 + .../metadata/android/ro/full_description.txt | 5 + .../metadata/android/ro/short_description.txt | 1 + .../metadata/android/ru/full_description.txt | 5 + .../metadata/android/ru/short_description.txt | 1 + .../metadata/android/si/short_description.txt | 1 + .../metadata/android/sk/full_description.txt | 5 + .../metadata/android/sk/short_description.txt | 1 + .../metadata/android/sl/full_description.txt | 5 + .../metadata/android/sl/short_description.txt | 1 + .../metadata/android/sr/full_description.txt | 5 + .../metadata/android/sr/short_description.txt | 1 + .../metadata/android/sv/full_description.txt | 5 + .../metadata/android/sv/short_description.txt | 1 + .../metadata/android/szl/full_description.txt | 5 + .../android/szl/short_description.txt | 1 + .../metadata/android/uk/full_description.txt | 5 + .../metadata/android/uk/short_description.txt | 1 + .../metadata/android/vi/full_description.txt | 5 + .../metadata/android/vi/short_description.txt | 1 + .../android/zh-TW/full_description.txt | 5 + .../android/zh-TW/short_description.txt | 1 + .../android/zh-rTW/full_description.txt | 5 + .../android/zh-rTW/short_description.txt | 1 + .../metadata/android/zh/full_description.txt | 5 + .../metadata/android/zh/short_description.txt | 1 + gradle.properties | 22 + gradle/libs.versions.toml | 116 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43583 bytes gradle/wrapper/gradle-wrapper.properties | 11 + gradlew | 252 + gradlew.bat | 94 + scripts/copy-compiled.sh | 9 + scripts/fetch-db.sh | 5 + scripts/fetch-translations.sh | 17 + scripts/gen-contacts.rb | 13 + scripts/rewrite-translators.rb | 24 + settings.gradle.kts | 22 + 644 files changed, 99721 insertions(+), 2 deletions(-) create mode 100644 AUTHORS create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 app/.gitignore create mode 100644 app/build.gradle.kts create mode 100644 app/lint.xml create mode 100644 app/proguard-rules-release.pro create mode 100644 app/schemas/at.bitfire.davdroid.db.AppDatabase/10.json create mode 100644 app/schemas/at.bitfire.davdroid.db.AppDatabase/11.json create mode 100644 app/schemas/at.bitfire.davdroid.db.AppDatabase/12.json create mode 100644 app/schemas/at.bitfire.davdroid.db.AppDatabase/13.json create mode 100644 app/schemas/at.bitfire.davdroid.db.AppDatabase/14.json create mode 100644 app/schemas/at.bitfire.davdroid.db.AppDatabase/15.json create mode 100644 app/schemas/at.bitfire.davdroid.db.AppDatabase/16.json create mode 100644 app/schemas/at.bitfire.davdroid.db.AppDatabase/17.json create mode 100644 app/schemas/at.bitfire.davdroid.db.AppDatabase/18.json create mode 100644 app/schemas/at.bitfire.davdroid.db.AppDatabase/8.json create mode 100644 app/schemas/at.bitfire.davdroid.db.AppDatabase/9.json create mode 100644 app/src/.gitignore create mode 100644 app/src/androidTest/AndroidManifest.xml create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/ExternalLibrariesTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/HiltTestRunner.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/TestUtils.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/db/AppDatabaseTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/db/CollectionTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/db/HomeSetDaoTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/db/MemoryDbModule.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/db/PrincipalDaoTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/db/SyncStatsDaoTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/db/WebDavDocumentDaoTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/AutoMigration16Test.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/AutoMigration18Test.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/DatabaseMigrationTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/di/FakeSyncAdapterModule.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/di/TestCoroutineDispatchersModule.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/di/TestTasksAppWatcherModule.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/network/Android10ResolverTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/network/DnsRecordResolverTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/network/HttpClientTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/network/OkhttpClientTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/push/PushMessageHandlerTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/push/UnifiedPushServiceTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/repository/AccountRepositoryTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalAddressBookStoreTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalAddressBookTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalCalendarTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalEventTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalGroupTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalTestAddressBook.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalTestAddressBookProvider.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/CachedGroupMembershipHandlerTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/GroupMembershipBuilderTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/GroupMembershipHandlerTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownPropertiesBuilderTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownPropertiesHandlerTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/CollectionsWithoutHomeSetRefresherTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinderTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/HomeSetRefresherTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/PrincipalsRefresherTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/ServiceRefresherTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/settings/AccountSettingsTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/settings/SettingsManagerTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration17Test.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration18Test.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration19Test.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration20Test.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/sync/AndroidSyncFrameworkTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/sync/FakeSyncAdapter.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/sync/JtxSyncManagerTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/sync/LocalTestCollection.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/sync/LocalTestResource.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncAdapterImplTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncManagerTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncerTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/sync/TestSyncManager.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorkerTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/SystemAccountUtilsTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/TestAccount.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorkerTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/SyncConditionsTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManagerTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/ui/CollectionSelectedUseCaseTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/ui/DebugInfoActivityTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/ui/setup/LoginActivityTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/webdav/CredentialsStoreTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/webdav/WebDavMountRepositoryTest.kt create mode 100644 app/src/androidTest/kotlin/at/bitfire/davdroid/webdav/operation/QueryChildDocumentsOperationTest.kt create mode 100644 app/src/androidTest/res/drawable-hdpi/ic_launcher.png create mode 100644 app/src/androidTest/res/drawable-ldpi/ic_launcher.png create mode 100644 app/src/androidTest/res/drawable-mdpi/ic_launcher.png create mode 100644 app/src/androidTest/res/drawable-xhdpi/ic_launcher.png create mode 100644 app/src/androidTest/res/values/strings.xml create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/assets/gplv3.html create mode 100644 app/src/main/assets/logging.properties create mode 100644 app/src/main/assets/translators.json create mode 100644 app/src/main/ic_launcher-web.png create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/App.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/Constants.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/TextTable.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/AppDatabase.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/Collection.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/CollectionDao.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/Converters.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/HomeSet.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/HomeSetDao.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/Principal.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/PrincipalDao.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/Service.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/ServiceDao.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/SyncStats.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/SyncStatsDao.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/WebDavDocument.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/WebDavDocumentDao.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/WebDavMount.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/WebDavMountDao.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/WebDavMountWithRootDocument.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/migration/AutoMigration12.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/migration/AutoMigration16.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/migration/AutoMigration18.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration2.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration3.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration4.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration5.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration6.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration7.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration8.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration9.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/di/CoroutineDispatchersModule.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/di/CoroutineScopesModule.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/di/LoggerModule.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/log/LogFileHandler.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/log/LogManager.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/log/StringHandler.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/network/Android10Resolver.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/network/ClientCertKeyManager.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/network/DnsRecordResolver.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/network/MemoryCookieStore.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/network/NextcloudLoginFlow.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/network/OAuthFastmail.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/network/OAuthGoogle.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/network/OAuthIntegration.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/network/OAuthInterceptor.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/network/OAuthModule.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/network/UserAgentInterceptor.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/push/PushMessageHandler.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/push/PushNotificationManager.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/push/PushRegistrationManager.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/push/PushRegistrationWorker.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/push/UnifiedPushService.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/repository/DavHomeSetRepository.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/repository/DavServiceRepository.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/repository/DavSyncStatsRepository.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/repository/PreferenceRepository.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/repository/PrincipalRepository.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddress.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBookStore.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendar.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendarStore.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCollection.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalContact.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalDataStore.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalEvent.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalGroup.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollection.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollectionStore.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxICalObject.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalResource.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTask.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskList.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskListStore.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/SyncState.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/CachedGroupMembershipHandler.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/GroupMembershipBuilder.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/GroupMembershipHandler.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownProperties.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownPropertiesBuilder.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownPropertiesHandler.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/workaround/Android7DirtyVerifier.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/workaround/ContactDirtyVerifier.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/servicedetection/CollectionsWithoutHomeSetRefresher.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/servicedetection/HomeSetRefresher.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/servicedetection/PrincipalsRefresher.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/servicedetection/ServiceDetectionUtils.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/servicedetection/ServiceRefresher.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/Credentials.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/DefaultsProvider.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/Settings.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/SettingsManager.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/SettingsProvider.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/SharedPreferencesProvider.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration10.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration11.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration12.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration13.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration14.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration15.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration16.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration17.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration18.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration19.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration20.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration7.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration8.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration9.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/startup/CrashHandlerSetup.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/startup/StartupPlugin.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/startup/TasksAppWatcher.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/AddressBookSyncer.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/AutomaticSyncManager.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncManager.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncer.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/ContactsSyncManager.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncManager.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncer.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/ResyncType.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/SyncConditions.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/SyncDataType.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/SyncException.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/SyncNotificationManager.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/SyncResult.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/Syncer.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/TaskSyncer.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/TasksAppManager.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/TasksSyncManager.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/account/AccountAuthenticatorService.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorker.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/account/AddressBookAuthenticatorService.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/account/InvalidAccountException.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/account/SystemAccountUtils.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncAdapter.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncAdapterImpl.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncAdapterServices.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncFrameworkIntegration.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/groups/CategoriesStrategy.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/groups/ContactGroupStrategy.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/groups/VCard4Strategy.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/worker/BaseSyncWorker.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/worker/OneTimeSyncWorker.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorker.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManager.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/AboutActivity.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsDrawerHandler.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsScreen.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsScreen.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/AppTheme.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/CollectionSelectedUseCase.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoGenerator.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoScreen.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/ExternalUris.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/ForegroundTracker.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/NotificationRegistry.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/OseAccountsDrawerHandler.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsActivity.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsScreen.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/TasksActivity.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/TasksModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/TasksScreen.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/UiUtils.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountProgress.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountProgressUseCase.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreen.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreenModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsScreen.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionActivity.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionScreen.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionScreenModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionSelectedUseCase.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionsList.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateAddressBookActivity.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateAddressBookModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateAddressBookScreen.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateCalendarActivity.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateCalendarModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateCalendarScreen.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/GetBindableHomeSetsFromServiceUseCase.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/GetServiceCollectionPagerUseCase.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/HomeSetSelection.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/RenameAccountDialog.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsActivity.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsScreen.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/composable/ActionCard.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/composable/Assistant.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/composable/Boxes.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/composable/CardWithImage.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/composable/ExceptionInfoDialog.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/composable/InputDialogs.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/composable/PasswordTextField.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/composable/PermissionSwitchRow.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/composable/ProgressBar.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/composable/RadioButtons.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/composable/RadioWithSwitch.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/composable/SafeAndroidUriHandler.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/composable/SelectClientCertificateCard.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/composable/Settings.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/icon/CalendarImport.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPage.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageContent.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroPage.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroPageFactory.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroScreen.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourcePage.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroPage.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroPage.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/intro/WelcomePage.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsPage.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AdvancedLogin.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AdvancedLoginModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/DetectResourcesPage.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/EmailLogin.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/EmailLoginModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/FastmailLogin.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/FastmailLoginModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/GoogleLogin.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/GoogleLoginModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginActivity.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginDetailsPage.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginInfo.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginScreen.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginScreenModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginType.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginTypePage.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginTypesProvider.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLogin.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLoginModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/UrlLogin.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/setup/UrlLoginModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivity.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountScreen.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsActivity.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsScreen.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/widget/CalendarColorPickerDialog.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/widget/IconSyncButtonWidget.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/widget/IconSyncButtonWidgetReceiver.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/widget/LabeledSyncButtonWidget.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/widget/LabeledSyncButtonWidgetReceiver.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/widget/SyncWidgetModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/util/BroadcastReceiverFlow.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/util/DavUtils.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/util/SensitiveString.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/util/StringUtils.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/CredentialsStore.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/DavHttpClientBuilder.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/DocumentProviderUtils.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/DocumentSortByMapper.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/DocumentState.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/DocumentsCursor.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/HeadResponse.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/PagingReader.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallback.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallbackWrapper.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/WebDavMountRepository.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/cache/DiskCache.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/cache/ThumbnailCache.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/CopyDocumentOperation.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/CreateDocumentOperation.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/DeleteDocumentOperation.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/IsChildDocumentOperation.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/MoveDocumentOperation.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/OpenDocumentOperation.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/OpenDocumentThumbnailOperation.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/QueryChildDocumentsOperation.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/QueryDocumentOperation.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/QueryRootsOperation.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/RenameDocumentOperation.kt create mode 100644 app/src/main/res/drawable-anydpi/ic_storage_notify.xml create mode 100644 app/src/main/res/drawable-hdpi/ic_storage_notify.png create mode 100644 app/src/main/res/drawable-mdpi/ic_storage_notify.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_storage_notify.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_storage_notify.png create mode 100644 app/src/main/res/drawable/accounts_background.xml create mode 100644 app/src/main/res/drawable/google_g_logo.xml create mode 100644 app/src/main/res/drawable/ic_database_off.xml create mode 100644 app/src/main/res/drawable/ic_foreground_notify.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_sd_card_notify.xml create mode 100644 app/src/main/res/drawable/ic_settings.xml create mode 100644 app/src/main/res/drawable/ic_share.xml create mode 100644 app/src/main/res/drawable/ic_sync.xml create mode 100644 app/src/main/res/drawable/ic_sync_problem_notify.xml create mode 100644 app/src/main/res/drawable/ic_sync_shortcut.xml create mode 100644 app/src/main/res/drawable/ic_warning_notify.xml create mode 100644 app/src/main/res/drawable/intro_open_source.xml create mode 100644 app/src/main/res/drawable/intro_permissions.xml create mode 100644 app/src/main/res/drawable/intro_tasks.xml create mode 100644 app/src/main/res/drawable/mastodon.xml create mode 100644 app/src/main/res/drawable/product_logomark_cloud_messaging_full_color.xml create mode 100644 app/src/main/res/drawable/shape_rounded_primary.xml create mode 100644 app/src/main/res/drawable/undraw_server_down.xml create mode 100644 app/src/main/res/layout/widget_preview_icon_sync_button.xml create mode 100644 app/src/main/res/layout/widget_preview_labeled_sync_button.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/resources.properties create mode 100644 app/src/main/res/values-ar/strings.xml create mode 100644 app/src/main/res/values-bg/strings.xml create mode 100644 app/src/main/res/values-ca/strings.xml create mode 100644 app/src/main/res/values-cs/strings.xml create mode 100644 app/src/main/res/values-da/strings.xml create mode 100644 app/src/main/res/values-de/strings.xml create mode 100644 app/src/main/res/values-el/strings.xml create mode 100644 app/src/main/res/values-en-rGB/strings.xml create mode 100644 app/src/main/res/values-es/strings.xml create mode 100644 app/src/main/res/values-et/strings.xml create mode 100644 app/src/main/res/values-eu/strings.xml create mode 100644 app/src/main/res/values-fa/strings.xml create mode 100644 app/src/main/res/values-fi/strings.xml create mode 100644 app/src/main/res/values-fr/strings.xml create mode 100644 app/src/main/res/values-gl/strings.xml create mode 100644 app/src/main/res/values-hr/strings.xml create mode 100644 app/src/main/res/values-hu/strings.xml create mode 100644 app/src/main/res/values-it/strings.xml create mode 100644 app/src/main/res/values-ja/strings.xml create mode 100644 app/src/main/res/values-ka/strings.xml create mode 100644 app/src/main/res/values-ko/strings.xml create mode 100644 app/src/main/res/values-nb/strings.xml create mode 100644 app/src/main/res/values-nl/strings.xml create mode 100644 app/src/main/res/values-pl/strings.xml create mode 100644 app/src/main/res/values-pt-rBR/strings.xml create mode 100644 app/src/main/res/values-pt/strings.xml create mode 100644 app/src/main/res/values-ro/strings.xml create mode 100644 app/src/main/res/values-ru/strings.xml create mode 100644 app/src/main/res/values-sk/strings.xml create mode 100644 app/src/main/res/values-sl/strings.xml create mode 100644 app/src/main/res/values-sr/strings.xml create mode 100644 app/src/main/res/values-sv/strings.xml create mode 100644 app/src/main/res/values-szl/strings.xml create mode 100644 app/src/main/res/values-tr/strings.xml create mode 100644 app/src/main/res/values-uk/strings.xml create mode 100644 app/src/main/res/values-vi/strings.xml create mode 100644 app/src/main/res/values-zh-rTW/strings.xml create mode 100644 app/src/main/res/values-zh/strings.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/xml/account_authenticator.xml create mode 100644 app/src/main/res/xml/account_authenticator_address_book.xml create mode 100644 app/src/main/res/xml/contacts.xml create mode 100644 app/src/main/res/xml/debug_paths.xml create mode 100644 app/src/main/res/xml/network_security_config.xml create mode 100644 app/src/main/res/xml/sync_calendars.xml create mode 100644 app/src/main/res/xml/sync_contacts.xml create mode 100644 app/src/main/res/xml/sync_notes.xml create mode 100644 app/src/main/res/xml/sync_opentasks.xml create mode 100644 app/src/main/res/xml/sync_prefs.xml create mode 100644 app/src/main/res/xml/sync_tasks_org.xml create mode 100644 app/src/main/res/xml/widget_info_icon_sync_button.xml create mode 100644 app/src/main/res/xml/widget_info_labeled_sync_button.xml create mode 100644 app/src/ose/AndroidManifest.xml create mode 100644 app/src/ose/kotlin/at/bitfire/davdroid/DebugInfoCrashHandler.kt create mode 100644 app/src/ose/kotlin/at/bitfire/davdroid/di/OseFlavorModule.kt create mode 100644 app/src/ose/kotlin/at/bitfire/davdroid/ui/OpenSourceLicenseInfoProvider.kt create mode 100644 app/src/ose/kotlin/at/bitfire/davdroid/ui/ThemeColors.kt create mode 100644 app/src/ose/kotlin/at/bitfire/davdroid/ui/intro/OseIntroPageFactory.kt create mode 100644 app/src/ose/kotlin/at/bitfire/davdroid/ui/setup/StandardLoginTypePage.kt create mode 100644 app/src/ose/kotlin/at/bitfire/davdroid/ui/setup/StandardLoginTypesProvider.kt create mode 100644 app/src/test/kotlin/at/bitfire/davdroid/DavUtilsTest.kt create mode 100644 app/src/test/kotlin/at/bitfire/davdroid/log/StringHandlerTest.kt create mode 100644 app/src/test/kotlin/at/bitfire/davdroid/network/MemoryCookieStoreTest.kt create mode 100644 app/src/test/kotlin/at/bitfire/davdroid/sync/SyncExceptionTest.kt create mode 100644 app/src/test/kotlin/at/bitfire/davdroid/util/SensitiveStringTest.kt create mode 100644 app/src/test/kotlin/at/bitfire/davdroid/util/StringUtilsTest.kt create mode 100644 app/src/test/kotlin/at/bitfire/davdroid/webdav/DiskCacheTest.kt create mode 100644 app/src/test/kotlin/at/bitfire/davdroid/webdav/DocumentSortByMapperTest.kt create mode 100644 app/src/test/kotlin/at/bitfire/davdroid/webdav/PagingReaderTest.kt create mode 100644 build.gradle.kts create mode 100644 doc/.gitignore create mode 100644 doc/DAVdroid-Linuxwochen-2016.odp create mode 100644 doc/NIST.SP.800-52r1.pdf create mode 100644 doc/caldav-proxy.txt create mode 100644 doc/how_davx5_works.svgz create mode 100644 doc/rfc3744-webdav-access-control-protocol.txt create mode 100644 doc/rfc4791-caldav.txt create mode 100644 doc/rfc4918-webdav.txt create mode 100644 doc/rfc5397-webdav-current-principal-extension.txt create mode 100644 doc/rfc5785-well-known-uris.txt create mode 100644 doc/rfc6352-carddav.txt create mode 100644 doc/rfc6638-scheduling-extensions-to-caldav.txt create mode 100644 doc/rfc6764-caldav-carddav-service-discovery.txt create mode 100644 doc/undraw-license.pdf create mode 100644 fastlane/metadata/android/ar/full_description.txt create mode 100644 fastlane/metadata/android/ar/short_description.txt create mode 100644 fastlane/metadata/android/bg/full_description.txt create mode 100644 fastlane/metadata/android/bg/short_description.txt create mode 100644 fastlane/metadata/android/ca/full_description.txt create mode 100644 fastlane/metadata/android/ca/short_description.txt create mode 100644 fastlane/metadata/android/cs/full_description.txt create mode 100644 fastlane/metadata/android/cs/short_description.txt create mode 100644 fastlane/metadata/android/da/full_description.txt create mode 100644 fastlane/metadata/android/da/short_description.txt create mode 100644 fastlane/metadata/android/de/full_description.txt create mode 100644 fastlane/metadata/android/de/short_description.txt create mode 100644 fastlane/metadata/android/el/full_description.txt create mode 100644 fastlane/metadata/android/el/short_description.txt create mode 100644 fastlane/metadata/android/en-GB/full_description.txt create mode 100644 fastlane/metadata/android/en-GB/short_description.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/300000003.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/301000012.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/302000003.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/303000006.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/303010001.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/303020005.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/303030005.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/303060003.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/303070004.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/303100003.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/303110004.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/303120005.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/328.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/329.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/331.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/400000005.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/401000005.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/401010002.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/402000006.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/402020000.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/402030001.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/402040002.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/402050001.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/403000005.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/405000000.txt create mode 100644 fastlane/metadata/android/en-US/full_description.txt create mode 100644 fastlane/metadata/android/en-US/images/featureGraphic.png create mode 100644 fastlane/metadata/android/en-US/images/icon.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/1.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/2.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/3.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/4.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/5.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/6.png create mode 100644 fastlane/metadata/android/en-US/short_description.txt create mode 100644 fastlane/metadata/android/en-US/title.txt create mode 100644 fastlane/metadata/android/en-US/video.txt create mode 100644 fastlane/metadata/android/en-rGB/full_description.txt create mode 100644 fastlane/metadata/android/en-rGB/short_description.txt create mode 100644 fastlane/metadata/android/es/full_description.txt create mode 100644 fastlane/metadata/android/es/short_description.txt create mode 100644 fastlane/metadata/android/et/full_description.txt create mode 100644 fastlane/metadata/android/et/short_description.txt create mode 100644 fastlane/metadata/android/eu/full_description.txt create mode 100644 fastlane/metadata/android/eu/short_description.txt create mode 100644 fastlane/metadata/android/fa/full_description.txt create mode 100644 fastlane/metadata/android/fi/full_description.txt create mode 100644 fastlane/metadata/android/fi/short_description.txt create mode 100644 fastlane/metadata/android/fr-FR/full_description.txt create mode 100644 fastlane/metadata/android/fr-FR/short_description.txt create mode 100644 fastlane/metadata/android/fr-rFR/full_description.txt create mode 100644 fastlane/metadata/android/fr-rFR/short_description.txt create mode 100644 fastlane/metadata/android/fr/full_description.txt create mode 100644 fastlane/metadata/android/fr/short_description.txt create mode 100644 fastlane/metadata/android/gl/full_description.txt create mode 100644 fastlane/metadata/android/gl/short_description.txt create mode 100644 fastlane/metadata/android/hr/full_description.txt create mode 100644 fastlane/metadata/android/hr/short_description.txt create mode 100644 fastlane/metadata/android/hu/full_description.txt create mode 100644 fastlane/metadata/android/hu/short_description.txt create mode 100644 fastlane/metadata/android/id/full_description.txt create mode 100644 fastlane/metadata/android/id/short_description.txt create mode 100644 fastlane/metadata/android/it/full_description.txt create mode 100644 fastlane/metadata/android/it/short_description.txt create mode 100644 fastlane/metadata/android/ja/full_description.txt create mode 100644 fastlane/metadata/android/ja/short_description.txt create mode 100644 fastlane/metadata/android/ka/full_description.txt create mode 100644 fastlane/metadata/android/ka/short_description.txt create mode 100644 fastlane/metadata/android/ko/full_description.txt create mode 100644 fastlane/metadata/android/ko/short_description.txt create mode 100644 fastlane/metadata/android/nl/full_description.txt create mode 100644 fastlane/metadata/android/nl/short_description.txt create mode 100644 fastlane/metadata/android/pl/full_description.txt create mode 100644 fastlane/metadata/android/pl/short_description.txt create mode 100644 fastlane/metadata/android/pt-rBR/full_description.txt create mode 100644 fastlane/metadata/android/pt-rBR/short_description.txt create mode 100644 fastlane/metadata/android/pt/full_description.txt create mode 100644 fastlane/metadata/android/pt/short_description.txt create mode 100644 fastlane/metadata/android/ro/full_description.txt create mode 100644 fastlane/metadata/android/ro/short_description.txt create mode 100644 fastlane/metadata/android/ru/full_description.txt create mode 100644 fastlane/metadata/android/ru/short_description.txt create mode 100644 fastlane/metadata/android/si/short_description.txt create mode 100644 fastlane/metadata/android/sk/full_description.txt create mode 100644 fastlane/metadata/android/sk/short_description.txt create mode 100644 fastlane/metadata/android/sl/full_description.txt create mode 100644 fastlane/metadata/android/sl/short_description.txt create mode 100644 fastlane/metadata/android/sr/full_description.txt create mode 100644 fastlane/metadata/android/sr/short_description.txt create mode 100644 fastlane/metadata/android/sv/full_description.txt create mode 100644 fastlane/metadata/android/sv/short_description.txt create mode 100644 fastlane/metadata/android/szl/full_description.txt create mode 100644 fastlane/metadata/android/szl/short_description.txt create mode 100644 fastlane/metadata/android/uk/full_description.txt create mode 100644 fastlane/metadata/android/uk/short_description.txt create mode 100644 fastlane/metadata/android/vi/full_description.txt create mode 100644 fastlane/metadata/android/vi/short_description.txt create mode 100644 fastlane/metadata/android/zh-TW/full_description.txt create mode 100644 fastlane/metadata/android/zh-TW/short_description.txt create mode 100644 fastlane/metadata/android/zh-rTW/full_description.txt create mode 100644 fastlane/metadata/android/zh-rTW/short_description.txt create mode 100644 fastlane/metadata/android/zh/full_description.txt create mode 100644 fastlane/metadata/android/zh/short_description.txt create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100755 scripts/copy-compiled.sh create mode 100755 scripts/fetch-db.sh create mode 100755 scripts/fetch-translations.sh create mode 100755 scripts/gen-contacts.rb create mode 100755 scripts/rewrite-translators.rb create mode 100644 settings.gradle.kts diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..8fb2370 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,7 @@ +You can view the list of people who have contributed to the code base in the version control history: +https://github.com/bitfireAT/davx5-ose/graphs/contributors + +Translators are not mentioned in the history explicitly. +The list of translators can be found in the About screen. + +Every contribution is welcome. There are many other forms of contributing besides writing code! diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..010673d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,117 @@ + +**Thank you for your interest in contributing to DAVx⁵!** + + +# Licensing + +All work in this repository is [licensed under the GPLv3](LICENSE). + +We (bitfire.at, initial and main contributors) are also asking you to give us +permission to use your contribution for related non-open source projects +like [Managed DAVx⁵](https://www.davx5.com/organizations/managed-davx5). + +If you send us a pull request, our CLA bot will ask you to sign the +Contributor's License Agreement so that we can use your contribution. + + +# Copyright + +Make sure that every file that contains significant work (at least every code file) +starts with the copyright header: + +``` +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ +``` + +You can set this in Android Studio: + +1. Settings / Editor / Copyright / Copyright Profiles +2. Paste the text above (without the stars). +3. Set Formatting so that the preview exactly looks like above; one blank line after the block. +4. Set this copyright profile as the default profile for the project. +5. Apply copyright: right-click in file tree / Update copyright. + + +# Style guide + +Please adhere to the [Kotlin style guide](https://developer.android.com/kotlin/style-guide) and +the following hints to make the source code uniform. + +**Have a look at similar files and copy their style if you're not certain.** + +Sample file (pay attention to blank lines and other formatting): + +``` + + +class MyClass(int arg1) : SuperClass() { + + companion object { + + const val CONSTANT_STRING = "Constant String"; + + fun staticMethod() { // Use static methods when you don't need the object context. + // … + } + + } + + var someProperty: String = "12345" + var someRelatedProperty: Int = 12345 + + init { + // constructor + } + + + /** + * Use KDoc to document important methods. Don't use it dogmatically, but writing proper documentation + * (not just the method name with spaces) helps you to re-think what the method shall really do. + */ + fun aFun1() { // Group methods by some logic (for instance, the order in which they will be called) + } // and alphabetically within a group. + + fun anotherFun() { + // … + } + + + fun somethingCompletelyDifferent() { // two blank lines to separate groups + } + + fun helperForSomethingCompletelyDifferent() { + someCall(arg1, arg2, arg3, arg4) // function calls: stick to one line unless it becomes confusing + } + + + class Model( // two blank lines before inner classes + someArgument: SomeLongClass, // arguments in multiple lines when they're too long for one line + anotherArgument: AnotherLongType, + thirdArgument: AnotherLongTypeName + ) : ViewModel() { + + fun abc() { + } + + } + +} +``` + +In general, use one blank line to separate things within one group of things, and two blank lines +to separate groups. In rare cases, when methods are tightly coupled and are only helpers for another +method, they may follow the calling method without separating blank lines. + +## Tests + +Test classes should be in the appropriate directory (see existing tests) and in the same package as the +tested class. Tests are usually be named like `methodToBeTested_Condition()`, see +[Test apps on Android](https://developer.android.com/training/testing/). + + +# Authors + +If you make significant contributions, feel free to add yourself to the [AUTHORS file](AUTHORS). + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index cdffa2b..f1bcccb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,45 @@ -# davx5 -Contact and Calendar Synchronisation Android \ No newline at end of file +[![Website](https://img.shields.io/website?style=flat-square&up_color=%237cb342&url=https%3A%2F%2Fwww.davx5.com)](https://www.davx5.com/) +[![F-Droid](https://img.shields.io/f-droid/v/at.bitfire.davdroid?style=flat-square)](https://f-droid.org/packages/at.bitfire.davdroid/) +[![License](https://img.shields.io/github/license/bitfireAT/davx5-ose?style=flat-square)](https://github.com/bitfireAT/davx5-ose/blob/main/LICENSE) +[![Follow @davx5app@fosstodon.org](https://img.shields.io/mastodon/follow/109598783742737223?domain=https%3A%2F%2Ffosstodon.org&style=flat-square)](https://fosstodon.org/@davx5app) +[![Development tests](https://github.com/bitfireAT/davx5-ose/actions/workflows/test-dev.yml/badge.svg)](https://github.com/bitfireAT/davx5-ose/actions/workflows/test-dev.yml) + +![DAVx⁵ logo](app/src/main/res/mipmap-xxxhdpi/ic_launcher.png) + + +DAVx⁵ +======== + +Please see the [DAVx⁵ Web site](https://www.davx5.com) for +comprehensive information about DAVx⁵, including a list of services it has been tested with. + +DAVx⁵ is licensed under the [GPLv3 License](LICENSE). + +News and updates: + +* [@davx5app@fosstodon.org](https://fosstodon.org/@davx5app) on Mastodon + +**Help, feature requests, bug reports: [DAVx⁵ discussions](https://github.com/bitfireAT/davx5-ose/discussions)** + +Parts of DAVx⁵ have been outsourced into these libraries: + +* [cert4android](https://github.com/bitfireAT/cert4android) – custom certificate management +* [dav4jvm](https://github.com/bitfireAT/dav4jvm) – WebDAV/CalDav/CardDAV framework +* [synctools](https://github.com/bitfireAT/synctools) – iCalendar/vCard/Tasks processing and content provider access + +**If you want to support DAVx⁵, please consider [donating to DAVx⁵](https://www.davx5.com/donate) +or [purchasing it](https://www.davx5.com/download).** + + +USED THIRD-PARTY LIBRARIES +========================== + +The most important libraries which are used by DAVx⁵ (alphabetically): + +* [dnsjava](https://github.com/dnsjava/dnsjava) – [BSD License](https://github.com/dnsjava/dnsjava/blob/master/LICENSE) +* [ez-vcard](https://github.com/mangstadt/ez-vcard) – [New BSD License](https://github.com/mangstadt/ez-vcard/blob/master/LICENSE) +* [iCal4j](https://github.com/ical4j/ical4j) – [New BSD License](https://github.com/ical4j/ical4j/blob/develop/LICENSE.txt) +* [okhttp](https://square.github.io/okhttp) – [Apache License, Version 2.0](https://square.github.io/okhttp/#license) + +See _About / Libraries_ in the app for all used libraries and their licenses. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..7161044 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +## Reporting a Vulnerability + +Please report security vulnerabilities using our [secure support form](https://www.davx5.com/support) or via email to support-en@davx5.com. diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..6eeec87 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,2 @@ +build +target diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..45fd005 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,227 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.hilt) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.ksp) + + alias(libs.plugins.mikepenz.aboutLibraries.android) +} + +// Android configuration +android { + compileSdk = 36 + + defaultConfig { + applicationId = "at.bitfire.davdroid" + + versionCode = 405050004 + versionName = "4.5.5" + + base.archivesName = "davx5-ose-$versionName" + + minSdk = 24 // Android 7.0 + targetSdk = 36 // Android 16 + + buildConfigField("boolean", "customCertsUI", "true") + + testInstrumentationRunner = "at.bitfire.davdroid.HiltTestRunner" + } + + java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } + } + + compileOptions { + // required for + // - dnsjava 3.x: java.nio.file.Path + // - ical4android: time API + isCoreLibraryDesugaringEnabled = true + } + + buildFeatures { + buildConfig = true + compose = true + } + + // Java namespace for our classes (not to be confused with Android package ID) + namespace = "at.bitfire.davdroid" + + flavorDimensions += "distribution" + productFlavors { + create("ose") { + dimension = "distribution" + versionNameSuffix = "-ose" + } + } + + sourceSets { + getByName("androidTest") { + assets.srcDir("$projectDir/schemas") + } + } + + signingConfigs { + create("bitfire") { + storeFile = file(System.getenv("ANDROID_KEYSTORE") ?: "/dev/null") + storePassword = System.getenv("ANDROID_KEYSTORE_PASSWORD") + keyAlias = System.getenv("ANDROID_KEY_ALIAS") + keyPassword = System.getenv("ANDROID_KEY_PASSWORD") + } + } + + buildTypes { + getByName("release") { + isMinifyEnabled = true + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules-release.pro") + + isShrinkResources = true + + signingConfig = signingConfigs.findByName("bitfire") + } + } + + lint { + disable += arrayOf("GoogleAppIndexingWarning", "ImpliedQuantity", "MissingQuantity", "MissingTranslation", "ExtraTranslation", "RtlEnabled", "RtlHardcoded", "Typos") + } + + androidResources { + generateLocaleConfig = true + } + + packaging { + resources { + // multiple (test) dependencies have LICENSE files at same location + merges += arrayOf("META-INF/LICENSE*") + } + } + + @Suppress("UnstableApiUsage") + testOptions { + managedDevices { + localDevices { + create("virtual") { + device = "Pixel 3" + // TBD: API level 35 and higher causes network tests to fail sometimes, see https://github.com/bitfireAT/davx5-ose/issues/1525 + // Suspected reason: https://developer.android.com/about/versions/15/behavior-changes-all#background-network-access + apiLevel = 34 + systemImageSource = "aosp-atd" + } + } + } + } +} + +ksp { + arg("room.schemaLocation", "$projectDir/schemas") +} + +aboutLibraries { + export { + // exclude timestamps for reproducible builds [https://github.com/bitfireAT/davx5-ose/issues/994] + excludeFields.add("generated") + } +} + +dependencies { + // core + implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines) + coreLibraryDesugaring(libs.android.desugaring) + + // Hilt + implementation(libs.hilt.android.base) + ksp(libs.androidx.hilt.compiler) + ksp(libs.hilt.android.compiler) + + // support libs + implementation(libs.androidx.activityCompose) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.browser) + implementation(libs.androidx.core) + implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.androidx.hilt.work) + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.androidx.lifecycle.viewmodel.base) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.paging) + implementation(libs.androidx.paging.compose) + implementation(libs.androidx.preference) + implementation(libs.androidx.security) + implementation(libs.androidx.work.base) + + // Jetpack Compose + implementation(libs.compose.accompanist.permissions) + implementation(platform(libs.compose.bom)) + implementation(libs.compose.material3) + implementation(libs.compose.materialIconsExtended) + debugImplementation(libs.compose.ui.tooling) + implementation(libs.compose.ui.toolingPreview) + + // Glance Widgets + implementation(libs.glance.base) + implementation(libs.glance.material) + + // Jetpack Room + implementation(libs.room.runtime) + implementation(libs.room.base) + implementation(libs.room.paging) + ksp(libs.room.compiler) + + // own libraries + implementation(libs.bitfire.cert4android) + implementation(libs.bitfire.dav4jvm) { + exclude(group="junit") + exclude(group="org.ogce", module="xpp3") // Android has its own XmlPullParser implementation + } + implementation(libs.bitfire.synctools) { + exclude(group="androidx.test") // synctools declares test rules, but we don't want them in non-test code + exclude(group = "junit") + } + + // third-party libs + @Suppress("RedundantSuppression") + implementation(libs.dnsjava) + implementation(libs.guava) + implementation(libs.mikepenz.aboutLibraries.m3) + implementation(libs.okhttp.base) + implementation(libs.okhttp.brotli) + implementation(libs.okhttp.logging) + implementation(libs.openid.appauth) + implementation(libs.unifiedpush) { + // UnifiedPush connector seems to be using a workaround by importing this library. + // Will be removed after https://github.com/tink-crypto/tink-java-apps/pull/5 is merged. + // See: https://codeberg.org/UnifiedPush/android-connector/src/commit/28cb0d622ed0a972996041ab9cc85b701abc48c6/connector/build.gradle#L56-L59 + exclude(group = "com.google.crypto.tink", module = "tink") + } + implementation(libs.unifiedpush.fcm) + + // force some versions for compatibility with our minSdk level (see version catalog for details) + implementation(libs.commons.codec) + implementation(libs.commons.lang) + + // for tests + androidTestImplementation(libs.androidx.arch.core.testing) + androidTestImplementation(libs.androidx.test.core) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.rules) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.work.testing) + androidTestImplementation(libs.hilt.android.testing) + androidTestImplementation(libs.junit) + androidTestImplementation(libs.kotlinx.coroutines.test) + androidTestImplementation(libs.mockk.android) + androidTestImplementation(libs.okhttp.mockwebserver) + androidTestImplementation(libs.room.testing) + + testImplementation(libs.bitfire.dav4jvm) + testImplementation(libs.junit) + testImplementation(libs.mockk) + testImplementation(libs.okhttp.mockwebserver) +} diff --git a/app/lint.xml b/app/lint.xml new file mode 100644 index 0000000..abd3b91 --- /dev/null +++ b/app/lint.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/app/proguard-rules-release.pro b/app/proguard-rules-release.pro new file mode 100644 index 0000000..71fc87f --- /dev/null +++ b/app/proguard-rules-release.pro @@ -0,0 +1,31 @@ + +# R8 usage for DAVx⁵: +# shrinking yes (only in release builds) +# optimization yes (on by R8 defaults) +# full-mode no (see gradle.properties) +# obfuscation no (open-source) + +-dontobfuscate +-printusage build/reports/r8-usage.txt + +# keep rules +-keep class at.bitfire.** { *; } # all DAVx5 code is required +-keep class org.xmlpull.** { *; } + +# Additional rules which are now required since missing classes can't be ignored in R8 anymore. +# [https://developer.android.com/build/releases/past-releases/agp-7-0-0-release-notes#r8-missing-class-warning] +-dontwarn org.xmlpull.** + +# dnsjava +-dontwarn com.sun.jna.** +-dontwarn lombok.** +-dontwarn javax.naming.NamingException +-dontwarn javax.naming.directory.** +-dontwarn sun.net.spi.nameservice.NameService +-dontwarn sun.net.spi.nameservice.NameServiceDescriptor +-dontwarn org.xbill.DNS.spi.DnsjavaInetAddressResolverProvider + +# okhttp +# https://github.com/bitfireAT/davx5/issues/711 / https://github.com/square/okhttp/issues/8574 +-keep class okhttp3.internal.idn.IdnaMappingTable { *; } +-keep class okhttp3.internal.idn.IdnaMappingTableInstanceKt{ *; } diff --git a/app/schemas/at.bitfire.davdroid.db.AppDatabase/10.json b/app/schemas/at.bitfire.davdroid.db.AppDatabase/10.json new file mode 100644 index 0000000..f1cee14 --- /dev/null +++ b/app/schemas/at.bitfire.davdroid.db.AppDatabase/10.json @@ -0,0 +1,398 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "6fcabe50cbd00a4215dbe536a565dd2a", + "entities": [ + { + "tableName": "service", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "principal", + "columnName": "principal", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_service_accountName_type", + "unique": true, + "columnNames": [ + "accountName", + "type" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "homeset", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "personal", + "columnName": "personal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privBind", + "columnName": "privBind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_homeset_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "collection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `owner` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homeSetId", + "columnName": "homeSetId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privWriteContent", + "columnName": "privWriteContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privUnbind", + "columnName": "privUnbind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "forceReadOnly", + "columnName": "forceReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "owner", + "columnName": "owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timezone", + "columnName": "timezone", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "supportsVEVENT", + "columnName": "supportsVEVENT", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVTODO", + "columnName": "supportsVTODO", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVJOURNAL", + "columnName": "supportsVJOURNAL", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sync", + "columnName": "sync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_collection_serviceId_type", + "unique": false, + "columnNames": [ + "serviceId", + "type" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)" + }, + { + "name": "index_collection_homeSetId_type", + "unique": false, + "columnNames": [ + "homeSetId", + "type" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)" + }, + { + "name": "index_collection_url", + "unique": false, + "columnNames": [ + "url" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "homeset", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "homeSetId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "syncstats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "collectionId", + "columnName": "collectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authority", + "columnName": "authority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "lastSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_syncstats_collectionId_authority", + "unique": true, + "columnNames": [ + "collectionId", + "authority" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)" + } + ], + "foreignKeys": [ + { + "table": "collection", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "collectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_mount", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "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, '6fcabe50cbd00a4215dbe536a565dd2a')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/at.bitfire.davdroid.db.AppDatabase/11.json b/app/schemas/at.bitfire.davdroid.db.AppDatabase/11.json new file mode 100644 index 0000000..77f44ee --- /dev/null +++ b/app/schemas/at.bitfire.davdroid.db.AppDatabase/11.json @@ -0,0 +1,536 @@ +{ + "formatVersion": 1, + "database": { + "version": 11, + "identityHash": "223aa7f0fd53730921ca212a663585d8", + "entities": [ + { + "tableName": "service", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "principal", + "columnName": "principal", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_service_accountName_type", + "unique": true, + "columnNames": [ + "accountName", + "type" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "homeset", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "personal", + "columnName": "personal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privBind", + "columnName": "privBind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_homeset_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "collection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `owner` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homeSetId", + "columnName": "homeSetId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privWriteContent", + "columnName": "privWriteContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privUnbind", + "columnName": "privUnbind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "forceReadOnly", + "columnName": "forceReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "owner", + "columnName": "owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timezone", + "columnName": "timezone", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "supportsVEVENT", + "columnName": "supportsVEVENT", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVTODO", + "columnName": "supportsVTODO", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVJOURNAL", + "columnName": "supportsVJOURNAL", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sync", + "columnName": "sync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_collection_serviceId_type", + "unique": false, + "columnNames": [ + "serviceId", + "type" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)" + }, + { + "name": "index_collection_homeSetId_type", + "unique": false, + "columnNames": [ + "homeSetId", + "type" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)" + }, + { + "name": "index_collection_url", + "unique": false, + "columnNames": [ + "url" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "homeset", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "homeSetId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "syncstats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "collectionId", + "columnName": "collectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authority", + "columnName": "authority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "lastSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_syncstats_collectionId_authority", + "unique": true, + "columnNames": [ + "collectionId", + "authority" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)" + } + ], + "foreignKeys": [ + { + "table": "collection", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "collectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_document", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mountId` INTEGER NOT NULL, `parentId` INTEGER, `name` TEXT NOT NULL, `isDirectory` INTEGER NOT NULL, `displayName` TEXT, `mimeType` TEXT, `eTag` TEXT, `lastModified` INTEGER, `size` INTEGER, `mayBind` INTEGER, `mayUnbind` INTEGER, `mayWriteContent` INTEGER, `quotaAvailable` INTEGER, `quotaUsed` INTEGER, FOREIGN KEY(`mountId`) REFERENCES `webdav_mount`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`parentId`) REFERENCES `webdav_document`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mountId", + "columnName": "mountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDirectory", + "columnName": "isDirectory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "eTag", + "columnName": "eTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastModified", + "columnName": "lastModified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayBind", + "columnName": "mayBind", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayUnbind", + "columnName": "mayUnbind", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayWriteContent", + "columnName": "mayWriteContent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quotaAvailable", + "columnName": "quotaAvailable", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quotaUsed", + "columnName": "quotaUsed", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_webdav_document_mountId_parentId_name", + "unique": true, + "columnNames": [ + "mountId", + "parentId", + "name" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_webdav_document_mountId_parentId_name` ON `${TABLE_NAME}` (`mountId`, `parentId`, `name`)" + } + ], + "foreignKeys": [ + { + "table": "webdav_mount", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "mountId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "webdav_document", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_mount", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "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, '223aa7f0fd53730921ca212a663585d8')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/at.bitfire.davdroid.db.AppDatabase/12.json b/app/schemas/at.bitfire.davdroid.db.AppDatabase/12.json new file mode 100644 index 0000000..ea96701 --- /dev/null +++ b/app/schemas/at.bitfire.davdroid.db.AppDatabase/12.json @@ -0,0 +1,615 @@ +{ + "formatVersion": 1, + "database": { + "version": 12, + "identityHash": "67fafceecee2d97cac6a62d46fa2c3e2", + "entities": [ + { + "tableName": "service", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "principal", + "columnName": "principal", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_service_accountName_type", + "unique": true, + "columnNames": [ + "accountName", + "type" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "homeset", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "personal", + "columnName": "personal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privBind", + "columnName": "privBind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_homeset_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "collection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `ownerId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`ownerId`) REFERENCES `principal`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homeSetId", + "columnName": "homeSetId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privWriteContent", + "columnName": "privWriteContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privUnbind", + "columnName": "privUnbind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "forceReadOnly", + "columnName": "forceReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timezone", + "columnName": "timezone", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "supportsVEVENT", + "columnName": "supportsVEVENT", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVTODO", + "columnName": "supportsVTODO", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVJOURNAL", + "columnName": "supportsVJOURNAL", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sync", + "columnName": "sync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_collection_serviceId_type", + "unique": false, + "columnNames": [ + "serviceId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)" + }, + { + "name": "index_collection_homeSetId_type", + "unique": false, + "columnNames": [ + "homeSetId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)" + }, + { + "name": "index_collection_url", + "unique": false, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "homeset", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "homeSetId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "principal", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "ownerId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "principal", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `url` TEXT NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_principal_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_principal_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "syncstats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "collectionId", + "columnName": "collectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authority", + "columnName": "authority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "lastSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_syncstats_collectionId_authority", + "unique": true, + "columnNames": [ + "collectionId", + "authority" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)" + } + ], + "foreignKeys": [ + { + "table": "collection", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "collectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_document", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mountId` INTEGER NOT NULL, `parentId` INTEGER, `name` TEXT NOT NULL, `isDirectory` INTEGER NOT NULL, `displayName` TEXT, `mimeType` TEXT, `eTag` TEXT, `lastModified` INTEGER, `size` INTEGER, `mayBind` INTEGER, `mayUnbind` INTEGER, `mayWriteContent` INTEGER, `quotaAvailable` INTEGER, `quotaUsed` INTEGER, FOREIGN KEY(`mountId`) REFERENCES `webdav_mount`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`parentId`) REFERENCES `webdav_document`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mountId", + "columnName": "mountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDirectory", + "columnName": "isDirectory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "eTag", + "columnName": "eTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastModified", + "columnName": "lastModified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayBind", + "columnName": "mayBind", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayUnbind", + "columnName": "mayUnbind", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayWriteContent", + "columnName": "mayWriteContent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quotaAvailable", + "columnName": "quotaAvailable", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quotaUsed", + "columnName": "quotaUsed", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_webdav_document_mountId_parentId_name", + "unique": true, + "columnNames": [ + "mountId", + "parentId", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_webdav_document_mountId_parentId_name` ON `${TABLE_NAME}` (`mountId`, `parentId`, `name`)" + } + ], + "foreignKeys": [ + { + "table": "webdav_mount", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "mountId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "webdav_document", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_mount", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "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, '67fafceecee2d97cac6a62d46fa2c3e2')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/at.bitfire.davdroid.db.AppDatabase/13.json b/app/schemas/at.bitfire.davdroid.db.AppDatabase/13.json new file mode 100644 index 0000000..7d37106 --- /dev/null +++ b/app/schemas/at.bitfire.davdroid.db.AppDatabase/13.json @@ -0,0 +1,640 @@ +{ + "formatVersion": 1, + "database": { + "version": 13, + "identityHash": "0a6a9705ff471acd766ab96e3edf8ac3", + "entities": [ + { + "tableName": "service", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "principal", + "columnName": "principal", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_service_accountName_type", + "unique": true, + "columnNames": [ + "accountName", + "type" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "homeset", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "personal", + "columnName": "personal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privBind", + "columnName": "privBind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_homeset_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "collection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `ownerId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, `pushTopic` TEXT, `supportsWebPush` INTEGER NOT NULL DEFAULT 0, `pushSubscription` TEXT, `pushSubscriptionCreated` INTEGER, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`ownerId`) REFERENCES `principal`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homeSetId", + "columnName": "homeSetId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privWriteContent", + "columnName": "privWriteContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privUnbind", + "columnName": "privUnbind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "forceReadOnly", + "columnName": "forceReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timezone", + "columnName": "timezone", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "supportsVEVENT", + "columnName": "supportsVEVENT", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVTODO", + "columnName": "supportsVTODO", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVJOURNAL", + "columnName": "supportsVJOURNAL", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sync", + "columnName": "sync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pushTopic", + "columnName": "pushTopic", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "supportsWebPush", + "columnName": "supportsWebPush", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "pushSubscription", + "columnName": "pushSubscription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pushSubscriptionCreated", + "columnName": "pushSubscriptionCreated", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_collection_serviceId_type", + "unique": false, + "columnNames": [ + "serviceId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)" + }, + { + "name": "index_collection_homeSetId_type", + "unique": false, + "columnNames": [ + "homeSetId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)" + }, + { + "name": "index_collection_url", + "unique": false, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "homeset", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "homeSetId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "principal", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "ownerId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "principal", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `url` TEXT NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_principal_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_principal_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "syncstats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "collectionId", + "columnName": "collectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authority", + "columnName": "authority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "lastSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_syncstats_collectionId_authority", + "unique": true, + "columnNames": [ + "collectionId", + "authority" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)" + } + ], + "foreignKeys": [ + { + "table": "collection", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "collectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_document", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mountId` INTEGER NOT NULL, `parentId` INTEGER, `name` TEXT NOT NULL, `isDirectory` INTEGER NOT NULL, `displayName` TEXT, `mimeType` TEXT, `eTag` TEXT, `lastModified` INTEGER, `size` INTEGER, `mayBind` INTEGER, `mayUnbind` INTEGER, `mayWriteContent` INTEGER, `quotaAvailable` INTEGER, `quotaUsed` INTEGER, FOREIGN KEY(`mountId`) REFERENCES `webdav_mount`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`parentId`) REFERENCES `webdav_document`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mountId", + "columnName": "mountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDirectory", + "columnName": "isDirectory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "eTag", + "columnName": "eTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastModified", + "columnName": "lastModified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayBind", + "columnName": "mayBind", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayUnbind", + "columnName": "mayUnbind", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayWriteContent", + "columnName": "mayWriteContent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quotaAvailable", + "columnName": "quotaAvailable", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quotaUsed", + "columnName": "quotaUsed", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_webdav_document_mountId_parentId_name", + "unique": true, + "columnNames": [ + "mountId", + "parentId", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_webdav_document_mountId_parentId_name` ON `${TABLE_NAME}` (`mountId`, `parentId`, `name`)" + } + ], + "foreignKeys": [ + { + "table": "webdav_mount", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "mountId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "webdav_document", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_mount", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "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, '0a6a9705ff471acd766ab96e3edf8ac3')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/at.bitfire.davdroid.db.AppDatabase/14.json b/app/schemas/at.bitfire.davdroid.db.AppDatabase/14.json new file mode 100644 index 0000000..9dc16cd --- /dev/null +++ b/app/schemas/at.bitfire.davdroid.db.AppDatabase/14.json @@ -0,0 +1,669 @@ +{ + "formatVersion": 1, + "database": { + "version": 14, + "identityHash": "9a0eb47f27473eab254db568081a4585", + "entities": [ + { + "tableName": "service", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "principal", + "columnName": "principal", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_service_accountName_type", + "unique": true, + "columnNames": [ + "accountName", + "type" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "homeset", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "personal", + "columnName": "personal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privBind", + "columnName": "privBind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_homeset_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "collection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `ownerId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, `pushTopic` TEXT, `supportsWebPush` INTEGER NOT NULL DEFAULT 0, `pushSubscription` TEXT, `pushSubscriptionCreated` INTEGER, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`ownerId`) REFERENCES `principal`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homeSetId", + "columnName": "homeSetId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privWriteContent", + "columnName": "privWriteContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privUnbind", + "columnName": "privUnbind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "forceReadOnly", + "columnName": "forceReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timezone", + "columnName": "timezone", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "supportsVEVENT", + "columnName": "supportsVEVENT", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVTODO", + "columnName": "supportsVTODO", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVJOURNAL", + "columnName": "supportsVJOURNAL", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sync", + "columnName": "sync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pushTopic", + "columnName": "pushTopic", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "supportsWebPush", + "columnName": "supportsWebPush", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "pushSubscription", + "columnName": "pushSubscription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pushSubscriptionCreated", + "columnName": "pushSubscriptionCreated", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_collection_serviceId_type", + "unique": false, + "columnNames": [ + "serviceId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)" + }, + { + "name": "index_collection_homeSetId_type", + "unique": false, + "columnNames": [ + "homeSetId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)" + }, + { + "name": "index_collection_ownerId_type", + "unique": false, + "columnNames": [ + "ownerId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_ownerId_type` ON `${TABLE_NAME}` (`ownerId`, `type`)" + }, + { + "name": "index_collection_pushTopic_type", + "unique": false, + "columnNames": [ + "pushTopic", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_pushTopic_type` ON `${TABLE_NAME}` (`pushTopic`, `type`)" + }, + { + "name": "index_collection_url", + "unique": false, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "homeset", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "homeSetId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "principal", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "ownerId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "principal", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `url` TEXT NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_principal_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_principal_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "syncstats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "collectionId", + "columnName": "collectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authority", + "columnName": "authority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "lastSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_syncstats_collectionId_authority", + "unique": true, + "columnNames": [ + "collectionId", + "authority" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)" + } + ], + "foreignKeys": [ + { + "table": "collection", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "collectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_document", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mountId` INTEGER NOT NULL, `parentId` INTEGER, `name` TEXT NOT NULL, `isDirectory` INTEGER NOT NULL, `displayName` TEXT, `mimeType` TEXT, `eTag` TEXT, `lastModified` INTEGER, `size` INTEGER, `mayBind` INTEGER, `mayUnbind` INTEGER, `mayWriteContent` INTEGER, `quotaAvailable` INTEGER, `quotaUsed` INTEGER, FOREIGN KEY(`mountId`) REFERENCES `webdav_mount`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`parentId`) REFERENCES `webdav_document`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mountId", + "columnName": "mountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDirectory", + "columnName": "isDirectory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "eTag", + "columnName": "eTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastModified", + "columnName": "lastModified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayBind", + "columnName": "mayBind", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayUnbind", + "columnName": "mayUnbind", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayWriteContent", + "columnName": "mayWriteContent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quotaAvailable", + "columnName": "quotaAvailable", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quotaUsed", + "columnName": "quotaUsed", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_webdav_document_mountId_parentId_name", + "unique": true, + "columnNames": [ + "mountId", + "parentId", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_webdav_document_mountId_parentId_name` ON `${TABLE_NAME}` (`mountId`, `parentId`, `name`)" + }, + { + "name": "index_webdav_document_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_webdav_document_parentId` ON `${TABLE_NAME}` (`parentId`)" + } + ], + "foreignKeys": [ + { + "table": "webdav_mount", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "mountId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "webdav_document", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_mount", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "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, '9a0eb47f27473eab254db568081a4585')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/at.bitfire.davdroid.db.AppDatabase/15.json b/app/schemas/at.bitfire.davdroid.db.AppDatabase/15.json new file mode 100644 index 0000000..17e8d8e --- /dev/null +++ b/app/schemas/at.bitfire.davdroid.db.AppDatabase/15.json @@ -0,0 +1,675 @@ +{ + "formatVersion": 1, + "database": { + "version": 15, + "identityHash": "ab1cb6057d8e050f6648bea46ae0943d", + "entities": [ + { + "tableName": "service", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "principal", + "columnName": "principal", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_service_accountName_type", + "unique": true, + "columnNames": [ + "accountName", + "type" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "homeset", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "personal", + "columnName": "personal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privBind", + "columnName": "privBind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_homeset_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "collection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `ownerId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, `pushTopic` TEXT, `supportsWebPush` INTEGER NOT NULL DEFAULT 0, `pushSubscription` TEXT, `pushSubscriptionExpires` INTEGER, `pushSubscriptionCreated` INTEGER, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`ownerId`) REFERENCES `principal`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homeSetId", + "columnName": "homeSetId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privWriteContent", + "columnName": "privWriteContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privUnbind", + "columnName": "privUnbind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "forceReadOnly", + "columnName": "forceReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timezone", + "columnName": "timezone", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "supportsVEVENT", + "columnName": "supportsVEVENT", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVTODO", + "columnName": "supportsVTODO", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVJOURNAL", + "columnName": "supportsVJOURNAL", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sync", + "columnName": "sync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pushTopic", + "columnName": "pushTopic", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "supportsWebPush", + "columnName": "supportsWebPush", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "pushSubscription", + "columnName": "pushSubscription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pushSubscriptionExpires", + "columnName": "pushSubscriptionExpires", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pushSubscriptionCreated", + "columnName": "pushSubscriptionCreated", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_collection_serviceId_type", + "unique": false, + "columnNames": [ + "serviceId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)" + }, + { + "name": "index_collection_homeSetId_type", + "unique": false, + "columnNames": [ + "homeSetId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)" + }, + { + "name": "index_collection_ownerId_type", + "unique": false, + "columnNames": [ + "ownerId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_ownerId_type` ON `${TABLE_NAME}` (`ownerId`, `type`)" + }, + { + "name": "index_collection_pushTopic_type", + "unique": false, + "columnNames": [ + "pushTopic", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_pushTopic_type` ON `${TABLE_NAME}` (`pushTopic`, `type`)" + }, + { + "name": "index_collection_url", + "unique": false, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "homeset", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "homeSetId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "principal", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "ownerId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "principal", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `url` TEXT NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_principal_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_principal_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "syncstats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "collectionId", + "columnName": "collectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authority", + "columnName": "authority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "lastSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_syncstats_collectionId_authority", + "unique": true, + "columnNames": [ + "collectionId", + "authority" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)" + } + ], + "foreignKeys": [ + { + "table": "collection", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "collectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_document", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mountId` INTEGER NOT NULL, `parentId` INTEGER, `name` TEXT NOT NULL, `isDirectory` INTEGER NOT NULL, `displayName` TEXT, `mimeType` TEXT, `eTag` TEXT, `lastModified` INTEGER, `size` INTEGER, `mayBind` INTEGER, `mayUnbind` INTEGER, `mayWriteContent` INTEGER, `quotaAvailable` INTEGER, `quotaUsed` INTEGER, FOREIGN KEY(`mountId`) REFERENCES `webdav_mount`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`parentId`) REFERENCES `webdav_document`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mountId", + "columnName": "mountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDirectory", + "columnName": "isDirectory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "eTag", + "columnName": "eTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastModified", + "columnName": "lastModified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayBind", + "columnName": "mayBind", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayUnbind", + "columnName": "mayUnbind", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayWriteContent", + "columnName": "mayWriteContent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quotaAvailable", + "columnName": "quotaAvailable", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quotaUsed", + "columnName": "quotaUsed", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_webdav_document_mountId_parentId_name", + "unique": true, + "columnNames": [ + "mountId", + "parentId", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_webdav_document_mountId_parentId_name` ON `${TABLE_NAME}` (`mountId`, `parentId`, `name`)" + }, + { + "name": "index_webdav_document_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_webdav_document_parentId` ON `${TABLE_NAME}` (`parentId`)" + } + ], + "foreignKeys": [ + { + "table": "webdav_mount", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "mountId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "webdav_document", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_mount", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "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, 'ab1cb6057d8e050f6648bea46ae0943d')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/at.bitfire.davdroid.db.AppDatabase/16.json b/app/schemas/at.bitfire.davdroid.db.AppDatabase/16.json new file mode 100644 index 0000000..259bc4e --- /dev/null +++ b/app/schemas/at.bitfire.davdroid.db.AppDatabase/16.json @@ -0,0 +1,675 @@ +{ + "formatVersion": 1, + "database": { + "version": 16, + "identityHash": "2ff7560d957e03a78b4b7de88aa9593b", + "entities": [ + { + "tableName": "service", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "principal", + "columnName": "principal", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_service_accountName_type", + "unique": true, + "columnNames": [ + "accountName", + "type" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "homeset", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "personal", + "columnName": "personal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privBind", + "columnName": "privBind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_homeset_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "collection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `ownerId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `color` INTEGER, `timezoneId` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, `pushTopic` TEXT, `supportsWebPush` INTEGER NOT NULL DEFAULT 0, `pushSubscription` TEXT, `pushSubscriptionExpires` INTEGER, `pushSubscriptionCreated` INTEGER, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`ownerId`) REFERENCES `principal`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homeSetId", + "columnName": "homeSetId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privWriteContent", + "columnName": "privWriteContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privUnbind", + "columnName": "privUnbind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "forceReadOnly", + "columnName": "forceReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timezoneId", + "columnName": "timezoneId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "supportsVEVENT", + "columnName": "supportsVEVENT", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVTODO", + "columnName": "supportsVTODO", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVJOURNAL", + "columnName": "supportsVJOURNAL", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sync", + "columnName": "sync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pushTopic", + "columnName": "pushTopic", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "supportsWebPush", + "columnName": "supportsWebPush", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "pushSubscription", + "columnName": "pushSubscription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pushSubscriptionExpires", + "columnName": "pushSubscriptionExpires", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pushSubscriptionCreated", + "columnName": "pushSubscriptionCreated", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_collection_serviceId_type", + "unique": false, + "columnNames": [ + "serviceId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)" + }, + { + "name": "index_collection_homeSetId_type", + "unique": false, + "columnNames": [ + "homeSetId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)" + }, + { + "name": "index_collection_ownerId_type", + "unique": false, + "columnNames": [ + "ownerId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_ownerId_type` ON `${TABLE_NAME}` (`ownerId`, `type`)" + }, + { + "name": "index_collection_pushTopic_type", + "unique": false, + "columnNames": [ + "pushTopic", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_pushTopic_type` ON `${TABLE_NAME}` (`pushTopic`, `type`)" + }, + { + "name": "index_collection_url", + "unique": false, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "homeset", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "homeSetId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "principal", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "ownerId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "principal", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `url` TEXT NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_principal_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_principal_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "syncstats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "collectionId", + "columnName": "collectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authority", + "columnName": "authority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "lastSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_syncstats_collectionId_authority", + "unique": true, + "columnNames": [ + "collectionId", + "authority" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)" + } + ], + "foreignKeys": [ + { + "table": "collection", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "collectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_document", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mountId` INTEGER NOT NULL, `parentId` INTEGER, `name` TEXT NOT NULL, `isDirectory` INTEGER NOT NULL, `displayName` TEXT, `mimeType` TEXT, `eTag` TEXT, `lastModified` INTEGER, `size` INTEGER, `mayBind` INTEGER, `mayUnbind` INTEGER, `mayWriteContent` INTEGER, `quotaAvailable` INTEGER, `quotaUsed` INTEGER, FOREIGN KEY(`mountId`) REFERENCES `webdav_mount`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`parentId`) REFERENCES `webdav_document`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mountId", + "columnName": "mountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDirectory", + "columnName": "isDirectory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "eTag", + "columnName": "eTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastModified", + "columnName": "lastModified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayBind", + "columnName": "mayBind", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayUnbind", + "columnName": "mayUnbind", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mayWriteContent", + "columnName": "mayWriteContent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quotaAvailable", + "columnName": "quotaAvailable", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quotaUsed", + "columnName": "quotaUsed", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_webdav_document_mountId_parentId_name", + "unique": true, + "columnNames": [ + "mountId", + "parentId", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_webdav_document_mountId_parentId_name` ON `${TABLE_NAME}` (`mountId`, `parentId`, `name`)" + }, + { + "name": "index_webdav_document_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_webdav_document_parentId` ON `${TABLE_NAME}` (`parentId`)" + } + ], + "foreignKeys": [ + { + "table": "webdav_mount", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "mountId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "webdav_document", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_mount", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "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, '2ff7560d957e03a78b4b7de88aa9593b')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/at.bitfire.davdroid.db.AppDatabase/17.json b/app/schemas/at.bitfire.davdroid.db.AppDatabase/17.json new file mode 100644 index 0000000..f1a3e67 --- /dev/null +++ b/app/schemas/at.bitfire.davdroid.db.AppDatabase/17.json @@ -0,0 +1,648 @@ +{ + "formatVersion": 1, + "database": { + "version": 17, + "identityHash": "cd15d368408570cc2e57252816869de2", + "entities": [ + { + "tableName": "service", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "principal", + "columnName": "principal", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_service_accountName_type", + "unique": true, + "columnNames": [ + "accountName", + "type" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)" + } + ] + }, + { + "tableName": "homeset", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "personal", + "columnName": "personal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privBind", + "columnName": "privBind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_homeset_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "collection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `ownerId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `color` INTEGER, `timezoneId` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, `pushTopic` TEXT, `supportsWebPush` INTEGER NOT NULL DEFAULT 0, `pushVapidKey` TEXT, `pushSubscription` TEXT, `pushSubscriptionExpires` INTEGER, `pushSubscriptionCreated` INTEGER, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`ownerId`) REFERENCES `principal`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homeSetId", + "columnName": "homeSetId", + "affinity": "INTEGER" + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privWriteContent", + "columnName": "privWriteContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privUnbind", + "columnName": "privUnbind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "forceReadOnly", + "columnName": "forceReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER" + }, + { + "fieldPath": "timezoneId", + "columnName": "timezoneId", + "affinity": "TEXT" + }, + { + "fieldPath": "supportsVEVENT", + "columnName": "supportsVEVENT", + "affinity": "INTEGER" + }, + { + "fieldPath": "supportsVTODO", + "columnName": "supportsVTODO", + "affinity": "INTEGER" + }, + { + "fieldPath": "supportsVJOURNAL", + "columnName": "supportsVJOURNAL", + "affinity": "INTEGER" + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT" + }, + { + "fieldPath": "sync", + "columnName": "sync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pushTopic", + "columnName": "pushTopic", + "affinity": "TEXT" + }, + { + "fieldPath": "supportsWebPush", + "columnName": "supportsWebPush", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "pushVapidKey", + "columnName": "pushVapidKey", + "affinity": "TEXT" + }, + { + "fieldPath": "pushSubscription", + "columnName": "pushSubscription", + "affinity": "TEXT" + }, + { + "fieldPath": "pushSubscriptionExpires", + "columnName": "pushSubscriptionExpires", + "affinity": "INTEGER" + }, + { + "fieldPath": "pushSubscriptionCreated", + "columnName": "pushSubscriptionCreated", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_collection_serviceId_type", + "unique": false, + "columnNames": [ + "serviceId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)" + }, + { + "name": "index_collection_homeSetId_type", + "unique": false, + "columnNames": [ + "homeSetId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)" + }, + { + "name": "index_collection_ownerId_type", + "unique": false, + "columnNames": [ + "ownerId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_ownerId_type` ON `${TABLE_NAME}` (`ownerId`, `type`)" + }, + { + "name": "index_collection_pushTopic_type", + "unique": false, + "columnNames": [ + "pushTopic", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_pushTopic_type` ON `${TABLE_NAME}` (`pushTopic`, `type`)" + }, + { + "name": "index_collection_url", + "unique": false, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "homeset", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "homeSetId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "principal", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "ownerId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "principal", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `url` TEXT NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_principal_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_principal_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "syncstats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "collectionId", + "columnName": "collectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authority", + "columnName": "authority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "lastSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_syncstats_collectionId_authority", + "unique": true, + "columnNames": [ + "collectionId", + "authority" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)" + } + ], + "foreignKeys": [ + { + "table": "collection", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "collectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_document", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mountId` INTEGER NOT NULL, `parentId` INTEGER, `name` TEXT NOT NULL, `isDirectory` INTEGER NOT NULL, `displayName` TEXT, `mimeType` TEXT, `eTag` TEXT, `lastModified` INTEGER, `size` INTEGER, `mayBind` INTEGER, `mayUnbind` INTEGER, `mayWriteContent` INTEGER, `quotaAvailable` INTEGER, `quotaUsed` INTEGER, FOREIGN KEY(`mountId`) REFERENCES `webdav_mount`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`parentId`) REFERENCES `webdav_document`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mountId", + "columnName": "mountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDirectory", + "columnName": "isDirectory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT" + }, + { + "fieldPath": "eTag", + "columnName": "eTag", + "affinity": "TEXT" + }, + { + "fieldPath": "lastModified", + "columnName": "lastModified", + "affinity": "INTEGER" + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER" + }, + { + "fieldPath": "mayBind", + "columnName": "mayBind", + "affinity": "INTEGER" + }, + { + "fieldPath": "mayUnbind", + "columnName": "mayUnbind", + "affinity": "INTEGER" + }, + { + "fieldPath": "mayWriteContent", + "columnName": "mayWriteContent", + "affinity": "INTEGER" + }, + { + "fieldPath": "quotaAvailable", + "columnName": "quotaAvailable", + "affinity": "INTEGER" + }, + { + "fieldPath": "quotaUsed", + "columnName": "quotaUsed", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_webdav_document_mountId_parentId_name", + "unique": true, + "columnNames": [ + "mountId", + "parentId", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_webdav_document_mountId_parentId_name` ON `${TABLE_NAME}` (`mountId`, `parentId`, `name`)" + }, + { + "name": "index_webdav_document_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_webdav_document_parentId` ON `${TABLE_NAME}` (`parentId`)" + } + ], + "foreignKeys": [ + { + "table": "webdav_mount", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "mountId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "webdav_document", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_mount", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "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, 'cd15d368408570cc2e57252816869de2')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/at.bitfire.davdroid.db.AppDatabase/18.json b/app/schemas/at.bitfire.davdroid.db.AppDatabase/18.json new file mode 100644 index 0000000..2246bcf --- /dev/null +++ b/app/schemas/at.bitfire.davdroid.db.AppDatabase/18.json @@ -0,0 +1,648 @@ +{ + "formatVersion": 1, + "database": { + "version": 18, + "identityHash": "6a0f7e1553e1f621ae7913ea14370fd0", + "entities": [ + { + "tableName": "service", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "principal", + "columnName": "principal", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_service_accountName_type", + "unique": true, + "columnNames": [ + "accountName", + "type" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)" + } + ] + }, + { + "tableName": "homeset", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "personal", + "columnName": "personal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privBind", + "columnName": "privBind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_homeset_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "collection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `ownerId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `color` INTEGER, `timezoneId` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, `pushTopic` TEXT, `supportsWebPush` INTEGER NOT NULL DEFAULT 0, `pushVapidKey` TEXT, `pushSubscription` TEXT, `pushSubscriptionExpires` INTEGER, `pushSubscriptionCreated` INTEGER, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`ownerId`) REFERENCES `principal`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homeSetId", + "columnName": "homeSetId", + "affinity": "INTEGER" + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privWriteContent", + "columnName": "privWriteContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privUnbind", + "columnName": "privUnbind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "forceReadOnly", + "columnName": "forceReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER" + }, + { + "fieldPath": "timezoneId", + "columnName": "timezoneId", + "affinity": "TEXT" + }, + { + "fieldPath": "supportsVEVENT", + "columnName": "supportsVEVENT", + "affinity": "INTEGER" + }, + { + "fieldPath": "supportsVTODO", + "columnName": "supportsVTODO", + "affinity": "INTEGER" + }, + { + "fieldPath": "supportsVJOURNAL", + "columnName": "supportsVJOURNAL", + "affinity": "INTEGER" + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT" + }, + { + "fieldPath": "sync", + "columnName": "sync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pushTopic", + "columnName": "pushTopic", + "affinity": "TEXT" + }, + { + "fieldPath": "supportsWebPush", + "columnName": "supportsWebPush", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "pushVapidKey", + "columnName": "pushVapidKey", + "affinity": "TEXT" + }, + { + "fieldPath": "pushSubscription", + "columnName": "pushSubscription", + "affinity": "TEXT" + }, + { + "fieldPath": "pushSubscriptionExpires", + "columnName": "pushSubscriptionExpires", + "affinity": "INTEGER" + }, + { + "fieldPath": "pushSubscriptionCreated", + "columnName": "pushSubscriptionCreated", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_collection_serviceId_type", + "unique": false, + "columnNames": [ + "serviceId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)" + }, + { + "name": "index_collection_homeSetId_type", + "unique": false, + "columnNames": [ + "homeSetId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)" + }, + { + "name": "index_collection_ownerId_type", + "unique": false, + "columnNames": [ + "ownerId", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_ownerId_type` ON `${TABLE_NAME}` (`ownerId`, `type`)" + }, + { + "name": "index_collection_pushTopic_type", + "unique": false, + "columnNames": [ + "pushTopic", + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_pushTopic_type` ON `${TABLE_NAME}` (`pushTopic`, `type`)" + }, + { + "name": "index_collection_url", + "unique": false, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "homeset", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "homeSetId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "principal", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "ownerId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "principal", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `url` TEXT NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_principal_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_principal_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "syncstats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `dataType` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "collectionId", + "columnName": "collectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dataType", + "columnName": "dataType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "lastSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_syncstats_collectionId_dataType", + "unique": true, + "columnNames": [ + "collectionId", + "dataType" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_dataType` ON `${TABLE_NAME}` (`collectionId`, `dataType`)" + } + ], + "foreignKeys": [ + { + "table": "collection", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "collectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_document", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mountId` INTEGER NOT NULL, `parentId` INTEGER, `name` TEXT NOT NULL, `isDirectory` INTEGER NOT NULL, `displayName` TEXT, `mimeType` TEXT, `eTag` TEXT, `lastModified` INTEGER, `size` INTEGER, `mayBind` INTEGER, `mayUnbind` INTEGER, `mayWriteContent` INTEGER, `quotaAvailable` INTEGER, `quotaUsed` INTEGER, FOREIGN KEY(`mountId`) REFERENCES `webdav_mount`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`parentId`) REFERENCES `webdav_document`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mountId", + "columnName": "mountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDirectory", + "columnName": "isDirectory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT" + }, + { + "fieldPath": "eTag", + "columnName": "eTag", + "affinity": "TEXT" + }, + { + "fieldPath": "lastModified", + "columnName": "lastModified", + "affinity": "INTEGER" + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER" + }, + { + "fieldPath": "mayBind", + "columnName": "mayBind", + "affinity": "INTEGER" + }, + { + "fieldPath": "mayUnbind", + "columnName": "mayUnbind", + "affinity": "INTEGER" + }, + { + "fieldPath": "mayWriteContent", + "columnName": "mayWriteContent", + "affinity": "INTEGER" + }, + { + "fieldPath": "quotaAvailable", + "columnName": "quotaAvailable", + "affinity": "INTEGER" + }, + { + "fieldPath": "quotaUsed", + "columnName": "quotaUsed", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_webdav_document_mountId_parentId_name", + "unique": true, + "columnNames": [ + "mountId", + "parentId", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_webdav_document_mountId_parentId_name` ON `${TABLE_NAME}` (`mountId`, `parentId`, `name`)" + }, + { + "name": "index_webdav_document_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_webdav_document_parentId` ON `${TABLE_NAME}` (`parentId`)" + } + ], + "foreignKeys": [ + { + "table": "webdav_mount", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "mountId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "webdav_document", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "webdav_mount", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "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, '6a0f7e1553e1f621ae7913ea14370fd0')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/at.bitfire.davdroid.db.AppDatabase/8.json b/app/schemas/at.bitfire.davdroid.db.AppDatabase/8.json new file mode 100644 index 0000000..681fb49 --- /dev/null +++ b/app/schemas/at.bitfire.davdroid.db.AppDatabase/8.json @@ -0,0 +1,298 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "b8699ef3cc4c62e8851df4360fb69e00", + "entities": [ + { + "tableName": "service", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "principal", + "columnName": "principal", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_service_accountName_type", + "unique": true, + "columnNames": [ + "accountName", + "type" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "homeset", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "personal", + "columnName": "personal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privBind", + "columnName": "privBind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_homeset_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "collection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `owner` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homeSetId", + "columnName": "homeSetId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privWriteContent", + "columnName": "privWriteContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privUnbind", + "columnName": "privUnbind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "forceReadOnly", + "columnName": "forceReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "owner", + "columnName": "owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timezone", + "columnName": "timezone", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "supportsVEVENT", + "columnName": "supportsVEVENT", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVTODO", + "columnName": "supportsVTODO", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVJOURNAL", + "columnName": "supportsVJOURNAL", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sync", + "columnName": "sync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_collection_serviceId_type", + "unique": false, + "columnNames": [ + "serviceId", + "type" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)" + }, + { + "name": "index_collection_homeSetId_type", + "unique": false, + "columnNames": [ + "homeSetId", + "type" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "homeset", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "homeSetId" + ], + "referencedColumns": [ + "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, 'b8699ef3cc4c62e8851df4360fb69e00')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/at.bitfire.davdroid.db.AppDatabase/9.json b/app/schemas/at.bitfire.davdroid.db.AppDatabase/9.json new file mode 100644 index 0000000..a2a0035 --- /dev/null +++ b/app/schemas/at.bitfire.davdroid.db.AppDatabase/9.json @@ -0,0 +1,366 @@ +{ + "formatVersion": 1, + "database": { + "version": 9, + "identityHash": "7e4bfdf7f9fa3529c333cf9485f8cf50", + "entities": [ + { + "tableName": "service", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "principal", + "columnName": "principal", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_service_accountName_type", + "unique": true, + "columnNames": [ + "accountName", + "type" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "homeset", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "personal", + "columnName": "personal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privBind", + "columnName": "privBind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_homeset_serviceId_url", + "unique": true, + "columnNames": [ + "serviceId", + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "collection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `owner` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homeSetId", + "columnName": "homeSetId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privWriteContent", + "columnName": "privWriteContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privUnbind", + "columnName": "privUnbind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "forceReadOnly", + "columnName": "forceReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "owner", + "columnName": "owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timezone", + "columnName": "timezone", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "supportsVEVENT", + "columnName": "supportsVEVENT", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVTODO", + "columnName": "supportsVTODO", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "supportsVJOURNAL", + "columnName": "supportsVJOURNAL", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sync", + "columnName": "sync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_collection_serviceId_type", + "unique": false, + "columnNames": [ + "serviceId", + "type" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)" + }, + { + "name": "index_collection_homeSetId_type", + "unique": false, + "columnNames": [ + "homeSetId", + "type" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)" + }, + { + "name": "index_collection_url", + "unique": false, + "columnNames": [ + "url" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [ + { + "table": "service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "homeset", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "homeSetId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "syncstats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "collectionId", + "columnName": "collectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authority", + "columnName": "authority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "lastSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_syncstats_collectionId_authority", + "unique": true, + "columnNames": [ + "collectionId", + "authority" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)" + } + ], + "foreignKeys": [ + { + "table": "collection", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "collectionId" + ], + "referencedColumns": [ + "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, '7e4bfdf7f9fa3529c333cf9485f8cf50')" + ] + } +} \ No newline at end of file diff --git a/app/src/.gitignore b/app/src/.gitignore new file mode 100644 index 0000000..213f776 --- /dev/null +++ b/app/src/.gitignore @@ -0,0 +1 @@ +espressoTest diff --git a/app/src/androidTest/AndroidManifest.xml b/app/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000..8278e3a --- /dev/null +++ b/app/src/androidTest/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/ExternalLibrariesTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/ExternalLibrariesTest.kt new file mode 100644 index 0000000..0f9ba03 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/ExternalLibrariesTest.kt @@ -0,0 +1,30 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid + +import android.util.Xml +import at.bitfire.dav4jvm.XmlUtils +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class ExternalLibrariesTest { + + @Test + fun test_Dav4jvm_XmlUtils_NewPullParser_RelaxedParsing() { + val parser = XmlUtils.newPullParser() + assertTrue(parser.getFeature(Xml.FEATURE_RELAXED)) + } + + @Test + fun testOkhttpHttpUrl_PublicSuffixList() { + // HttpUrl.topPrivateDomain() requires okhttp's internal PublicSuffixList. + // In Android, loading the PublicSuffixList is done over AndroidX startup. + // This test verifies that everything is working. + assertEquals("example.com", "http://example.com".toHttpUrl().topPrivateDomain()) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/HiltTestRunner.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/HiltTestRunner.kt new file mode 100644 index 0000000..42de9d0 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/HiltTestRunner.kt @@ -0,0 +1,42 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid + +import android.app.Application +import android.content.Context +import android.os.Build +import android.os.Bundle +import androidx.test.runner.AndroidJUnitRunner +import at.bitfire.davdroid.di.TestCoroutineDispatchersModule +import at.bitfire.davdroid.test.BuildConfig +import at.bitfire.synctools.log.LogcatHandler +import dagger.hilt.android.testing.HiltTestApplication +import java.util.logging.Level +import java.util.logging.Logger + +@Suppress("unused") +class HiltTestRunner : AndroidJUnitRunner() { + + override fun newApplication(cl: ClassLoader, name: String, context: Context): Application = + super.newApplication(cl, HiltTestApplication::class.java.name, context) + + override fun onCreate(arguments: Bundle?) { + super.onCreate(arguments) + + // set root logger to adb Logcat + val rootLogger = Logger.getLogger("") + rootLogger.level = Level.ALL + rootLogger.handlers.forEach { rootLogger.removeHandler(it) } + rootLogger.addHandler(LogcatHandler(BuildConfig.APPLICATION_ID)) + + // MockK requirements + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) + throw AssertionError("MockK requires Android P [https://mockk.io/ANDROID.html]") + + // set main dispatcher for tests (especially runTest) + TestCoroutineDispatchersModule.initMainDispatcher() + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/TestUtils.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/TestUtils.kt new file mode 100644 index 0000000..28961cb --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/TestUtils.kt @@ -0,0 +1,58 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid + +import android.content.Context +import android.util.Log +import androidx.work.Configuration +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkQuery +import androidx.work.WorkerFactory +import androidx.work.testing.WorkManagerTestInitHelper +import org.junit.Assert.assertTrue +import kotlin.math.abs + +object TestUtils { + + fun assertWithin(expected: Long, actual: Long, tolerance: Long) { + val absDifference = abs(expected - actual) + assertTrue( + "$actual not within ($expected ± $tolerance)", + absDifference <= tolerance + ) + } + + /** + * Initializes WorkManager for instrumentation tests. + */ + fun setUpWorkManager(context: Context, workerFactory: WorkerFactory? = null) { + val config = Configuration.Builder().setMinimumLoggingLevel(Log.DEBUG) + if (workerFactory != null) + config.setWorkerFactory(workerFactory) + WorkManagerTestInitHelper.initializeTestWorkManager(context, config.build()) + } + + fun workInStates(context: Context, workerName: String, states: List): Boolean = + WorkManager.getInstance(context).getWorkInfos(WorkQuery.Builder + .fromUniqueWorkNames(listOf(workerName)) + .addStates(states) + .build() + ).get().isNotEmpty() + + fun workScheduledOrRunning(context: Context, workerName: String): Boolean = + workInStates(context, workerName, listOf( + WorkInfo.State.ENQUEUED, + WorkInfo.State.RUNNING + )) + + fun workScheduledOrRunningOrSuccessful(context: Context, workerName: String): Boolean = + workInStates(context, workerName, listOf( + WorkInfo.State.ENQUEUED, + WorkInfo.State.RUNNING, + WorkInfo.State.SUCCEEDED + )) + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/db/AppDatabaseTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/AppDatabaseTest.kt new file mode 100644 index 0000000..8a257cd --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/AppDatabaseTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import android.content.Context +import androidx.room.Room +import androidx.room.migration.AutoMigrationSpec +import androidx.room.migration.Migration +import androidx.room.testing.MigrationTestHelper +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import androidx.test.platform.app.InstrumentationRegistry +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.logging.Logger +import javax.inject.Inject + +@HiltAndroidTest +class AppDatabaseTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @Inject @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var autoMigrations: Set<@JvmSuppressWildcards AutoMigrationSpec> + + @Inject + lateinit var logger: Logger + + @Inject + lateinit var manualMigrations: Set<@JvmSuppressWildcards Migration> + + @Before + fun setup() { + hiltRule.inject() + } + + + /** + * Creates a database with schema version 8 (the first exported one) and then migrates it to the latest version. + */ + @Test + fun testAllMigrations() { + // Create DB with v8 + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + listOf(), // no auto migrations until v8 + FrameworkSQLiteOpenHelperFactory() + ).createDatabase(TEST_DB, 8).close() + + // open and migrate (to current version) database + Room.databaseBuilder(context, AppDatabase::class.java, TEST_DB) + // manual migrations + .addMigrations(*manualMigrations.toTypedArray()) + // auto-migrations that need to be specified explicitly + .apply { + for (spec in autoMigrations) + addAutoMigrationSpec(spec) + } + .build() + .openHelper.writableDatabase // this will run all migrations + .close() + } + + + companion object { + const val TEST_DB = "test" + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/db/CollectionTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/CollectionTest.kt new file mode 100644 index 0000000..56166c8 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/CollectionTest.kt @@ -0,0 +1,207 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import android.security.NetworkSecurityPolicy +import androidx.test.filters.SmallTest +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.property.webdav.ResourceType +import at.bitfire.davdroid.network.HttpClient +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assume +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class CollectionTest { + + @Inject + lateinit var httpClientBuilder: HttpClient.Builder + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + private lateinit var httpClient: HttpClient + private val server = MockWebServer() + + @Before + fun setup() { + hiltRule.inject() + + httpClient = httpClientBuilder.build() + Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted) + } + + @After + fun teardown() { + httpClient.close() + } + + + @Test + @SmallTest + fun testFromDavResponseAddressBook() { + // r/w address book + server.enqueue(MockResponse() + .setResponseCode(207) + .setBody("" + + "" + + " /" + + " " + + " " + + " My Contacts" + + " My Contacts Description" + + " " + + "" + + "")) + + lateinit var info: Collection + DavResource(httpClient.okHttpClient, server.url("/")) + .propfind(0, ResourceType.NAME) { response, _ -> + info = Collection.fromDavResponse(response) ?: throw IllegalArgumentException() + } + assertEquals(Collection.TYPE_ADDRESSBOOK, info.type) + assertTrue(info.privWriteContent) + assertTrue(info.privUnbind) + assertNull(info.supportsVEVENT) + assertNull(info.supportsVTODO) + assertNull(info.supportsVJOURNAL) + assertEquals("My Contacts", info.displayName) + assertEquals("My Contacts Description", info.description) + } + + @Test + @SmallTest + fun testFromDavResponseCalendar_FullTimezone() { + // read-only calendar, no display name + server.enqueue(MockResponse() + .setResponseCode(207) + .setBody("" + + "" + + " /" + + " " + + " " + + " " + + " My Calendar" + + " BEGIN:VCALENDAR\n" + + "PRODID:-//Example Corp.//CalDAV Client//EN\n" + + "VERSION:2.0\n" + + "BEGIN:VTIMEZONE\n" + + "TZID:US-Eastern\n" + + "LAST-MODIFIED:19870101T000000Z\n" + + "BEGIN:STANDARD\n" + + "DTSTART:19671029T020000\n" + + "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\n" + + "TZOFFSETFROM:-0400\n" + + "TZOFFSETTO:-0500\n" + + "TZNAME:Eastern Standard Time (US & Canada)\n" + + "END:STANDARD\n" + + "BEGIN:DAYLIGHT\n" + + "DTSTART:19870405T020000\n" + + "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\n" + + "TZOFFSETFROM:-0500\n" + + "TZOFFSETTO:-0400\n" + + "TZNAME:Eastern Daylight Time (US & Canada)\n" + + "END:DAYLIGHT\n" + + "END:VTIMEZONE\n" + + "END:VCALENDAR\n" + + "" + + " #ff0000" + + " " + + "" + + "")) + + lateinit var info: Collection + DavResource(httpClient.okHttpClient, server.url("/")) + .propfind(0, ResourceType.NAME) { response, _ -> + info = Collection.fromDavResponse(response)!! + } + assertEquals(Collection.TYPE_CALENDAR, info.type) + assertFalse(info.privWriteContent) + assertFalse(info.privUnbind) + assertNull(info.displayName) + assertEquals("My Calendar", info.description) + assertEquals(0xFFFF0000.toInt(), info.color) + assertEquals("US-Eastern", info.timezoneId) + assertTrue(info.supportsVEVENT!!) + assertTrue(info.supportsVTODO!!) + assertTrue(info.supportsVJOURNAL!!) + } + + @Test + @SmallTest + fun testFromDavResponseCalendar_OnlyTzId() { + // read-only calendar, no display name + server.enqueue(MockResponse() + .setResponseCode(207) + .setBody("" + + "" + + " /" + + " " + + " " + + " " + + " My Calendar" + + " US-Eastern" + + " #ff0000" + + " " + + "" + + "")) + + lateinit var info: Collection + DavResource(httpClient.okHttpClient, server.url("/")) + .propfind(0, ResourceType.NAME) { response, _ -> + info = Collection.fromDavResponse(response)!! + } + assertEquals(Collection.TYPE_CALENDAR, info.type) + assertFalse(info.privWriteContent) + assertFalse(info.privUnbind) + assertNull(info.displayName) + assertEquals("My Calendar", info.description) + assertEquals(0xFFFF0000.toInt(), info.color) + assertEquals("US-Eastern", info.timezoneId) + assertTrue(info.supportsVEVENT!!) + assertTrue(info.supportsVTODO!!) + assertTrue(info.supportsVJOURNAL!!) + } + + @Test + @SmallTest + fun testFromDavResponseWebcal() { + // Webcal subscription + server.enqueue(MockResponse() + .setResponseCode(207) + .setBody("" + + "" + + " /webcal1" + + " " + + " Sample Subscription" + + " " + + " webcals://example.com/1.ics" + + " " + + "" + + "")) + + lateinit var info: Collection + DavResource(httpClient.okHttpClient, server.url("/")) + .propfind(0, ResourceType.NAME) { response, _ -> + info = Collection.fromDavResponse(response) ?: throw IllegalArgumentException() + } + assertEquals(Collection.TYPE_WEBCAL, info.type) + assertEquals("Sample Subscription", info.displayName) + assertEquals("https://example.com/1.ics".toHttpUrl(), info.source) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/db/HomeSetDaoTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/HomeSetDaoTest.kt new file mode 100644 index 0000000..10896dd --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/HomeSetDaoTest.kt @@ -0,0 +1,97 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class HomeSetDaoTest { + + @get:Rule + var hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var db: AppDatabase + lateinit var dao: HomeSetDao + var serviceId: Long = 0 + + @Before + fun setUp() { + hiltRule.inject() + dao = db.homeSetDao() + + serviceId = db.serviceDao().insertOrReplace( + Service(id=0, accountName="test", type= Service.TYPE_CALDAV, principal = null) + ) + } + + @After + fun tearDown() { + db.serviceDao().deleteAll() + } + + + @Test + fun testInsertOrUpdate() { + // should insert new row or update (upsert) existing row - without changing its key! + val entry1 = HomeSet(id=0, serviceId=serviceId, personal=true, url="https://example.com/1".toHttpUrl()) + val insertId1 = dao.insertOrUpdateByUrlBlocking(entry1) + assertEquals(1L, insertId1) + assertEquals(entry1.copy(id = 1L), dao.getById(1)) + + val updatedEntry1 = HomeSet(id=0, serviceId=serviceId, personal=true, url="https://example.com/1".toHttpUrl(), displayName="Updated Entry") + val updateId1 = dao.insertOrUpdateByUrlBlocking(updatedEntry1) + assertEquals(1L, updateId1) + assertEquals(updatedEntry1.copy(id = 1L), dao.getById(1)) + + val entry2 = HomeSet(id=0, serviceId=serviceId, personal=true, url= "https://example.com/2".toHttpUrl()) + val insertId2 = dao.insertOrUpdateByUrlBlocking(entry2) + assertEquals(2L, insertId2) + assertEquals(entry2.copy(id = 2L), dao.getById(2)) + } + + @Test + fun testInsertOrUpdate_TransactionSafe() { + runBlocking(Dispatchers.IO) { + for (i in 0..9999) + launch { + dao.insertOrUpdateByUrlBlocking( + HomeSet( + id = 0, + serviceId = serviceId, + url = "https://example.com/".toHttpUrl(), + personal = true + ) + ) + } + } + assertEquals(1, dao.getByService(serviceId).size) + } + + @Test + fun testDelete() { + // should delete row with given primary key (id) + val entry1 = HomeSet(id=1, serviceId=serviceId, personal=true, url= "https://example.com/1".toHttpUrl()) + + val insertId1 = dao.insertOrUpdateByUrlBlocking(entry1) + assertEquals(1L, insertId1) + assertEquals(entry1, dao.getById(1L)) + + dao.delete(entry1) + assertEquals(null, dao.getById(1L)) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/db/MemoryDbModule.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/MemoryDbModule.kt new file mode 100644 index 0000000..fce8294 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/MemoryDbModule.kt @@ -0,0 +1,41 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import android.content.Context +import androidx.room.Room +import androidx.room.migration.AutoMigrationSpec +import dagger.Module +import dagger.Provides +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.hilt.testing.TestInstallIn +import javax.inject.Singleton + +@Module +@TestInstallIn( + components = [ SingletonComponent::class ], + replaces = [ + AppDatabase.AppDatabaseModule::class + ] +) +class MemoryDbModule { + + @Provides + @Singleton + fun inMemoryDatabase( + autoMigrations: Set<@JvmSuppressWildcards AutoMigrationSpec>, + @ApplicationContext context: Context + ): AppDatabase = + Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java) + // auto-migration specs that need to be specified explicitly + .apply { + for (spec in autoMigrations) { + addAutoMigrationSpec(spec) + } + } + .build() + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/db/PrincipalDaoTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/PrincipalDaoTest.kt new file mode 100644 index 0000000..fd80ab2 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/PrincipalDaoTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import android.database.sqlite.SQLiteConstraintException +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.junit4.MockKRule +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + + +@HiltAndroidTest +class PrincipalDaoTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val mockKRule = MockKRule(this) + + @Inject + lateinit var db: AppDatabase + + private lateinit var principalDao: PrincipalDao + private lateinit var service: Service + private val url = "https://example.com/dav/principal".toHttpUrl() + + @Before + fun setUp() { + hiltRule.inject() + principalDao = spyk(db.principalDao()) + + service = Service(id = 1, accountName = "account", type = "webdav") + db.serviceDao().insertOrReplace(service) + } + + @Test + fun insertOrUpdate_insertsIfNotExisting() = runTest { + val principal = Principal(serviceId = service.id, url = url, displayName = "principal") + val id = principalDao.insertOrUpdate(service.id, principal) + assertTrue(id > 0) + + val stored = principalDao.get(id) + assertEquals("principal", stored.displayName) + verify(exactly = 0) { principalDao.update(any()) } + } + + @Test + fun insertOrUpdate_doesNotUpdateIfDisplayNameIsEqual() = runTest { + val principalOld = Principal(serviceId = service.id, url = url, displayName = "principalOld") + val idOld = principalDao.insertOrUpdate(service.id, principalOld) + + val principalNew = Principal(serviceId = service.id, url = url, displayName = "principalOld") + val idNew = principalDao.insertOrUpdate(service.id, principalNew) + + assertEquals(idOld, idNew) + val stored = principalDao.get(idOld) + assertEquals("principalOld", stored.displayName) + verify(exactly = 0) { principalDao.update(any()) } + } + + @Test + fun insertOrUpdate_updatesIfDisplayNameIsDifferent() = runTest { + val principalOld = Principal(serviceId = service.id, url = url, displayName = "principalOld") + val idOld = principalDao.insertOrUpdate(service.id, principalOld) + + val principalNew = Principal(serviceId = service.id, url = url, displayName = "principalNew") + val idNew = principalDao.insertOrUpdate(service.id, principalNew) + + assertEquals(idOld, idNew) + + val updated = principalDao.get(idOld) + assertEquals("principalNew", updated.displayName) + verify(exactly = 1) { principalDao.update(any()) } + } + + @Test(expected = SQLiteConstraintException::class) + fun insertOrUpdate_throwsForeignKeyConstraintViolationException() = runTest { + // throws on non-existing service + val url = "https://example.com/dav/principal".toHttpUrl() + val principal1 = Principal(serviceId = 999, url = url, displayName = "p1") + principalDao.insertOrUpdate(999, principal1) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/db/SyncStatsDaoTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/SyncStatsDaoTest.kt new file mode 100644 index 0000000..3c96277 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/SyncStatsDaoTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import androidx.sqlite.SQLiteException +import at.bitfire.davdroid.sync.SyncDataType +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.coroutines.test.runTest +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class SyncStatsDaoTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var db: AppDatabase + var collectionId: Long = 0 + + @Before + fun setUp() { + hiltRule.inject() + + val serviceId = db.serviceDao().insertOrReplace(Service( + id = 0, + accountName = "test@example.com", + type = Service.TYPE_CALDAV + )) + collectionId = db.collectionDao().insert(Collection( + id = 0, + serviceId = serviceId, + type = Collection.TYPE_CALENDAR, + url = "https://example.com".toHttpUrl() + )) + } + + @After + fun tearDown() { + db.serviceDao().deleteAll() + } + + @Test + fun testInsertOrReplace_ExistingForeignKey() = runTest { + val dao = db.syncStatsDao() + dao.insertOrReplace( + SyncStats( + id = 0, + collectionId = collectionId, + dataType = SyncDataType.CONTACTS.toString(), + lastSync = System.currentTimeMillis() + ) + ) + } + + @Test(expected = SQLiteException::class) + fun testInsertOrReplace_MissingForeignKey() = runTest { + val dao = db.syncStatsDao() + dao.insertOrReplace( + SyncStats( + id = 0, + collectionId = 12345, + dataType = SyncDataType.CONTACTS.toString(), + lastSync = System.currentTimeMillis() + ) + ) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/db/WebDavDocumentDaoTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/WebDavDocumentDaoTest.kt new file mode 100644 index 0000000..24cb08e --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/WebDavDocumentDaoTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.coroutines.test.runTest +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject + +@HiltAndroidTest +class WebDavDocumentDaoTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var db: AppDatabase + + @Inject + lateinit var logger: Logger + + @Before + fun setUp() { + hiltRule.inject() + } + + + @Test + fun testGetChildren() = runTest { + val mountDao = db.webDavMountDao() + val dao = db.webDavDocumentDao() + + val mount = WebDavMount(id = 1, name = "Test", url = "https://example.com/".toHttpUrl()) + db.webDavMountDao().insert(mount) + + val root = WebDavDocument( + id = 1, + mountId = mount.id, + parentId = null, + name = "Root Document" + ) + dao.insertOrReplace(root) + dao.insertOrReplace(WebDavDocument(id = 0, mountId = mount.id, parentId = root.id, name = "Name 1", displayName = "DisplayName 2")) + dao.insertOrReplace(WebDavDocument(id = 0, mountId = mount.id, parentId = root.id, name = "Name 2", displayName = "DisplayName 1")) + dao.insertOrReplace(WebDavDocument(id = 0, mountId = mount.id, parentId = root.id, name = "Name 3", displayName = "Directory 1", isDirectory = true)) + try { + dao.getChildren(root.id, orderBy = "name DESC").let { result -> + logger.log(Level.INFO, "getChildren single sort Result", result) + + assertEquals(listOf( + "Name 3", + "Name 2", + "Name 1" + ), result.map { it.name }) + } + + dao.getChildren(root.id, orderBy = "isDirectory DESC, name ASC").let { result -> + logger.log(Level.INFO, "getChildren multiple sort Result", result) + + assertEquals(listOf( + "Name 3", + "Name 1", + "Name 2" + ), result.map { it.name }) + } + } finally { + mountDao.deleteAsync(mount) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/AutoMigration16Test.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/AutoMigration16Test.kt new file mode 100644 index 0000000..38a910a --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/AutoMigration16Test.kt @@ -0,0 +1,83 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db.migration + +import at.bitfire.davdroid.db.Collection.Companion.TYPE_CALENDAR +import at.bitfire.davdroid.db.Service +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +@HiltAndroidTest +class AutoMigration16Test: DatabaseMigrationTest(toVersion = 16) { + + @Test + fun testMigrate_WithTimeZone() = testMigration( + prepare = { db -> + val minimalVTimezone = """ + BEGIN:VCALENDAR + VERSION:2.0 + PRODID:DAVx5 + BEGIN:VTIMEZONE + TZID:America/New_York + END:VTIMEZONE + END:VCALENDAR + """.trimIndent() + db.execSQL( + "INSERT INTO service (id, accountName, type) VALUES (?, ?, ?)", + arrayOf(1, "test", Service.Companion.TYPE_CALDAV) + ) + db.execSQL( + "INSERT INTO collection (id, serviceId, type, url, privWriteContent, privUnbind, forceReadOnly, sync, timezone) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + arrayOf(1, 1, TYPE_CALENDAR, "https://example.com", true, true, false, false, minimalVTimezone) + ) + } + ) { db -> + db.query("SELECT timezoneId FROM collection WHERE id=1").use { cursor -> + cursor.moveToFirst() + assertEquals("America/New_York", cursor.getString(0)) + } + } + + @Test + fun testMigrate_WithTimeZone_Unparseable() = testMigration( + prepare = { db -> + db.execSQL( + "INSERT INTO service (id, accountName, type) VALUES (?, ?, ?)", + arrayOf(1, "test", Service.Companion.TYPE_CALDAV) + ) + db.execSQL( + "INSERT INTO collection (id, serviceId, type, url, privWriteContent, privUnbind, forceReadOnly, sync, timezone) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + arrayOf(1, 1, TYPE_CALENDAR, "https://example.com", true, true, false, false, "Some Garbage Content") + ) + } + ) { db -> + db.query("SELECT timezoneId FROM collection WHERE id=1").use { cursor -> + cursor.moveToFirst() + assertNull(cursor.getString(0)) + } + } + + @Test + fun testMigrate_WithoutTimezone() = testMigration( + prepare = { db -> + db.execSQL( + "INSERT INTO service (id, accountName, type) VALUES (?, ?, ?)", + arrayOf(1, "test", Service.Companion.TYPE_CALDAV) + ) + db.execSQL( + "INSERT INTO collection (id, serviceId, type, url, privWriteContent, privUnbind, forceReadOnly, sync) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + arrayOf(1, 1, TYPE_CALENDAR, "https://example.com", true, true, false, false) + ) + } + ) { db -> + db.query("SELECT timezoneId FROM collection WHERE id=1").use { cursor -> + cursor.moveToFirst() + assertNull(cursor.getString(0)) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/AutoMigration18Test.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/AutoMigration18Test.kt new file mode 100644 index 0000000..ab85d2c --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/AutoMigration18Test.kt @@ -0,0 +1,79 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db.migration + +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Assert.assertEquals +import org.junit.Test + +@HiltAndroidTest +class AutoMigration18Test : DatabaseMigrationTest(toVersion = 18) { + + @Test + fun testMigration_AllAuthorities() = testMigration( + prepare = { db -> + // Insert service and collection to respect relation constraints + db.execSQL("INSERT INTO service (id, accountName, type) VALUES (?, ?, ?)", arrayOf(1, "test", 1)) + listOf(1L, 2L, 3L).forEach { id -> + db.execSQL( + "INSERT INTO collection (id, serviceId, url, type, privWriteContent, privUnbind, forceReadOnly, sync) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + arrayOf(id, 1, "https://example.com/$id", 1, 1, 1, 0, 1) + ) + } + // Insert some syncstats with authorities and lastSync times + val syncstats = listOf( + Entry(1, 1, "com.android.contacts", 1000), + Entry(2, 1, "com.android.calendar", 1000), + Entry(3, 1, "org.dmfs.tasks", 1000), + Entry(4, 1, "org.tasks.opentasks", 2000), + Entry(5, 1, "at.techbee.jtx.provider", 3000), // highest lastSync for collection 1 + Entry(6, 1, "unknown.authority", 1000), // ignored + + Entry(7, 2, "org.dmfs.tasks", 1000), + Entry(8, 2, "org.tasks.opentasks", 2000), // highest lastSync for collection 2 + + Entry(9, 3, "org.tasks.opentasks", 1000), + ) + syncstats.forEach { (id, collectionId, authority, lastSync) -> + db.execSQL( + "INSERT INTO syncstats (id, collectionId, authority, lastSync) VALUES (?, ?, ?, ?)", + arrayOf(id, collectionId, authority, lastSync) + ) + } + }, + validate = { db -> + db.query("SELECT id, collectionId, dataType FROM syncstats ORDER BY id").use { cursor -> + val found = mutableListOf() + db.query("SELECT id, collectionId, dataType FROM syncstats ORDER BY id").use { cursor -> + val idIdx = cursor.getColumnIndex("id") + val colIdx = cursor.getColumnIndex("collectionId") + val typeIdx = cursor.getColumnIndex("dataType") + while (cursor.moveToNext()) + found.add( + Entry(cursor.getInt(idIdx), cursor.getLong(colIdx), cursor.getString(typeIdx)) + ) + } + + // Expect one TASKS row per collection (collections 1, 2, 3) + assertEquals( + listOf( + Entry(1, 1, "CONTACTS"), + Entry(2, 1, "EVENTS"), + Entry(5, 1, "TASKS"), // highest lastSync TASK for collection 1 is JTX Board + Entry(8, 2, "TASKS"), // highest lastSync TASK for collection 2 + Entry(9, 3, "TASKS"), // only TASK for collection 3 + ), found + ) + } + } + ) + + data class Entry( + val id: Int, + val collectionId: Long, + val dataType: String? = null, + val lastSync: Long? = null + ) +} diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/DatabaseMigrationTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/DatabaseMigrationTest.kt new file mode 100644 index 0000000..ed514f6 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/migration/DatabaseMigrationTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db.migration + +import androidx.room.migration.AutoMigrationSpec +import androidx.room.migration.Migration +import androidx.room.testing.MigrationTestHelper +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import androidx.test.platform.app.InstrumentationRegistry +import at.bitfire.davdroid.db.AppDatabase +import dagger.hilt.android.testing.HiltAndroidRule +import org.junit.Before +import org.junit.Rule +import javax.inject.Inject + +/** + * Helper for testing the database migration from [toVersion] - 1 to [toVersion]. + * + * @param toVersion The target version to migrate to. + */ +abstract class DatabaseMigrationTest( + private val toVersion: Int +) { + + @Inject + lateinit var autoMigrations: Set<@JvmSuppressWildcards AutoMigrationSpec> + + @Inject + lateinit var manualMigrations: Set<@JvmSuppressWildcards Migration> + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + + @Before + fun setup() { + hiltRule.inject() + } + + + /** + * Used for testing the migration process from [toVersion]-1 to [toVersion]. + * + * Note: SQLite's foreign key constraint enforcement is not enabled in tests. We need + * to enable it ourselves using setting "PRAGMA foreign_keys=ON" directly after opening + * a new database connection (works per connection). In tests it's usually more practical + * not to do so, however. In production database connections room enables it for us. + * + * @param prepare Callback to prepare the database. Will be run with database schema in version [toVersion] - 1. + * @param validate Callback to validate the migration result. Will be run with database schema in version [toVersion]. + */ + protected fun testMigration( + prepare: (SupportSQLiteDatabase) -> Unit, + validate: (SupportSQLiteDatabase) -> Unit + ) { + val helper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + autoMigrations.toList(), + FrameworkSQLiteOpenHelperFactory() + ) + + // Prepare the database with the initial version. + val dbName = "test" + helper.createDatabase(dbName, version = toVersion - 1).apply { + // We could enable foreign key constraint enforcement here + // by setting "PRAGMA foreign_keys=ON". + prepare(this) + close() + } + + // Re-open the database with the new version and provide all the migrations. + val db = helper.runMigrationsAndValidate( + name = dbName, + version = toVersion, + validateDroppedTables = true, + migrations = manualMigrations.toTypedArray() + ) + + validate(db) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/di/FakeSyncAdapterModule.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/di/FakeSyncAdapterModule.kt new file mode 100644 index 0000000..c09b9f9 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/di/FakeSyncAdapterModule.kt @@ -0,0 +1,20 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.di + +import at.bitfire.davdroid.sync.FakeSyncAdapter +import at.bitfire.davdroid.sync.adapter.SyncAdapter +import at.bitfire.davdroid.sync.adapter.SyncAdapterImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.components.SingletonComponent +import dagger.hilt.testing.TestInstallIn + +@Module +@TestInstallIn(components = [SingletonComponent::class], replaces = [SyncAdapterImpl.RealSyncAdapterModule::class]) +abstract class FakeSyncAdapterModule { + @Binds + abstract fun provide(impl: FakeSyncAdapter): SyncAdapter +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/di/TestCoroutineDispatchersModule.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/di/TestCoroutineDispatchersModule.kt new file mode 100644 index 0000000..e7e932d --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/di/TestCoroutineDispatchersModule.kt @@ -0,0 +1,59 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.di + +import at.bitfire.davdroid.di.TestCoroutineDispatchersModule.standardTestDispatcher +import dagger.Module +import dagger.Provides +import dagger.hilt.components.SingletonComponent +import dagger.hilt.testing.TestInstallIn +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.setMain + +/** + * Provides test dispatchers to be injected instead of the normal ones. + * + * The [standardTestDispatcher] is set as main dispatcher in [at.bitfire.davdroid.HiltTestRunner], + * so that tests can just use [kotlinx.coroutines.test.runTest] without providing [standardTestDispatcher]. + */ +@Module +@TestInstallIn( + components = [SingletonComponent::class], + replaces = [CoroutineDispatchersModule::class] +) +object TestCoroutineDispatchersModule { + + private val standardTestDispatcher = StandardTestDispatcher() + + @Provides + @DefaultDispatcher + fun defaultDispatcher(): CoroutineDispatcher = standardTestDispatcher + + @Provides + @IoDispatcher + fun ioDispatcher(): CoroutineDispatcher = standardTestDispatcher + + @Provides + @MainDispatcher + fun mainDispatcher(): CoroutineDispatcher = standardTestDispatcher + + @Provides + @SyncDispatcher + fun syncDispatcher(): CoroutineDispatcher = standardTestDispatcher + + /** + * Sets the [standardTestDispatcher] as [Dispatchers.Main] so that test dispatchers + * created in the future use the same scheduler. See [StandardTestDispatcher] docs + * for more information. + */ + @OptIn(ExperimentalCoroutinesApi::class) + fun initMainDispatcher() { + Dispatchers.setMain(standardTestDispatcher) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/di/TestTasksAppWatcherModule.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/di/TestTasksAppWatcherModule.kt new file mode 100644 index 0000000..9591f40 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/di/TestTasksAppWatcherModule.kt @@ -0,0 +1,24 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.di + +import at.bitfire.davdroid.startup.StartupPlugin +import at.bitfire.davdroid.startup.TasksAppWatcher +import dagger.Module +import dagger.hilt.components.SingletonComponent +import dagger.hilt.testing.TestInstallIn +import dagger.multibindings.Multibinds + +// remove TasksAppWatcherModule from Android tests +@Module +@TestInstallIn( + components = [SingletonComponent::class], + replaces = [TasksAppWatcher.TasksAppWatcherModule::class] +) +abstract class TestTasksAppWatcherModule { + // provides empty set of plugins + @Multibinds + abstract fun empty(): Set +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/network/Android10ResolverTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/network/Android10ResolverTest.kt new file mode 100644 index 0000000..df9bc95 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/network/Android10ResolverTest.kt @@ -0,0 +1,35 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.network + +import android.os.Build +import androidx.test.filters.SdkSuppress +import org.junit.Assert.assertEquals +import org.junit.Test +import org.xbill.DNS.ARecord +import org.xbill.DNS.Lookup +import org.xbill.DNS.Type +import java.net.Inet4Address +import java.net.InetAddress + +class Android10ResolverTest { + + val FQDN_DAVX5 = "www.davx5.com" + + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q, maxSdkVersion = 34) + fun testResolveA() { + val www = InetAddress.getAllByName(FQDN_DAVX5).filterIsInstance().first() + + val srvLookup = Lookup(FQDN_DAVX5, Type.A) + srvLookup.setResolver(Android10Resolver()) + val resultGeneric = srvLookup.run() + assertEquals(1, resultGeneric.size) + + val result = resultGeneric.first() as ARecord + assertEquals(www, result.address) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/network/DnsRecordResolverTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/network/DnsRecordResolverTest.kt new file mode 100644 index 0000000..7ef071f --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/network/DnsRecordResolverTest.kt @@ -0,0 +1,122 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.network + +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.xbill.DNS.DClass +import org.xbill.DNS.Name +import org.xbill.DNS.SRVRecord +import org.xbill.DNS.TXTRecord +import javax.inject.Inject +import kotlin.random.Random + +@HiltAndroidTest +class DnsRecordResolverTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var dnsRecordResolver: DnsRecordResolver + + @Before + fun setup() { + hiltRule.inject() + } + + + @Test + fun testBestSRVRecord_Empty() { + assertNull(dnsRecordResolver.bestSRVRecord(emptyArray())) + } + + @Test + fun testBestSRVRecord_MultipleRecords_Priority_Different() { + val dns1010 = SRVRecord( + Name.fromString("_caldavs._tcp.example.com."), + DClass.IN, 3600, 10, 10, 8443, Name.fromString("dav1010.example.com.") + ) + val dns2010 = SRVRecord( + Name.fromString("_caldavs._tcp.example.com."), + DClass.IN, 3600, 20, 20, 8443, Name.fromString("dav2010.example.com.") + ) + + // lowest priority first + val result = dnsRecordResolver.bestSRVRecord(arrayOf(dns1010, dns2010)) + assertEquals(dns1010, result) + } + + @Test + fun testBestSRVRecord_MultipleRecords_Priority_Same() { + val dns1010 = SRVRecord( + Name.fromString("_caldavs._tcp.example.com."), + DClass.IN, 3600, 10, 10, 8443, Name.fromString("dav1010.example.com.") + ) + val dns1020 = SRVRecord( + Name.fromString("_caldavs._tcp.example.com."), + DClass.IN, 3600, 10, 20, 8443, Name.fromString("dav1020.example.com.") + ) + val dns1030 = SRVRecord( + Name.fromString("_caldavs._tcp.example.com."), + DClass.IN, 3600, 10, 30, 8443, Name.fromString("dav1030.example.com.") + ) + val records = arrayOf(dns1010, dns1020, dns1030) + + val randomNumberGenerator = mockk() + for (i in 0..60) { + every { randomNumberGenerator.nextInt(0, 61) } returns i + val expected = when (i) { + in 0..10 -> dns1010 + in 11..30 -> dns1020 + else -> dns1030 + } + assertEquals(expected, dnsRecordResolver.bestSRVRecord(records, randomNumberGenerator)) + } + } + + @Test + fun testBestSRVRecord_OneRecord() { + val dns1010 = SRVRecord( + Name.fromString("_caldavs._tcp.example.com."), + DClass.IN, 3600, 10, 10, 8443, Name.fromString("dav1010.example.com.") + ) + val result = dnsRecordResolver.bestSRVRecord(arrayOf(dns1010)) + assertEquals(dns1010, result) + } + + + @Test + fun testPathsFromTXTRecords_Empty() { + assertTrue(dnsRecordResolver.pathsFromTXTRecords(arrayOf()).isEmpty()) + } + + @Test + fun testPathsFromTXTRecords_OnePath() { + val result = dnsRecordResolver.pathsFromTXTRecords(arrayOf( + TXTRecord(Name.fromString("example.com."), 0, 0L, listOf("something=else", "path=/path1")) + )).toTypedArray() + assertArrayEquals(arrayOf("/path1"), result) + } + + @Test + fun testPathsFromTXTRecords_TwoPaths() { + val result = dnsRecordResolver.pathsFromTXTRecords(arrayOf( + TXTRecord(Name.fromString("example.com."), 0, 0L, listOf("path=/path1", "something-else", "path=/path2")) + )).toTypedArray() + result.sort() + assertArrayEquals(arrayOf("/path1", "/path2"), result) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/network/HttpClientTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/network/HttpClientTest.kt new file mode 100644 index 0000000..1c12099 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/network/HttpClientTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.network + +import android.security.NetworkSecurityPolicy +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import okhttp3.Request +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assume +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class HttpClientTest { + + @get:Rule + var hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var httpClientBuilder: HttpClient.Builder + + lateinit var httpClient: HttpClient + lateinit var server: MockWebServer + + @Before + fun setUp() { + hiltRule.inject() + + httpClient = httpClientBuilder.build() + + server = MockWebServer() + server.start(30000) + } + + @After + fun tearDown() { + server.shutdown() + httpClient.close() + } + + + @Test + fun testCookies() { + Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted) + val url = server.url("/test") + + // set cookie for root path (/) and /test path in first response + server.enqueue(MockResponse() + .setResponseCode(200) + .addHeader("Set-Cookie", "cookie1=1; path=/") + .addHeader("Set-Cookie", "cookie2=2") + .setBody("Cookie set")) + httpClient.okHttpClient.newCall(Request.Builder() + .get().url(url) + .build()).execute() + assertNull(server.takeRequest().getHeader("Cookie")) + + // cookie should be sent with second request + // second response lets first cookie expire and overwrites second cookie + server.enqueue(MockResponse() + .addHeader("Set-Cookie", "cookie1=1a; path=/; Max-Age=0") + .addHeader("Set-Cookie", "cookie2=2a") + .setResponseCode(200)) + httpClient.okHttpClient.newCall(Request.Builder() + .get().url(url) + .build()).execute() + val header = server.takeRequest().getHeader("Cookie") + assertTrue(header == "cookie1=1; cookie2=2" || header == "cookie2=2; cookie1=1") + + server.enqueue(MockResponse() + .setResponseCode(200)) + httpClient.okHttpClient.newCall(Request.Builder() + .get().url(url) + .build()).execute() + assertEquals("cookie2=2a", server.takeRequest().getHeader("Cookie")) + } + +} diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/network/OkhttpClientTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/network/OkhttpClientTest.kt new file mode 100644 index 0000000..760332d --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/network/OkhttpClientTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.network + +import androidx.test.filters.SdkSuppress +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import okhttp3.Request +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class OkhttpClientTest { + + @Inject + lateinit var httpClientBuilder: HttpClient.Builder + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @Before + fun inject() { + hiltRule.inject() + } + + + @Test + @SdkSuppress(maxSdkVersion = 34) + fun testIcloudWithSettings() { + httpClientBuilder.build().use { client -> + client.okHttpClient + .newCall( + Request.Builder() + .get() + .url("https://icloud.com") + .build() + ) + .execute() + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/push/PushMessageHandlerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/push/PushMessageHandlerTest.kt new file mode 100644 index 0000000..9d1549e --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/push/PushMessageHandlerTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.push + +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class PushMessageHandlerTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var handler: PushMessageHandler + + @Before + fun setUp() { + hiltRule.inject() + } + + + @Test + fun testParse_InvalidXml() { + Assert.assertNull(handler.parse("Non-XML content")) + } + + @Test + fun testParse_WithXmlDeclAndTopic() { + val topic = handler.parse( + "" + + "" + + " O7M1nQ7cKkKTKsoS_j6Z3w" + + "" + ) + Assert.assertEquals("O7M1nQ7cKkKTKsoS_j6Z3w", topic) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/push/UnifiedPushServiceTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/push/UnifiedPushServiceTest.kt new file mode 100644 index 0000000..b324339 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/push/UnifiedPushServiceTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.push + +import android.content.Context +import android.content.Intent +import android.os.IBinder +import androidx.test.rule.ServiceTestRule +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.coVerify +import io.mockk.confirmVerified +import io.mockk.every +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.junit4.MockKRule +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.unifiedpush.android.connector.FailedReason +import org.unifiedpush.android.connector.PushService +import org.unifiedpush.android.connector.data.PushEndpoint +import javax.inject.Inject + +@OptIn(ExperimentalCoroutinesApi::class) +@HiltAndroidTest +class UnifiedPushServiceTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val mockKRule = MockKRule(this) + + @get:Rule + val serviceTestRule = ServiceTestRule() + + @Inject + @ApplicationContext + lateinit var context: Context + + @RelaxedMockK + @BindValue + lateinit var pushRegistrationManager: PushRegistrationManager + + lateinit var binder: IBinder + lateinit var unifiedPushService: UnifiedPushService + + + @Before + fun setUp() { + hiltRule.inject() + + binder = serviceTestRule.bindService(Intent(context, UnifiedPushService::class.java))!! + unifiedPushService = (binder as PushService.PushBinder).getService() as UnifiedPushService + } + + + @Test + fun testOnNewEndpoint() = runTest { + val endpoint = mockk { + every { url } returns "https://example.com/12" + } + unifiedPushService.onNewEndpoint(endpoint, "12") + + advanceUntilIdle() + coVerify { + pushRegistrationManager.processSubscription(12, endpoint) + } + confirmVerified(pushRegistrationManager) + } + + @Test + fun testOnRegistrationFailed() = runTest { + unifiedPushService.onRegistrationFailed(FailedReason.INTERNAL_ERROR, "34") + + advanceUntilIdle() + coVerify { + pushRegistrationManager.removeSubscription(34) + } + confirmVerified(pushRegistrationManager) + } + + @Test + fun testOnUnregistered() = runTest { + unifiedPushService.onUnregistered("45") + + advanceUntilIdle() + coVerify { + pushRegistrationManager.removeSubscription(45) + } + confirmVerified(pushRegistrationManager) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/AccountRepositoryTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/AccountRepositoryTest.kt new file mode 100644 index 0000000..9a06da1 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/AccountRepositoryTest.kt @@ -0,0 +1,213 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.repository + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import androidx.hilt.work.HiltWorkerFactory +import at.bitfire.davdroid.R +import at.bitfire.davdroid.TestUtils +import at.bitfire.davdroid.resource.LocalAddressBookStore +import at.bitfire.davdroid.resource.LocalCalendarStore +import at.bitfire.davdroid.resource.LocalDataStore +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.sync.AutomaticSyncManager +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.sync.TasksAppManager +import at.bitfire.davdroid.sync.account.AccountsCleanupWorker +import at.bitfire.davdroid.sync.account.TestAccount +import at.bitfire.davdroid.sync.worker.SyncWorkerManager +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.clearAllMocks +import io.mockk.coVerify +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.junit4.MockKRule +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkObject +import io.mockk.verify +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class AccountRepositoryTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val mockKRule = MockKRule(this) + + // System under test + + @Inject + lateinit var accountRepository: AccountRepository + + // Real injections + + @Inject + @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var accountSettingsFactory: AccountSettings.Factory + + @Inject + lateinit var workerFactory: HiltWorkerFactory + + // Dependency overrides + + @BindValue @MockK(relaxed = true) + lateinit var automaticSyncManager: AutomaticSyncManager + + @BindValue @MockK(relaxed = true) + lateinit var localAddressBookStore: LocalAddressBookStore + + @BindValue @MockK(relaxed = true) + lateinit var localCalendarStore: LocalCalendarStore + + @BindValue @MockK(relaxed = true) + lateinit var serviceRepository: DavServiceRepository + + @BindValue @MockK(relaxed = true) + lateinit var syncWorkerManager: SyncWorkerManager + + @BindValue @MockK(relaxed = true) + lateinit var tasksAppManager: TasksAppManager + + + // Account setup + private val newName = "Renamed Account" + lateinit var am: AccountManager + lateinit var accountType: String + lateinit var account: Account + + @Before + fun setUp() { + hiltRule.inject() + TestUtils.setUpWorkManager(context, workerFactory) + + // Account setup + am = AccountManager.get(context) + accountType = context.getString(R.string.account_type) + account = TestAccount.create() + + // AccountsCleanupWorker static mocking + mockkObject(AccountsCleanupWorker) + every { AccountsCleanupWorker.lockAccountsCleanup() } returns Unit + } + + @After + fun tearDown() { + am.getAccountsByType(accountType).forEach { account -> + am.removeAccountExplicitly(account) + } + + unmockkObject(AccountsCleanupWorker) + clearAllMocks() + } + + + // testRename + + @Test(expected = IllegalArgumentException::class) + fun testRename_checksForAlreadyExisting() = runTest { + val existing = Account("Existing Account", accountType) + am.addAccountExplicitly(existing, null, null) + + accountRepository.rename(account.name, existing.name) + } + + @Test + fun testRename_locksAccountsCleanup() = runTest { + accountRepository.rename(account.name, newName) + + verify { AccountsCleanupWorker.lockAccountsCleanup() } + } + + @Test + fun testRename_renamesAccountInAndroid() = runTest { + accountRepository.rename(account.name, newName) + + val accountsAfter = am.getAccountsByType(accountType) + assertTrue(accountsAfter.any { it.name == newName }) + } + + @Test + fun testRename_cancelsRunningSynchronizationOfOldAccount() = runTest { + accountRepository.rename(account.name, newName) + + coVerify { syncWorkerManager.cancelAllWork(account) } + } + + @Test + fun testRename_disablesPeriodicSyncsForOldAccount() = runTest { + accountRepository.rename(account.name, newName) + + for (dataType in SyncDataType.entries) + coVerify(exactly = 1) { + syncWorkerManager.disablePeriodic(account, dataType) + } + } + + @Test + fun testRename_updatesAccountNameReferencesInDatabase() = runTest { + accountRepository.rename(account.name, newName) + + coVerify { serviceRepository.renameAccount(account.name, newName) } + } + + @Test + fun testRename_updatesAddressBooks() = runTest { + accountRepository.rename(account.name, newName) + + val newAccount = accountRepository.fromName(newName) + coVerify { localAddressBookStore.updateAccount(account, newAccount) } + } + + @Test + fun testRename_updatesCalendarEvents() = runTest { + accountRepository.rename(account.name, newName) + + val newAccount = accountRepository.fromName(newName) + coVerify { localCalendarStore.updateAccount(account, newAccount) } + } + + @Test + fun testRename_updatesAccountNameOfLocalTasks() = runTest { + val mockDataStore = mockk>(relaxed = true) + every { tasksAppManager.getDataStore() } returns mockDataStore + accountRepository.rename(account.name, newName) + + coVerify { mockDataStore.updateAccount(account, accountRepository.fromName(newName)) } + } + + @Test + fun testRename_updatesAutomaticSync() = runTest { + accountRepository.rename(account.name, newName) + + val newAccount = accountRepository.fromName(newName) + coVerify { automaticSyncManager.updateAutomaticSync(newAccount) } + } + + @Test + fun testRename_releasesAccountsCleanupWorkerMutex() = runTest { + accountRepository.rename(account.name, newName) + + verify { AccountsCleanupWorker.lockAccountsCleanup() } + coVerify { serviceRepository.renameAccount(account.name, newName) } + } + +} diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalAddressBookStoreTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalAddressBookStoreTest.kt new file mode 100644 index 0000000..f4f9269 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalAddressBookStoreTest.kt @@ -0,0 +1,225 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.ContentProviderClient +import android.content.Context +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.sync.account.TestAccount +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.every +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.junit4.MockKRule +import io.mockk.mockk +import io.mockk.mockkObject +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class LocalAddressBookStoreTest { + + @Inject @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var db: AppDatabase + + @Inject + lateinit var localAddressBookStore: LocalAddressBookStore + + @RelaxedMockK + lateinit var provider: ContentProviderClient + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val mockkRule = MockKRule(this) + + lateinit var addressBookAccountType: String + + lateinit var addressBookAccount: Account + lateinit var account: Account + lateinit var service: Service + + @Before + fun setUp() { + hiltRule.inject() + + addressBookAccountType = context.getString(R.string.account_type_address_book) + + account = TestAccount.create() + service = Service( + id = 200, + accountName = account.name, + type = Service.Companion.TYPE_CARDDAV, + principal = null + ) + db.serviceDao().insertOrReplace(service) + addressBookAccount = Account( + "MrRobert@example.com", + addressBookAccountType + ) + } + + @After + fun tearDown() { + TestAccount.remove(account) + removeAddressBooks() + } + + + @Test + fun test_accountName_removesSpecialChars() { + // Should remove iso control characters and `, ", ', + val collection = mockk { + every { id } returns 1 + every { url } returns "https://example.com/addressbook/funnyfriends".toHttpUrl() + every { displayName } returns "手 M's_\"F-e\"\\(´д`)/;æøå% äöü #42" + every { serviceId } returns service.id + } + assertEquals("手 Ms_F-e\\(´д)/;æøå% äöü #42 (Test Account) #1", localAddressBookStore.accountName(collection)) + } + + @Test + fun test_accountName_missingService() { + val collection = mockk { + every { id } returns 42 + every { url } returns "https://example.com/addressbook/funnyfriends".toHttpUrl() + every { displayName } returns null + every { serviceId } returns 404 // missing service + } + assertEquals("funnyfriends #42", localAddressBookStore.accountName(collection)) + } + + @Test + fun test_accountName_missingDisplayName() { + val collection = mockk { + every { id } returns 42 + every { url } returns "https://example.com/addressbook/funnyfriends".toHttpUrl() + every { displayName } returns null + every { serviceId } returns service.id + } + val accountName = localAddressBookStore.accountName(collection) + assertEquals("funnyfriends (${account.name}) #42", accountName) + } + + @Test + fun test_accountName_missingDisplayNameAndService() { + val collection = mockk { + every { id } returns 1 + every { url } returns "https://example.com/addressbook/funnyfriends".toHttpUrl() + every { displayName } returns null + every { serviceId } returns 404 // missing service + } + assertEquals("funnyfriends #1", localAddressBookStore.accountName(collection)) + } + + + @Test + fun test_create_createAccountReturnsNull() { + val collection = mockk(relaxed = true) { + every { serviceId } returns service.id + every { id } returns 1 + every { url } returns "https://example.com/addressbook/funnyfriends".toHttpUrl() + } + + mockkObject(localAddressBookStore) + every { localAddressBookStore.createAddressBookAccount(any(), any(), any()) } returns null + + assertEquals(null, localAddressBookStore.create(provider, collection)) + } + + @Test + fun test_create_ReadOnly() { + val collection = mockk(relaxed = true) { + every { serviceId } returns service.id + every { id } returns 1 + every { url } returns "https://example.com/addressbook/funnyfriends".toHttpUrl() + every { readOnly() } returns true + } + val addrBook = localAddressBookStore.create(provider, collection)!! + assertEquals(Account("funnyfriends (Test Account) #1", addressBookAccountType), addrBook.addressBookAccount) + assertTrue(addrBook.readOnly) + } + + @Test + fun test_create_ReadWrite() { + val collection = mockk(relaxed = true) { + every { serviceId } returns service.id + every { id } returns 1 + every { url } returns "https://example.com/addressbook/funnyfriends".toHttpUrl() + every { readOnly() } returns false + } + + val addrBook = localAddressBookStore.create(provider, collection)!! + assertEquals(Account("funnyfriends (Test Account) #1", addressBookAccountType), addrBook.addressBookAccount) + assertFalse(addrBook.readOnly) + } + + + @Test + fun test_getAll_differentAccount() { + val accountManager = AccountManager.get(context) + mockkObject(accountManager) + every { accountManager.getAccountsByType(any()) } returns arrayOf(addressBookAccount) + every { accountManager.getUserData(addressBookAccount, LocalAddressBook.USER_DATA_ACCOUNT_NAME) } returns "Another Unrelated Account" + every { accountManager.getUserData(addressBookAccount, LocalAddressBook.USER_DATA_ACCOUNT_TYPE) } returns account.type + val result = localAddressBookStore.getAll(account, provider) + assertTrue(result.isEmpty()) + } + + @Test + fun test_getAll_sameAccount() { + val accountManager = AccountManager.get(context) + mockkObject(accountManager) + every { accountManager.getAccountsByType(any()) } returns arrayOf(addressBookAccount) + every { accountManager.getUserData(addressBookAccount, LocalAddressBook.USER_DATA_ACCOUNT_NAME) } returns account.name + every { accountManager.getUserData(addressBookAccount, LocalAddressBook.USER_DATA_ACCOUNT_TYPE) } returns account.type + val result = localAddressBookStore.getAll(account, provider) + assertEquals(1, result.size) + assertEquals(addressBookAccount, result.first().addressBookAccount) + } + + + /** + * Tests the calculation of read only state is correct + */ + @Test + fun test_shouldBeReadOnly() { + val collectionReadOnly = mockk { every { readOnly() } returns true } + assertTrue(LocalAddressBookStore.shouldBeReadOnly(collectionReadOnly, false)) + assertTrue(LocalAddressBookStore.shouldBeReadOnly(collectionReadOnly, true)) + + val collectionNotReadOnly = mockk { every { readOnly() } returns false } + assertFalse(LocalAddressBookStore.shouldBeReadOnly(collectionNotReadOnly, false)) + assertTrue(LocalAddressBookStore.shouldBeReadOnly(collectionNotReadOnly, true)) + } + + + // helpers + + private fun removeAddressBooks() { + val accountManager = AccountManager.get(context) + accountManager.getAccountsByType(addressBookAccountType).forEach { + accountManager.removeAccountExplicitly(it) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalAddressBookTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalAddressBookTest.kt new file mode 100644 index 0000000..ebe69fd --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalAddressBookTest.kt @@ -0,0 +1,177 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.Manifest +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentUris +import android.content.Context +import android.provider.ContactsContract +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import at.bitfire.vcard4android.Contact +import at.bitfire.vcard4android.LabeledProperty +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import ezvcard.property.Telephone +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Rule +import org.junit.Test +import java.io.FileNotFoundException +import java.util.LinkedList +import java.util.Optional +import javax.inject.Inject + +@HiltAndroidTest +class LocalAddressBookTest { + + @Inject @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var localTestAddressBookProvider: LocalTestAddressBookProvider + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + val account = Account("Test Account", "Test Account Type") + + @Before + fun setUp() { + hiltRule.inject() + } + + + /** + * Tests whether contacts are moved (and not lost) when an address book is renamed. + */ + @Test + fun test_renameAccount_retainsContacts() { + localTestAddressBookProvider.provide(account, provider) { addressBook -> + // insert contact with data row + val uid = "12345" + val contact = Contact( + uid = uid, + displayName = "Test Contact", + phoneNumbers = LinkedList(listOf(LabeledProperty(Telephone("1234567890")))) + ) + val uri = LocalContact(addressBook, contact, null, null, 0).add() + val id = ContentUris.parseId(uri) + val localContact = addressBook.findContactById(id) + localContact.resetDirty() + assertFalse("Contact is dirty before moving", isContactDirty(addressBook, id)) + + // rename address book + val newName = "New Name" + addressBook.renameAccount(newName) + assertEquals(newName, addressBook.addressBookAccount.name) + + // check whether contact is still here (including data rows) and not dirty + val result = addressBook.findContactById(id) + assertFalse("Contact is dirty after moving", isContactDirty(addressBook, id)) + + val contact2 = result.getContact() + assertEquals(uid, contact2.uid) + assertEquals("Test Contact", contact2.displayName) + assertEquals("1234567890", contact2.phoneNumbers.first().component1().text) + } + } + + /** + * Tests whether groups are moved (and not lost) when an address book is renamed. + */ + @Test + fun test_renameAccount_retainsGroups() { + localTestAddressBookProvider.provide(account, provider) { addressBook -> + // insert group + val localGroup = LocalGroup(addressBook, Contact(displayName = "Test Group"), null, null, 0) + val uri = localGroup.add() + val id = ContentUris.parseId(uri) + + // make sure it's not dirty + localGroup.clearDirty(Optional.empty(), null, null) + assertFalse("Group is dirty before moving", isGroupDirty(addressBook, id)) + + // rename address book + val newName = "New Name" + assertTrue(addressBook.renameAccount(newName)) + assertEquals(newName, addressBook.addressBookAccount.name) + + // check whether group is still here and not dirty + val result = addressBook.findGroupById(id) + assertFalse("Group is dirty after moving", isGroupDirty(addressBook, id)) + + val group = result.getContact() + assertEquals("Test Group", group.displayName) + } + } + + + // helpers + + /** + * Returns the dirty flag of the given contact. + * + * @return true if the contact is dirty, false otherwise + * + * @throws FileNotFoundException if the contact can't be found + */ + fun isContactDirty(adddressBook: LocalAddressBook, id: Long): Boolean { + val uri = ContentUris.withAppendedId(adddressBook.rawContactsSyncUri(), id) + provider.query(uri, arrayOf(ContactsContract.RawContacts.DIRTY), null, null, null)?.use { cursor -> + if (cursor.moveToFirst()) + return cursor.getInt(0) != 0 + } + throw FileNotFoundException() + } + + /** + * Returns the dirty flag of the given contact group. + * + * @return true if the group is dirty, false otherwise + * + * @throws FileNotFoundException if the group can't be found + */ + fun isGroupDirty(adddressBook: LocalAddressBook, id: Long): Boolean { + val uri = ContentUris.withAppendedId(adddressBook.groupsSyncUri(), id) + provider.query(uri, arrayOf(ContactsContract.Groups.DIRTY), null, null, null)?.use { cursor -> + if (cursor.moveToFirst()) + return cursor.getInt(0) != 0 + } + throw FileNotFoundException() + } + + + companion object { + + @JvmField + @ClassRule + val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!! + + private lateinit var provider: ContentProviderClient + + @BeforeClass + @JvmStatic + fun connect() { + val context = InstrumentationRegistry.getInstrumentation().context + provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!! + } + + @AfterClass + @JvmStatic + fun disconnect() { + provider.close() + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalCalendarTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalCalendarTest.kt new file mode 100644 index 0000000..f0c7863 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalCalendarTest.kt @@ -0,0 +1,236 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentUris +import android.content.ContentValues +import android.content.Entity +import android.provider.CalendarContract +import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL +import android.provider.CalendarContract.Events +import androidx.core.content.contentValuesOf +import androidx.test.platform.app.InstrumentationRegistry +import at.bitfire.ical4android.Event +import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter +import at.bitfire.ical4android.util.MiscUtils.closeCompat +import at.bitfire.synctools.storage.calendar.AndroidCalendar +import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider +import at.bitfire.synctools.storage.calendar.AndroidEvent2 +import at.bitfire.synctools.test.InitCalendarProviderRule +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import net.fortuna.ical4j.model.property.DtStart +import net.fortuna.ical4j.model.property.RRule +import net.fortuna.ical4j.model.property.RecurrenceId +import net.fortuna.ical4j.model.property.Status +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import javax.inject.Inject + +@HiltAndroidTest +class LocalCalendarTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val initCalendarProviderRule: TestRule = InitCalendarProviderRule.initialize() + + @Inject + lateinit var localCalendarFactory: LocalCalendar.Factory + + private val account = Account("LocalCalendarTest", ACCOUNT_TYPE_LOCAL) + private lateinit var androidCalendar: AndroidCalendar + private lateinit var client: ContentProviderClient + private lateinit var calendar: LocalCalendar + + @Before + fun setUp() { + hiltRule.inject() + + val context = InstrumentationRegistry.getInstrumentation().targetContext + client = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!! + + val provider = AndroidCalendarProvider(account, client) + androidCalendar = provider.createAndGetCalendar(ContentValues()) + calendar = localCalendarFactory.create(androidCalendar) + } + + @After + fun tearDown() { + androidCalendar.delete() + client.closeCompat() + } + + + @Test + fun testDeleteDirtyEventsWithoutInstances_NoInstances_CancelledExceptions() { + // create recurring event with only deleted/cancelled instances + val event = Event().apply { + dtStart = DtStart("20220120T010203Z") + summary = "Event with 3 instances" + rRules.add(RRule("FREQ=DAILY;COUNT=3")) + exceptions.add(Event().apply { + recurrenceId = RecurrenceId("20220120T010203Z") + dtStart = DtStart("20220120T010203Z") + summary = "Cancelled exception on 1st day" + status = Status.VEVENT_CANCELLED + }) + exceptions.add(Event().apply { + recurrenceId = RecurrenceId("20220121T010203Z") + dtStart = DtStart("20220121T010203Z") + summary = "Cancelled exception on 2nd day" + status = Status.VEVENT_CANCELLED + }) + exceptions.add(Event().apply { + recurrenceId = RecurrenceId("20220122T010203Z") + dtStart = DtStart("20220122T010203Z") + summary = "Cancelled exception on 3rd day" + status = Status.VEVENT_CANCELLED + }) + } + calendar.add( + event = event, + fileName = "filename.ics", + eTag = null, + scheduleTag = null, + flags = LocalResource.FLAG_REMOTELY_PRESENT + ) + val localEvent = calendar.findByName("filename.ics")!! + val eventId = localEvent.id + + // set event as dirty + client.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply { + put(Events.DIRTY, 1) + }, null, null) + + // this method should mark the event as deleted + calendar.deleteDirtyEventsWithoutInstances() + + // verify that event is now marked as deleted + client.query( + ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), + arrayOf(Events.DELETED), null, null, null + )!!.use { cursor -> + cursor.moveToNext() + assertEquals(1, cursor.getInt(0)) + } + } + + @Test + // Needs InitCalendarProviderRule + fun testDeleteDirtyEventsWithoutInstances_Recurring_Instances() { + val event = Event().apply { + dtStart = DtStart("20220120T010203Z") + summary = "Event with 3 instances" + rRules.add(RRule("FREQ=DAILY;COUNT=3")) + } + calendar.add( + event = event, + fileName = "filename.ics", + eTag = null, + scheduleTag = null, + flags = LocalResource.FLAG_REMOTELY_PRESENT + ) + val localEvent = calendar.findByName("filename.ics")!! + val eventUrl = androidCalendar.eventUri(localEvent.id) + + // set event as dirty + client.update(eventUrl, contentValuesOf( + Events.DIRTY to 1 + ), null, null) + + // this method should mark the event as deleted + calendar.deleteDirtyEventsWithoutInstances() + + // verify that event is not marked as deleted + client.query(eventUrl, arrayOf(Events.DELETED), null, null, null)!!.use { cursor -> + cursor.moveToNext() + assertEquals(0, cursor.getInt(0)) + } + } + + /** + * Verifies that [LocalCalendar.removeNotDirtyMarked] works as expected. + * @param contentValues values to set on the event. Required: + * - [Events._ID] + * - [Events.DIRTY] + */ + private fun testRemoveNotDirtyMarked(contentValues: ContentValues) { + val id = androidCalendar.addEvent(Entity( + contentValuesOf( + Events.CALENDAR_ID to androidCalendar.id, + Events.DTSTART to System.currentTimeMillis(), + Events.DTEND to System.currentTimeMillis(), + Events.TITLE to "Some Event", + AndroidEvent2.COLUMN_FLAGS to 123 + ).apply { putAll(contentValues) } + )) + + calendar.removeNotDirtyMarked(123) + + assertNull(androidCalendar.getEvent(id)) + } + + @Test + fun testRemoveNotDirtyMarked_IdLargerThanIntMaxValue() = testRemoveNotDirtyMarked( + contentValuesOf(Events._ID to Int.MAX_VALUE.toLong() + 10, Events.DIRTY to 0) + ) + + @Test + fun testRemoveNotDirtyMarked_DirtyIs0() = testRemoveNotDirtyMarked( + contentValuesOf(Events._ID to 1, Events.DIRTY to 0) + ) + + @Test + fun testRemoveNotDirtyMarked_DirtyNull() = testRemoveNotDirtyMarked( + contentValuesOf(Events._ID to 1, Events.DIRTY to null) + ) + + /** + * Verifies that [LocalCalendar.markNotDirty] works as expected. + * @param contentValues values to set on the event. Required: + * - [Events.DIRTY] + */ + private fun testMarkNotDirty(contentValues: ContentValues) { + val id = androidCalendar.addEvent(Entity( + contentValuesOf( + Events.CALENDAR_ID to androidCalendar.id, + Events._ID to 1, + Events.DTSTART to System.currentTimeMillis(), + Events.DTEND to System.currentTimeMillis(), + Events.TITLE to "Some Event", + AndroidEvent2.COLUMN_FLAGS to 123 + ).apply { putAll(contentValues) } + )) + + val updated = calendar.markNotDirty(321) + assertEquals(1, updated) + assertEquals(321, androidCalendar.getEvent(id)?.flags) + } + + @Test + fun test_markNotDirty_DirtyIs0() = testMarkNotDirty( + contentValuesOf( + Events.DIRTY to 0 + ) + ) + + @Test + fun test_markNotDirty_DirtyIsNull() = testMarkNotDirty( + contentValuesOf( + Events.DIRTY to null + ) + ) + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalEventTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalEventTest.kt new file mode 100644 index 0000000..053f035 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalEventTest.kt @@ -0,0 +1,265 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.Manifest +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentUris +import android.content.ContentValues +import android.provider.CalendarContract +import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL +import android.provider.CalendarContract.Events +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import at.bitfire.ical4android.Event +import at.bitfire.ical4android.util.MiscUtils.closeCompat +import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider +import at.techbee.jtx.JtxContract.asSyncAdapter +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import net.fortuna.ical4j.model.property.DtStart +import net.fortuna.ical4j.model.property.RRule +import net.fortuna.ical4j.model.property.RecurrenceId +import net.fortuna.ical4j.model.property.Status +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.UUID +import javax.inject.Inject + +@HiltAndroidTest +class LocalEventTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR) + + @Inject + lateinit var localCalendarFactory: LocalCalendar.Factory + + private val account = Account("LocalCalendarTest", ACCOUNT_TYPE_LOCAL) + private lateinit var client: ContentProviderClient + private lateinit var calendar: LocalCalendar + + @Before + fun setUp() { + hiltRule.inject() + + val context = InstrumentationRegistry.getInstrumentation().targetContext + client = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!! + + val provider = AndroidCalendarProvider(account, client) + calendar = localCalendarFactory.create(provider.createAndGetCalendar(ContentValues())) + } + + @After + fun tearDown() { + calendar.androidCalendar.delete() + client.closeCompat() + } + + + @Test + fun testPrepareForUpload_NoUid() { + // create event + val event = Event().apply { + dtStart = DtStart("20220120T010203Z") + summary = "Event without uid" + } + + calendar.add( + event = event, + fileName = "filename.ics", + eTag = null, + scheduleTag = null, + flags = LocalResource.FLAG_REMOTELY_PRESENT + ) + val localEvent = calendar.findByName("filename.ics")!! + + // prepare for upload - this should generate a new random uuid, returned as filename + val fileNameWithSuffix = localEvent.prepareForUpload() + val fileName = fileNameWithSuffix.removeSuffix(".ics") + + // throws an exception if fileName is not an UUID + UUID.fromString(fileName) + + // UID in calendar storage should be the same as file name + client.query( + ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account), + arrayOf(Events.UID_2445), null, null, null + )!!.use { cursor -> + cursor.moveToFirst() + assertEquals(fileName, cursor.getString(0)) + } + } + + @Test + fun testPrepareForUpload_NormalUid() { + // create event + val event = Event().apply { + dtStart = DtStart("20220120T010203Z") + summary = "Event with normal uid" + uid = "some-event@hostname.tld" // old UID format, UUID would be new format + } + calendar.add( + event = event, + fileName = "filename.ics", + eTag = null, + scheduleTag = null, + flags = LocalResource.FLAG_REMOTELY_PRESENT + ) + val localEvent = calendar.findByName("filename.ics")!! + + // prepare for upload - this should use the UID for the file name + val fileNameWithSuffix = localEvent.prepareForUpload() + val fileName = fileNameWithSuffix.removeSuffix(".ics") + + assertEquals(event.uid, fileName) + + // UID in calendar storage should still be set, too + client.query( + ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account), + arrayOf(Events.UID_2445), null, null, null + )!!.use { cursor -> + cursor.moveToFirst() + assertEquals(fileName, cursor.getString(0)) + } + } + + @Test + fun testPrepareForUpload_UidHasDangerousChars() { + // create event + val event = Event().apply { + dtStart = DtStart("20220120T010203Z") + summary = "Event with funny uid" + uid = "https://www.example.com/events/asdfewfe-cxyb-ewrws-sadfrwerxyvser-asdfxye-" + } + calendar.add( + event = event, + fileName = "filename.ics", + eTag = null, + scheduleTag = null, + flags = LocalResource.FLAG_REMOTELY_PRESENT + ) + val localEvent = calendar.findByName("filename.ics")!! + + // prepare for upload - this should generate a new random uuid, returned as filename + val fileNameWithSuffix = localEvent.prepareForUpload() + val fileName = fileNameWithSuffix.removeSuffix(".ics") + + // throws an exception if fileName is not an UUID + UUID.fromString(fileName) + + // UID in calendar storage shouldn't have been changed + client.query( + ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account), + arrayOf(Events.UID_2445), null, null, null + )!!.use { cursor -> + cursor.moveToFirst() + assertEquals(event.uid, cursor.getString(0)) + } + } + + + @Test + fun testDeleteDirtyEventsWithoutInstances_NoInstances_Exdate() { + // TODO + } + + @Test + fun testDeleteDirtyEventsWithoutInstances_NoInstances_CancelledExceptions() { + // create recurring event with only deleted/cancelled instances + val event = Event().apply { + dtStart = DtStart("20220120T010203Z") + summary = "Event with 3 instances" + rRules.add(RRule("FREQ=DAILY;COUNT=3")) + exceptions.add(Event().apply { + recurrenceId = RecurrenceId("20220120T010203Z") + dtStart = DtStart("20220120T010203Z") + summary = "Cancelled exception on 1st day" + status = Status.VEVENT_CANCELLED + }) + exceptions.add(Event().apply { + recurrenceId = RecurrenceId("20220121T010203Z") + dtStart = DtStart("20220121T010203Z") + summary = "Cancelled exception on 2nd day" + status = Status.VEVENT_CANCELLED + }) + exceptions.add(Event().apply { + recurrenceId = RecurrenceId("20220122T010203Z") + dtStart = DtStart("20220122T010203Z") + summary = "Cancelled exception on 3rd day" + status = Status.VEVENT_CANCELLED + }) + } + calendar.add( + event = event, + fileName = "filename.ics", + eTag = null, + scheduleTag = null, + flags = LocalResource.FLAG_REMOTELY_PRESENT + ) + val localEvent = calendar.findByName("filename.ics")!! + val eventId = localEvent.id!! + + // set event as dirty + client.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply { + put(Events.DIRTY, 1) + }, null, null) + + // this method should mark the event as deleted + calendar.deleteDirtyEventsWithoutInstances() + + // verify that event is now marked as deleted + client.query( + ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), + arrayOf(Events.DELETED), null, null, null + )!!.use { cursor -> + cursor.moveToNext() + assertEquals(1, cursor.getInt(0)) + } + } + + @Test + fun testDeleteDirtyEventsWithoutInstances_Recurring_Instances() { + val event = Event().apply { + dtStart = DtStart("20220120T010203Z") + summary = "Event with 3 instances" + rRules.add(RRule("FREQ=DAILY;COUNT=3")) + } + calendar.add( + event = event, + fileName = "filename.ics", + eTag = null, + scheduleTag = null, + flags = LocalResource.FLAG_REMOTELY_PRESENT + ) + val localEvent = calendar.findByName("filename.ics")!! + val eventId = localEvent.id!! + + // set event as dirty + client.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply { + put(Events.DIRTY, 1) + }, null, null) + + // this method should mark the event as deleted + calendar.deleteDirtyEventsWithoutInstances() + + // verify that event is not marked as deleted + client.query( + ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), + arrayOf(Events.DELETED), null, null, null + )!!.use { cursor -> + cursor.moveToNext() + assertEquals(0, cursor.getInt(0)) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalGroupTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalGroupTest.kt new file mode 100644 index 0000000..cfa0a52 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalGroupTest.kt @@ -0,0 +1,278 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.Manifest +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.provider.ContactsContract +import android.provider.ContactsContract.CommonDataKinds.GroupMembership +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import at.bitfire.synctools.storage.ContactsBatchOperation +import at.bitfire.vcard4android.CachedGroupMembership +import at.bitfire.vcard4android.Contact +import at.bitfire.vcard4android.GroupMethod +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.Optional +import javax.inject.Inject + +@HiltAndroidTest +class LocalGroupTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!! + + @Inject @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var localTestAddressBookProvider: LocalTestAddressBookProvider + + lateinit var provider: ContentProviderClient + + val account = Account("Test Account", "Test Account Type") + + @Before + fun setUp() { + hiltRule.inject() + + val context = InstrumentationRegistry.getInstrumentation().context + provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!! + } + + @After + fun tearDown() { + provider.close() + } + + @Test + fun testApplyPendingMemberships_addPendingMembership() { + localTestAddressBookProvider.provide(account, provider, GroupMethod.GROUP_VCARDS) { ab -> + val contact1 = LocalContact(ab, Contact().apply { + uid = "test1" + displayName = "Test" + }, "test1.vcf", null, 0) + contact1.add() + + val group = newGroup(ab) + // set pending membership of contact1 + ab.provider!!.update( + ContentUris.withAppendedId(ab.groupsSyncUri(), group.id!!), + ContentValues().apply { + put(LocalGroup.COLUMN_PENDING_MEMBERS, LocalGroup.PendingMemberships(setOf("test1")).toString()) + }, + null, null + ) + + // pending membership -> contact1 should be added to group + LocalGroup.applyPendingMemberships(ab) + + // check group membership + ab.provider!!.query( + ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), arrayOf(GroupMembership.GROUP_ROW_ID, GroupMembership.RAW_CONTACT_ID), + "${GroupMembership.MIMETYPE}=?", arrayOf(GroupMembership.CONTENT_ITEM_TYPE), + null + )!!.use { cursor -> + assertTrue(cursor.moveToNext()) + assertEquals(group.id, cursor.getLong(0)) + assertEquals(contact1.id, cursor.getLong(1)) + + assertFalse(cursor.moveToNext()) + } + // check cached group membership + ab.provider!!.query( + ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), arrayOf(CachedGroupMembership.GROUP_ID, CachedGroupMembership.RAW_CONTACT_ID), + "${CachedGroupMembership.MIMETYPE}=?", arrayOf(CachedGroupMembership.CONTENT_ITEM_TYPE), + null + )!!.use { cursor -> + assertTrue(cursor.moveToNext()) + assertEquals(group.id, cursor.getLong(0)) + assertEquals(contact1.id, cursor.getLong(1)) + + assertFalse(cursor.moveToNext()) + } + } + } + + @Test + fun testApplyPendingMemberships_removeMembership() { + localTestAddressBookProvider.provide(account, provider, GroupMethod.GROUP_VCARDS) { ab -> + val contact1 = LocalContact(ab, Contact().apply { + uid = "test1" + displayName = "Test" + }, "test1.vcf", null, 0) + contact1.add() + + val group = newGroup(ab) + + // add contact1 to group + val batch = ContactsBatchOperation(ab.provider!!) + contact1.addToGroup(batch, group.id!!) + batch.commit() + + // no pending memberships -> membership should be removed + LocalGroup.applyPendingMemberships(ab) + + // check group membership + ab.provider!!.query( + ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), + arrayOf(GroupMembership.GROUP_ROW_ID, GroupMembership.RAW_CONTACT_ID), + "${GroupMembership.MIMETYPE}=?", + arrayOf(GroupMembership.CONTENT_ITEM_TYPE), + null + )!!.use { cursor -> + assertFalse(cursor.moveToNext()) + } + // check cached group membership + ab.provider!!.query( + ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), + arrayOf(CachedGroupMembership.GROUP_ID, CachedGroupMembership.RAW_CONTACT_ID), + "${CachedGroupMembership.MIMETYPE}=?", + arrayOf(CachedGroupMembership.CONTENT_ITEM_TYPE), + null + )!!.use { cursor -> + assertFalse(cursor.moveToNext()) + } + } + } + + @Test + fun testClearDirty_addCachedGroupMembership() { + localTestAddressBookProvider.provide(account, provider, GroupMethod.CATEGORIES) { ab -> + val group = newGroup(ab) + + val contact1 = + LocalContact(ab, Contact().apply { displayName = "Test" }, "fn.vcf", null, 0) + contact1.add() + + // insert group membership, but no cached group membership + ab.provider!!.insert( + ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), ContentValues().apply { + put(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE) + put(GroupMembership.RAW_CONTACT_ID, contact1.id) + put(GroupMembership.GROUP_ROW_ID, group.id) + } + ) + + group.clearDirty(Optional.empty(), null) + + // check cached group membership + ab.provider!!.query( + ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), + arrayOf(CachedGroupMembership.GROUP_ID, CachedGroupMembership.RAW_CONTACT_ID), + "${CachedGroupMembership.MIMETYPE}=?", + arrayOf(CachedGroupMembership.CONTENT_ITEM_TYPE), + null + )!!.use { cursor -> + assertTrue(cursor.moveToNext()) + assertEquals(group.id, cursor.getLong(0)) + assertEquals(contact1.id, cursor.getLong(1)) + + assertFalse(cursor.moveToNext()) + } + } + } + + @Test + fun testClearDirty_removeCachedGroupMembership() { + localTestAddressBookProvider.provide(account, provider, GroupMethod.CATEGORIES) { ab -> + val group = newGroup(ab) + + val contact1 = LocalContact(ab, Contact().apply { displayName = "Test" }, "fn.vcf", null, 0) + contact1.add() + + // insert cached group membership, but no group membership + ab.provider!!.insert( + ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), ContentValues().apply { + put(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE) + put(CachedGroupMembership.RAW_CONTACT_ID, contact1.id) + put(CachedGroupMembership.GROUP_ID, group.id) + } + ) + + group.clearDirty(Optional.empty(), null) + + // cached group membership should be gone + ab.provider!!.query( + ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), arrayOf(CachedGroupMembership.GROUP_ID, CachedGroupMembership.RAW_CONTACT_ID), + "${CachedGroupMembership.MIMETYPE}=?", arrayOf(CachedGroupMembership.CONTENT_ITEM_TYPE), + null + )!!.use { cursor -> + assertFalse(cursor.moveToNext()) + } + } + } + + @Test + fun testMarkMembersDirty() { + localTestAddressBookProvider.provide(account, provider, GroupMethod.CATEGORIES) { ab -> + val group = newGroup(ab) + + val contact1 = + LocalContact(ab, Contact().apply { displayName = "Test" }, "fn.vcf", null, 0) + contact1.add() + + val batch = ContactsBatchOperation(ab.provider!!) + contact1.addToGroup(batch, group.id!!) + batch.commit() + + assertEquals(0, ab.findDirty().size) + group.markMembersDirty() + assertEquals(contact1.id, ab.findDirty().first().id) + } + } + + @Test + fun testPrepareForUpload() { + localTestAddressBookProvider.provide(account, provider, GroupMethod.CATEGORIES) { ab -> + val group = newGroup(ab) + assertNull(group.getContact().uid) + + val fileName = group.prepareForUpload() + val newUid = group.getContact().uid + assertNotNull(newUid) + assertEquals("$newUid.vcf", fileName) + } + } + + @Test + fun testUpdate() { + localTestAddressBookProvider.provide(account, provider) { ab -> + val group = newGroup(ab) + group.update(Contact(displayName = "New Group Name"), null, null, null, 0) + } + } + + + // helpers + + private fun newGroup(addressBook: LocalAddressBook): LocalGroup = + LocalGroup(addressBook, + Contact().apply { + displayName = "Test Group" + }, null, null, 0 + ).apply { + add() + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalTestAddressBook.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalTestAddressBook.kt new file mode 100644 index 0000000..a21caba --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalTestAddressBook.kt @@ -0,0 +1,59 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.Context +import at.bitfire.davdroid.repository.DavCollectionRepository +import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.sync.adapter.SyncFrameworkIntegration +import at.bitfire.vcard4android.GroupMethod +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.qualifiers.ApplicationContext +import java.util.Optional +import java.util.logging.Logger + +/** + * A local address book that provides an easy way to set the group method in tests. + */ +class LocalTestAddressBook @AssistedInject constructor( + @Assisted account: Account, + @Assisted("addressBook") addressBookAccount: Account, + @Assisted provider: ContentProviderClient, + @Assisted override val groupMethod: GroupMethod, + accountSettingsFactory: AccountSettings.Factory, + collectionRepository: DavCollectionRepository, + @ApplicationContext context: Context, + logger: Logger, + serviceRepository: DavServiceRepository, + syncFramework: SyncFrameworkIntegration +): LocalAddressBook( + account = account, + _addressBookAccount = addressBookAccount, + provider = provider, + accountSettingsFactory = accountSettingsFactory, + collectionRepository = collectionRepository, + context = context, + dirtyVerifier = Optional.empty(), + logger = logger, + serviceRepository = serviceRepository, + syncFramework = syncFramework +) { + + @AssistedFactory + interface Factory { + fun create( + account: Account, + @Assisted("addressBook") addressBookAccount: Account, + provider: ContentProviderClient, + groupMethod: GroupMethod + ): LocalTestAddressBook + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalTestAddressBookProvider.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalTestAddressBookProvider.kt new file mode 100644 index 0000000..83b8d9d --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalTestAddressBookProvider.kt @@ -0,0 +1,72 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.ContentProviderClient +import android.content.Context +import at.bitfire.davdroid.R +import at.bitfire.vcard4android.GroupMethod +import dagger.hilt.android.qualifiers.ApplicationContext +import org.junit.Assert.assertTrue +import java.util.concurrent.atomic.AtomicInteger +import javax.inject.Inject + +/** + * Provides [LocalTestAddressBook]s in tests. + */ +class LocalTestAddressBookProvider @Inject constructor( + @ApplicationContext context: Context, + private val localTestAddressBookFactory: LocalTestAddressBook.Factory +) { + + /** + * Counter for creating unique address book names. + */ + val counter = AtomicInteger() + + val accountManager = AccountManager.get(context) + val accountType = context.getString(R.string.account_type_address_book) + + /** + * Creates and provides a new temporary [LocalTestAddressBook] for the given [account] and + * removes it again. + * + * @param account The DAVx5 account to use for the address book + * @param provider Content provider needed to access and modify the address book + * @param groupMethod The group method the address book should use + * @param block Function to execute with the temporary available address book + */ + fun provide( + account: Account, + provider: ContentProviderClient, + groupMethod: GroupMethod = GroupMethod.GROUP_VCARDS, + block: (LocalTestAddressBook) -> Unit + ) { + // create new address book account + val addressBookAccount = Account("Test Address Book ${counter.incrementAndGet()}", accountType) + assertTrue(accountManager.addAccountExplicitly(addressBookAccount, null, null)) + val addressBook = localTestAddressBookFactory.create(account, addressBookAccount, provider, groupMethod) + + // Empty the address book (Needed by LocalGroupTest) + for (contact in addressBook.queryContacts(null, null)) + contact.delete() + for (group in addressBook.queryGroups(null, null)) + group.delete() + + try { + // provide address book + block(addressBook) + } finally { + // recreate account of provided address book, since the account might have been renamed + val renamedAccount = Account(addressBook.addressBookAccount.name, addressBook.addressBookAccount.type) + + // remove address book account / address book + assertTrue(accountManager.removeAccountExplicitly(renamedAccount)) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/CachedGroupMembershipHandlerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/CachedGroupMembershipHandlerTest.kt new file mode 100644 index 0000000..ea9c5b6 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/CachedGroupMembershipHandlerTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource.contactrow + +import android.Manifest +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentValues +import android.content.Context +import android.provider.ContactsContract +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import at.bitfire.davdroid.resource.LocalContact +import at.bitfire.davdroid.resource.LocalTestAddressBookProvider +import at.bitfire.vcard4android.CachedGroupMembership +import at.bitfire.vcard4android.Contact +import at.bitfire.vcard4android.GroupMethod +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.AfterClass +import org.junit.Assert.assertArrayEquals +import org.junit.Before +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class CachedGroupMembershipHandlerTest { + + @Inject + @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var localTestAddressBookProvider: LocalTestAddressBookProvider + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + val account = Account("Test Account", "Test Account Type") + + @Before + fun inject() { + hiltRule.inject() + } + + + @Test + fun testMembership() { + localTestAddressBookProvider.provide(account, provider, GroupMethod.GROUP_VCARDS) { addressBook -> + val contact = Contact() + val localContact = LocalContact(addressBook, contact, null, null, 0) + CachedGroupMembershipHandler(localContact).handle(ContentValues().apply { + put(CachedGroupMembership.GROUP_ID, 123456) + put(CachedGroupMembership.RAW_CONTACT_ID, 789) + }, contact) + assertArrayEquals(arrayOf(123456L), localContact.cachedGroupMemberships.toArray()) + } + } + + + companion object { + + @JvmField + @ClassRule + val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!! + + private lateinit var provider: ContentProviderClient + + @BeforeClass + @JvmStatic + fun connect() { + val context = InstrumentationRegistry.getInstrumentation().context + provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!! + } + + @AfterClass + @JvmStatic + fun disconnect() { + provider.close() + } + + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/GroupMembershipBuilderTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/GroupMembershipBuilderTest.kt new file mode 100644 index 0000000..2e9e067 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/GroupMembershipBuilderTest.kt @@ -0,0 +1,103 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource.contactrow + +import android.Manifest +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.Context +import android.net.Uri +import android.provider.ContactsContract +import android.provider.ContactsContract.CommonDataKinds.GroupMembership +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import at.bitfire.davdroid.resource.LocalTestAddressBookProvider +import at.bitfire.vcard4android.Contact +import at.bitfire.vcard4android.GroupMethod +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class GroupMembershipBuilderTest { + + @Inject @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var localTestAddressBookProvider: LocalTestAddressBookProvider + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + val account = Account("Test Account", "Test Account Type") + + + @Before + fun inject() { + hiltRule.inject() + } + + + @Test + fun testCategories_GroupsAsCategories() { + val contact = Contact().apply { + categories += "TEST GROUP" + } + localTestAddressBookProvider.provide(account, provider, GroupMethod.CATEGORIES) { addressBookGroupsAsCategories -> + GroupMembershipBuilder(Uri.EMPTY, null, contact, addressBookGroupsAsCategories, false).build().also { result -> + assertEquals(1, result.size) + assertEquals(GroupMembership.CONTENT_ITEM_TYPE, result[0].values[GroupMembership.MIMETYPE]) + assertEquals(addressBookGroupsAsCategories.findOrCreateGroup("TEST GROUP"), result[0].values[GroupMembership.GROUP_ROW_ID]) + } + } + } + + @Test + fun testCategories_GroupsAsVCards() { + val contact = Contact().apply { + categories += "TEST GROUP" + } + localTestAddressBookProvider.provide(account, provider, GroupMethod.GROUP_VCARDS) { addressBookGroupsAsVCards -> + GroupMembershipBuilder(Uri.EMPTY, null, contact, addressBookGroupsAsVCards, false).build().also { result -> + // group membership is constructed during post-processing + assertEquals(0, result.size) + } + } + } + + + companion object { + + @JvmField + @ClassRule + val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!! + + private lateinit var provider: ContentProviderClient + + @BeforeClass + @JvmStatic + fun connect() { + val context: Context = InstrumentationRegistry.getInstrumentation().context + provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!! + } + + @AfterClass + @JvmStatic + fun disconnect() { + provider.close() + } + + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/GroupMembershipHandlerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/GroupMembershipHandlerTest.kt new file mode 100644 index 0000000..baeb26b --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/GroupMembershipHandlerTest.kt @@ -0,0 +1,110 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource.contactrow + +import android.Manifest +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentValues +import android.content.Context +import android.provider.ContactsContract +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import at.bitfire.davdroid.resource.LocalContact +import at.bitfire.davdroid.resource.LocalTestAddressBookProvider +import at.bitfire.vcard4android.CachedGroupMembership +import at.bitfire.vcard4android.Contact +import at.bitfire.vcard4android.GroupMethod +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.AfterClass +import org.junit.Assert +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class GroupMembershipHandlerTest { + + @Inject @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var localTestAddressBookProvider: LocalTestAddressBookProvider + + @get:Rule + var hiltRule = HiltAndroidRule(this) + + val account = Account("Test Account", "Test Account Type") + + @Before + fun inject() { + hiltRule.inject() + } + + + @Test + fun testMembership_GroupsAsCategories() { + localTestAddressBookProvider.provide(account, provider, GroupMethod.CATEGORIES) { addressBookGroupsAsCategories -> + val addressBookGroupsAsCategoriesGroup = addressBookGroupsAsCategories.findOrCreateGroup("TEST GROUP") + + val contact = Contact() + val localContact = LocalContact(addressBookGroupsAsCategories, contact, null, null, 0) + GroupMembershipHandler(localContact).handle(ContentValues().apply { + put(CachedGroupMembership.GROUP_ID, addressBookGroupsAsCategoriesGroup) + put(CachedGroupMembership.RAW_CONTACT_ID, -1) + }, contact) + assertArrayEquals(arrayOf(addressBookGroupsAsCategoriesGroup), localContact.groupMemberships.toArray()) + assertArrayEquals(arrayOf("TEST GROUP"), contact.categories.toArray()) + } + } + + + @Test + fun testMembership_GroupsAsVCards() { + localTestAddressBookProvider.provide(account, provider, GroupMethod.GROUP_VCARDS) { addressBookGroupsAsVCards -> + val contact = Contact() + val localContact = LocalContact(addressBookGroupsAsVCards, contact, null, null, 0) + GroupMembershipHandler(localContact).handle(ContentValues().apply { + put(CachedGroupMembership.GROUP_ID, 12345) // because the group name is not queried and put into CATEGORIES, the group doesn't have to really exist + put(CachedGroupMembership.RAW_CONTACT_ID, -1) + }, contact) + assertArrayEquals(arrayOf(12345L), localContact.groupMemberships.toArray()) + assertTrue(contact.categories.isEmpty()) + } + } + + + companion object { + + @JvmField + @ClassRule + val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!! + + private lateinit var provider: ContentProviderClient + + @BeforeClass + @JvmStatic + fun connect() { + val context: Context = InstrumentationRegistry.getInstrumentation().context + provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!! + Assert.assertNotNull(provider) + } + + @AfterClass + @JvmStatic + fun disconnect() { + provider.close() + } + + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownPropertiesBuilderTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownPropertiesBuilderTest.kt new file mode 100644 index 0000000..5795687 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownPropertiesBuilderTest.kt @@ -0,0 +1,32 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource.contactrow + +import android.net.Uri +import at.bitfire.vcard4android.Contact +import org.junit.Assert.assertEquals +import org.junit.Test + +class UnknownPropertiesBuilderTest { + + @Test + fun testUnknownProperties_None() { + UnknownPropertiesBuilder(Uri.EMPTY, null, Contact(), false).build().also { result -> + assertEquals(0, result.size) + } + } + + @Test + fun testUnknownProperties_Properties() { + UnknownPropertiesBuilder(Uri.EMPTY, null, Contact().apply { + unknownProperties = "X-TEST:12345" + }, false).build().also { result -> + assertEquals(1, result.size) + assertEquals(UnknownProperties.CONTENT_ITEM_TYPE, result[0].values[UnknownProperties.MIMETYPE]) + assertEquals("X-TEST:12345", result[0].values[UnknownProperties.UNKNOWN_PROPERTIES]) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownPropertiesHandlerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownPropertiesHandlerTest.kt new file mode 100644 index 0000000..f1a5a24 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownPropertiesHandlerTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource.contactrow + +import android.content.ContentValues +import at.bitfire.vcard4android.Contact +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class UnknownPropertiesHandlerTest { + + @Test + fun testUnknownProperties_Empty() { + val contact = Contact() + UnknownPropertiesHandler.handle(ContentValues().apply { + putNull(UnknownProperties.UNKNOWN_PROPERTIES) + }, contact) + assertNull(contact.unknownProperties) + } + + @Test + fun testUnknownProperties_Values() { + val contact = Contact() + UnknownPropertiesHandler.handle(ContentValues().apply { + put(UnknownProperties.UNKNOWN_PROPERTIES, "X-TEST:12345") + }, contact) + assertEquals("X-TEST:12345", contact.unknownProperties) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/CollectionsWithoutHomeSetRefresherTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/CollectionsWithoutHomeSetRefresherTest.kt new file mode 100644 index 0000000..aa4fcf5 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/CollectionsWithoutHomeSetRefresherTest.kt @@ -0,0 +1,222 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.servicedetection + +import android.security.NetworkSecurityPolicy +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.settings.SettingsManager +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.impl.annotations.MockK +import io.mockk.junit4.MockKRule +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assume +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.logging.Logger +import javax.inject.Inject + +@HiltAndroidTest +class CollectionsWithoutHomeSetRefresherTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val mockKRule = MockKRule(this) + + @Inject + lateinit var db: AppDatabase + + @Inject + lateinit var httpClientBuilder: HttpClient.Builder + + @Inject + lateinit var logger: Logger + + @Inject + lateinit var refresherFactory: CollectionsWithoutHomeSetRefresher.Factory + + @BindValue + @MockK(relaxed = true) + lateinit var settings: SettingsManager + + private lateinit var client: HttpClient + private lateinit var mockServer: MockWebServer + private lateinit var service: Service + + @Before + fun setUp() { + hiltRule.inject() + + // Start mock web server + mockServer = MockWebServer().apply { + dispatcher = TestDispatcher(logger) + start() + } + + // build HTTP client + client = httpClientBuilder.build() + Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted) + + // insert test service + val serviceId = db.serviceDao().insertOrReplace( + Service(id = 0, accountName = "test", type = Service.TYPE_CARDDAV, principal = null) + ) + service = db.serviceDao().get(serviceId)!! + } + + @After + fun tearDown() { + client.close() + mockServer.shutdown() + } + + + // refreshCollectionsWithoutHomeSet + + @Test + fun refreshCollectionsWithoutHomeSet_updatesExistingCollection() { + // place homeless collection in DB + val collectionId = db.collectionDao().insertOrUpdateByUrl( + Collection( + 0, + service.id, + null, + null, + Collection.TYPE_ADDRESSBOOK, + mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), + ) + ) + + // Refresh + refresherFactory.create(service, client.okHttpClient).refreshCollectionsWithoutHomeSet() + + // Check the collection got updated - with display name and description + assertEquals( + Collection( + collectionId, + service.id, + null, + 1, // will have gotten an owner too + Collection.TYPE_ADDRESSBOOK, + mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), + displayName = "My Contacts", + description = "My Contacts Description" + ), + db.collectionDao().get(collectionId) + ) + } + + @Test + fun refreshCollectionsWithoutHomeSet_deletesInaccessibleCollectionsWithoutHomeSet() { + // place homeless collection in DB - it is also inaccessible + val collectionId = db.collectionDao().insertOrUpdateByUrl( + Collection( + 0, + service.id, + null, + null, + Collection.TYPE_ADDRESSBOOK, + mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_INACCESSIBLE") + ) + ) + + // Refresh - should delete collection + refresherFactory.create(service, client.okHttpClient).refreshCollectionsWithoutHomeSet() + + // Check the collection got deleted + assertEquals(null, db.collectionDao().get(collectionId)) + } + + @Test + fun refreshCollectionsWithoutHomeSet_addsOwnerUrls() { + // place homeless collection in DB + val collectionId = db.collectionDao().insertOrUpdateByUrl( + Collection( + 0, + service.id, + null, + null, + Collection.TYPE_ADDRESSBOOK, + mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), + ) + ) + + // Refresh homeless collections + assertEquals(0, db.principalDao().getByService(service.id).size) + refresherFactory.create(service, client.okHttpClient).refreshCollectionsWithoutHomeSet() + + // Check principal saved and the collection was updated with its reference + val principals = db.principalDao().getByService(service.id) + assertEquals(1, principals.size) + assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL"), principals[0].url) + assertEquals(null, principals[0].displayName) + assertEquals( + principals[0].id, + db.collectionDao().get(collectionId)!!.ownerId + ) + } + + + companion object { + private const val PATH_CARDDAV = "/carddav" + private const val SUBPATH_PRINCIPAL = "/principal" + private const val SUBPATH_ADDRESSBOOK = "/addressbooks/my-contacts" + private const val SUBPATH_ADDRESSBOOK_INACCESSIBLE = "/addressbooks/inaccessible-contacts" + } + + class TestDispatcher( + private val logger: Logger + ): Dispatcher() { + + override fun dispatch(request: RecordedRequest): MockResponse { + val path = request.path!!.trimEnd('/') + logger.info("${request.method} on $path") + + if (request.method.equals("PROPFIND", true)) { + val properties = when (path) { + PATH_CARDDAV + SUBPATH_ADDRESSBOOK -> + "" + + " " + + " " + + "" + + "My Contacts" + + "My Contacts Description" + + "" + + " ${PATH_CARDDAV + SUBPATH_PRINCIPAL}" + + "" + + else -> "" + } + + return MockResponse() + .setResponseCode(207) + .setBody("" + + "" + + " $path" + + " "+ + properties + + " " + + "" + + "") + } + + return MockResponse().setResponseCode(404) + } + + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinderTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinderTest.kt new file mode 100644 index 0000000..a77ca84 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinderTest.kt @@ -0,0 +1,230 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.servicedetection + +import android.security.NetworkSecurityPolicy +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.property.carddav.AddressbookHomeSet +import at.bitfire.dav4jvm.property.webdav.ResourceType +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.servicedetection.DavResourceFinder.Configuration.ServiceInfo +import at.bitfire.davdroid.settings.Credentials +import at.bitfire.davdroid.util.SensitiveString.Companion.toSensitiveString +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.junit.After +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assume +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.net.URI +import java.util.logging.Logger +import javax.inject.Inject + +@HiltAndroidTest +class DavResourceFinderTest { + + companion object { + private const val PATH_NO_DAV = "/nodav" + private const val PATH_CALDAV = "/caldav" + private const val PATH_CARDDAV = "/carddav" + private const val PATH_CALDAV_AND_CARDDAV = "/both-caldav-carddav" + + private const val SUBPATH_PRINCIPAL = "/principal" + private const val SUBPATH_ADDRESSBOOK_HOMESET = "/addressbooks" + private const val SUBPATH_ADDRESSBOOK = "/addressbooks/private-contacts" + } + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var httpClientBuilder: HttpClient.Builder + + @Inject + lateinit var logger: Logger + + @Inject + lateinit var resourceFinderFactory: DavResourceFinder.Factory + + private lateinit var server: MockWebServer + private lateinit var client: HttpClient + private lateinit var finder: DavResourceFinder + + @Before + fun setUp() { + hiltRule.inject() + + server = MockWebServer().apply { + dispatcher = TestDispatcher(logger) + start() + } + + val credentials = Credentials(username = "mock", password = "12345".toSensitiveString()) + client = httpClientBuilder + .authenticate(host = null, getCredentials = { credentials }) + .build() + Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted) + + val baseURI = URI.create("/") + finder = resourceFinderFactory.create(baseURI, credentials) + } + + @After + fun tearDown() { + client.close() + server.shutdown() + } + + + @Test + fun testRememberIfAddressBookOrHomeset() { + // recognize home set + var info = ServiceInfo() + DavResource(client.okHttpClient, server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL)) + .propfind(0, AddressbookHomeSet.NAME) { response, _ -> + finder.scanResponse(ResourceType.ADDRESSBOOK, response, info) + } + assertEquals(0, info.collections.size) + assertEquals(1, info.homeSets.size) + assertEquals(server.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET/"), info.homeSets.first()) + + // recognize address book + info = ServiceInfo() + DavResource(client.okHttpClient, server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK)) + .propfind(0, ResourceType.NAME) { response, _ -> + finder.scanResponse(ResourceType.ADDRESSBOOK, response, info) + } + assertEquals(1, info.collections.size) + assertEquals(server.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), info.collections.keys.first()) + assertEquals(0, info.homeSets.size) + } + + @Test + fun testProvidesService() { + assertFalse(finder.providesService(server.url(PATH_NO_DAV), DavResourceFinder.Service.CALDAV)) + assertFalse(finder.providesService(server.url(PATH_NO_DAV), DavResourceFinder.Service.CARDDAV)) + + assertTrue(finder.providesService(server.url(PATH_CALDAV), DavResourceFinder.Service.CALDAV)) + assertFalse(finder.providesService(server.url(PATH_CALDAV), DavResourceFinder.Service.CARDDAV)) + + assertTrue(finder.providesService(server.url(PATH_CARDDAV), DavResourceFinder.Service.CARDDAV)) + assertFalse(finder.providesService(server.url(PATH_CARDDAV), DavResourceFinder.Service.CALDAV)) + + assertTrue(finder.providesService(server.url(PATH_CALDAV_AND_CARDDAV), DavResourceFinder.Service.CALDAV)) + assertTrue(finder.providesService(server.url(PATH_CALDAV_AND_CARDDAV), DavResourceFinder.Service.CARDDAV)) + } + + @Test + fun testGetCurrentUserPrincipal() { + assertNull(finder.getCurrentUserPrincipal(server.url(PATH_NO_DAV), DavResourceFinder.Service.CALDAV)) + assertNull(finder.getCurrentUserPrincipal(server.url(PATH_NO_DAV), DavResourceFinder.Service.CARDDAV)) + + assertEquals( + server.url(PATH_CALDAV + SUBPATH_PRINCIPAL), + finder.getCurrentUserPrincipal(server.url(PATH_CALDAV), DavResourceFinder.Service.CALDAV) + ) + assertNull(finder.getCurrentUserPrincipal(server.url(PATH_CALDAV), DavResourceFinder.Service.CARDDAV)) + + assertEquals( + server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL), + finder.getCurrentUserPrincipal(server.url(PATH_CARDDAV), DavResourceFinder.Service.CARDDAV) + ) + assertNull(finder.getCurrentUserPrincipal(server.url(PATH_CARDDAV), DavResourceFinder.Service.CALDAV)) + } + + @Test + fun testQueryEmailAddress() { + var info = ServiceInfo() + assertArrayEquals( + arrayOf("email1@example.com", "email2@example.com"), + finder.queryEmailAddress(server.url(PATH_CALDAV + SUBPATH_PRINCIPAL)).toTypedArray() + ) + assertTrue(finder.queryEmailAddress(server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL)).isEmpty()) + } + + + // mock server + + class TestDispatcher( + private val logger: Logger + ): Dispatcher() { + + override fun dispatch(request: RecordedRequest): MockResponse { + if (!checkAuth(request)) { + val authenticate = MockResponse().setResponseCode(401) + authenticate.setHeader("WWW-Authenticate", "Basic realm=\"test\"") + return authenticate + } + + val path = request.path!! + + if (request.method.equals("OPTIONS", true)) { + val dav = when { + path.startsWith(PATH_CALDAV) -> "calendar-access" + path.startsWith(PATH_CARDDAV) -> "addressbook" + path.startsWith(PATH_CALDAV_AND_CARDDAV) -> "calendar-access, addressbook" + else -> null + } + val response = MockResponse().setResponseCode(200) + if (dav != null) + response.addHeader("DAV", dav) + return response + } else if (request.method.equals("PROPFIND", true)) { + val props: String? + when (path) { + PATH_CALDAV, + PATH_CARDDAV -> + props = "$path$SUBPATH_PRINCIPAL" + + PATH_CARDDAV + SUBPATH_PRINCIPAL -> + props = "" + + " $PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET" + + "" + + PATH_CARDDAV + SUBPATH_ADDRESSBOOK -> + props = "" + + " " + + " " + + "" + + PATH_CALDAV + SUBPATH_PRINCIPAL -> + props = "" + + " urn:unknown-entry" + + " mailto:email1@example.com" + + " mailto:email2@example.com" + + "" + + else -> props = null + } + logger.info("Sending props: $props") + return MockResponse() + .setResponseCode(207) + .setBody("" + + "" + + " ${request.path}" + + " $props" + + "" + + "") + } + + return MockResponse().setResponseCode(404) + } + + private fun checkAuth(rq: RecordedRequest) = + rq.getHeader("Authorization") == "Basic bW9jazoxMjM0NQ==" + + } + +} diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/HomeSetRefresherTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/HomeSetRefresherTest.kt new file mode 100644 index 0000000..5607b75 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/HomeSetRefresherTest.kt @@ -0,0 +1,473 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.servicedetection + +import android.security.NetworkSecurityPolicy +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.HomeSet +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.settings.Settings +import at.bitfire.davdroid.settings.SettingsManager +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.junit4.MockKRule +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assume +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.logging.Logger +import javax.inject.Inject + +@HiltAndroidTest +class HomeSetRefresherTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val mockKRule = MockKRule(this) + + @Inject + lateinit var db: AppDatabase + + @Inject + lateinit var httpClientBuilder: HttpClient.Builder + + @Inject + lateinit var logger: Logger + + @Inject + lateinit var homeSetRefresherFactory: HomeSetRefresher.Factory + + @BindValue + @MockK(relaxed = true) + lateinit var settings: SettingsManager + + private lateinit var client: HttpClient + private lateinit var mockServer: MockWebServer + private lateinit var service: Service + + @Before + fun setUp() { + hiltRule.inject() + + // Start mock web server + mockServer = MockWebServer().apply { + dispatcher = TestDispatcher(logger) + start() + } + + // build HTTP client + client = httpClientBuilder.build() + Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted) + + // insert test service + val serviceId = db.serviceDao().insertOrReplace( + Service(id = 0, accountName = "test", type = Service.TYPE_CARDDAV, principal = null) + ) + service = db.serviceDao().get(serviceId)!! + } + + @After + fun tearDown() { + client.close() + mockServer.shutdown() + } + + + // refreshHomesetsAndTheirCollections + + @Test + fun refreshHomesetsAndTheirCollections_addsNewCollection() = runTest { + // save homeset in DB + val homesetId = db.homeSetDao().insert( + HomeSet(id = 0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL")) + ) + + // Refresh + homeSetRefresherFactory.create(service, client.okHttpClient) + .refreshHomesetsAndTheirCollections() + + // Check the collection defined in homeset is now in the database + assertEquals( + Collection( + 1, + service.id, + homesetId, + 1, // will have gotten an owner too + Collection.TYPE_ADDRESSBOOK, + mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), + displayName = "My Contacts", + description = "My Contacts Description" + ), + db.collectionDao().getByService(service.id).first() + ) + } + + @Test + fun refreshHomesetsAndTheirCollections_updatesExistingCollection() { + // save "old" collection in DB + val collectionId = db.collectionDao().insertOrUpdateByUrl( + Collection( + 0, + service.id, + null, + null, + Collection.TYPE_ADDRESSBOOK, + mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), + displayName = "My Contacts", + description = "My Contacts Description" + ) + ) + + // Refresh + homeSetRefresherFactory.create(service, client.okHttpClient).refreshHomesetsAndTheirCollections() + + // Check the collection got updated + assertEquals( + Collection( + collectionId, + service.id, + null, + null, + Collection.TYPE_ADDRESSBOOK, + mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), + displayName = "My Contacts", + description = "My Contacts Description" + ), + db.collectionDao().get(collectionId) + ) + } + + @Test + fun refreshHomesetsAndTheirCollections_preservesCollectionFlags() { + // save "old" collection in DB - with set flags + val collectionId = db.collectionDao().insertOrUpdateByUrl( + Collection( + 0, + service.id, + null, + null, + Collection.TYPE_ADDRESSBOOK, + mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), + displayName = "My Contacts", + description = "My Contacts Description", + forceReadOnly = true, + sync = true + ) + ) + + // Refresh + homeSetRefresherFactory.create(service, client.okHttpClient).refreshHomesetsAndTheirCollections() + + // Check the collection got updated + assertEquals( + Collection( + collectionId, + service.id, + null, + null, + Collection.TYPE_ADDRESSBOOK, + mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), + displayName = "My Contacts", + description = "My Contacts Description", + forceReadOnly = true, + sync = true + ), + db.collectionDao().get(collectionId) + ) + } + + @Test + fun refreshHomesetsAndTheirCollections_marksRemovedCollectionsAsHomeless() { + // save homeset in DB - which is empty (zero address books) on the serverside + val homesetId = db.homeSetDao().insert( + HomeSet(id = 0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_EMPTY")) + ) + + // place collection in DB - as part of the homeset + val collectionId = db.collectionDao().insertOrUpdateByUrl( + Collection( + 0, + service.id, + homesetId, + null, + Collection.TYPE_ADDRESSBOOK, + mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/") + ) + ) + + // Refresh - should mark collection as homeless, because serverside homeset is empty. + homeSetRefresherFactory.create(service, client.okHttpClient).refreshHomesetsAndTheirCollections() + + // Check the collection, is now marked as homeless + assertEquals(null, db.collectionDao().get(collectionId)!!.homeSetId) + } + + @Test + fun refreshHomesetsAndTheirCollections_addsOwnerUrls() { + // save a homeset in DB + val homesetId = db.homeSetDao().insert( + HomeSet(id = 0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL")) + ) + + // place collection in DB - as part of the homeset + val collectionId = db.collectionDao().insertOrUpdateByUrl( + Collection( + 0, + service.id, + homesetId, // part of above home set + null, + Collection.TYPE_ADDRESSBOOK, + mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/") + ) + ) + + // Refresh - homesets and their collections + assertEquals(0, db.principalDao().getByService(service.id).size) + homeSetRefresherFactory.create(service, client.okHttpClient).refreshHomesetsAndTheirCollections() + + // Check principal saved and the collection was updated with its reference + val principals = db.principalDao().getByService(service.id) + assertEquals(1, principals.size) + assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL"), principals[0].url) + assertEquals(null, principals[0].displayName) + assertEquals( + principals[0].id, + db.collectionDao().get(collectionId)!!.ownerId + ) + } + + + // other + + @Test + fun shouldPreselect_none() { + every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_NONE + every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns "" + + val collection = Collection( + 0, + service.id, + 0, + type = Collection.TYPE_ADDRESSBOOK, + url = mockServer.url("/addressbook-homeset/addressbook/") + ) + val homesets = listOf( + HomeSet( + id = 0, + serviceId = service.id, + personal = true, + url = mockServer.url("/addressbook-homeset/") + ) + ) + + val refresher = homeSetRefresherFactory.create(service, client.okHttpClient) + assertFalse(refresher.shouldPreselect(collection, homesets)) + } + + @Test + fun shouldPreselect_all() { + every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_ALL + every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns "" + + val collection = Collection( + 0, + service.id, + 0, + type = Collection.TYPE_ADDRESSBOOK, + url = mockServer.url("/addressbook-homeset/addressbook/") + ) + val homesets = listOf( + HomeSet( + id = 0, + serviceId = service.id, + personal = false, + url = mockServer.url("/addressbook-homeset/") + ) + ) + + val refresher = homeSetRefresherFactory.create(service, client.okHttpClient) + assertTrue(refresher.shouldPreselect(collection, homesets)) + } + + @Test + fun shouldPreselect_all_blacklisted() { + val url = mockServer.url("/addressbook-homeset/addressbook/") + + every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_ALL + every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns url.toString() + + val collection = Collection( + id = 0, + serviceId = service.id, + homeSetId = 0, + type = Collection.TYPE_ADDRESSBOOK, + url = url + ) + val homesets = listOf( + HomeSet( + id = 0, + serviceId = service.id, + personal = false, + url = mockServer.url("/addressbook-homeset/") + ) + ) + + val refresher = homeSetRefresherFactory.create(service, client.okHttpClient) + assertFalse(refresher.shouldPreselect(collection, homesets)) + } + + @Test + fun shouldPreselect_personal_notPersonal() { + every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_PERSONAL + every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns "" + + val collection = Collection( + id = 0, + serviceId = service.id, + homeSetId = 0, + type = Collection.TYPE_ADDRESSBOOK, + url = mockServer.url("/addressbook-homeset/addressbook/") + ) + val homesets = listOf( + HomeSet( + id = 0, + serviceId = service.id, + personal = false, + url = mockServer.url("/addressbook-homeset/") + ) + ) + + val refresher = homeSetRefresherFactory.create(service, client.okHttpClient) + assertFalse(refresher.shouldPreselect(collection, homesets)) + } + + @Test + fun shouldPreselect_personal_isPersonal() { + every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_PERSONAL + every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns "" + + val collection = Collection( + 0, + service.id, + 0, + type = Collection.TYPE_ADDRESSBOOK, + url = mockServer.url("/addressbook-homeset/addressbook/") + ) + val homesets = listOf( + HomeSet( + id = 0, + serviceId = service.id, + personal = true, + url = mockServer.url("/addressbook-homeset/") + ) + ) + + val refresher = homeSetRefresherFactory.create(service, client.okHttpClient) + assertTrue(refresher.shouldPreselect(collection, homesets)) + } + + @Test + fun shouldPreselect_personal_isPersonalButBlacklisted() { + val collectionUrl = mockServer.url("/addressbook-homeset/addressbook/") + + every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_PERSONAL + every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns collectionUrl.toString() + + val collection = Collection( + id = 0, + serviceId = service.id, + homeSetId = 0, + type = Collection.TYPE_ADDRESSBOOK, + url = collectionUrl + ) + val homesets = listOf( + HomeSet( + id = 0, + serviceId = service.id, + personal = true, + url = mockServer.url("/addressbook-homeset/") + ) + ) + + val refresher = homeSetRefresherFactory.create(service, client.okHttpClient) + assertFalse(refresher.shouldPreselect(collection, homesets)) + } + + + companion object { + + private const val PATH_CARDDAV = "/carddav" + + private const val SUBPATH_PRINCIPAL = "/principal" + private const val SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL = "/addressbooks-homeset" + private const val SUBPATH_ADDRESSBOOK_HOMESET_EMPTY = "/addressbooks-homeset-empty" + private const val SUBPATH_ADDRESSBOOK = "/addressbooks/my-contacts" + + } + + class TestDispatcher( + private val logger: Logger + ) : Dispatcher() { + + override fun dispatch(request: RecordedRequest): MockResponse { + val path = request.path!!.trimEnd('/') + + if (request.method.equals("PROPFIND", true)) { + val properties = when (path) { + + PATH_CARDDAV + SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL -> + "" + + " " + + " " + + "" + + "My Contacts" + + "My Contacts Description" + + "" + + " ${PATH_CARDDAV + SUBPATH_PRINCIPAL}" + + "" + + SUBPATH_ADDRESSBOOK_HOMESET_EMPTY -> "" + + else -> "" + } + + logger.info("Queried: $path") + return MockResponse() + .setResponseCode(207) + .setBody( + "" + + "" + + " ${PATH_CARDDAV + SUBPATH_ADDRESSBOOK}" + + " " + + properties + + " " + + " HTTP/1.1 200 OK" + + "" + + "" + ) + } + + return MockResponse().setResponseCode(404) + } + + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/PrincipalsRefresherTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/PrincipalsRefresherTest.kt new file mode 100644 index 0000000..231f734 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/PrincipalsRefresherTest.kt @@ -0,0 +1,236 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.servicedetection + +import android.security.NetworkSecurityPolicy +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Principal +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.settings.SettingsManager +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.impl.annotations.MockK +import io.mockk.junit4.MockKRule +import junit.framework.TestCase.assertEquals +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.logging.Logger +import javax.inject.Inject + +@HiltAndroidTest +class PrincipalsRefresherTest { + + @Inject + lateinit var db: AppDatabase + + @Inject + lateinit var httpClientBuilder: HttpClient.Builder + + @Inject + lateinit var logger: Logger + + @Inject + lateinit var principalsRefresher: PrincipalsRefresher.Factory + + @BindValue + @MockK(relaxed = true) + lateinit var settings: SettingsManager + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val mockKRule = MockKRule(this) + + private lateinit var client: HttpClient + private lateinit var mockServer: MockWebServer + private lateinit var service: Service + + @Before + fun setUp() { + hiltRule.inject() + + // Start mock web server + mockServer = MockWebServer().apply { + dispatcher = TestDispatcher(logger) + start() + } + + // build HTTP client + client = httpClientBuilder.build() + Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted) + + // insert test service + val serviceId = db.serviceDao().insertOrReplace( + Service(id = 0, accountName = "test", type = Service.TYPE_CARDDAV, principal = null) + ) + service = db.serviceDao().get(serviceId)!! + } + + @After + fun tearDown() { + client.close() + mockServer.shutdown() + } + + + @Test + fun refreshPrincipals_inaccessiblePrincipal() { + // place principal without display name in db + val principalId = db.principalDao().insert( + Principal( + 0, + service.id, + mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL_INACCESSIBLE"), // no trailing slash + null // no display name for now + ) + ) + // add an associated collection - as the principal is rightfully removed otherwise + db.collectionDao().insertOrUpdateByUrl( + Collection( + 0, + service.id, + null, + principalId, // create association with principal + Collection.TYPE_ADDRESSBOOK, + mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), // with trailing slash + ) + ) + + // Refresh principals + principalsRefresher.create(service, client.okHttpClient).refreshPrincipals() + + // Check principal was not updated + val principals = db.principalDao().getByService(service.id) + assertEquals(1, principals.size) + assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL_INACCESSIBLE"), principals[0].url) + assertEquals(null, principals[0].displayName) + } + + @Test + fun refreshPrincipals_updatesPrincipal() { + // place principal without display name in db + val principalId = db.principalDao().insert( + Principal( + 0, + service.id, + mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL"), // no trailing slash + null // no display name for now + ) + ) + // add an associated collection - as the principal is rightfully removed otherwise + db.collectionDao().insertOrUpdateByUrl( + Collection( + 0, + service.id, + null, + principalId, // create association with principal + Collection.TYPE_ADDRESSBOOK, + mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), // with trailing slash + ) + ) + + // Refresh principals + principalsRefresher.create(service, client.okHttpClient).refreshPrincipals() + + // Check principal now got a display name + val principals = db.principalDao().getByService(service.id) + assertEquals(1, principals.size) + assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL"), principals[0].url) + assertEquals("Mr. Wobbles", principals[0].displayName) + } + + @Test + fun refreshPrincipals_deletesPrincipalsWithoutCollections() { + // place principal without collections in DB + db.principalDao().insert( + Principal( + 0, + service.id, + mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL_WITHOUT_COLLECTIONS/") + ) + ) + + // Refresh principals - detecting it does not own collections + principalsRefresher.create(service, client.okHttpClient).refreshPrincipals() + + // Check principal was deleted + val principals = db.principalDao().getByService(service.id) + assertEquals(0, principals.size) + } + + + companion object { + + private const val PATH_CARDDAV = "/carddav" + + private const val SUBPATH_PRINCIPAL = "/principal" + private const val SUBPATH_PRINCIPAL_INACCESSIBLE = "/inaccessible-principal" + private const val SUBPATH_PRINCIPAL_WITHOUT_COLLECTIONS = "/principal2" + private const val SUBPATH_GROUPPRINCIPAL_0 = "/groups/0" + private const val SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL = "/addressbooks-homeset" + private const val SUBPATH_ADDRESSBOOK_HOMESET_EMPTY = "/addressbooks-homeset-empty" + private const val SUBPATH_ADDRESSBOOK = "/addressbooks/my-contacts" + + } + + class TestDispatcher( + private val logger: Logger + ) : Dispatcher() { + + override fun dispatch(request: RecordedRequest): MockResponse { + val path = request.path!!.trimEnd('/') + + if (request.method.equals("PROPFIND", true)) { + val properties = when (path) { + + PATH_CARDDAV + SUBPATH_PRINCIPAL -> + "" + + "Mr. Wobbles" + "" + " ${PATH_CARDDAV}${SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL}" + "" + "" + " ${PATH_CARDDAV}${SUBPATH_GROUPPRINCIPAL_0}" + + "" + + PATH_CARDDAV + SUBPATH_PRINCIPAL_WITHOUT_COLLECTIONS -> + "" + + " ${PATH_CARDDAV}${SUBPATH_ADDRESSBOOK_HOMESET_EMPTY}" + + "" + + "Mr. Wobbles Jr." + + + SUBPATH_ADDRESSBOOK_HOMESET_EMPTY -> "" + + else -> "" + } + + logger.info("Queried: $path") + return MockResponse() + .setResponseCode(207) + .setBody( + "" + + "" + + " $path" + + " " + + properties + + " " + + "" + + "" + ) + } + + return MockResponse().setResponseCode(404) + } + + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/ServiceRefresherTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/ServiceRefresherTest.kt new file mode 100644 index 0000000..24ed912 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/ServiceRefresherTest.kt @@ -0,0 +1,163 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.servicedetection + +import android.security.NetworkSecurityPolicy +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.network.HttpClient +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assume +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.logging.Logger +import javax.inject.Inject + +@HiltAndroidTest +class ServiceRefresherTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var db: AppDatabase + + @Inject + lateinit var httpClientBuilder: HttpClient.Builder + + @Inject + lateinit var logger: Logger + + @Inject + lateinit var serviceRefresherFactory: ServiceRefresher.Factory + + private lateinit var client: HttpClient + private lateinit var mockServer: MockWebServer + private lateinit var service: Service + + @Before + fun setUp() { + hiltRule.inject() + + // Start mock web server + mockServer = MockWebServer().apply { + dispatcher = TestDispatcher(logger) + start() + } + + // build HTTP client + client = httpClientBuilder.build() + Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted) + + // insert test service + val serviceId = db.serviceDao().insertOrReplace( + Service(id = 0, accountName = "test", type = Service.TYPE_CARDDAV, principal = null) + ) + service = db.serviceDao().get(serviceId)!! + } + + @After + fun tearDown() { + client.close() + mockServer.shutdown() + } + + + @Test + fun testDiscoverHomesets() { + val baseUrl = mockServer.url(PATH_CARDDAV + SUBPATH_PRINCIPAL) + + // Query home sets + serviceRefresherFactory.create(service, client.okHttpClient) + .discoverHomesets(baseUrl) + + // Check home set has been saved correctly to database + val savedHomesets = db.homeSetDao().getByService(service.id) + assertEquals(2, savedHomesets.size) + + // Home set from current-user-principal + val personalHomeset = savedHomesets[1] + assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL/"), personalHomeset.url) + assertEquals(service.id, personalHomeset.serviceId) + // personal should be true for homesets detected at first query of current-user-principal (Even if they occur in a group principal as well!!!) + assertEquals(true, personalHomeset.personal) + + // Home set found in a group principal + val groupHomeset = savedHomesets[0] + assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_NON_PERSONAL/"), groupHomeset.url) + assertEquals(service.id, groupHomeset.serviceId) + // personal should be false for homesets not detected at the first query of current-user-principal (IE. in groups) + assertEquals(false, groupHomeset.personal) + } + + + companion object { + private const val PATH_CARDDAV = "/carddav" + + private const val SUBPATH_PRINCIPAL = "/principal" + private const val SUBPATH_GROUPPRINCIPAL_0 = "/groups/0" + private const val SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL = "/addressbooks-homeset" + private const val SUBPATH_ADDRESSBOOK_HOMESET_NON_PERSONAL = "/addressbooks-homeset-non-personal" + + } + + class TestDispatcher( + private val logger: Logger + ) : Dispatcher() { + + override fun dispatch(request: RecordedRequest): MockResponse { + val path = request.path!!.trimEnd('/') + logger.info("Query: ${request.method} on $path ") + + if (request.method.equals("PROPFIND", true)) { + val properties = when (path) { + PATH_CARDDAV + SUBPATH_PRINCIPAL -> + "" + + "Mr. Wobbles" + + "" + + " ${PATH_CARDDAV}${SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL}" + + "" + + "" + + " ${PATH_CARDDAV}${SUBPATH_GROUPPRINCIPAL_0}" + + "" + + PATH_CARDDAV + SUBPATH_GROUPPRINCIPAL_0 -> + "" + + "All address books" + + "" + + " ${PATH_CARDDAV}${SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL}" + + " ${PATH_CARDDAV}${SUBPATH_ADDRESSBOOK_HOMESET_NON_PERSONAL}" + + "" + + else -> "" + } + return MockResponse() + .setResponseCode(207) + .setBody( + "" + + "" + + " $path" + + " " + + properties + + " " + + "" + + "" + ) + } + + return MockResponse().setResponseCode(404) + } + + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/AccountSettingsTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/AccountSettingsTest.kt new file mode 100644 index 0000000..67eb29a --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/AccountSettingsTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings + +import android.accounts.AccountManager +import android.content.Context +import at.bitfire.davdroid.TestUtils +import at.bitfire.davdroid.sync.account.TestAccount +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class AccountSettingsTest { + + @Inject @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var accountSettingsFactory: AccountSettings.Factory + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + + @Before + fun setUp() { + hiltRule.inject() + TestUtils.setUpWorkManager(context) + } + + + @Test(expected = IllegalArgumentException::class) + fun testUpdate_MissingMigrations() { + TestAccount.provide(version = 1) { account -> + // will run AccountSettings.update + accountSettingsFactory.create(account, abortOnMissingMigration = true) + } + } + + @Test + fun testUpdate_RunAllMigrations() { + TestAccount.provide(version = 6) { account -> + // will run AccountSettings.update + accountSettingsFactory.create(account, abortOnMissingMigration = true) + + val accountManager = AccountManager.get(context) + val version = accountManager.getUserData(account, AccountSettings.KEY_SETTINGS_VERSION).toInt() + assertEquals(AccountSettings.CURRENT_VERSION, version) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/SettingsManagerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/SettingsManagerTest.kt new file mode 100644 index 0000000..277ce77 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/SettingsManagerTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toSet +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class SettingsManagerTest { + + companion object { + /** Use this setting to test SettingsManager methods. Will be removed after every test run. */ + const val SETTING_TEST = "test" + } + + + @get:Rule + val hiltRule = HiltAndroidRule(this) + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Inject lateinit var settingsManager: SettingsManager + + @Before + fun inject() { + hiltRule.inject() + } + + @After + fun removeTestSetting() { + settingsManager.remove(SETTING_TEST) + } + + + @Test + fun test_containsKey_NotExisting() { + assertFalse(settingsManager.containsKey("notExisting")) + } + + @Test + fun test_containsKey_Existing() { + // provided by DefaultsProvider + assertEquals(Settings.PROXY_TYPE_SYSTEM, settingsManager.getInt(Settings.PROXY_TYPE)) + } + + + @Test + fun test_observerFlow_initialValue() = runTest { + var counter = 0 + val live = settingsManager.observerFlow { + if (counter++ == 0) + 23 + else + throw AssertionError("A second value was requested") + } + assertEquals(23, live.first()) + } + + @Test + fun test_observerFlow_updatedValue() = runTest { + var counter = 0 + val live = settingsManager.observerFlow { + when (counter++) { + 0 -> { + // update some setting so that we will be called a second time + settingsManager.putBoolean(SETTING_TEST, true) + // and emit initial value + 23 + } + 1 -> 42 // updated value + else -> throw AssertionError() + } + } + + val result = live.take(2).toSet() + assertEquals(setOf(23, 42), result) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration17Test.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration17Test.kt new file mode 100644 index 0000000..50fbdfb --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration17Test.kt @@ -0,0 +1,102 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import androidx.test.rule.GrantPermissionRule +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.sync.account.TestAccount +import at.bitfire.davdroid.sync.account.setAndVerifyUserData +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class AccountSettingsMigration17Test { + + @Inject @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var db: AppDatabase + + @Inject + lateinit var migration: AccountSettingsMigration17 + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val permissionRule = GrantPermissionRule.grant(android.Manifest.permission.READ_CONTACTS, android.Manifest.permission.WRITE_CONTACTS) + + + @Before + fun setUp() { + hiltRule.inject() + } + + + @Test + fun testMigrate_OldAddressBook_CollectionInDB() { + val localAddressBookUserDataUrl = "url" + TestAccount.provide(version = 16) { account -> + val accountManager = AccountManager.get(context) + val addressBookAccountType = context.getString(R.string.account_type_address_book) + var addressBookAccount = Account("Address Book", addressBookAccountType) + assertTrue(accountManager.addAccountExplicitly(addressBookAccount, null, null)) + + try { + // address book has account + URL + val url = "https://example.com/address-book" + accountManager.setAndVerifyUserData(addressBookAccount, "real_account_name", account.name) + accountManager.setAndVerifyUserData(addressBookAccount, localAddressBookUserDataUrl, url) + + // and is known in database + db.serviceDao().insertOrReplace( + Service( + id = 1, accountName = account.name, type = Service.TYPE_CARDDAV, principal = null + ) + ) + db.collectionDao().insert( + Collection( + id = 100, + serviceId = 1, + url = url.toHttpUrl(), + type = Collection.TYPE_ADDRESSBOOK, + displayName = "Some Address Book" + ) + ) + + // run migration + migration.migrate(account) + + // migration renames address book, update account + addressBookAccount = accountManager.getAccountsByType(addressBookAccountType).filter { + accountManager.getUserData(it, localAddressBookUserDataUrl) == url + }.first() + assertEquals("Some Address Book (${account.name}) #100", addressBookAccount.name) + + // ID is now assigned + assertEquals(100L, accountManager.getUserData(addressBookAccount, LocalAddressBook.USER_DATA_COLLECTION_ID)?.toLong()) + } finally { + accountManager.removeAccountExplicitly(addressBookAccount) + } + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration18Test.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration18Test.kt new file mode 100644 index 0000000..18fa030 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration18Test.kt @@ -0,0 +1,122 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.resource.LocalAddressBook +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.every +import io.mockk.junit4.MockKRule +import io.mockk.mockkObject +import io.mockk.verify +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class AccountSettingsMigration18Test { + + @Inject @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var db: AppDatabase + + @Inject + lateinit var migration: AccountSettingsMigration18 + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val mockkRule = MockKRule(this) + + + @Before + fun setUp() { + hiltRule.inject() + } + + + @Test + fun testMigrate_AddressBook_InvalidCollection() { + val addressBookAccountType = context.getString(R.string.account_type_address_book) + var addressBookAccount = Account("Address Book", addressBookAccountType) + + val accountManager = AccountManager.get(context) + mockkObject(accountManager) + every { accountManager.getAccountsByType(addressBookAccountType) } returns arrayOf(addressBookAccount) + every { accountManager.getUserData(addressBookAccount, LocalAddressBook.USER_DATA_COLLECTION_ID) } returns "123" + + val account = Account("test", "test") + migration.migrate(account) + + verify(exactly = 0) { + accountManager.setUserData(addressBookAccount, any(), any()) + } + } + + @Test + fun testMigrate_AddressBook_NoCollection() { + val addressBookAccountType = context.getString(R.string.account_type_address_book) + var addressBookAccount = Account("Address Book", addressBookAccountType) + + val accountManager = AccountManager.get(context) + mockkObject(accountManager) + every { accountManager.getAccountsByType(addressBookAccountType) } returns arrayOf(addressBookAccount) + every { accountManager.getUserData(addressBookAccount, LocalAddressBook.USER_DATA_COLLECTION_ID) } returns "123" + + val account = Account("test", "test") + migration.migrate(account) + + verify(exactly = 0) { + accountManager.setUserData(addressBookAccount, any(), any()) + } + } + + @Test + fun testMigrate_AddressBook_ValidCollection() { + val account = Account("test", "test") + + db.serviceDao().insertOrReplace(Service( + id = 10, + accountName = account.name, + type = Service.TYPE_CARDDAV, + principal = null + )) + db.collectionDao().insertOrUpdateByUrl(Collection( + id = 100, + serviceId = 10, + url = "http://example.com".toHttpUrl(), + type = Collection.TYPE_ADDRESSBOOK + )) + + val addressBookAccountType = context.getString(R.string.account_type_address_book) + var addressBookAccount = Account("Address Book", addressBookAccountType) + + val accountManager = AccountManager.get(context) + mockkObject(accountManager) + every { accountManager.getAccountsByType(addressBookAccountType) } returns arrayOf(addressBookAccount) + every { accountManager.getUserData(addressBookAccount, LocalAddressBook.USER_DATA_COLLECTION_ID) } returns "100" + + migration.migrate(account) + + verify { + accountManager.setUserData(addressBookAccount, LocalAddressBook.USER_DATA_ACCOUNT_NAME, account.name) + accountManager.setUserData(addressBookAccount, LocalAddressBook.USER_DATA_ACCOUNT_TYPE, account.type) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration19Test.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration19Test.kt new file mode 100644 index 0000000..f12a009 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration19Test.kt @@ -0,0 +1,83 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import android.content.Context +import android.util.Log +import androidx.hilt.work.HiltWorkerFactory +import androidx.work.Configuration +import androidx.work.WorkManager +import androidx.work.testing.WorkManagerTestInitHelper +import at.bitfire.davdroid.sync.AutomaticSyncManager +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.junit4.MockKRule +import io.mockk.mockkObject +import io.mockk.verify +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class AccountSettingsMigration19Test { + + @Inject @ApplicationContext + lateinit var context: Context + + @BindValue + @RelaxedMockK + lateinit var automaticSyncManager: AutomaticSyncManager + + @Inject + lateinit var migration: AccountSettingsMigration19 + + @Inject + lateinit var workerFactory: HiltWorkerFactory + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val mockkRule = MockKRule(this) + + + @Before + fun setUp() { + hiltRule.inject() + + // Initialize WorkManager for instrumentation tests. + val config = Configuration.Builder() + .setMinimumLoggingLevel(Log.DEBUG) + .setWorkerFactory(workerFactory) + .build() + WorkManagerTestInitHelper.initializeTestWorkManager(context, config) + } + + + @Test + fun testMigrate_CancelsOldWorkersAndUpdatesAutomaticSync() { + val workManager = WorkManager.getInstance(context) + mockkObject(workManager) + + val account = Account("Some", "Test") + migration.migrate(account) + + verify { + workManager.cancelUniqueWork("periodic-sync at.bitfire.davdroid.addressbooks Test/Some") + workManager.cancelUniqueWork("periodic-sync com.android.calendar Test/Some") + workManager.cancelUniqueWork("periodic-sync at.techbee.jtx.provider Test/Some") + workManager.cancelUniqueWork("periodic-sync org.dmfs.tasks Test/Some") + workManager.cancelUniqueWork("periodic-sync org.tasks.opentasks Test/Some") + + automaticSyncManager.updateAutomaticSync(account) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration20Test.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration20Test.kt new file mode 100644 index 0000000..771eea7 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration20Test.kt @@ -0,0 +1,144 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.Manifest +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import android.provider.CalendarContract +import android.provider.CalendarContract.Calendars +import androidx.core.content.contentValuesOf +import androidx.core.database.getLongOrNull +import androidx.test.rule.GrantPermissionRule +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.resource.LocalCalendarStore +import at.bitfire.davdroid.resource.LocalTestAddressBookProvider +import at.bitfire.davdroid.sync.account.setAndVerifyUserData +import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter +import at.bitfire.vcard4android.GroupMethod +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.junit4.MockKRule +import io.mockk.mockk +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class AccountSettingsMigration20Test { + + @Inject + lateinit var calendarStore: LocalCalendarStore + + @Inject @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var db: AppDatabase + + @Inject + lateinit var migration: AccountSettingsMigration20 + + @Inject + lateinit var localTestAddressBookProvider: LocalTestAddressBookProvider + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val mockkRule = MockKRule(this) + + @get:Rule + val permissionsRule = GrantPermissionRule.grant( + Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS, + Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR + ) + + val accountManager by lazy { AccountManager.get(context) } + + @Before + fun setUp() { + hiltRule.inject() + } + + + @Test + fun testMigrateAddressBooks_UrlMatchesCollection() { + // set up legacy address-book with URL, but without collection ID + val account = Account("test", "test") + val url = "https://example.com/" + + db.serviceDao().insertOrReplace(Service(id = 1, accountName = account.name, type = Service.TYPE_CARDDAV, principal = null)) + val collectionId = db.collectionDao().insert(Collection( + serviceId = 1, + type = Collection.Companion.TYPE_ADDRESSBOOK, + url = url.toHttpUrl() + )) + + localTestAddressBookProvider.provide(account, mockk(relaxed = true), GroupMethod.GROUP_VCARDS) { addressBook -> + + accountManager.setAndVerifyUserData(addressBook.addressBookAccount, LocalAddressBook.USER_DATA_ACCOUNT_NAME, account.name) + accountManager.setAndVerifyUserData(addressBook.addressBookAccount, LocalAddressBook.USER_DATA_ACCOUNT_TYPE, account.type) + accountManager.setAndVerifyUserData(addressBook.addressBookAccount, AccountSettingsMigration20.ADDRESS_BOOK_USER_DATA_URL, url) + accountManager.setAndVerifyUserData(addressBook.addressBookAccount, LocalAddressBook.USER_DATA_COLLECTION_ID, null) + + migration.migrateAddressBooks(account, cardDavServiceId = 1) + + assertEquals( + collectionId, + accountManager.getUserData(addressBook.addressBookAccount, LocalAddressBook.USER_DATA_COLLECTION_ID).toLongOrNull() + ) + } + } + + + @Test + fun testMigrateCalendars_UrlMatchesCollection() { + // set up legacy calendar with URL, but without collection ID + val account = Account("test", CalendarContract.ACCOUNT_TYPE_LOCAL) + val url = "https://example.com/" + + db.serviceDao().insertOrReplace(Service(id = 1, accountName = account.name, type = Service.TYPE_CALDAV, principal = null)) + val collectionId = db.collectionDao().insert( + Collection( + serviceId = 1, + type = Collection.Companion.TYPE_CALENDAR, + url = url.toHttpUrl() + ) + ) + + context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!!.use { provider -> + val uri = provider.insert( + Calendars.CONTENT_URI.asSyncAdapter(account), + contentValuesOf( + Calendars.ACCOUNT_NAME to account.name, + Calendars.ACCOUNT_TYPE to account.type, + Calendars.CALENDAR_DISPLAY_NAME to "Test", + Calendars.NAME to url, + Calendars.SYNC_EVENTS to 1 + ) + )!!.asSyncAdapter(account) + try { + migration.migrateCalendars(account, 1) + + provider.query(uri, arrayOf(Calendars._SYNC_ID), null, null, null)!!.use { cursor -> + cursor.moveToNext() + assertEquals(collectionId, cursor.getLongOrNull(0)) + } + } finally { + provider.delete(uri, null, null) + } + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/AndroidSyncFrameworkTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/AndroidSyncFrameworkTest.kt new file mode 100644 index 0000000..da251a4 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/AndroidSyncFrameworkTest.kt @@ -0,0 +1,238 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.accounts.Account +import android.content.ContentResolver +import android.content.SyncRequest +import android.content.SyncStatusObserver +import android.os.Bundle +import android.provider.CalendarContract +import androidx.test.filters.SdkSuppress +import at.bitfire.davdroid.sync.account.TestAccount +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import org.junit.After +import org.junit.AfterClass +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Rule +import org.junit.Test +import java.util.Collections +import java.util.LinkedList +import java.util.logging.Logger +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds + +@HiltAndroidTest +class AndroidSyncFrameworkTest: SyncStatusObserver { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var logger: Logger + + lateinit var account: Account + val authority = CalendarContract.AUTHORITY + + private lateinit var stateChangeListener: Any + private val recordedStates = Collections.synchronizedList(LinkedList()) + + @Before + fun setUp() { + hiltRule.inject() + + account = TestAccount.create() + + // Enable sync globally and for the test account + ContentResolver.setIsSyncable(account, authority, 1) + + // Remember states the sync framework reports as pairs of (sync pending, sync active). + recordedStates.clear() + onStatusChanged(0) // record first entry (pending = false, active = false) + stateChangeListener = ContentResolver.addStatusChangeListener( + ContentResolver.SYNC_OBSERVER_TYPE_PENDING or ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE, + this + ) + } + + @After + fun tearDown() { + ContentResolver.removeStatusChangeListener(stateChangeListener) + TestAccount.remove(account) + } + + + /** + * Correct behaviour of the sync framework on Android 13 and below. + * Pending state is correctly reflected + */ + @SdkSuppress(maxSdkVersion = 33) + @Test + fun testVerifySyncAlwaysPending_correctBehaviour_android13() { + verifySyncStates( + listOf( + State(pending = false, active = false), // no sync pending or active + State(pending = true, active = false, optional = true), // sync becomes pending + State(pending = true, active = true), // ... and pending and active at the same time + State(pending = false, active = true), // ... and then only active + State(pending = false, active = false) // sync finished + ) + ) + } + + /** + * Wrong behaviour of the sync framework on Android 14+. + * Pending state stays true forever (after initial run), active state behaves correctly + */ + @SdkSuppress(minSdkVersion = 34 /*, maxSdkVersion = 36 */) + @Test + fun testVerifySyncAlwaysPending_wrongBehaviour_android14() { + verifySyncStates( + listOf( + State(pending = false, active = false), // no sync pending or active + State(pending = true, active = false, optional = true), // sync becomes pending + State(pending = true, active = true), // ... and pending and active at the same time + State(pending = true, active = false) // ... and finishes, but stays pending + ) + ) + } + + + // helpers + + private fun syncRequest() = SyncRequest.Builder() + .setSyncAdapter(account, authority) + .syncOnce() + .setExtras(Bundle()) // needed for Android 9 + .setExpedited(true) // sync request will be scheduled at the front of the sync request queue + .setManual(true) // equivalent of setting both SYNC_EXTRAS_IGNORE_SETTINGS and SYNC_EXTRAS_IGNORE_BACKOFF + .build() + + /** + * Verifies that the given expected states match the recorded states. + */ + private fun verifySyncStates(expectedStates: List) = runBlocking { + // Verify that last state is non-optional. + if (expectedStates.last().optional) + throw IllegalArgumentException("Last expected state must not be optional") + + // We use runBlocking for these tests because it uses the default dispatcher + // which does not auto-advance virtual time and we need real system time to + // test the sync framework behavior. + + ContentResolver.requestSync(syncRequest()) + + // Even though the always-pending-bug is present on Android 14+, the sync active + // state behaves correctly, so we can record the state changes as pairs (pending, + // active) and expect a certain sequence of state pairs to verify the presence or + // absence of the bug on different Android versions. + withTimeout(60.seconds) { // Usually takes less than 30 seconds + while (recordedStates.size < expectedStates.size) { + // verify already known states + if (recordedStates.isNotEmpty()) + assertStatesEqual(expectedStates, recordedStates, fullMatch = false) + + delay(500) // avoid busy-waiting + } + + assertStatesEqual(expectedStates, recordedStates, fullMatch = true) + } + } + + private fun assertStatesEqual(expectedStates: List, actualStates: List, fullMatch: Boolean) { + assertTrue("Expected states=$expectedStates, actual=$actualStates", statesMatch(expectedStates, actualStates, fullMatch)) + } + + /** + * Checks whether [actualStates] have matching [expectedStates], under the condition + * that expected states with the [State.optional] flag can be skipped. + * + * Note: When [fullMatch] is not set, this method can return _true_ even if not all expected states are used. + * + * @param expectedStates expected states (can include optional states which don't have to be present in actual states) + * @param actualStates actual states + * @param fullMatch whether all non-optional expected states must be present in actual states + */ + private fun statesMatch(expectedStates: List, actualStates: List, fullMatch: Boolean): Boolean { + // iterate through entries + val expectedIterator = expectedStates.iterator() + for (actual in actualStates) { + if (!expectedIterator.hasNext()) + return false + var expected = expectedIterator.next() + + // skip optional expected entries if they don't match the actual entry + while (!actual.stateEquals(expected) && expected.optional) { + if (!expectedIterator.hasNext()) + return false + expected = expectedIterator.next() + } + + // we now have a non-optional expected state and it must match + if (!actual.stateEquals(expected)) + return false + } + + // full match: all expected states must have been used + if (fullMatch && expectedIterator.hasNext()) + return false + + return true + } + + + // SyncStatusObserver implementation and data class + + override fun onStatusChanged(which: Int) { + val state = State( + pending = ContentResolver.isSyncPending(account, authority), + active = ContentResolver.isSyncActive(account, authority) + ) + synchronized(recordedStates) { + if (recordedStates.lastOrNull() != state) { + logger.info("$account syncState = $state") + recordedStates += state + } + } + } + + data class State( + val pending: Boolean, + val active: Boolean, + val optional: Boolean = false + ) { + fun stateEquals(other: State) = + pending == other.pending && active == other.active + } + + + companion object { + + var globalAutoSyncBeforeTest = false + + @BeforeClass + @JvmStatic + fun before() { + globalAutoSyncBeforeTest = ContentResolver.getMasterSyncAutomatically() + + // We'll request syncs explicitly and with SYNC_EXTRAS_IGNORE_SETTINGS + ContentResolver.setMasterSyncAutomatically(false) + } + + @AfterClass + @JvmStatic + fun after() { + ContentResolver.setMasterSyncAutomatically(globalAutoSyncBeforeTest) + } + + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/FakeSyncAdapter.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/FakeSyncAdapter.kt new file mode 100644 index 0000000..a47305e --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/FakeSyncAdapter.kt @@ -0,0 +1,51 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.accounts.Account +import android.content.AbstractThreadedSyncAdapter +import android.content.ContentProviderClient +import android.content.Context +import android.content.SyncResult +import android.os.Bundle +import android.os.IBinder +import at.bitfire.davdroid.sync.adapter.SyncAdapter +import dagger.hilt.android.qualifiers.ApplicationContext +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject + +class FakeSyncAdapter @Inject constructor( + @ApplicationContext context: Context, + private val logger: Logger +): AbstractThreadedSyncAdapter(context, true), SyncAdapter { + + init { + logger.info("FakeSyncAdapter created") + } + + override fun onPerformSync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) { + logger.log( + Level.INFO, + "onPerformSync(account=$account, extras=$extras, authority=$authority, syncResult=$syncResult)", + extras.keySet().map { key -> "extras[$key] = ${extras[key]}" } + ) + + // fake 5 sec sync + try { + Thread.sleep(5000) + } catch (_: InterruptedException) { + logger.info("onPerformSync($account) cancelled") + } + + logger.info("onPerformSync($account) finished") + } + + + // SyncAdapter implementation and Hilt module + + override fun getBinder(): IBinder = syncAdapterBinder + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/JtxSyncManagerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/JtxSyncManagerTest.kt new file mode 100644 index 0000000..8ac7f01 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/JtxSyncManagerTest.kt @@ -0,0 +1,187 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.Context +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.resource.LocalJtxCollection +import at.bitfire.davdroid.resource.LocalJtxCollectionStore +import at.bitfire.davdroid.sync.account.TestAccount +import at.bitfire.davdroid.util.PermissionUtils +import at.bitfire.ical4android.TaskProvider +import at.bitfire.ical4android.util.MiscUtils.closeCompat +import at.bitfire.synctools.test.GrantPermissionOrSkipRule +import at.techbee.jtx.JtxContract +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assume.assumeNotNull +import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.io.StringReader +import javax.inject.Inject + + +/** + * Ensure you have jtxBoard installed on the emulator, before running these tests. Otherwise they + * will be skipped. + */ +@HiltAndroidTest +class JtxSyncManagerTest { + + @Inject + @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var httpClientBuilder: HttpClient.Builder + + @Inject + lateinit var serviceRepository: DavServiceRepository + + @Inject + lateinit var localJtxCollectionStore: LocalJtxCollectionStore + + @Inject + lateinit var jtxSyncManagerFactory: JtxSyncManager.Factory + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val permissionRule = GrantPermissionOrSkipRule(TaskProvider.PERMISSIONS_JTX.toSet()) + + lateinit var account: Account + + private lateinit var provider: ContentProviderClient + private lateinit var syncManager: JtxSyncManager + private lateinit var localJtxCollection: LocalJtxCollection + + @Before + fun setUp() { + hiltRule.inject() + + // Check jtxBoard permissions were granted (+jtxBoard is installed); skip test otherwise + assumeTrue(PermissionUtils.havePermissions(context, TaskProvider.PERMISSIONS_JTX)) + + // Acquire the jtx content provider + val providerOrNull = context.contentResolver.acquireContentProviderClient(JtxContract.AUTHORITY) + assumeNotNull(providerOrNull) + provider = providerOrNull!! + + account = TestAccount.create() + + // Create dummy dependencies + val service = Service(0, account.name, Service.TYPE_CALDAV, null) + val serviceId = serviceRepository.insertOrReplaceBlocking(service) + val dbCollection = Collection( + 0, + serviceId, + type = Collection.TYPE_CALENDAR, + url = "https://example.com".toHttpUrl() + ) + localJtxCollection = localJtxCollectionStore.create(provider, dbCollection)!! + syncManager = jtxSyncManagerFactory.jtxSyncManager( + account = account, + httpClient = httpClientBuilder.build(), + syncResult = SyncResult(), + localCollection = localJtxCollection, + collection = dbCollection, + resync = null + ) + } + + @After + fun tearDown() { + if (this::localJtxCollection.isInitialized) + localJtxCollectionStore.delete(localJtxCollection) + serviceRepository.deleteAllBlocking() + + if (this::provider.isInitialized) + provider.closeCompat() + + if (this::account.isInitialized) + TestAccount.remove(account) + } + + + @Test + fun testProcessICalObject_addsVtodo() { + val calendar = "BEGIN:VCALENDAR\n" + + "PRODID:-Vivaldi Calendar V1.0//EN\n" + + "VERSION:2.0\n" + + "BEGIN:VTODO\n" + + "SUMMARY:Test Task (Main VTODO)\n" + + "DTSTAMP;VALUE=DATE-TIME:20250228T032800Z\n" + + "UID:47a23c66-8c1a-4b44-bbe8-ebf33f8cf80f\n" + + "END:VTODO\n" + + "END:VCALENDAR" + + // Should create "demo-calendar" + syncManager.processICalObject("demo-calendar", "abc123", StringReader(calendar)) + + // Verify main VTODO is created + val localJtxIcalObject = localJtxCollection.findByName("demo-calendar")!! + assertEquals("47a23c66-8c1a-4b44-bbe8-ebf33f8cf80f", localJtxIcalObject.uid) + assertEquals("abc123", localJtxIcalObject.eTag) + assertEquals("Test Task (Main VTODO)", localJtxIcalObject.summary) + } + + @Test + fun testProcessICalObject_addsRecurringVtodo_withoutDtStart() { + // Valid calendar example (See bitfireAT/davx5-ose#1265) + // Note: We don't support starting a recurrence from DUE (RFC 5545 leaves it open to interpretation) + val calendar = "BEGIN:VCALENDAR\n" + + "PRODID:-Vivaldi Calendar V1.0//EN\n" + + "VERSION:2.0\n" + + "BEGIN:VTODO\n" + + + "SUMMARY:Test Task (Exception)\n" + + "DTSTAMP;VALUE=DATE-TIME:20250228T032800Z\n" + + "DUE;TZID=America/New_York:20250228T130000\n" + + "RECURRENCE-ID;TZID=America/New_York:20250228T130000\n" + + "UID:47a23c66-8c1a-4b44-bbe8-ebf33f8cf80f\n" + + + "END:VTODO\n" + + "BEGIN:VTODO\n" + + + "SUMMARY:Test Task (Main VTODO)\n" + + "DTSTAMP;VALUE=DATE-TIME:20250228T032800Z\n" + + "DUE;TZID=America/New_York:20250228T130000\n" + // Due date will NOT be assumed as start for recurrence + "SEQUENCE:1\n" + + "UID:47a23c66-8c1a-4b44-bbe8-ebf33f8cf80f\n" + + "RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=FR;UNTIL=20250505T235959Z\n" + + + "END:VTODO\n" + + "END:VCALENDAR" + + // Create and store calendar + syncManager.processICalObject("demo-calendar", "abc123", StringReader(calendar)) + + // Verify main VTODO was created with RRULE present + val mainVtodo = localJtxCollection.findByName("demo-calendar")!! + assertEquals("Test Task (Main VTODO)", mainVtodo.summary) + assertEquals("FREQ=WEEKLY;UNTIL=20250505T235959Z;INTERVAL=1;BYDAY=FR", mainVtodo.rrule) + + // Verify the RRULE exception instance was created with correct recurrence-id timezone + val vtodoException = localJtxCollection.findRecurInstance( + uid = "47a23c66-8c1a-4b44-bbe8-ebf33f8cf80f", + recurid = "20250228T130000" + )!! + assertEquals("Test Task (Exception)", vtodoException.summary) + assertEquals("America/New_York", vtodoException.recuridTimezone) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/LocalTestCollection.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/LocalTestCollection.kt new file mode 100644 index 0000000..86a37c5 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/LocalTestCollection.kt @@ -0,0 +1,47 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import at.bitfire.davdroid.resource.LocalCollection +import at.bitfire.davdroid.resource.SyncState + +class LocalTestCollection( + override val dbCollectionId: Long = 0L +): LocalCollection { + + override val tag = "LocalTestCollection" + override val title = "Local Test Collection" + + override var lastSyncState: SyncState? = null + + val entries = mutableListOf() + + override val readOnly: Boolean + get() = throw NotImplementedError() + + override fun findDeleted() = entries.filter { it.deleted } + override fun findDirty() = entries.filter { it.dirty } + + override fun findByName(name: String) = entries.firstOrNull { it.fileName == name } + + override fun markNotDirty(flags: Int): Int { + var updated = 0 + for (dirty in findDirty()) { + dirty.flags = flags + updated++ + } + return updated + } + + override fun removeNotDirtyMarked(flags: Int): Int { + val numBefore = entries.size + entries.removeIf { !it.dirty && it.flags == flags } + return numBefore - entries.size + } + + override fun forgetETags() { + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/LocalTestResource.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/LocalTestResource.kt new file mode 100644 index 0000000..f41b3e8 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/LocalTestResource.kt @@ -0,0 +1,44 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.content.Context +import at.bitfire.davdroid.resource.LocalResource +import java.util.Optional + +class LocalTestResource: LocalResource { + + override val id: Long? = null + override var fileName: String? = null + override var eTag: String? = null + override var scheduleTag: String? = null + override var flags: Int = 0 + + var deleted = false + var dirty = false + + override fun prepareForUpload() = "generated-file.txt" + + override fun clearDirty(fileName: Optional, eTag: String?, scheduleTag: String?) { + dirty = false + if (fileName.isPresent) + this.fileName = fileName.get() + this.eTag = eTag + this.scheduleTag = scheduleTag + } + + override fun updateFlags(flags: Int) { + this.flags = flags + } + + override fun update(data: Any, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) = throw NotImplementedError() + override fun deleteLocal() = throw NotImplementedError() + override fun resetDeleted() = throw NotImplementedError() + + override fun getDebugSummary() = "Test Resource" + + override fun getViewUri(context: Context) = null + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncAdapterImplTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncAdapterImplTest.kt new file mode 100644 index 0000000..a6c06d3 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncAdapterImplTest.kt @@ -0,0 +1,158 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.accounts.Account +import android.content.ContentResolver +import android.content.Context +import android.content.SyncResult +import android.os.Bundle +import android.provider.CalendarContract +import androidx.hilt.work.HiltWorkerFactory +import androidx.work.WorkInfo +import androidx.work.WorkManager +import at.bitfire.davdroid.TestUtils +import at.bitfire.davdroid.sync.account.TestAccount +import at.bitfire.davdroid.sync.adapter.SyncAdapterImpl +import at.bitfire.davdroid.sync.worker.SyncWorkerManager +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.Awaits +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.junit4.MockKRule +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.mockkStatic +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withTimeout +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject +import javax.inject.Provider +import kotlin.coroutines.cancellation.CancellationException + +@HiltAndroidTest +class SyncAdapterImplTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val mockkRule = MockKRule(this) + + @Inject @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var syncAdapterImplProvider: Provider + + @BindValue @MockK + lateinit var syncWorkerManager: SyncWorkerManager + + @Inject + lateinit var workerFactory: HiltWorkerFactory + + lateinit var account: Account + + private var masterSyncStateBeforeTest = ContentResolver.getMasterSyncAutomatically() + + @Before + fun setUp() { + hiltRule.inject() + TestUtils.setUpWorkManager(context, workerFactory) + + account = TestAccount.create() + + ContentResolver.setMasterSyncAutomatically(true) + ContentResolver.setSyncAutomatically(account, CalendarContract.AUTHORITY, true) + ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1) + } + + @After + fun tearDown() { + ContentResolver.setMasterSyncAutomatically(masterSyncStateBeforeTest) + TestAccount.remove(account) + } + + + @Test + fun testSyncAdapter_onPerformSync_cancellation() = runTest { + val workManager = WorkManager.getInstance(context) + val syncAdapter = syncAdapterImplProvider.get() + + mockkObject(workManager) { + // don't actually create a worker + every { syncWorkerManager.enqueueOneTime(any(), any()) } returns "TheSyncWorker" + + // assume worker takes a long time + every { workManager.getWorkInfosForUniqueWorkFlow("TheSyncWorker") } just Awaits + + val sync = launch { + syncAdapter.onPerformSync(account, Bundle(), CalendarContract.AUTHORITY, mockk(), SyncResult()) + } + + // simulate incoming cancellation from sync framework + syncAdapter.onSyncCanceled() + + // wait for sync to finish (should happen immediately) + sync.join() + } + } + + @Test + fun testSyncAdapter_onPerformSync_returnsAfterTimeout() { + val workManager = WorkManager.getInstance(context) + val syncAdapter = syncAdapterImplProvider.get() + + mockkObject(workManager) { + // don't actually create a worker + every { syncWorkerManager.enqueueOneTime(any(), any()) } returns "TheSyncWorker" + + // assume worker takes a long time + every { workManager.getWorkInfosForUniqueWorkFlow("TheSyncWorker") } just Awaits + + mockkStatic("kotlinx.coroutines.TimeoutKt") { // mock global extension function + // immediate timeout (instead of really waiting) + coEvery { withTimeout(any(), any Unit>()) } throws CancellationException("Simulated timeout") + + syncAdapter.onPerformSync(account, Bundle(), CalendarContract.AUTHORITY, mockk(), SyncResult()) + } + } + } + + @Test + fun testSyncAdapter_onPerformSync_runsInTime() { + val workManager = WorkManager.getInstance(context) + val syncAdapter = syncAdapterImplProvider.get() + + mockkObject(workManager) { + // don't actually create a worker + every { syncWorkerManager.enqueueOneTime(any(), any()) } returns "TheSyncWorker" + + // assume worker immediately returns with success + val success = mockk() + every { success.state } returns WorkInfo.State.SUCCEEDED + every { workManager.getWorkInfosForUniqueWorkFlow("TheSyncWorker") } returns flow { + emit(listOf(success)) + delay(60000) // keep the flow active + } + + // should just run + syncAdapter.onPerformSync(account, Bundle(), CalendarContract.AUTHORITY, mockk(), SyncResult()) + } + } + +} diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncManagerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncManagerTest.kt new file mode 100644 index 0000000..fefbd0c --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncManagerTest.kt @@ -0,0 +1,503 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.accounts.Account +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import androidx.hilt.work.HiltWorkerFactory +import at.bitfire.dav4jvm.PropStat +import at.bitfire.dav4jvm.Response +import at.bitfire.dav4jvm.Response.HrefRelation +import at.bitfire.dav4jvm.property.webdav.GetETag +import at.bitfire.davdroid.TestUtils +import at.bitfire.davdroid.TestUtils.assertWithin +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.repository.DavSyncStatsRepository +import at.bitfire.davdroid.resource.SyncState +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.sync.account.TestAccount +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.every +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.junit4.MockKRule +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import okhttp3.Protocol +import okhttp3.internal.http.StatusLine +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.time.Instant +import javax.inject.Inject + +@HiltAndroidTest +class SyncManagerTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val mockKRule = MockKRule(this) + + @Inject + lateinit var accountSettingsFactory: AccountSettings.Factory + + @Inject @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var httpClientBuilder: HttpClient.Builder + + @Inject + lateinit var syncManagerFactory: TestSyncManager.Factory + + @BindValue + @RelaxedMockK + lateinit var syncStatsRepository: DavSyncStatsRepository + + @Inject + lateinit var workerFactory: HiltWorkerFactory + + private lateinit var account: Account + private lateinit var server: MockWebServer + + @Before + fun setUp() { + hiltRule.inject() + + TestUtils.setUpWorkManager(context, workerFactory) + + account = TestAccount.create() + + server = MockWebServer().apply { + start() + } + } + + @After + fun tearDown() { + TestAccount.remove(account) + + // clear annoying syncError notifications + NotificationManagerCompat.from(context).cancelAll() + + server.close() + } + + + private fun queryCapabilitiesResponse(cTag: String? = null): MockResponse { + val body = StringBuilder() + body.append( + "\n" + + "\n" + + " \n" + + " /\n" + + " \n" + + " \n" + ) + if (cTag != null) + body.append("$cTag\n") + body.append( + " \n" + + " \n" + + " \n" + + "" + ) + return MockResponse() + .setResponseCode(207) + .setHeader("Content-Type", "text/xml") + .setBody(body.toString()) + } + + + @Test + fun testPerformSync_503RetryAfter_DelaySeconds() = runTest { + server.enqueue(MockResponse() + .setResponseCode(503) + .setHeader("Retry-After", "60")) // 60 seconds + + val result = SyncResult() + val syncManager = syncManager(LocalTestCollection(), result) + syncManager.performSync() + + val expected = Instant.now() + .plusSeconds(60) + .toEpochMilli() + // 5 sec tolerance for test + assertWithin(expected, result.delayUntil*1000, 5000) + } + + @Test + fun testPerformSync_FirstSync_Empty() = runTest { + val collection = LocalTestCollection() /* no last known ctag */ + server.enqueue(queryCapabilitiesResponse()) + + val syncManager = syncManager(collection) + syncManager.performSync() + + assertFalse(syncManager.didGenerateUpload) + assertTrue(syncManager.didListAllRemote) + assertFalse(syncManager.didDownloadRemote) + assertFalse(syncManager.syncResult.hasError()) + assertTrue(collection.entries.isEmpty()) + } + + @Test + fun testPerformSync_UploadNewMember_ETagOnPut() = runTest { + val collection = LocalTestCollection().apply { + lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag") + entries += LocalTestResource().apply { + dirty = true + } + } + server.enqueue(queryCapabilitiesResponse("ctag1")) + + // PUT -> 204 No Content + server.enqueue(MockResponse() + .setResponseCode(204) + .setHeader("ETag", "etag-from-put")) + + // modifications sent, so DAVx5 will query CTag again + server.enqueue(queryCapabilitiesResponse("ctag2")) + + val syncManager = syncManager(collection).apply { + listAllRemoteResult = listOf( + Pair(Response( + server.url("/"), + server.url("/generated-file.txt"), + null, + listOf(PropStat( + listOf( + GetETag("\"etag-from-put\"") + ), + StatusLine(Protocol.HTTP_1_1, 200, "OK") + ) + )), HrefRelation.MEMBER) + ) + } + syncManager.performSync() + + assertTrue(syncManager.didGenerateUpload) + assertTrue(syncManager.didListAllRemote) + assertFalse(syncManager.didDownloadRemote) + assertFalse(syncManager.syncResult.hasError()) + assertEquals(1, collection.entries.size) + assertEquals("etag-from-put", collection.entries.first().eTag) + } + + @Test + fun testPerformSync_UploadModifiedMember_ETagOnPut() = runTest { + val collection = LocalTestCollection().apply { + lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag") + entries += LocalTestResource().apply { + fileName = "existing-file.txt" + eTag = "old-etag-like-on-server" + dirty = true + } + } + server.enqueue(queryCapabilitiesResponse("ctag1")) + + // PUT -> 204 No Content + server.enqueue(MockResponse() + .setResponseCode(204) + .addHeader("ETag", "etag-from-put")) + + // modifications sent, so DAVx5 will query CTag again + server.enqueue(queryCapabilitiesResponse("ctag2")) + + val syncManager = syncManager(collection).apply { + listAllRemoteResult = listOf( + Pair(Response( + server.url("/"), + server.url("/existing-file.txt"), + null, + listOf(PropStat( + listOf( + GetETag("etag-from-put") + ), + StatusLine(Protocol.HTTP_1_1, 200, "OK") + ) + )), HrefRelation.MEMBER) + ) + + assertDownloadRemote = mapOf(Pair(server.url("/existing-file.txt"), "etag-from-put")) + } + syncManager.performSync() + + assertTrue(syncManager.didGenerateUpload) + assertTrue(syncManager.didListAllRemote) + assertFalse(syncManager.didDownloadRemote) + assertFalse(syncManager.syncResult.hasError()) + assertEquals(1, collection.entries.size) + assertEquals("etag-from-put", collection.entries.first().eTag) + } + + @Test + fun testPerformSync_UploadModifiedMember_NoETagOnPut() = runTest { + val collection = LocalTestCollection().apply { + lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag") + entries += LocalTestResource().apply { + fileName = "existing-file.txt" + eTag = "old-etag-like-on-server" + dirty = true + } + } + server.enqueue(queryCapabilitiesResponse("ctag1")) + + // PUT -> 204 No Content + server.enqueue(MockResponse().setResponseCode(204)) + + // modifications sent, so DAVx5 will query CTag again + server.enqueue(queryCapabilitiesResponse("ctag2")) + + val syncManager = syncManager(collection).apply { + listAllRemoteResult = listOf( + Pair(Response( + server.url("/"), + server.url("/existing-file.txt"), + null, + listOf(PropStat( + listOf( + GetETag("etag-from-propfind") + ), + StatusLine(Protocol.HTTP_1_1, 200, "OK") + ) + )), HrefRelation.MEMBER) + ) + + assertDownloadRemote = mapOf(Pair(server.url("/existing-file.txt"), "etag-from-propfind")) + } + syncManager.performSync() + + assertTrue(syncManager.didGenerateUpload) + assertTrue(syncManager.didListAllRemote) + assertTrue(syncManager.didDownloadRemote) + assertFalse(syncManager.syncResult.hasError()) + assertEquals(1, collection.entries.size) + assertEquals("etag-from-propfind", collection.entries.first().eTag) + } + + @Test + fun testPerformSync_UploadModifiedMember_412PreconditionFailed() = runTest { + val collection = LocalTestCollection().apply { + lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag") + entries += LocalTestResource().apply { + fileName = "existing-file.txt" + eTag = "etag-that-has-been-changed-on-server-in-the-meanwhile" + dirty = true + } + } + server.enqueue(queryCapabilitiesResponse("ctag1")) + + // PUT -> 412 Precondition Failed + server.enqueue(MockResponse() + .setResponseCode(412)) + + // modifications sent, so DAVx5 will query CTag again + server.enqueue(queryCapabilitiesResponse("ctag1")) + + val syncManager = syncManager(collection).apply { + listAllRemoteResult = listOf( + Pair(Response( + server.url("/"), + server.url("/existing-file.txt"), + null, + listOf(PropStat( + listOf( + GetETag("changed-etag-from-server") + ), + StatusLine(Protocol.HTTP_1_1, 200, "OK") + ) + )), HrefRelation.MEMBER) + ) + + assertDownloadRemote = mapOf(Pair(server.url("/existing-file.txt"), "changed-etag-from-server")) + } + syncManager.performSync() + + assertTrue(syncManager.didGenerateUpload) + assertTrue(syncManager.didListAllRemote) + assertTrue(syncManager.didDownloadRemote) + assertFalse(syncManager.syncResult.hasError()) + assertEquals(1, collection.entries.size) + assertEquals("changed-etag-from-server", collection.entries.first().eTag) + } + + @Test + fun testPerformSync_NoopOnMemberWithSameETag() = runTest { + val collection = LocalTestCollection().apply { + lastSyncState = SyncState(SyncState.Type.CTAG, "ctag1") + entries += LocalTestResource().apply { + fileName = "downloaded-member.txt" + eTag = "MemberETag1" + } + } + server.enqueue(queryCapabilitiesResponse("ctag2")) + + val syncManager = syncManager(collection).apply { + listAllRemoteResult = listOf( + Pair(Response( + server.url("/"), + server.url("/downloaded-member.txt"), + null, + listOf(PropStat( + listOf( + GetETag("\"MemberETag1\"") + ), + StatusLine(Protocol.HTTP_1_1, 200, "OK") + ) + )), HrefRelation.MEMBER) + ) + + } + syncManager.performSync() + + assertFalse(syncManager.didGenerateUpload) + assertTrue(syncManager.didListAllRemote) + assertFalse(syncManager.didDownloadRemote) + assertFalse(syncManager.syncResult.hasError()) + assertEquals(1, collection.entries.size) + assertEquals("MemberETag1", collection.entries.first().eTag) + } + + @Test + fun testPerformSync_DownloadNewMember() = runTest { + val collection = LocalTestCollection().apply { + lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag") + } + server.enqueue(queryCapabilitiesResponse(cTag = "new-ctag")) + + val syncManager = syncManager(collection).apply { + listAllRemoteResult = listOf( + Pair(Response( + server.url("/"), + server.url("/new-member.txt"), + null, + listOf(PropStat( + listOf( + GetETag("\"NewMemberETag1\"") + ), + StatusLine(Protocol.HTTP_1_1, 200, "OK") + ) + )), HrefRelation.MEMBER) + ) + + assertDownloadRemote = mapOf(Pair(server.url("/new-member.txt"), "NewMemberETag1")) + } + syncManager.performSync() + + assertFalse(syncManager.didGenerateUpload) + assertTrue(syncManager.didListAllRemote) + assertTrue(syncManager.didDownloadRemote) + assertFalse(syncManager.syncResult.hasError()) + assertEquals(1, collection.entries.size) + assertEquals("NewMemberETag1", collection.entries.first().eTag) + } + + @Test + fun testPerformSync_DownloadUpdatedMember() = runTest { + val collection = LocalTestCollection().apply { + lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag") + entries += LocalTestResource().apply { + fileName = "downloaded-member.txt" + eTag = "MemberETag1" + } + } + server.enqueue(queryCapabilitiesResponse(cTag = "new-ctag")) + + val syncManager = syncManager(collection).apply { + listAllRemoteResult = listOf( + Pair(Response( + server.url("/"), + server.url("/downloaded-member.txt"), + null, + listOf(PropStat( + listOf( + GetETag("\"MemberETag2\"") + ), + StatusLine(Protocol.HTTP_1_1, 200, "OK") + ) + )), HrefRelation.MEMBER) + ) + + assertDownloadRemote = mapOf(Pair(server.url("/downloaded-member.txt"), "MemberETag2")) + } + syncManager.performSync() + + assertFalse(syncManager.didGenerateUpload) + assertTrue(syncManager.didListAllRemote) + assertTrue(syncManager.didDownloadRemote) + assertFalse(syncManager.syncResult.hasError()) + assertEquals(1, collection.entries.size) + assertEquals("MemberETag2", collection.entries.first().eTag) + } + + @Test + fun testPerformSync_RemoveVanishedMember() = runTest { + val collection = LocalTestCollection().apply { + lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag") + entries += LocalTestResource().apply { + fileName = "downloaded-member.txt" + } + } + server.enqueue(queryCapabilitiesResponse(cTag = "new-ctag")) + + val syncManager = syncManager(collection) + syncManager.performSync() + + assertFalse(syncManager.didGenerateUpload) + assertTrue(syncManager.didListAllRemote) + assertFalse(syncManager.didDownloadRemote) + assertFalse(syncManager.syncResult.hasError()) + assertTrue(collection.entries.isEmpty()) + } + + @Test + fun testPerformSync_CTagDidntChange() = runTest { + val collection = LocalTestCollection().apply { + lastSyncState = SyncState(SyncState.Type.CTAG, "ctag1") + } + server.enqueue(queryCapabilitiesResponse("ctag1")) + + val syncManager = syncManager(collection) + syncManager.performSync() + + assertFalse(syncManager.didGenerateUpload) + assertFalse(syncManager.didListAllRemote) + assertFalse(syncManager.didDownloadRemote) + assertFalse(syncManager.syncResult.hasError()) + assertTrue(collection.entries.isEmpty()) + } + + + // helpers + + private fun syncManager( + localCollection: LocalTestCollection, + syncResult: SyncResult = SyncResult(), + collection: Collection = mockk(relaxed = true) { + every { id } returns 1 + every { url } returns server.url("/") + } + ) = syncManagerFactory.create( + account, + httpClientBuilder.build(), + syncResult, + localCollection, + collection + ) + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncerTest.kt new file mode 100644 index 0000000..ee54409 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncerTest.kt @@ -0,0 +1,228 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.accounts.Account +import android.content.ContentProviderClient +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.resource.LocalDataStore +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.impl.annotations.SpyK +import io.mockk.junit4.MockKRule +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import java.util.logging.Logger + +class SyncerTest { + + @get:Rule + val mockkRule = MockKRule(this) + + @RelaxedMockK + lateinit var logger: Logger + + val dataStore: LocalTestStore = mockk(relaxed = true) + val provider: ContentProviderClient = mockk(relaxed = true) + + @SpyK + @InjectMockKs + var syncer = TestSyncer(mockk(relaxed = true), null, SyncResult(), dataStore) + + + @Test + fun testSync_prepare_fails() { + every { syncer.prepare(provider) } returns false + every { syncer.getSyncEnabledCollections() } returns emptyMap() + + // Should stop the sync after prepare returns false + syncer.sync(provider) + verify(exactly = 1) { syncer.prepare(provider) } + verify(exactly = 0) { syncer.getSyncEnabledCollections() } + } + + @Test + fun testSync_prepare_succeeds() { + every { syncer.prepare(provider) } returns true + every { syncer.getSyncEnabledCollections() } returns emptyMap() + + // Should continue the sync after prepare returns true + syncer.sync(provider) + verify(exactly = 1) { syncer.prepare(provider) } + verify(exactly = 1) { syncer.getSyncEnabledCollections() } + } + + + @Test + fun testUpdateCollections_deletesCollection() { + val localCollection = mockk { + every { dbCollectionId } returns 0L + every { title } returns "Collection to be deleted locally" + } + + // Should delete the localCollection if dbCollection (remote) does not exist + val localCollections = mutableListOf(localCollection) + val result = syncer.updateCollections(mockk(), localCollections, emptyMap()) + verify(exactly = 1) { dataStore.delete(localCollection) } + + // Updated local collection list should be empty + assertTrue(result.isEmpty()) + } + + @Test + fun testUpdateCollections_updatesCollection() { + val localCollection = mockk { + every { dbCollectionId } returns 0L + every { title } returns "The Local Collection" + } + val dbCollection = mockk { + every { id } returns 0L + } + val dbCollections = mapOf(0L to dbCollection) + + // Should update the localCollection if it exists + val result = syncer.updateCollections(provider, listOf(localCollection), dbCollections) + verify(exactly = 1) { dataStore.update(provider, localCollection, dbCollection) } + + // Updated local collection list should be same as input + assertArrayEquals(arrayOf(localCollection), result.toTypedArray()) + } + + @Test + fun testUpdateCollections_findsNewCollection() { + val dbCollection = mockk { + every { id } returns 0L + } + val localCollections = listOf(mockk { + every { dbCollectionId } returns 0L + }) + val dbCollections = listOf(dbCollection) + val dbCollectionsMap = mapOf(dbCollection.id to dbCollection) + every { syncer.createLocalCollections(provider, dbCollections) } returns localCollections + + // Should return the new collection, because it was not updated + val result = syncer.updateCollections(provider, emptyList(), dbCollectionsMap) + + // Updated local collection list contain new entry + assertEquals(1, result.size) + assertEquals(dbCollection.id, result[0].dbCollectionId) + } + + + @Test + fun testCreateLocalCollections() { + val localCollection = mockk() + val dbCollection = mockk() + every { dataStore.create(provider, dbCollection) } returns localCollection + + // Should return list of newly created local collections + val result = syncer.createLocalCollections(provider, listOf(dbCollection)) + assertEquals(listOf(localCollection), result) + } + + + @Test + fun testSyncCollectionContents() { + val dbCollection1 = mockk() + val dbCollection2 = mockk() + val dbCollections = mapOf( + 0L to dbCollection1, + 1L to dbCollection2 + ) + val localCollection1 = mockk { every { dbCollectionId } returns 0L } + val localCollection2 = mockk { every { dbCollectionId } returns 1L } + val localCollections = listOf(localCollection1, localCollection2) + every { localCollection1.dbCollectionId } returns 0L + every { localCollection2.dbCollectionId } returns 1L + every { syncer.syncCollection(provider, any(), any()) } just runs + + // Should call the collection content sync on both collections + syncer.syncCollectionContents(provider, localCollections, dbCollections) + verify(exactly = 1) { syncer.syncCollection(provider, localCollection1, dbCollection1) } + verify(exactly = 1) { syncer.syncCollection(provider, localCollection2, dbCollection2) } + } + + + // Test helpers + + class TestSyncer( + account: Account, + resyncType: ResyncType?, + syncResult: SyncResult, + theDataStore: LocalTestStore + ) : Syncer(account, resyncType, syncResult) { + + override val dataStore: LocalTestStore = + theDataStore + + override val serviceType: String + get() = throw NotImplementedError() + + override fun prepare(provider: ContentProviderClient): Boolean = + throw NotImplementedError() + + override fun getDbSyncCollections(serviceId: Long): List = + throw NotImplementedError() + + override fun syncCollection( + provider: ContentProviderClient, + localCollection: LocalTestCollection, + remoteCollection: Collection + ) { + throw NotImplementedError() + } + + } + + class LocalTestStore : LocalDataStore { + + override val authority: String + get() = throw NotImplementedError() + + override fun acquireContentProvider(throwOnMissingPermissions: Boolean): ContentProviderClient? { + throw NotImplementedError() + } + + override fun create( + provider: ContentProviderClient, + fromCollection: Collection + ): LocalTestCollection? { + throw NotImplementedError() + } + + override fun getAll( + account: Account, + provider: ContentProviderClient + ): List { + throw NotImplementedError() + } + + override fun update( + provider: ContentProviderClient, + localCollection: LocalTestCollection, + fromCollection: Collection + ) { + throw NotImplementedError() + } + + override fun delete(localCollection: LocalTestCollection) { + throw NotImplementedError() + } + + override fun updateAccount(oldAccount: Account, newAccount: Account) { + throw NotImplementedError() + } + + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/TestSyncManager.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/TestSyncManager.kt new file mode 100644 index 0000000..43bcc45 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/TestSyncManager.kt @@ -0,0 +1,123 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.accounts.Account +import at.bitfire.dav4jvm.DavCollection +import at.bitfire.dav4jvm.MultiResponseCallback +import at.bitfire.dav4jvm.Response +import at.bitfire.dav4jvm.property.caldav.GetCTag +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.di.SyncDispatcher +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.resource.LocalResource +import at.bitfire.davdroid.resource.SyncState +import at.bitfire.davdroid.util.DavUtils.lastSegment +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineDispatcher +import okhttp3.HttpUrl +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import org.junit.Assert.assertEquals + +class TestSyncManager @AssistedInject constructor( + @Assisted account: Account, + @Assisted httpClient: HttpClient, + @Assisted syncResult: SyncResult, + @Assisted localCollection: LocalTestCollection, + @Assisted collection: Collection, + @SyncDispatcher syncDispatcher: CoroutineDispatcher +): SyncManager( + account, + httpClient, + SyncDataType.EVENTS, + syncResult, + localCollection, + collection, + resync = null, + syncDispatcher +) { + + @AssistedFactory + interface Factory { + fun create( + account: Account, + httpClient: HttpClient, + syncResult: SyncResult, + localCollection: LocalTestCollection, + collection: Collection + ): TestSyncManager + } + + override fun prepare(): Boolean { + davCollection = DavCollection(httpClient.okHttpClient, collection.url) + return true + } + + var didQueryCapabilities = false + override suspend fun queryCapabilities(): SyncState? { + if (didQueryCapabilities) + throw IllegalStateException("queryCapabilities() must not be called twice") + didQueryCapabilities = true + + var cTag: SyncState? = null + davCollection.propfind(0, GetCTag.NAME) { response, rel -> + if (rel == Response.HrefRelation.SELF) + response[GetCTag::class.java]?.cTag?.let { + cTag = SyncState(SyncState.Type.CTAG, it) + } + } + + return cTag + } + + var didGenerateUpload = false + override fun generateUpload(resource: LocalTestResource): RequestBody { + didGenerateUpload = true + return resource.toString().toRequestBody() + } + + override fun syncAlgorithm() = SyncAlgorithm.PROPFIND_REPORT + + var listAllRemoteResult = emptyList>() + var didListAllRemote = false + override suspend fun listAllRemote(callback: MultiResponseCallback) { + if (didListAllRemote) + throw IllegalStateException("listAllRemote() must not be called twice") + didListAllRemote = true + for (result in listAllRemoteResult) + callback.onResponse(result.first, result.second) + } + + var assertDownloadRemote = emptyMap() + var didDownloadRemote = false + override suspend fun downloadRemote(bunch: List) { + didDownloadRemote = true + assertEquals(assertDownloadRemote.keys.toList(), bunch) + + for ((url, eTag) in assertDownloadRemote) { + val fileName = url.lastSegment + var localEntry = localCollection.entries.firstOrNull { it.fileName == fileName } + if (localEntry == null) { + val newEntry = LocalTestResource().also { + it.fileName = fileName + } + localCollection.entries += newEntry + localEntry = newEntry + } + localEntry.eTag = eTag + localEntry.flags = LocalResource.FLAG_REMOTELY_PRESENT + } + } + + override fun postProcess() { + } + + override fun notifyInvalidResourceTitle() = + throw NotImplementedError() + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorkerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorkerTest.kt new file mode 100644 index 0000000..5ce6f34 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorkerTest.kt @@ -0,0 +1,163 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync.account + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import android.os.Bundle +import androidx.hilt.work.HiltWorkerFactory +import androidx.work.testing.TestListenableWorkerBuilder +import at.bitfire.davdroid.R +import at.bitfire.davdroid.TestUtils +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.settings.SettingsManager +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class AccountsCleanupWorkerTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var accountsCleanupWorkerFactory: AccountsCleanupWorker.Factory + + @Inject @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var db: AppDatabase + + @Inject + lateinit var settingsManager: SettingsManager + + @Inject + lateinit var workerFactory: HiltWorkerFactory + + lateinit var accountManager: AccountManager + lateinit var addressBookAccountType: String + lateinit var addressBookAccount: Account + lateinit var service: Service + + @Before + fun setUp() { + hiltRule.inject() + TestUtils.setUpWorkManager(context, workerFactory) + + accountManager = AccountManager.get(context) + service = createTestService() + + addressBookAccountType = context.getString(R.string.account_type_address_book) + addressBookAccount = Account("Fancy address book account", addressBookAccountType) + } + + @After + fun tearDown() { + // Remove the account here in any case; Nice to have when the test fails + accountManager.removeAccountExplicitly(addressBookAccount) + } + + + @Test + fun testCleanUpServices_noAccount() { + // Insert service that reference to invalid account + db.serviceDao().insertOrReplace(Service(id = 1, accountName = "test", type = Service.TYPE_CALDAV, principal = null)) + assertNotNull(db.serviceDao().get(1)) + + // Create worker and run the method + val worker = TestListenableWorkerBuilder(context) + .setWorkerFactory(workerFactory) + .build() + worker.cleanUpServices() + + // Verify that service is deleted + assertNull(db.serviceDao().get(1)) + } + + @Test + fun testCleanUpServices_oneAccount() { + TestAccount.provide { existingAccount -> + // Insert services, one that reference the existing account and one that references an invalid account + db.serviceDao().insertOrReplace(Service(id = 1, accountName = existingAccount.name, type = Service.TYPE_CALDAV, principal = null)) + assertNotNull(db.serviceDao().get(1)) + + db.serviceDao().insertOrReplace(Service(id = 2, accountName = "not existing", type = Service.TYPE_CARDDAV, principal = null)) + assertNotNull(db.serviceDao().get(2)) + + // Create worker and run the method + val worker = TestListenableWorkerBuilder(context) + .setWorkerFactory(workerFactory) + .build() + worker.cleanUpServices() + + // Verify that one service is deleted and the other one is kept + assertNotNull(db.serviceDao().get(1)) + assertNull(db.serviceDao().get(2)) + } + } + + + @Test + fun testCleanUpAddressBooks_deletesAddressBookWithoutAccount() { + // Create address book account without corresponding account + assertTrue(accountManager.addAccountExplicitly(addressBookAccount, null, null)) + assertEquals(listOf(addressBookAccount), accountManager.getAccountsByType(addressBookAccountType).toList()) + + // Create worker and run the method + val worker = TestListenableWorkerBuilder(context) + .setWorkerFactory(workerFactory) + .build() + worker.cleanUpAddressBooks() + + // Verify account was deleted + assertTrue(accountManager.getAccountsByType(addressBookAccountType).isEmpty()) + } + + @Test + fun testCleanUpAddressBooks_keepsAddressBookWithAccount() { + TestAccount.provide { existingAccount -> + // Create address book account _with_ corresponding account and verify + val userData = Bundle(2).apply { + putString(LocalAddressBook.USER_DATA_ACCOUNT_NAME, existingAccount.name) + putString(LocalAddressBook.USER_DATA_ACCOUNT_TYPE, existingAccount.type) + } + assertTrue(accountManager.addAccountExplicitly(addressBookAccount, null, userData)) + assertEquals(listOf(addressBookAccount), accountManager.getAccountsByType(addressBookAccountType).toList()) + + // Create worker and run the method + val worker = TestListenableWorkerBuilder(context) + .setWorkerFactory(workerFactory) + .build() + worker.cleanUpAddressBooks() + + // Verify account was _not_ deleted + assertEquals(listOf(addressBookAccount), accountManager.getAccountsByType(addressBookAccountType).toList()) + } + } + + + // helpers + + private fun createTestService(): Service { + val service = Service(id=0, accountName="test", type=Service.TYPE_CARDDAV, principal = null) + val serviceId = db.serviceDao().insertOrReplace(service) + return db.serviceDao().get(serviceId)!! + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/SystemAccountUtilsTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/SystemAccountUtilsTest.kt new file mode 100644 index 0000000..b05b36d --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/SystemAccountUtilsTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync.account + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import android.os.Bundle +import at.bitfire.davdroid.R +import at.bitfire.davdroid.settings.SettingsManager +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class SystemAccountUtilsTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @Inject @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var settingsManager: SettingsManager + + @Before + fun setUp() { + hiltRule.inject() + } + + + @Test + fun testCreateAccount() { + val userData = Bundle(2) + userData.putString("int", "1") + userData.putString("string", "abc/\"-") + + val account = Account("AccountUtilsTest", context.getString(R.string.account_type)) + val manager = AccountManager.get(context) + try { + assertTrue(SystemAccountUtils.createAccount(context, account, userData)) + + // validate user data + assertEquals("1", manager.getUserData(account, "int")) + assertEquals("abc/\"-", manager.getUserData(account, "string")) + } finally { + assertTrue(manager.removeAccountExplicitly(account)) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/TestAccount.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/TestAccount.kt new file mode 100644 index 0000000..c354f97 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/TestAccount.kt @@ -0,0 +1,53 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ +package at.bitfire.davdroid.sync.account + +import android.accounts.Account +import android.accounts.AccountManager +import androidx.test.platform.app.InstrumentationRegistry +import at.bitfire.davdroid.R +import at.bitfire.davdroid.settings.AccountSettings +import org.junit.Assert.assertTrue + +object TestAccount { + + private val targetContext by lazy { InstrumentationRegistry.getInstrumentation().targetContext } + + /** + * Creates a test account, usually in the `Before` setUp of a test. + * + * Remove it with [remove]. + */ + fun create(version: Int = AccountSettings.CURRENT_VERSION, accountName: String = "Test Account"): Account { + val accountType = targetContext.getString(R.string.account_type) + val account = Account(accountName, accountType) + + val initialData = AccountSettings.initialUserData(null) + initialData.putString(AccountSettings.KEY_SETTINGS_VERSION, version.toString()) + assertTrue(SystemAccountUtils.createAccount(targetContext, account, initialData)) + + return account + } + + /** + * Removes a test account, usually in the `@After` tearDown of a test. + */ + fun remove(account: Account) { + val am = AccountManager.get(targetContext) + assertTrue(am.removeAccountExplicitly(account)) + } + + /** + * Convenience method to create a test account and remove it after executing the block. + */ + fun provide(version: Int = AccountSettings.CURRENT_VERSION, block: (Account) -> Unit) { + val account = create(version) + try { + block(account) + } finally { + remove(account) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorkerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorkerTest.kt new file mode 100644 index 0000000..5c59409 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorkerTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync.worker + +import android.accounts.Account +import android.content.Context +import androidx.work.ListenableWorker +import androidx.work.WorkManager +import androidx.work.WorkerFactory +import androidx.work.WorkerParameters +import androidx.work.testing.TestListenableWorkerBuilder +import androidx.work.workDataOf +import at.bitfire.davdroid.R +import at.bitfire.davdroid.TestUtils +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.sync.account.TestAccount +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.junit4.MockKRule +import io.mockk.mockkObject +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class PeriodicSyncWorkerTest { + + @Inject @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var syncWorkerFactory: PeriodicSyncWorker.Factory + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val mockkRule = MockKRule(this) + + lateinit var account: Account + + @Before + fun setUp() { + hiltRule.inject() + TestUtils.setUpWorkManager(context) + + account = TestAccount.create() + } + + @After + fun tearDown() { + TestAccount.remove(account) + } + + + @Test + fun doWork_cancelsItselfOnInvalidAccount() = runTest { + val invalidAccount = Account("invalid", context.getString(R.string.account_type)) + + // Run PeriodicSyncWorker as TestWorker + val inputData = workDataOf( + BaseSyncWorker.INPUT_DATA_TYPE to SyncDataType.EVENTS.toString(), + BaseSyncWorker.INPUT_ACCOUNT_NAME to invalidAccount.name, + BaseSyncWorker.INPUT_ACCOUNT_TYPE to invalidAccount.type + ) + + // observe WorkManager cancellation call + val workManager = WorkManager.getInstance(context) + mockkObject(workManager) + + // run test worker, expect failure + val testWorker = TestListenableWorkerBuilder(context, inputData) + .setWorkerFactory(object: WorkerFactory() { + override fun createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters) = + syncWorkerFactory.create(appContext, workerParameters) + }) + .build() + val result = testWorker.doWork() + assertTrue(result is ListenableWorker.Result.Failure) + + // verify that worker called WorkManager.cancelWorkById() + verify { + workManager.cancelWorkById(testWorker.id) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/SyncConditionsTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/SyncConditionsTest.kt new file mode 100644 index 0000000..391e1ab --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/SyncConditionsTest.kt @@ -0,0 +1,280 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync.worker + +import android.accounts.Account +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN +import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED +import android.net.NetworkCapabilities.TRANSPORT_WIFI +import android.net.wifi.WifiInfo +import android.net.wifi.WifiManager +import androidx.core.content.getSystemService +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.sync.SyncConditions +import at.bitfire.davdroid.util.PermissionUtils +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.junit4.MockKRule +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.spyk +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class SyncConditionsTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val mockkRule = MockKRule(this) + + @MockK + lateinit var capabilities: NetworkCapabilities + + @Inject + @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var factory: SyncConditions.Factory + + @MockK + lateinit var network1: Network + + @MockK + lateinit var network2: Network + + + private lateinit var accountSettings: AccountSettings + + private lateinit var conditions: SyncConditions + + private lateinit var connectivityManager: ConnectivityManager + + @Before + fun setup() { + hiltRule.inject() + + // prepare accountSettings with some necessary data + accountSettings = mockk { + every { account } returns Account("test", "test") + every { getIgnoreVpns() } returns false // default value + } + + conditions = factory.create(accountSettings) + + connectivityManager = context.getSystemService()!!.also { cm -> + mockkObject(cm) + every { cm.allNetworks } returns arrayOf(network1, network2) + every { cm.getNetworkInfo(network1) } returns mockk() + every { cm.getNetworkInfo(network2) } returns mockk() + every { cm.getNetworkCapabilities(network1) } returns capabilities + every { cm.getNetworkCapabilities(network2) } returns capabilities + } + } + + + @Test + fun testCorrectWifiSsid_CorrectWiFiSsid() { + every { accountSettings.getSyncWifiOnlySSIDs() } returns listOf("SampleWiFi1","ConnectedWiFi") + + mockkObject(PermissionUtils) + every { PermissionUtils.canAccessWifiSsid(any()) } returns true + + val wifiManager = context.getSystemService()!! + mockkObject(wifiManager) + every { wifiManager.connectionInfo } returns spyk().apply { + every { ssid } returns "ConnectedWiFi" + } + + assertTrue(conditions.correctWifiSsid()) + } + + @Test + fun testCorrectWifiSsid_WrongWiFiSsid() { + every { accountSettings.getSyncWifiOnlySSIDs() } returns listOf("SampleWiFi1","SampleWiFi2") + + mockkObject(PermissionUtils) + every { PermissionUtils.canAccessWifiSsid(any()) } returns true + + val wifiManager = context.getSystemService()!! + mockkObject(wifiManager) + every { wifiManager.connectionInfo } returns spyk().apply { + every { ssid } returns "ConnectedWiFi" + } + + assertFalse(conditions.correctWifiSsid()) + } + + + @Test + fun testInternetAvailable_capabilitiesNull() { + every { connectivityManager.getNetworkCapabilities(network1) } returns null + every { connectivityManager.getNetworkCapabilities(network2) } returns null + assertFalse(conditions.internetAvailable()) + } + + @Test + fun testInternetAvailable_Internet() { + every { capabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns true + every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns false + assertFalse(conditions.internetAvailable()) + } + + @Test + fun testInternetAvailable_Validated() { + every { capabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns false + every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns true + assertFalse(conditions.internetAvailable()) + } + + @Test + fun testInternetAvailable_InternetValidated() { + every { capabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns true + every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns true + assertTrue(conditions.internetAvailable()) + } + + @Test + fun testInternetAvailable_ignoreVpns() { + every { accountSettings.getIgnoreVpns() } returns true + every { capabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns true + every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns true + every { capabilities.hasCapability(NET_CAPABILITY_NOT_VPN) } returns false + assertFalse(conditions.internetAvailable()) + } + + @Test + fun testInternetAvailable_ignoreVpns_NotVpn() { + every { accountSettings.getIgnoreVpns() } returns true + every { capabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns true + every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns true + every { capabilities.hasCapability(NET_CAPABILITY_NOT_VPN) } returns true + assertTrue(conditions.internetAvailable()) + } + + @Test + fun testInternetAvailable_twoConnectionsFirstOneWithoutInternet() { + // The real case that failed in davx5-ose#395 is that the connection list contains (in this order) + // 1. a mobile network without INTERNET, but with VALIDATED + // 2. a WiFi network with INTERNET and VALIDATED + + // The "return false" of hasINTERNET will trigger at the first connection, the + // "andThen true" will trigger for the second connection + every { capabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns false andThen true + every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns true + + // There is an internet connection if any(!) connection has both INTERNET and VALIDATED. + assertTrue(conditions.internetAvailable()) + } + + @Test + fun testInternetAvailable_twoConnectionsFirstOneWithoutValidated() { + every { capabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns true + every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns false andThen true + assertTrue(conditions.internetAvailable()) + } + + @Test + fun testInternetAvailable_twoConnectionsFirstOneWithoutNotVpn() { + every { accountSettings.getIgnoreVpns() } returns true + every { capabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns true + every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns true + every { capabilities.hasCapability(NET_CAPABILITY_NOT_VPN) } returns false andThen true + assertTrue(conditions.internetAvailable()) + } + + + @Test + fun testWifiAvailable_capabilitiesNull() { + every { connectivityManager.getNetworkCapabilities(network1) } returns null + every { connectivityManager.getNetworkCapabilities(network2) } returns null + assertFalse(conditions.wifiAvailable()) + } + + @Test + fun testWifiAvailable() { + every { capabilities.hasTransport(TRANSPORT_WIFI) } returns false + every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns false + assertFalse(conditions.wifiAvailable()) + } + + @Test + fun testWifiAvailable_wifi() { + every { capabilities.hasTransport(TRANSPORT_WIFI) } returns true + every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns false + assertFalse(conditions.wifiAvailable()) + } + + @Test + fun testWifiAvailable_validated() { + every { capabilities.hasTransport(TRANSPORT_WIFI) } returns false + every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns true + assertFalse(conditions.wifiAvailable()) + } + + @Test + fun testWifiAvailable_wifiValidated() { + every { capabilities.hasTransport(TRANSPORT_WIFI) } returns true + every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns true + assertTrue(conditions.wifiAvailable()) + } + + + @Test + fun testWifiConditionsMet_withoutWifi() { + // "Sync only over Wi-Fi" is disabled + every { accountSettings.getSyncWifiOnly() } returns false + + assertTrue(factory.create(accountSettings).wifiConditionsMet()) + } + + @Test + fun testWifiConditionsMet_anyWifi_wifiEnabled() { + // "Sync only over Wi-Fi" is enabled + every { accountSettings.getSyncWifiOnly() } returns true + + // Wi-Fi is available + mockkObject(conditions) { + // Wi-Fi is available + every { conditions.wifiAvailable() } returns true + + // Wi-Fi SSID is correct + every { conditions.correctWifiSsid() } returns true + + assertTrue(conditions.wifiConditionsMet()) + } + } + + @Test + fun testWifiConditionsMet_anyWifi_wifiDisabled() { + // "Sync only over Wi-Fi" is enabled + every { accountSettings.getSyncWifiOnly() } returns true + + mockkObject(conditions) { + // Wi-Fi is not available + every { conditions.wifiAvailable() } returns false + + // Wi-Fi SSID is correct + every { conditions.correctWifiSsid() } returns true + + assertFalse(conditions.wifiConditionsMet()) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManagerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManagerTest.kt new file mode 100644 index 0000000..c7f8be8 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManagerTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync.worker + +import android.accounts.Account +import android.content.Context +import androidx.hilt.work.HiltWorkerFactory +import at.bitfire.davdroid.TestUtils +import at.bitfire.davdroid.TestUtils.workScheduledOrRunning +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.sync.account.TestAccount +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class SyncWorkerManagerTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @Inject + @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var syncWorkerManager: SyncWorkerManager + + @Inject + lateinit var workerFactory: HiltWorkerFactory + + lateinit var account: Account + + @Before + fun setUp() { + hiltRule.inject() + TestUtils.setUpWorkManager(context, workerFactory) + + account = TestAccount.create() + } + + @After + fun tearDown() { + TestAccount.remove(account) + } + + + // one-time sync workers + + @Test + fun testEnqueueOneTime() { + val workerName = OneTimeSyncWorker.workerName(account, SyncDataType.EVENTS) + assertFalse(TestUtils.workScheduledOrRunningOrSuccessful(context, workerName)) + + val returnedName = syncWorkerManager.enqueueOneTime(account, SyncDataType.EVENTS) + assertEquals(workerName, returnedName) + assertTrue(TestUtils.workScheduledOrRunningOrSuccessful(context, workerName)) + } + + + // periodic sync workers + + @Test + fun enablePeriodic() { + syncWorkerManager.enablePeriodic(account, SyncDataType.EVENTS, 60, false).result.get() + + val workerName = PeriodicSyncWorker.workerName(account, SyncDataType.EVENTS) + assertTrue(workScheduledOrRunning(context, workerName)) + } + + @Test + fun disablePeriodic() { + syncWorkerManager.enablePeriodic(account, SyncDataType.EVENTS, 60, false).result.get() + syncWorkerManager.disablePeriodic(account, SyncDataType.EVENTS).result.get() + + val workerName = PeriodicSyncWorker.workerName(account, SyncDataType.EVENTS) + assertFalse(workScheduledOrRunning(context, workerName)) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/ui/CollectionSelectedUseCaseTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/ui/CollectionSelectedUseCaseTest.kt new file mode 100644 index 0000000..b1dbe17 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/ui/CollectionSelectedUseCaseTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.push.PushRegistrationManager +import at.bitfire.davdroid.repository.DavCollectionRepository +import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.sync.worker.SyncWorkerManager +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.coVerify +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.junit4.MockKRule +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@OptIn(ExperimentalCoroutinesApi::class) +@HiltAndroidTest +class CollectionSelectedUseCaseTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val mockkRule = MockKRule(this) + + val collection = Collection( + id = 2, + serviceId = 1, + type = Collection.Companion.TYPE_CALENDAR, + url = "https://example.com".toHttpUrl() + ) + + @Inject + lateinit var collectionRepository: DavCollectionRepository + + val service = Service( + id = 1, + type = Service.Companion.TYPE_CALDAV, + accountName = "test@example.com" + ) + + @BindValue + @RelaxedMockK + lateinit var pushRegistrationManager: PushRegistrationManager + + @Inject + lateinit var serviceRepository: DavServiceRepository + + @BindValue + @RelaxedMockK + lateinit var syncWorkerManager: SyncWorkerManager + + @Inject + lateinit var useCase: CollectionSelectedUseCase + + @Before + fun setUp() { + hiltRule.inject() + + serviceRepository.insertOrReplaceBlocking(service) + collectionRepository.insertOrUpdateByUrl(collection) + } + + @After + fun tearDown() { + serviceRepository.deleteAllBlocking() + } + + + @Test + fun testHandleWithDelay() = runTest { + useCase.handleWithDelay(collectionId = collection.id) + + advanceUntilIdle() + coVerify { + syncWorkerManager.enqueueOneTimeAllAuthorities(any()) + pushRegistrationManager.update(service.id) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/ui/DebugInfoActivityTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/ui/DebugInfoActivityTest.kt new file mode 100644 index 0000000..d0f7593 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/ui/DebugInfoActivityTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test + +class DebugInfoActivityTest { + + @Test + fun testIntentBuilder_LargeLocalResource() { + val a = 'A'.code.toByte() + val intent = DebugInfoActivity.IntentBuilder(InstrumentationRegistry.getInstrumentation().context) + .withLocalResource(String(ByteArray(1024*1024) { a })) + .build() + val expected = StringBuilder(DebugInfoActivity.IntentBuilder.MAX_ELEMENT_SIZE) + expected.append(String(ByteArray(DebugInfoActivity.IntentBuilder.MAX_ELEMENT_SIZE - 3) { a })) + expected.append("...") + assertEquals(expected.toString(), intent.getStringExtra(DebugInfoActivity.EXTRA_LOCAL_RESOURCE_SUMMARY)) + } + + @Test + fun testIntentBuilder_LargeLogs() { + val a = 'A'.code.toByte() + val intent = DebugInfoActivity.IntentBuilder(InstrumentationRegistry.getInstrumentation().context) + .withLogs(String(ByteArray(1024*1024) { a })) + .build() + val expected = StringBuilder(DebugInfoActivity.IntentBuilder.MAX_ELEMENT_SIZE) + expected.append(String(ByteArray(DebugInfoActivity.IntentBuilder.MAX_ELEMENT_SIZE - 3) { a })) + expected.append("...") + assertEquals(expected.toString(), intent.getStringExtra("logs")) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/ui/setup/LoginActivityTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/ui/setup/LoginActivityTest.kt new file mode 100644 index 0000000..25ff4f7 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/ui/setup/LoginActivityTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import android.content.Intent +import android.net.Uri +import org.junit.Assert.assertEquals +import org.junit.Test + +class LoginActivityTest { + + @Test + fun loginInfoFromIntent() { + val intent = Intent().apply { + data = Uri.parse("https://example.com/nextcloud") + putExtra(LoginActivity.EXTRA_USERNAME, "user") + putExtra(LoginActivity.EXTRA_PASSWORD, "password") + } + val loginInfo = LoginActivity.loginInfoFromIntent(intent) + assertEquals("https://example.com/nextcloud", loginInfo.baseUri.toString()) + assertEquals("user", loginInfo.credentials!!.username) + assertEquals("password", loginInfo.credentials.password?.asString()) + } + + @Test + fun loginInfoFromIntent_withPort() { + val intent = Intent().apply { + data = Uri.parse("https://example.com:444/nextcloud") + putExtra(LoginActivity.EXTRA_USERNAME, "user") + putExtra(LoginActivity.EXTRA_PASSWORD, "password") + } + val loginInfo = LoginActivity.loginInfoFromIntent(intent) + assertEquals("https://example.com:444/nextcloud", loginInfo.baseUri.toString()) + assertEquals("user", loginInfo.credentials!!.username) + assertEquals("password", loginInfo.credentials.password?.asString()) + } + + @Test + fun loginInfoFromIntent_implicit() { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("davx5://user:password@example.com/path")) + val loginInfo = LoginActivity.loginInfoFromIntent(intent) + assertEquals("https://example.com/path", loginInfo.baseUri.toString()) + assertEquals("user", loginInfo.credentials!!.username) + assertEquals("password", loginInfo.credentials.password?.asString()) + } + + @Test + fun loginInfoFromIntent_implicit_withPort() { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("davx5://user:password@example.com:0/path")) + val loginInfo = LoginActivity.loginInfoFromIntent(intent) + assertEquals("https://example.com:0/path", loginInfo.baseUri.toString()) + assertEquals("user", loginInfo.credentials!!.username) + assertEquals("password", loginInfo.credentials.password?.asString()) + } + + @Test + fun loginInfoFromIntent_implicit_email() { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("mailto:user@example.com")) + val loginInfo = LoginActivity.loginInfoFromIntent(intent) + assertEquals(null, loginInfo.baseUri) + assertEquals("user@example.com", loginInfo.credentials!!.username) + assertEquals(null, loginInfo.credentials.password?.asString()) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/webdav/CredentialsStoreTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/webdav/CredentialsStoreTest.kt new file mode 100644 index 0000000..121e70d --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/webdav/CredentialsStoreTest.kt @@ -0,0 +1,41 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav + +import at.bitfire.davdroid.settings.Credentials +import at.bitfire.davdroid.util.SensitiveString.Companion.toSensitiveString +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class CredentialsStoreTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var store: CredentialsStore + + @Before + fun setUp() { + hiltRule.inject() + } + + @Test + fun testSetGetDelete() { + store.setCredentials(0, Credentials(username = "myname", password = "12345".toSensitiveString())) + assertEquals(Credentials(username = "myname", password = "12345".toSensitiveString()), store.getCredentials(0)) + + store.setCredentials(0, null) + assertNull(store.getCredentials(0)) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/webdav/WebDavMountRepositoryTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/webdav/WebDavMountRepositoryTest.kt new file mode 100644 index 0000000..ae79b95 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/webdav/WebDavMountRepositoryTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav + +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class WebDavMountRepositoryTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var repository: WebDavMountRepository + + @Before + fun setUp() { + hiltRule.inject() + } + + val web = MockWebServer() + val url = web.url("/") + + @Test + fun testHasWebDav_NoDavHeader() = runTest { + web.enqueue(MockResponse().setResponseCode(200)) + assertNull(repository.hasWebDav(url, null)) + } + + @Test + fun testHasWebDav_DavClass1() = runTest { + web.enqueue(MockResponse() + .setResponseCode(200) + .addHeader("DAV: 1")) + assertEquals(url, repository.hasWebDav(url, null)) + } + + @Test + fun testHasWebDav_DavClass2() = runTest { + web.enqueue(MockResponse() + .setResponseCode(200) + .addHeader("DAV: 1, 2")) + assertEquals(url,repository.hasWebDav(url, null)) + } + + @Test + fun testHasWebDav_DavClass3() = runTest { + web.enqueue(MockResponse() + .setResponseCode(200) + .addHeader("DAV: 1, 3")) + assertEquals(url,repository.hasWebDav(url, null)) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/webdav/operation/QueryChildDocumentsOperationTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/webdav/operation/QueryChildDocumentsOperationTest.kt new file mode 100644 index 0000000..e3f3204 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/webdav/operation/QueryChildDocumentsOperationTest.kt @@ -0,0 +1,247 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav.operation + +import android.content.Context +import android.security.NetworkSecurityPolicy +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.WebDavDocument +import at.bitfire.davdroid.db.WebDavMount +import at.bitfire.davdroid.network.HttpClient +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.junit4.MockKRule +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.logging.Logger +import javax.inject.Inject + +@HiltAndroidTest +class QueryChildDocumentsOperationTest { + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val mockkRule = MockKRule(this) + + @Inject @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var db: AppDatabase + + @Inject + lateinit var operation: QueryChildDocumentsOperation + + @Inject + lateinit var httpClientBuilder: HttpClient.Builder + + @Inject + lateinit var testDispatcher: TestDispatcher + + private lateinit var server: MockWebServer + private lateinit var client: HttpClient + + private lateinit var mount: WebDavMount + private lateinit var rootDocument: WebDavDocument + + @Before + fun setUp() { + hiltRule.inject() + + // create server and client + server = MockWebServer().apply { + dispatcher = testDispatcher + start() + } + + client = httpClientBuilder.build() + + // mock server delivers HTTP without encryption + assertTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted) + + // create WebDAV mount and root document in DB + runBlocking { + val mountId = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", server.url(PATH_WEBDAV_ROOT))) + mount = db.webDavMountDao().getById(mountId) + rootDocument = db.webDavDocumentDao().getOrCreateRoot(mount) + } + } + + @After + fun tearDown() { + client.close() + server.shutdown() + + runBlocking { + db.webDavMountDao().deleteAsync(mount) + } + } + + + @Test + fun testDoQueryChildren_insert() = runTest { + // Query + operation.queryChildren(rootDocument) + + // Assert new children were inserted into db + assertEquals(3, db.webDavDocumentDao().getChildren(rootDocument.id).size) + assertEquals("Library", db.webDavDocumentDao().getChildren(rootDocument.id)[0].displayName) + assertEquals("MeowMeow_Cats.docx", db.webDavDocumentDao().getChildren(rootDocument.id)[1].displayName) + assertEquals("Secret_Document.pages", db.webDavDocumentDao().getChildren(rootDocument.id)[2].displayName) + } + + @Test + fun testDoQueryChildren_update() = runTest { + // Create parent and root in database + assertEquals("Cat food storage", db.webDavDocumentDao().get(rootDocument.id)!!.displayName) + + // Create a folder + val folderId = db.webDavDocumentDao().insert( + WebDavDocument( + 0, + mount.id, + rootDocument.id, + "My_Books", + true, + "My Books", + ) + ) + assertEquals("My_Books", db.webDavDocumentDao().get(folderId)!!.name) + assertEquals("My Books", db.webDavDocumentDao().get(folderId)!!.displayName) + + // Query - should update the parent displayname and folder name + operation.queryChildren(rootDocument) + + // Assert parent and children were updated in database + assertEquals("Cats WebDAV", db.webDavDocumentDao().get(rootDocument.id)!!.displayName) + assertEquals("Library", db.webDavDocumentDao().getChildren(rootDocument.id)[0].name) + assertEquals("Library", db.webDavDocumentDao().getChildren(rootDocument.id)[0].displayName) + + } + + @Test + fun testDoQueryChildren_delete() = runTest { + // Create a folder + val folderId = db.webDavDocumentDao().insert( + WebDavDocument(0, mount.id, rootDocument.id, "deleteme", true, "Should be deleted") + ) + assertEquals("deleteme", db.webDavDocumentDao().get(folderId)!!.name) + + // Query - discovers serverside deletion + operation.queryChildren(rootDocument) + + // Assert folder got deleted + assertEquals(null, db.webDavDocumentDao().get(folderId)) + } + + @Test + fun testDoQueryChildren_updateTwoDirectoriesSimultaneously() = runTest { + // Create two directories + val parent1Id = db.webDavDocumentDao().insert(WebDavDocument(0, mount.id, rootDocument.id, "parent1", true)) + val parent2Id = db.webDavDocumentDao().insert(WebDavDocument(0, mount.id, rootDocument.id, "parent2", true)) + val parent1 = db.webDavDocumentDao().get(parent1Id)!! + val parent2 = db.webDavDocumentDao().get(parent2Id)!! + assertEquals("parent1", parent1.name) + assertEquals("parent2", parent2.name) + + // Query - find children of two nodes simultaneously + operation.queryChildren(parent1) + operation.queryChildren(parent2) + + // Assert the two folders names have changed + assertEquals("childOne.txt", db.webDavDocumentDao().getChildren(parent1Id)[0].name) + assertEquals("childTwo.txt", db.webDavDocumentDao().getChildren(parent2Id)[0].name) + } + + + // mock server + + class TestDispatcher @Inject constructor( + private val logger: Logger + ): Dispatcher() { + + data class Resource( + val name: String, + val props: String + ) + + override fun dispatch(request: RecordedRequest): MockResponse { + logger.info("Request: $request") + val requestPath = request.path!!.trimEnd('/') + + if (request.method.equals("PROPFIND", true)) { + val propsMap = mutableMapOf( + PATH_WEBDAV_ROOT to arrayOf( + Resource("", + "" + + "Cats WebDAV" + ), + Resource("Secret_Document.pages", + "Secret_Document.pages", + ), + Resource("MeowMeow_Cats.docx", + "MeowMeow_Cats.docx" + ), + Resource("Library", + "" + + "Library" + ) + ), + + "$PATH_WEBDAV_ROOT/parent1" to arrayOf( + Resource("childOne.txt", + "childOne.txt" + ), + ), + "$PATH_WEBDAV_ROOT/parent2" to arrayOf( + Resource("childTwo.txt", + "childTwo.txt" + ) + ) + ) + + val responses = propsMap[requestPath]?.joinToString { resource -> + "$requestPath/${resource.name}" + + resource.props + + "" + } + + val multistatus = + "" + + responses + + "" + + logger.info("Response: $multistatus") + return MockResponse() + .setResponseCode(207) + .setBody(multistatus) + } + + return MockResponse().setResponseCode(404) + } + + } + + + companion object { + private const val PATH_WEBDAV_ROOT = "/webdav" + } + +} \ No newline at end of file diff --git a/app/src/androidTest/res/drawable-hdpi/ic_launcher.png b/app/src/androidTest/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..96a442e5b8e9394ccf50bab9988cb2316026245d GIT binary patch literal 9397 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 literal 0 HcmV?d00001 diff --git a/app/src/androidTest/res/drawable-mdpi/ic_launcher.png b/app/src/androidTest/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..359047dfa4ed206e41e2354f9c6b307e713efe32 GIT binary patch literal 5237 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 literal 0 HcmV?d00001 diff --git a/app/src/androidTest/res/drawable-xhdpi/ic_launcher.png b/app/src/androidTest/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..71c6d760f05183ef8a47c614d8d13380c8528499 GIT binary patch literal 14383 zcmV+~IMBz5P)>IR{Zx9EA~4K?jU8DyU!%BVu|c#=(H1 zIAFva(2=Yn8AKWhO=@Vm>As!A%_mpwu-+fLs?Ir051^0kZ=Q9(`cB=t=bYMm<@H-@ z?@QQC#}7(lHuiOKOg-hI-&yJQ@X z>38Dx`mgcs{{O@!m2+^EdNUPDF+a6!8!8*d@!BI^jeED=gH;btqEI5d{e*jVDP7bq z{q~MSBE(fsoQg6}7k95+Ji!s3$poDp-qlOkXAwnM{3JB1P1P!!MLkm@C24>Si7~v(J@mNzG-t<6(_#~IP~Z}QN`;~#%u^^ zBv=E1KsZ>EXwWhEA%MjWSj+&p1YiKMScFGKjPH_0g9QS9!hVpahud$BNHq6km8f&$y)VmTQ`qJPd+?0zVd*nDN_N;fDC>PCKgkkd- zF&a`~zS4LCy*S)Om}M0r157c%Vz&|}g=6?|;XWKwAQT*MxQ#H?lrYWC!I5q;pTUZZ zoF|S^mMxt;_qPCIXf(txX5a0Ww;uk~=vd{jwJXPI%UbvK`FqRT9{O`bUiO)BJM_2% z(XOY!tbcIB+EHv;)4J*BV9|&y5&#Sa0{{$SB&foHK?p!lAcP=9mJn^Q zEdF4f`u+CiwmYVjr%WuN^Du#n`yU&B^3IJzBL_Zu-$?zTyBfz|`{R*^-t)z|a`kd+ z3q1~f(k6y5Nm3x1Yb_kKdg+KYV*sjIe!V z{5>Bz^<6`n@li*u;}T2+4lyJ`2oxNk906cBFdVfoiU|zCpa} z1i&zeF@X)3#Clk0*p&E|Ev$2}*1}l_W2{Z$7(q~!&ar*`feE?ciQuhsm(q`Gl}fN+ z@eJbtu1z-J9Kjlg^G?2Vm(yjpIN`_LzXAXv^r3($xF(p5y?b9P1*F-Cr~YXsj=g)| zS$n>$x7f>y=ZgXCM@>wqVLVI>hXL%1sn{O{%!kA@0KEW80E%#MFwm*p_a{B zD)9ll)VtgP1B?cSF@g0+Q1@mB1{Ma^85pZ!tc5iO#u!-ZV6}xY4oPBJCzg_?K&wta zn%L5Rj?vAeG*Bm!j&+Mc0?>)WhhMvFm(gdJCt~yENoevA*5h{EDh@*#(_{(r%m&=? zu|e$lr34M$iU-{w?Joo(Y{qhgD4~QIkSM}}!O$?MLZbI-s18e=OF&ai&7-M0rh0zYyI+(=47^@pK8?@?t)yRhO zzs%pSswcJ+l9+kcqH%0n*9V;dpM3NE&pVBFsSjxAt=MWGLVz-sxL2ty_6bwL*y%l( z^9>+yo3UI7lth3j7{MAa0$2!WSj1?ejxkiQ4K<7-K?@ef2cKYAaNFUg(T{h&499@8 zfO7ildBY909A~mi5d(n62vetXrh7` z4HzV;U3Zyv?>JqX@EIcrL17PGz;pl_gtaW`qV2(}?K z7!zhaTCssiN~pzE)ZG|bt^v&&Iw!VCuMKp5YG@e$;~cE9-qBhIYucx?3~Lx{30fye zS{fl{!|4FcxRUz?fTWbfM0}x+#ep9=eVP@JqE)w;wWx(pTzXQP1!_hCDgS-E@^?9S!F42HJ_S_#uc_5Su zs5YV8=8;EdD(d~XBf)i7k@eOjOu}f!6L8G}mPQ{ykK7Z1=*K{C7^dQQG~*hqW*BXt zwShMNOtkjDYl9@w(22=Uqtnw^7;U{qm`pPmt+!FL;E8XQ{Y&G*#ZExj-eADv1EkRiA9p=HbW9mXn&pE zx6s<=(T*{$-anb}*Q^f2@NW}!Ypi#4-44eZ5;wFGR z2l-#ffa_PC34p;4_~V9Ch1H=Mop@k2T=ZsZ95ER2~w$V2Qwf@K~R83 zvJIQ6w*fXxCEOy(CETXcuAvj1GDN3@H|;ZhZ>JU*V<1q%=E-}pVf-!#5kQI%P6I0* zTLpFk*7~tCJ3&MYqC=<6ZM^c6Z@7>dv20Zp<}9uM?_~fH0U)$$1VND)+d76o^q=A^ zEr^rEHJg*7*_`x*)CPi!7_L8n$2VUEYYnzlmg6rQKZCm73TFhg)~N(r7^9)J_GT#Y z=E!J+L>qrUGe4>H>r4xD=7=p^O5i)6{5&4r@Eg=yoNE;R%JeoxjiXN3-XX0XM8Z3x+2kseod+K#}a>@yV^%M}^*#iQp1F zAst%zV+r1|H5(QIra@x@LRv&YFN9=BDFGr7sAH&E#DX-22b|;do=c^e;n;zlgR|aA zyY$*QZ{k|5CRq1iVqyY?LIkChclb`g8G$6Wu3oE&%0x0;uh6maSl?4UGb=(U=b9CT zAAD)W^Fp)dRRgSbAYouM5g5E}`|w<2-3dk;YPD)2(M=f5sbl0cDunQcOk3Ku&N5x^1FSJ=M3mZon=-*VILENo0tgU=eUPES)PX*zAoL7o z=^+bdICcU=mYo}9XOEjc^IkZoMNjft0EE-uvH$-*2E<7n^$EZlD+Y?kfE~ZUXxp14 zEf*&Z@EgTT(Y7k=$iK(SA|BR=ybI5Z(;@VwCMZ!$sa_=8wT7h@fN5QG4U zvlvfCab)odtTZ3MLn~IoCYzzuBK6l5SDPdEd-X-eRX!@EFbu5#2NG>lLPR;HL-}yh z`_wi&MC5}HqLgS1BLC{41#goav%lv!HA~s6mwsoR&nay7yEk7xf5)QejjzT(&AaOVO#?>xa{z!6%4qPn@N-<8|7}ThG@fYqze_s}1$89iq|O`10Jds> zYaEiem4=mV>361M;_0g=f=i>8)OmJ>lG;J1CPwF4k%DWP#OL>1TN^ShV9rgEXOi~~ zo@v>AmuiBAwT9R;XvwTawOIhrs)H{7(gpbBM@FC!BA{L{Kms92D$+oBAOK+VhGBg7 zc3)5U{+-ADeGFL39|7~7nBW-O`9f^QpHak8ybYhG0{W>$Q)!!B3u9_nx2~CC?^LgC zw{LpU1qHTp&{+jz9CbniodoVWt?PyotcB^iXFaoWV!JN0<83{suyab>OdC2+=C-z^ z*N%~DOvW?==a`rY)^SNHJ^KfD&w!Ai3aa?hC9_FWO<7cBACBb`&gR+lG2YO;P7w)N z$40Dvd?O~u8W0k=P_IuBrh5qCR6NJtRo;Uu{YcZwM}hWjy#XVYoCUvLpd zn?q7ah~9Dw)-ffue$<-Vr!$MGYy)F7V6=nL-sT&_xx^dO37}>6x)aZ_usS8a%cMPf zzwKh0F>OY;)b6|VyE8_(G-_&JBaQvN3G>W?H+4=hAT(PCWA*%fj=K_LBQ@Gqt;@M| z0ZT|@FlvE~(|`wNGT+_rM8!xctgZCX?71^U5PB0x1YCU0kH~j9c;9A zYgg6?07kd90N`nW-cG@|S^K;O3l@!{FPe@H@;ShX>*$mw_$j6^H?+9E=;4JzVe!A@_?7{ll9hUq1mbgaVweTVAJ>>5RxDy zfyg`1+@W^8a!MHF63fmz-L`Zicf>A}NqK&zoP2oG6*0z51&Nt7Xq#*6oY5hmlvF>Uo>Ti(<_Xtp)F~;ksPsCeiHJgq7 zn$5=R4m)V>q0WihPCt1@ef7GAsEk=IlmzNki#xB|p40kiCCT4D^jduClFfL-Sv@e^ zq6;hk={{Bbz?2dOzty0|8!a3{^g%#iL_dXUZG5(F%43_g;A~0i{de7X?|+~1_Lqu} z|7ndFoN~|&f4=+SEz(T;R$MDCC9*6F4U%CCGKx{`Arwmi!h%2$3aF4ga|D3|00Km= zqm;J_I=921Ib{Opzk;3UNYv8Prgq*kOu|TFhq%dTH7uHSz{U}59Kkd~#0`PT>R4;r z*3qB6=(O->fBDloG%$^<-m+w9!-M}_oKl}V(7!?8r*DX#7%u# zqiRa;J8#t~r@W!xW`h%=JMerO17z636 z>Mb-fJc&3q&`AQ4jHsXxMuey+Q78!%N`#<5P)Z>xNCcroSP&p$2q6&!5-MaMt^Vc| zPeWE~7&-y0wP4542_uOu;-<%xlGq|?IJ|60S##{G0sLlSv?cqe2e#FWpP2z*0cQeKM=O$hoZYsudfZqvbY?RiHsquN31R{S z0>CNg*igOhM72^+CdV655EMRErtjZ%@l}86Iq1lP-m}kvi!p0H>ql3u3HDgW*t#yn z)(sXTTY<6dEliBY7#@kytXt?9ND{yq_^zwxbnKYQFtUpAP7eV{38;XeLZDCx5EUhQ z`T~@D6^gwAJ^dOzQ=dY)M{-|ZKNTkJ85`G@zCy6ewr-p}R9j}CAtu5EK^OvzHZ~P& zv|0v9lWAf^^R`XRg8}?z+r}m>+`HE&c+bRu=EMLn8`!d8f@lwkiS6ouM!Z2XVnZZ} zg!InY5u5{zwn$nAjYgtc4ab!+w-}&k-kf6x*RNUKSE+8n)c*Nu!QvU%V{eOMG!^U^ z^=1XFra|0vXw`w*q(;4(pjowO)HLd~1dUpPxMh*F99k`pjQY$u%^949O_Q+9JP83v zMUYBBDFGFD^A;5(!h-Z#6%nF>M4==R6@+I-Kv03VcSd^?Rj)d7Y^-%mlES^`(fP~X z`^AHcjk>1VWK1eFkTUTo1_RDGXzjddYd9n=qGp}>?Ju|ouQ_`GKKQD?;zM6O@R=Fl zbO;b5X+)SoAHa`qeOsYf6CCRVQYe6QZgVrcYP3V#vZz-yRmNighLdVfZ>5UU7AU}H@0rcd5CEg?Gc!Pt!ZA}W!(}(TI#qBn!3=VaL7hz@xpV7?oe3bJ zdJa5tR(}-sRpORy7`8oOBALjM3)zi_o|!!u`^Dj6v?Eq9p-V)oXiw-F^3s( zGX_Y(8W2ebDg9`PDDC6-s_6;lnFH5NW$#Km9BhYhfe8eO#59oT7@;ad$pDTmIw`?u z19cu|KzBaC$g^SR+Cs(-IW&>YlaNb@;PybeXpvLjKQB`Nk&PJuv}<(Jc}K$MQ>Gn| z$j(4JpIye)lw2u7sf`AlXgf>mCCs`G>9a1yW_B=TopzMlh^Axq!)1v$X<=+~8x#*> z-jo->B!r2|b{Jy-R_(+sBeLrzen!~LbaDsrokMPDIlX2NOL%&ue{6q$N8;E;CZA#w zaXtGW05mJzGXFnoKn@VMO;}oV$|Z`snBY<(k#9wosn*!G84wn5zQ5Mn^z?hY4@jTm z+FIb!=Tn-Mwc{J2UW1DA?tu3mx$H*`L^tI?Z91X>{FLJiu_yR&#Cwa5{Qs25|buw&r+a zojE^m|EX=`vJ8(D3BP!vJblLWa-a&W_FxFPjn3@1OY0pXv$fncA!a}d1?L=MU4hmH z1LeJN+<~vh{tHh=Pia~%2s5VciBpgLERGs~6PB<3Z#=sGT1+;!BMM6hgJMd2(`B1G zCAU+_^WY|py4pS^P4t{`%*u!2sbEo;eeC!O-<3yz@6H1}2KFo(&|%a3@0C;vsQnCX zzb};*4=WJ>mMS1Aq-4&K#Y{ajtx0_W5yE!VDZ{PF;$ZANesHv+rAR|EeqT*t+X5T3LfYMTmlO%4pjaGG=pN&O+S| zMsyICJZwfp6nV*ZkR4H2Zk*HWP9M^FIM;pe=}?3SQi=9Bog~@tlSH0yWISNUd4!S) z2{Tyhn4Pu649X_!Z6KweNkh-{b0j3?N1!?Da?|o37v?^|T#kh>!=~ zUj1WZoFtOH{yC1AWgdBTa-i*yI|7N!S>st4(B@EHIuvcKXb&N-H!g^JRGvOpLO^F|o(F{~cf1z(-Y(%2 zIFgPtZS5lWj)P}*sTax1NZK z6_m6>1a0l;kd}PHOh`-<{iOw1IQT+b^!>Ns%y%A!>;Lc@z)46U(~gGc42^aj)>#k{ zq*SO^8~DLbzkyTE+zXfe_>0(Q?kSKc!dQdOfFf;8L=g0#RG6NVh#>LU(5>X0>7I92 zMvR=HnWJ{8>B(MgHx#t9k|bmL)J0xB0T3t#$Z?KMba1{SBkYj6Ac$1ZzS*5McNWBv zI^7xl2jC4SeG?a5a4qI7nTpSU`*k?yBQM2Wci-$WAt6#mSUlU20dUL=DJ1Ik27YtZ z6?oHm$KaAHK7gZ+J_J50^Tlr|C9HAy{Y_Wm zSJz&Qr#9b%Lk>I!A9>$ZIPS1hA%wtWWgPXYfeYFhaCd@5I}DR}-Npw)A_}u`)@SBf zCeUFOoC6R*$*?2(Nyp3G<9-?g-uR-+ap6y2;E_lGBs!em4){nH@zV)p4N&L`gR?9& zjhHe%r0_yBo&*3`XAr0eFFxu`IO@QE#!bt9u>+An5<56z-;4V+ z3C)tn6uTmcdOXoX5arHbvK_{DV2IPJub;JAZdhnw&H4z9oLyZGouSK;XW z-+;HA@nI}kvZw#7wZ4fLz+aZ#fh&IXpLlfbAF#(>3-G~rei<)1;*A*SpOrI>h;pE@ zv$&r})|o>S?SV3bo#j|c(FO&&61G&xkY&~kcs+I6#Ib+2;SSn7GXwg2r)496ps>M= zI)J{6xw$lVG9pt{-(^4mEC8FosUyiD+3mnOQBNO9wHYxubs^4t`4@4*p>M)X_kIW0 z-E;-s@$sMIWk;WbH=KSh7A{w#>;o zN+}=20uVx2fUFPAkcVM;5u`%}DXmsXNdiCuxOz6X9A4QWjN3`Jz5^qCb~|^*zIf{^ zFUE<7zZKWtekrcH;hVT^*_Bv4=TQ9h;Tth9vw#nr_bI&mgnz}%X^XogUW)&DJ$jCa zb_hSa)S|$*!XWiIl;xzkx8|JaT|&mlg{a+%p9M9~;sg94+Tj$7E=07WD$^DFrbJ@^ zLQ$!dt3y|I$UePy+>!P0(_-UpMx@zo%7}%t55c)-eiyGe;a&LNl^?^hzg~;ePk$rM zKI@AZoH{QhssWMABf0`z++;^%uafT zm}kV@W7=tFoDd?X4~aCx$`Gbbsofz=aE_UX5EY^V5rI2805Ubrq^%3YdJcIOrP;7! z3u85w%sm`0I^th2cX0`?dBr&xoH`H2Bw%(BLOm_xeERpbr8PgSc0 zr0O1Mra4`5n1OlOrSlwXW4=3LzdM_x5RhpK9)&%1BGf4j>pN?qS?2+zgUudntxx-; z2)ca*x79vpBA$~1>~JuMgl~&63@NEyxqA+u1%Otofkva|%@lX~HqL!nXVFPW!Oo>E z8qYB9_MAM(Xmr*vmc4e9e5VZPTpWQk3T~I&IOlYyA8l6$JpKQBskgK1zm0pelY8Fa2xLiE_7`ioC6%Bo zLCq`xfE~cb6q;iJfOQh3~E(;W$QhLqV%s3Q#Pd=|I0WrxYP z{m9>^18IQ$_kEnuZjVWCWOEWE(V?pVV488gW)ddnI+4hoJf5?%E5TXT8qyPXR6fXP4Cm>~aQT~4j z8T^cv|JtYelpFKR-nQA^q8;*?1Gx4Y8y>s7AOR5*)4CvSmvGFs)m^mjC_2 z(^0QKOGy#{nstk!801$Rf4EeYqKzB0-dRD;S!bQi2;DJ5z%e_c8F7>AI;QmiP>6aM zP{Dw2}f>-}+^|?~^CtC%^tW>h&t5^x5olDZ)IH8OjJRrNZ`+E%^H7pTOB4 zd>L-N`!^^Si@t^+(BX_TEXQM8k?IE=u~JgC^q7X}`E;Wy!Dc{(G*b)iw{X1QFST{U2Bp$xAj>lInhY-&J4ZZj7hcNxrSt!yX_njL)g!;Jp z>g0s@X9!sigGg)J63+QGw8juyExB0>s5)t7qvpPS)G;$3zWJ(ED3zw#vY7_s>hL=q zrZ@@OOS8egIcv$%`Pj5>3_rg56ZqrpKfxLQ{9e5L#s7k0v6xoT9Au8|WKMYJqMt1{ zl~O`Vh0(F?xcc`$!f&ttE+*@nF=N&M=Jw7(5F$lqvj*f8OUN-Sh7vun7E~w%4Anr= zto=$BsaTuTUo3}n=9Ef)Pq`#XP}3FY=A^WVS=WpwKODw;-F)t+PY{>?$6a=^au67d zD0&VWaLq68#@+YbjHm~0*#mbHK=(E)!CB+m-L~3jIdJv)GM*R|wb6c2AMKOX;j*et zkZ4rRw>Phz_>>b<6#yuyxWBvrf&yf%dU@1}4!a3PSYXUuI2DH;y#%U%8!r3R`|!R` zy#jx_?YACb71F~U&UK0W4l!1WfcmOfv(>=QfBS8md;ZDz@$Wu|zCn!x4q1qqb9+$g zZ!gH$5tO1GmOruMdZXE>UGVV_!3igw!xi=B@QK4?YtEmn4FA5>sy(W8^ATfOH&|Ey z=t%v+7dk_~?U`8<{pFbs0M32Wr6?9kxb5l<&#nRQIsbJ0||h!8Pz&|T}y%N2P2E8mafjyef|-+GMNnIb?L7UiI1 zfFy}=Q$4R`fm%d zeLdXL!=wW9DnY&f`RQ}6x@e!*Lrw1o?)omw`!76^ozqYe$-Va8!*1HR38%h&0bY3Q z3wNrmJJoNat{I(=7_D2kO@LaNTG1co!8*pkG&FK`~JDG;YJ*A=mN}`-3J*m zWI%rTQa}g-0j2!91V(2Ucsn`+$aisrw<2F zz(N2Z3n47#FPee<4w;4Z{yQXJ7XL(^U#w+TVe)CAma7wwnA&` zNEq|A-|fw(op>-#J7IrRDn~F0ZP*45>`>~nSTg+}%$dFiuDo<;r*wYCH0J#OJQcSt zy8(MI+7HD-8A53M*B9=`8RyO=Ye51bw22vE%&s;S);TO$v?mtru~68!=z`E3;AH*& zYP?n%H!6h827}nA{zB3uKmd>TzJ`AaMa-k;?_UkDrOJvbK_zCGqG zS_LkU%CBS;J1kY&ktmtD%F}%AScAn1!`rH8H4Wx0=*Pr(4Xvs`-_#<6wCM`TZ0%Xc zGcvoL<}P`1$bR{h)*8e`L~=G@3Z`1Es%^t-Rwx;~xY`;XE(e1!PIGm#g`0n~>A8^Z zS&zRHO5FLeeB0%??zeX$Dg6~Lp5Mj_)1LKZ3X`Rw+)CR1vh9DUz34tQm3ct0m>)7j`{o*_J`~IhWHtD(n@@Liu zIJfs&uKV^1Yquf(mfpYqG4sR>4^bYXo%SD_(3%E{zF1W8SQ#SnDmYJ(pMhr_w6?cnyrMj9+v}s zdu(OaS81acCULxf94EpU$AU`~1yd2KUJyrMr@*WL4&ZD`C|1a`X_f#Kh!uzeND4s| zK!^~6B1joRsRATLkTQax2!sL%5r`rXhX99Qr{J7|(*o8guu~3BS#4X=*qQ+8$AU0? z%kc2J-wEmyM;vj2tJfdHjVmfR<&b~DPcOaYd866$zIE{}*FTIGzIX zSQwP#o{JW_&%XCsocNlB*mrOaEXMKhJS=J!VWPSbjxDB7St7QL zuB38tx;^Q*vuECT>rYp09eupF+#7IM2&owLAPW0Y2>PH@(RW6BY|`UFWWjJCB1Z&H zyY$mMK&0y#gdk*#yJbgdwG)G~a8AS67>TZPyTsKTCFNtdIGT-hjvvsZUMqUN&zJUgsK2R0ZCC1 zp(;?IN))ORML~%IRiHvtLaA6rp-@B=MF^t+Dj*2u;JAf2nMAcViqX-n*tBs2#Cmj8MC|07kNe(W+0 z$d2>B{7TH3GaqB46PPl!k3R6`%lVJXzB~Q)yRLm=<*NIqwHlV2bwf$)7i*C4n`{J; zL=Z`Yp@32fg<=s>f%~VH?+-#XDM(EbLKcM}_Bn-O9lIrsMy+IxL!y&>3*#g+3ui(IzkR{wpI^Sq=(EfJ zhs>8gdL6#`%d_!+-uDZ9``70J0KzDAK_s|XR#1u%MgltBpTQ)))uh#MXjVDhhMo}x z7Ol8pbwj>u`8}KOKmH7arD@<0ply@je?RlTrd)mfFK>SA$p;T4NGAjdAMPrTiYf^y zebf|20x}?k5s_d{65FZ|&KR&O?p=+s%~NpjOCnS^7ZAtIT}pglH~kwcsnS&bTbS2@EKBEdP1Bn0PBgumxA@4T2xe)}9)BAIuB z`>yAoU4F-Iqsea3fD8i2@b^|SPErX{fj|_c8z~hf3h7zuktp^kL`5&LA_dWe^hEsn z$Nmbf8IB9+EzII`PP&GcF4?yZLL&v*Sf&}V3R3hl5(o|k;nk!v?nz)7gBm@m5MkF0!SIyT4SR6 z+ViGBn--t;wncE%0#EU+9-Y~5?gPSQ2=9tbG}TKf6@A2H8% z>^2`zES69#^kHb|N%;0vvVw?h+QdlA;B5aOmu_urvpO*#IYJ;E*ITP%1OTH9KtU?v z*PgPEWOhzU)d~W|5RQXTLInaUkRG&{{iLudV|?5HV-I`rAPkF$qB07F9z=z*D@46$ z#^V&*;ct_`q_IY9cqHcj8M~GKyEhZ=Db7bweU05~;Tkbz8g3t6MgPu>i~DmseyDp`}_M6@#}p zXMfV)Gjmp{)C=okM?$bv3W5}@WzneDMI{*#QpBGh-n{vHhaI+`KtbF6j_*gSx_c9W z-KGIj5=JH-!%=)57S4Ey+p=XuY#)2#8;yGF)x*PEme(qpgc(o)&r$);PznPIt{}8d zwiw%Ze^OlW?nYeT-o65yW$q~~M%-$`I*lZ0V%4fgU92aBl;S24Brj?tTYeNL6SXib zik{Md>?ux@g|Jr=gt4x5j}xuaO{4tjB}?}cebXhMwDcWVH#C7;ezj${GGLd((VfRt zk9-#Q-SPlV*!Ln_bI+U5)Z1lTW81Xb3Xz(2VlkR}Tp{XTq+}==Zd0OL_f1xZZYqaM z$80m8n72X(f|FK)sZ-~pS{cEdh5fK@9HXNXsMa@O!Mwwz3}Rcbi!oxB&F?QSIIdWj zx>(6VaVGmk*5<(bg6N3tnEv$EiVjmlm zKuU#5Wh;L1&Bp-%AN|S+IN+dtu>8SW;MiEQQXoi>G#VR3kNlOA0hCa%=}ubL{Rw#g z8>O^z*aor(V1b*ij4|}&n%zkb0KoqRbb1&ct<2Ko0000bbVXQnWMOn=I%9HWVRU5x zGB7bQEigGPGBQ*!IXW{kIx{jYFgH3dFsPDZ%m4rYC3HntbYx+4WjbwdWNBu305UK! pF)c7TEipD!FgH3fH###mEigAaFfey&@l*f+002ovPDHLkV1iQC3p)S+ literal 0 HcmV?d00001 diff --git a/app/src/androidTest/res/values/strings.xml b/app/src/androidTest/res/values/strings.xml new file mode 100644 index 0000000..95b8d49 --- /dev/null +++ b/app/src/androidTest/res/values/strings.xml @@ -0,0 +1,6 @@ + + + + Davx5Test + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..02ee4ca --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,353 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/gplv3.html b/app/src/main/assets/gplv3.html new file mode 100644 index 0000000..ad6baab --- /dev/null +++ b/app/src/main/assets/gplv3.html @@ -0,0 +1,628 @@ +

GNU GENERAL PUBLIC LICENSE

+

Version 3, 29 June 2007

+ +

Copyright © 2007 Free Software Foundation, Inc. +<http://fsf.org/>

+Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed.

+ +

Preamble

+ +

The GNU General Public License is a free, copyleft license for +software and other kinds of works.

+ +

The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too.

+ +

When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things.

+ +

To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others.

+ +

For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights.

+ +

Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it.

+ +

For the developers\' and authors\' protection, the GPL clearly explains +that there is no warranty for this free software. For both users\' and +authors\' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions.

+ +

Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users\' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users.

+ +

Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free.

+ +

The precise terms and conditions for copying, distribution and +modification follow.

+ +

TERMS AND CONDITIONS

+ +

0. Definitions.

+ +

“This License” refers to version 3 of the GNU General Public License.

+ +

“Copyright” also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks.

+ +

“The Program” refers to any copyrightable work licensed under this +License. Each licensee is addressed as “you”. “Licensees” and +“recipients” may be individuals or organizations.

+ +

To “modify” a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a “modified version” of the +earlier work or a work “based on” the earlier work.

+ +

A “covered work” means either the unmodified Program or a work based +on the Program.

+ +

To “propagate” a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well.

+ +

To “convey” a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying.

+ +

An interactive user interface displays “Appropriate Legal Notices” +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion.

+ +

1. Source Code.

+ +

The “source code” for a work means the preferred form of the work +for making modifications to it. “Object code” means any non-source +form of a work.

+ +

A “Standard Interface” means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language.

+ +

The “System Libraries” of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +“Major Component”, in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it.

+ +

The “Corresponding Source” for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work\'s +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work.

+ +

The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source.

+ +

The Corresponding Source for a work in source code form is that +same work.

+ +

2. Basic Permissions.

+ +

All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law.

+ +

You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you.

+ +

Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary.

+ +

3. Protecting Users\' Legal Rights From Anti-Circumvention Law.

+ +

No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures.

+ +

When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work\'s +users, your or third parties\' legal rights to forbid circumvention of +technological measures.

+ +

4. Conveying Verbatim Copies.

+ +

You may convey verbatim copies of the Program\'s source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program.

+ +

You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee.

+ +

5. Conveying Modified Source Versions.

+ +

You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions:

+ +
    +
  • a) The work must carry prominent notices stating that you modified + it, and giving a relevant date.
  • + +
  • b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + “keep intact all notices”.
  • + +
  • c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it.
  • + +
  • d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so.
  • +
+ +

A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +“aggregate” if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation\'s users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate.

+ +

6. Conveying Non-Source Forms.

+ +

You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways:

+ +
    +
  • a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange.
  • + +
  • b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge.
  • + +
  • c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b.
  • + +
  • d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements.
  • + +
  • e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d.
  • +
+ +

A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work.

+ +

A “User Product” is either (1) a “consumer product”, which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, “normally used” refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product.

+ +

“Installation Information” for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made.

+ +

If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM).

+ +

The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network.

+ +

Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying.

+ +

7. Additional Terms.

+ +

“Additional permissions” are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions.

+ +

When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission.

+ +

Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms:

+ +
    +
  • a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or
  • + +
  • b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or
  • + +
  • c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or
  • + +
  • d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or
  • + +
  • e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or
  • + +
  • f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors.
  • +
+ +

All other non-permissive additional terms are considered “further +restrictions” within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying.

+ +

If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms.

+ +

Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way.

+ +

8. Termination.

+ +

You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11).

+ +

However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation.

+ +

Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice.

+ +

Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10.

+ +

9. Acceptance Not Required for Having Copies.

+ +

You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so.

+ +

10. Automatic Licensing of Downstream Recipients.

+ +

Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License.

+ +

An “entity transaction” is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party\'s predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts.

+ +

You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it.

+ +

11. Patents.

+ +

A “contributor” is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor\'s “contributor version”.

+ +

A contributor\'s “essential patent claims” are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, “control” includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License.

+ +

Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor\'s essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version.

+ +

In the following three paragraphs, a “patent license” is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To “grant” such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party.

+ +

If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. “Knowingly relying” means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient\'s use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid.

+ +

If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it.

+ +

A patent license is “discriminatory” if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007.

+ +

Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law.

+ +

12. No Surrender of Others\' Freedom.

+ +

If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program.

+ +

13. Use with the GNU Affero General Public License.

+ +

Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such.

+ +

14. Revised Versions of this License.

+ +

The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns.

+ +

Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License “or any later version” applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation.

+ +

If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy\'s +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program.

+ +

Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version.

+ +

15. Disclaimer of Warranty.

+ +

THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

+ +

16. Limitation of Liability.

+ +

IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES.

+ +

17. Interpretation of Sections 15 and 16.

+ +

If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee.

+ +

END OF TERMS AND CONDITIONS

diff --git a/app/src/main/assets/logging.properties b/app/src/main/assets/logging.properties new file mode 100644 index 0000000..c9e100f --- /dev/null +++ b/app/src/main/assets/logging.properties @@ -0,0 +1,4 @@ + +# reduce verbose of some otherwise annoying ical4j messages +net.fortuna.ical4j.data.level = INFO +net.fortuna.ical4j.model.Recur.level = INFO diff --git a/app/src/main/assets/translators.json b/app/src/main/assets/translators.json new file mode 100644 index 0000000..2feba3a --- /dev/null +++ b/app/src/main/assets/translators.json @@ -0,0 +1 @@ +{"ar_SA":["abdunnasir"],"bg":["dpa_transifex"],"ca":["Kintu","jordibrus","zagur"],"cs":["pavelb","svetlemodry","tomas.odehnal"],"da":["Tntdruid_","knutztar","mjjzf","twikedk"],"de":["Atalanttore","TheName","Waldmeisda","Wyrrrd","YvanM","amandablue","anestiskaci","corppneq","crit12","hammaschlach","maxkl","nicolas_git","owncube"],"el":["KristinaQejvanaj","anestiskaci","diamond_gr"],"es":["Ark74","Elhea","GranPC","aluaces","jcvielma","plaguna","polkhas","xphnx"],"eu":["Osoitz","Thadah","cockeredradiation"],"fa":["Numb","ahangarha","amiraliakbari","joojoojoo","maryambehzi","mtashackori","taranehsaei"],"fr":["AlainR","Amadeen","Floflr","JorisBodin","Llorc","LoiX07","Novick","Poussinou","Thecross","YvanM","alkino2","boutil","callmemagnus","chrcha","grenatrad","jokx","mathieugfortin","paullbn","vincen","ÉricB."],"fr_FR":["Llorc","Poussinou","chrcha"],"gl":["aluaces"],"hu":["Roshek","infeeeee","jtg"],"it":["Damtux","FranzMari","ed0","malaerba","noccio","nwandy","rickyroo","technezio"],"it_IT":["malaerba"],"ja":["Naofumi","yanorei32"],"nb_NO":["elonus"],"nl":["XtremeNova","davtemp","dehart","erikhubers","frankyboy1963"],"pl":["TORminator","TheName","Valdnet","gsz","mg6","oskarjakiela"],"pt":["amalvarenga","wanderlei.huttel"],"pt_BR":["wanderlei.huttel"],"ru":["aigoshin","anm","ashed","astalavister","nick.savin","vaddd"],"sk_SK":["brango67","tiborepcek"],"sl_SI":["MrLaaky","uroszor"],"sr":["daimonion"],"sv":["Mikaelb","campbelldavid"],"szl":["chlodny"],"tr_TR":["ooguz","pultars"],"uk":["androsua","olexn","twixi007"],"uk_UA":["astalavister"],"zh_CN":["anolir","jxj2zzz79pfp9bpo","linuxbckp","mofitt2016","oksjd","phy","spice2wolf"],"zh_TW":["linuxbckp","mofitt2016","phy","waiabsfabuloushk"]} diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png new file mode 100644 index 0000000000000000000000000000000000000000..4cc81409080c7754124235d4d31c11ce7a8434f0 GIT binary patch literal 23286 zcmc$`g;$i{-v#;%-QC?Gl1fN-h;&LLV9>}&$AH8Tk_t!&f&v1P0!sIQNJ)bm{wV8 z^liv2sYywr*kJ;gJgFSZk{k`e(|gaF6FItbH~&@V;6MkpazUw!Nx?6Y626|E?lMrg z@sO_L3rf#^ZsFJXO%t>RwMS8INT-{N)&3Ivkb82tJt=*G0)COuT`bB6`3 z8b)x`4-4!S;)kNqhA;b?`(KB_Eaf6Xsh*KL$6?)eY zLYOGtN7h8983syPO*h7qOBE}=K{Wz$XmmUx###1b}=`-CM{?Ud7a#Fp> zUrCsWwjM~_7`5-BMVcsGiUyC(@2k59FJD+WAR(dPr84Fe&W2A8%#RcG<}5+s?wqmQ|JZ6&a(h_(<7%R6#!EiFaDA!48v5?Qhh|tEe4pSh|v8 zkgorMTjlu2+>dEK7Whp2Aw(=S&K-N}r(x zxEWE55dpp9k*q4n&26QddN<07zla3%AwV69h$rV+g_elvPPQ`Ql8M=FXkdCB36_!`xkrA z1z*0fc%PlNoJZop=;Gu}H&jFyzofja@ZV|MwWvf#IT7dd8&CcX-1rvEHY|4kIbHV- zah)?Pt@&VxX@}}9^iT?>qTm}SOlMYfX--2*sz+a^!oezAaKz=azl2VlHj6}B3O|BP7lvQE|djUIHjz@QEvTF=;IPNFdFC(?~ZGaO-F zxm_lB@vL!FdVZ1^Hkvx#leyW$J;Msh>-Zfs#A3?Mi_O(hEzal9zZ`o%04tMjDE=;Y zcFaq>nj_0ywx~8q0IcBzUs0#5k zc2d1caz$&AqGw`c=Swh~%QiE|w8&gcMIs6McN|rhUg%Wt%1?x1j+WX(0zL&ie^}FLMvFub%gjno>p@q0J@Vk0F@B$b%) zy8pmY=W22>g$w5{HsS*sBJ?!*2K)ZR={DE~G|k4ZXkV@2T1irRaG5$>2=6Tok9r|R7q=_>olYVd~IYgZgiVQa1@e9=)nxp_di7HcpO93Yj>at zi#NHN;GX{bLS=%U^P18XP6?SY5I~^&31`V4&J

=`J@7GtVO-rNfjD<+^5U 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 V`--=zXLU1yrm7z5y|QKa{{WyfOz;2z literal 0 HcmV?d00001 diff --git a/app/src/main/kotlin/at/bitfire/davdroid/App.kt b/app/src/main/kotlin/at/bitfire/davdroid/App.kt new file mode 100644 index 0000000..e96836c --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/App.kt @@ -0,0 +1,83 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid + +import android.app.Application +import androidx.hilt.work.HiltWorkerFactory +import androidx.work.Configuration +import at.bitfire.davdroid.di.DefaultDispatcher +import at.bitfire.davdroid.log.LogManager +import at.bitfire.davdroid.startup.StartupPlugin +import at.bitfire.davdroid.sync.account.AccountsCleanupWorker +import at.bitfire.davdroid.ui.UiUtils +import dagger.hilt.android.HiltAndroidApp +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.util.logging.Logger +import javax.inject.Inject + +@HiltAndroidApp +class App: Application(), Configuration.Provider { + + @Inject + lateinit var logger: Logger + + /** + * Creates the [LogManager] singleton and thus initializes logging. + */ + @Inject + lateinit var logManager: LogManager + + @Inject + @DefaultDispatcher + lateinit var defaultDispatcher: CoroutineDispatcher + + @Inject + lateinit var plugins: Set<@JvmSuppressWildcards StartupPlugin> + + @Inject + lateinit var workerFactory: HiltWorkerFactory + + override val workManagerConfiguration: Configuration + get() = Configuration.Builder() + .setWorkerFactory(workerFactory) + .build() + + + override fun onCreate() { + super.onCreate() + + logger.fine("Logging using LogManager $logManager") + + // set light/dark mode + UiUtils.updateTheme(this) // when this is called in the asynchronous thread below, it recreates + // some current activity and causes an IllegalStateException in rare cases + + // run startup plugins (sync) + for (plugin in plugins.sortedBy { it.priority() }) { + logger.fine("Running startup plugin: $plugin (onAppCreate)") + plugin.onAppCreate() + } + + // don't block UI for some background checks + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(defaultDispatcher) { + // clean up orphaned accounts in DB from time to time + AccountsCleanupWorker.enable(this@App) + + // create/update app shortcuts + UiUtils.updateShortcuts(this@App) + + // run startup plugins (async) + for (plugin in plugins.sortedBy { it.priorityAsync() }) { + logger.fine("Running startup plugin: $plugin (onAppCreateAsync)") + plugin.onAppCreateAsync() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/Constants.kt b/app/src/main/kotlin/at/bitfire/davdroid/Constants.kt new file mode 100644 index 0000000..9e302cd --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/Constants.kt @@ -0,0 +1,23 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ +package at.bitfire.davdroid + +import at.bitfire.synctools.icalendar.ical4jVersion +import ezvcard.Ezvcard +import net.fortuna.ical4j.model.property.ProdId + +/** + * Brand-specific constants like (non-theme) colors, homepage URLs etc. + */ +object Constants { + + const val DAVDROID_GREEN_RGBA = 0xFF8bc34a.toInt() + + + // product IDs for iCalendar/vCard + + val iCalProdId = ProdId("DAVx5/${BuildConfig.VERSION_NAME} ical4j/$ical4jVersion") + const val vCardProdId = "+//IDN bitfire.at//DAVx5/${BuildConfig.VERSION_NAME} ez-vcard/${Ezvcard.VERSION}" + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/TextTable.kt b/app/src/main/kotlin/at/bitfire/davdroid/TextTable.kt new file mode 100644 index 0000000..ed6e3c5 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/TextTable.kt @@ -0,0 +1,86 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid + +import java.util.Collections + +class TextTable( + val headers: List +) { + + companion object { + + fun indent(str: String, pos: Int): String = + " ".repeat(pos) + + str.split('\n').joinToString("\n" + " ".repeat(pos)) + + } + + constructor(vararg headers: String): this(headers.toList()) + + + private val lines = mutableListOf>() + + fun addLine(values: List) { + if (values.size != headers.size) + throw IllegalArgumentException("Table line must have ${headers.size} column(s)") + lines += values.map { + it?.toString() ?: "—" + }.toTypedArray() + } + + fun addLine(vararg values: Any?) = addLine(values.toList()) + + override fun toString(): String { + val sb = StringBuilder() + + val headerWidths = headers.map { it.length } + val colWidths = Array(headers.size) { colIdx -> + Collections.max(listOf(headerWidths[colIdx]) + lines.map { it[colIdx] }.map { it.length }) + } + + // first line + sb.append("\n┌") + for (colIdx in headers.indices) + sb .append("─".repeat(colWidths[colIdx] + 2)) + .append(if (colIdx == headers.size - 1) '┐' else '┬') + sb.append('\n') + + // header + sb.append('│') + for (colIdx in headers.indices) + sb .append(' ') + .append(headers[colIdx].padEnd(colWidths[colIdx] + 1)) + .append('│') + sb.append('\n') + + // separator between header and body + sb.append('├') + for (colIdx in headers.indices) { + sb .append("─".repeat(colWidths[colIdx] + 2)) + .append(if (colIdx == headers.size - 1) '┤' else '┼') + } + sb.append('\n') + + // body + for (line in lines) { + for (colIdx in headers.indices) + sb .append("│ ") + .append(line[colIdx].padEnd(colWidths[colIdx] + 1)) + sb.append("│\n") + } + + // last line + sb.append("└") + for (colIdx in headers.indices) { + sb .append("─".repeat(colWidths[colIdx] + 2)) + .append(if (colIdx == headers.size - 1) '┘' else '┴') + } + sb.append("\n\n") + + return sb.toString() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/AppDatabase.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/AppDatabase.kt new file mode 100644 index 0000000..047150e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/AppDatabase.kt @@ -0,0 +1,160 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import android.accounts.AccountManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.database.sqlite.SQLiteQueryBuilder +import androidx.core.app.NotificationCompat +import androidx.core.app.TaskStackBuilder +import androidx.core.database.getStringOrNull +import androidx.room.AutoMigration +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import androidx.room.migration.AutoMigrationSpec +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import at.bitfire.davdroid.R +import at.bitfire.davdroid.TextTable +import at.bitfire.davdroid.db.migration.AutoMigration12 +import at.bitfire.davdroid.db.migration.AutoMigration16 +import at.bitfire.davdroid.db.migration.AutoMigration18 +import at.bitfire.davdroid.ui.AccountsActivity +import at.bitfire.davdroid.ui.NotificationRegistry +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import java.io.Writer +import javax.inject.Singleton + +/** + * The app database. Managed via android jetpack room. Room provides an abstraction + * layer over SQLite. + * + * Note: In SQLite PRAGMA foreign_keys is off by default. Room activates it for + * production (non-test) databases. + */ +@Database(entities = [ + Service::class, + HomeSet::class, + Collection::class, + Principal::class, + SyncStats::class, + WebDavDocument::class, + WebDavMount::class +], exportSchema = true, version = 18, autoMigrations = [ + AutoMigration(from = 17, to = 18, spec = AutoMigration18::class), + AutoMigration(from = 16, to = 17), // collection: add VAPID key + AutoMigration(from = 15, to = 16, spec = AutoMigration16::class), + AutoMigration(from = 14, to = 15), + AutoMigration(from = 13, to = 14), + AutoMigration(from = 12, to = 13), + AutoMigration(from = 11, to = 12, spec = AutoMigration12::class), + AutoMigration(from = 10, to = 11), + AutoMigration(from = 9, to = 10) +]) +@TypeConverters(Converters::class) +abstract class AppDatabase: RoomDatabase() { + + @Module + @InstallIn(SingletonComponent::class) + object AppDatabaseModule { + + @Provides + @Singleton + fun appDatabase( + autoMigrations: Set<@JvmSuppressWildcards AutoMigrationSpec>, + @ApplicationContext context: Context, + manualMigrations: Set<@JvmSuppressWildcards Migration>, + notificationRegistry: NotificationRegistry + ): AppDatabase = Room + .databaseBuilder(context, AppDatabase::class.java, "services.db") + .addMigrations(*manualMigrations.toTypedArray()) + .apply { + for (spec in autoMigrations) + addAutoMigrationSpec(spec) + } + .fallbackToDestructiveMigration() // as a last fallback, recreate database instead of crashing + .addCallback(object: Callback() { + override fun onDestructiveMigration(db: SupportSQLiteDatabase) { + notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_DATABASE_CORRUPTED) { + val launcherIntent = Intent(context, AccountsActivity::class.java) + NotificationCompat.Builder(context, notificationRegistry.CHANNEL_GENERAL) + .setSmallIcon(R.drawable.ic_warning_notify) + .setContentTitle(context.getString(R.string.database_destructive_migration_title)) + .setContentText(context.getString(R.string.database_destructive_migration_text)) + .setCategory(NotificationCompat.CATEGORY_ERROR) + .setContentIntent( + TaskStackBuilder.create(context) + .addNextIntent(launcherIntent) + .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + ) + .setAutoCancel(true) + .build() + } + + // remove all accounts because they're unfortunately useless without database + val am = AccountManager.get(context) + for (account in am.getAccountsByType(context.getString(R.string.account_type))) + am.removeAccountExplicitly(account) + } + }) + .build() + + } + + + // DAOs + + abstract fun serviceDao(): ServiceDao + abstract fun homeSetDao(): HomeSetDao + abstract fun collectionDao(): CollectionDao + abstract fun principalDao(): PrincipalDao + abstract fun syncStatsDao(): SyncStatsDao + abstract fun webDavDocumentDao(): WebDavDocumentDao + abstract fun webDavMountDao(): WebDavMountDao + + + // helpers + + fun dump(writer: Writer, ignoreTables: Array) { + val db = openHelper.readableDatabase + db.beginTransactionNonExclusive() + + // iterate through all tables + db.query(SQLiteQueryBuilder.buildQueryString(false, "sqlite_master", arrayOf("name"), "type='table'", null, null, null, null)).use { cursorTables -> + while (cursorTables.moveToNext()) { + val tableName = cursorTables.getString(0) + if (ignoreTables.contains(tableName)) { + writer.append("$tableName: ") + db.query("SELECT COUNT(*) FROM $tableName").use { cursor -> + if (cursor.moveToNext()) + writer.append("${cursor.getInt(0)} row(s), data not listed here\n\n") + } + } else { + writer.append("$tableName\n") + db.query("SELECT * FROM $tableName").use { cursor -> + val table = TextTable(*cursor.columnNames) + val cols = cursor.columnCount + // print rows + while (cursor.moveToNext()) { + val values = Array(cols) { idx -> cursor.getStringOrNull(idx) } + table.addLine(*values) + } + writer.append(table.toString()) + } + } + } + db.endTransaction() + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/Collection.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/Collection.kt new file mode 100644 index 0000000..ef6dedb --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/Collection.kt @@ -0,0 +1,266 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import androidx.annotation.StringDef +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import at.bitfire.dav4jvm.Response +import at.bitfire.dav4jvm.UrlUtils +import at.bitfire.dav4jvm.property.caldav.CalendarColor +import at.bitfire.dav4jvm.property.caldav.CalendarDescription +import at.bitfire.dav4jvm.property.caldav.CalendarTimezone +import at.bitfire.dav4jvm.property.caldav.CalendarTimezoneId +import at.bitfire.dav4jvm.property.caldav.Source +import at.bitfire.dav4jvm.property.caldav.SupportedCalendarComponentSet +import at.bitfire.dav4jvm.property.carddav.AddressbookDescription +import at.bitfire.dav4jvm.property.push.PushTransports +import at.bitfire.dav4jvm.property.push.Topic +import at.bitfire.dav4jvm.property.push.WebPush +import at.bitfire.dav4jvm.property.webdav.CurrentUserPrivilegeSet +import at.bitfire.dav4jvm.property.webdav.DisplayName +import at.bitfire.dav4jvm.property.webdav.ResourceType +import at.bitfire.davdroid.util.DavUtils.lastSegment +import at.bitfire.davdroid.util.trimToNull +import at.bitfire.ical4android.util.DateUtils +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull + +@Retention(AnnotationRetention.SOURCE) +@StringDef( + Collection.TYPE_ADDRESSBOOK, + Collection.TYPE_CALENDAR, + Collection.TYPE_WEBCAL +) +annotation class CollectionType + +@Entity(tableName = "collection", + foreignKeys = [ + ForeignKey(entity = Service::class, parentColumns = arrayOf("id"), childColumns = arrayOf("serviceId"), onDelete = ForeignKey.CASCADE), + ForeignKey(entity = HomeSet::class, parentColumns = arrayOf("id"), childColumns = arrayOf("homeSetId"), onDelete = ForeignKey.SET_NULL), + ForeignKey(entity = Principal::class, parentColumns = arrayOf("id"), childColumns = arrayOf("ownerId"), onDelete = ForeignKey.SET_NULL) + ], + indices = [ + Index("serviceId","type"), + Index("homeSetId","type"), + Index("ownerId","type"), + Index("pushTopic","type"), + Index("url") + ] +) +data class Collection( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + + /** + * Service, which this collection belongs to. Services are unique, so a [Collection] is uniquely + * identifiable via its [serviceId] and [url]. + */ + val serviceId: Long = 0, + + /** + * A home set this collection belongs to. Multiple homesets are not supported. + * If *null* the collection is considered homeless. + */ + val homeSetId: Long? = null, + + /** + * Principal who is owner of this collection. + */ + val ownerId: Long? = null, + + /** + * Type of service. CalDAV or CardDAV + */ + @CollectionType + val type: String, + + /** + * Address where this collection lives - with trailing slash + */ + val url: HttpUrl, + + /** + * Whether we have the permission to change contents of the collection on the server. + * Even if this flag is set, there may still be other reasons why a collection is effectively read-only. + */ + val privWriteContent: Boolean = true, + /** + * Whether we have the permission to delete the collection on the server + */ + val privUnbind: Boolean = true, + /** + * Whether the user has manually set the "force read-only" flag. + * Even if this flag is not set, there may still be other reasons why a collection is effectively read-only. + */ + val forceReadOnly: Boolean = false, + + /** + * Human-readable name of the collection + */ + val displayName: String? = null, + /** + * Human-readable description of the collection + */ + val description: String? = null, + + // CalDAV only + val color: Int? = null, + + /** default timezone (only timezone ID, like `Europe/Vienna`) */ + val timezoneId: String? = null, + + /** whether the collection supports VEVENT; in case of calendars: null means true */ + val supportsVEVENT: Boolean? = null, + + /** whether the collection supports VTODO; in case of calendars: null means true */ + val supportsVTODO: Boolean? = null, + + /** whether the collection supports VJOURNAL; in case of calendars: null means true */ + val supportsVJOURNAL: Boolean? = null, + + /** Webcal subscription source URL */ + val source: HttpUrl? = null, + + /** whether this collection has been selected for synchronization */ + val sync: Boolean = false, + + /** WebDAV-Push topic */ + val pushTopic: String? = null, + + /** WebDAV-Push: whether this collection supports the Web Push Transport */ + @ColumnInfo(defaultValue = "0") + val supportsWebPush: Boolean = false, + + /** WebDAV-Push: VAPID public key */ + val pushVapidKey: String? = null, + + /** WebDAV-Push subscription URL */ + val pushSubscription: String? = null, + + /** when the [pushSubscription] expires (timestamp, used to determine whether we need to re-subscribe) */ + val pushSubscriptionExpires: Long? = null, + + /** when the [pushSubscription] was created/updated (timestamp) */ + val pushSubscriptionCreated: Long? = null + +) { + + companion object { + + const val TYPE_ADDRESSBOOK = "ADDRESS_BOOK" + const val TYPE_CALENDAR = "CALENDAR" + const val TYPE_WEBCAL = "WEBCAL" + + /** + * Generates a collection entity from a WebDAV response. + * @param dav WebDAV response + * @return null if the response doesn't represent a collection + */ + fun fromDavResponse(dav: Response): Collection? { + val url = UrlUtils.withTrailingSlash(dav.href) + val type: String = dav[ResourceType::class.java]?.let { resourceType -> + when { + resourceType.types.contains(ResourceType.ADDRESSBOOK) -> TYPE_ADDRESSBOOK + resourceType.types.contains(ResourceType.CALENDAR) -> TYPE_CALENDAR + resourceType.types.contains(ResourceType.SUBSCRIBED) -> TYPE_WEBCAL + else -> null + } + } ?: return null + + var privWriteContent = true + var privUnbind = true + dav[CurrentUserPrivilegeSet::class.java]?.let { privilegeSet -> + privWriteContent = privilegeSet.mayWriteContent + privUnbind = privilegeSet.mayUnbind + } + + val displayName = dav[DisplayName::class.java]?.displayName.trimToNull() + + var description: String? = null + var color: Int? = null + var timezoneId: String? = null + var supportsVEVENT: Boolean? = null + var supportsVTODO: Boolean? = null + var supportsVJOURNAL: Boolean? = null + var source: HttpUrl? = null + when (type) { + TYPE_ADDRESSBOOK -> { + dav[AddressbookDescription::class.java]?.let { description = it.description } + } + TYPE_CALENDAR, TYPE_WEBCAL -> { + dav[CalendarDescription::class.java]?.let { description = it.description } + dav[CalendarColor::class.java]?.let { color = it.color } + dav[CalendarTimezoneId::class.java]?.let { timezoneId = it.identifier } + if (timezoneId == null) + dav[CalendarTimezone::class.java]?.vTimeZone?.let { + timezoneId = DateUtils.parseVTimeZone(it)?.timeZoneId?.value + } + + if (type == TYPE_CALENDAR) { + supportsVEVENT = true + supportsVTODO = true + supportsVJOURNAL = true + dav[SupportedCalendarComponentSet::class.java]?.let { + supportsVEVENT = it.supportsEvents + supportsVTODO = it.supportsTasks + supportsVJOURNAL = it.supportsJournal + } + } else { // Type.WEBCAL + dav[Source::class.java]?.let { + source = it.hrefs.firstOrNull()?.let { rawHref -> + val href = rawHref + .replace("^webcal://".toRegex(), "http://") + .replace("^webcals://".toRegex(), "https://") + href.toHttpUrlOrNull() + } + } + supportsVEVENT = true + } + } + } + + // WebDAV-Push + var supportsWebPush = false + var vapidPublicKey: String? = null + dav[PushTransports::class.java]?.let { pushTransports -> + for (transport in pushTransports.transports) + if (transport is WebPush) { + supportsWebPush = true + vapidPublicKey = transport.vapidPublicKey?.key + } + } + val pushTopic = dav[Topic::class.java]?.topic + + return Collection( + type = type, + url = url, + privWriteContent = privWriteContent, + privUnbind = privUnbind, + displayName = displayName, + description = description, + color = color, + timezoneId = timezoneId, + supportsVEVENT = supportsVEVENT, + supportsVTODO = supportsVTODO, + supportsVJOURNAL = supportsVJOURNAL, + source = source, + supportsWebPush = supportsWebPush, + pushVapidKey = vapidPublicKey, + pushTopic = pushTopic + ) + } + + } + + // calculated properties + + fun title() = displayName ?: url.lastSegment + fun readOnly() = forceReadOnly || !privWriteContent + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/CollectionDao.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/CollectionDao.kt new file mode 100644 index 0000000..9586872 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/CollectionDao.kt @@ -0,0 +1,132 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import kotlinx.coroutines.flow.Flow + +@Dao +interface CollectionDao { + + @Query("SELECT * FROM collection WHERE id=:id") + fun get(id: Long): Collection? + + @Query("SELECT * FROM collection WHERE id=:id") + suspend fun getAsync(id: Long): Collection? + + @Query("SELECT * FROM collection WHERE id=:id") + fun getFlow(id: Long): Flow + + @Query("SELECT * FROM collection WHERE serviceId=:serviceId") + suspend fun getByService(serviceId: Long): List + + @Query("SELECT * FROM collection WHERE serviceId=:serviceId AND homeSetId IS :homeSetId") + fun getByServiceAndHomeset(serviceId: Long, homeSetId: Long?): List + + @Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type=:type ORDER BY displayName COLLATE NOCASE, url COLLATE NOCASE") + fun getByServiceAndType(serviceId: Long, @CollectionType type: String): List + + @Query("SELECT * FROM collection WHERE pushTopic=:topic AND sync") + suspend fun getSyncableByPushTopic(topic: String): Collection? + + @Suppress("unused") // for build variant + @Query("SELECT * FROM collection WHERE sync") + fun getSyncCollections(): List + + @Query("SELECT pushVapidKey FROM collection WHERE serviceId=:serviceId AND pushVapidKey IS NOT NULL LIMIT 1") + suspend fun getFirstVapidKey(serviceId: Long): String? + + @Query("SELECT COUNT(*) FROM collection WHERE serviceId=:serviceId AND type=:type") + suspend fun anyOfType(serviceId: Long, @CollectionType type: String): Boolean + + @Query("SELECT COUNT(*) FROM collection WHERE supportsWebPush AND pushTopic IS NOT NULL") + suspend fun anyPushCapable(): Boolean + + /** + * Returns collections which + * - support VEVENT and/or VTODO (= supported calendar collections), or + * - have supportsVEVENT = supportsVTODO = null (= address books) + */ + @Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type=:type " + + "AND (supportsVTODO OR supportsVEVENT OR supportsVJOURNAL OR (supportsVEVENT IS NULL AND supportsVTODO IS NULL AND supportsVJOURNAL IS NULL)) ORDER BY displayName COLLATE NOCASE, URL COLLATE NOCASE") + fun pageByServiceAndType(serviceId: Long, @CollectionType type: String): PagingSource + + @Query("SELECT * FROM collection WHERE serviceId=:serviceId AND sync") + fun getByServiceAndSync(serviceId: Long): List + + @Query("SELECT collection.* FROM collection, homeset WHERE collection.serviceId=:serviceId AND type=:type AND homeSetId=homeset.id AND homeset.personal ORDER BY collection.displayName COLLATE NOCASE, collection.url COLLATE NOCASE") + fun pagePersonalByServiceAndType(serviceId: Long, @CollectionType type: String): PagingSource + + @Query("SELECT * FROM collection WHERE serviceId=:serviceId AND url=:url") + fun getByServiceAndUrl(serviceId: Long, url: String): Collection? + + @Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type='${Collection.TYPE_CALENDAR}' AND supportsVEVENT AND sync ORDER BY displayName COLLATE NOCASE, url COLLATE NOCASE") + fun getSyncCalendars(serviceId: Long): List + + @Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type='${Collection.TYPE_CALENDAR}' AND (supportsVTODO OR supportsVJOURNAL) AND sync ORDER BY displayName COLLATE NOCASE, url COLLATE NOCASE") + fun getSyncJtxCollections(serviceId: Long): List + + @Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type='${Collection.TYPE_CALENDAR}' AND supportsVTODO AND sync ORDER BY displayName COLLATE NOCASE, url COLLATE NOCASE") + fun getSyncTaskLists(serviceId: Long): List + + /** + * Get a list of collections that are both sync enabled and push capable (supportsWebPush and + * pushTopic is available). + */ + @Query("SELECT * FROM collection WHERE serviceId=:serviceId AND sync AND supportsWebPush AND pushTopic IS NOT NULL") + suspend fun getPushCapableSyncCollections(serviceId: Long): List + + @Query("SELECT * FROM collection WHERE serviceId=:serviceId AND pushSubscription IS NOT NULL") + suspend fun getPushRegistered(serviceId: Long): List + + @Query("SELECT * FROM collection WHERE serviceId=:serviceId AND pushSubscription IS NOT NULL AND NOT sync") + suspend fun getPushRegisteredAndNotSyncable(serviceId: Long): List + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insert(collection: Collection): Long + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insertAsync(collection: Collection): Long + + @Update + fun update(collection: Collection) + + @Query("UPDATE collection SET forceReadOnly=:forceReadOnly WHERE id=:id") + suspend fun updateForceReadOnly(id: Long, forceReadOnly: Boolean) + + @Query("UPDATE collection SET pushSubscription=:pushSubscription, pushSubscriptionExpires=:pushSubscriptionExpires, pushSubscriptionCreated=:updatedAt WHERE id=:id") + suspend fun updatePushSubscription(id: Long, pushSubscription: String?, pushSubscriptionExpires: Long?, updatedAt: Long = System.currentTimeMillis()/1000) + + @Query("UPDATE collection SET sync=:sync WHERE id=:id") + suspend fun updateSync(id: Long, sync: Boolean) + + /** + * Tries to insert new row, but updates existing row if already present. + * This method preserves the primary key, as opposed to using "@Insert(onConflict = OnConflictStrategy.REPLACE)" + * which will create a new row with incremented ID and thus breaks entity relationships! + * + * @param collection Collection to be inserted or updated + * @return ID of the row, that has been inserted or updated. -1 If the insert fails due to other reasons. + */ + @Transaction + fun insertOrUpdateByUrl(collection: Collection): Long = getByServiceAndUrl( + collection.serviceId, + collection.url.toString() + )?.let { localCollection -> + update(collection.copy(id = localCollection.id)) + localCollection.id + } ?: insert(collection) + + @Delete + fun delete(collection: Collection) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/Converters.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/Converters.kt new file mode 100644 index 0000000..10240c4 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/Converters.kt @@ -0,0 +1,31 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import androidx.room.TypeConverter +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull + +class Converters { + + @TypeConverter + fun httpUrlToString(url: HttpUrl?) = + url?.toString() + + @TypeConverter + fun mediaTypeToString(mediaType: MediaType?) = + mediaType?.toString() + + @TypeConverter + fun stringToHttpUrl(url: String?): HttpUrl? = + url?.toHttpUrlOrNull() + + @TypeConverter + fun stringToMediaType(mimeType: String?): MediaType? = + mimeType?.toMediaTypeOrNull() + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/HomeSet.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/HomeSet.kt new file mode 100644 index 0000000..9f32a33 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/HomeSet.kt @@ -0,0 +1,43 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import at.bitfire.davdroid.util.DavUtils.lastSegment +import okhttp3.HttpUrl + +@Entity(tableName = "homeset", + foreignKeys = [ + ForeignKey(entity = Service::class, parentColumns = ["id"], childColumns = ["serviceId"], onDelete = ForeignKey.CASCADE) + ], + indices = [ + // index by service; no duplicate URLs per service + Index("serviceId", "url", unique = true) + ] +) +data class HomeSet( + @PrimaryKey(autoGenerate = true) + val id: Long, + + val serviceId: Long, + + /** + * Whether this homeset belongs to the [Service.principal] given by [serviceId]. + */ + val personal: Boolean, + + val url: HttpUrl, + + val privBind: Boolean = true, + + val displayName: String? = null +) { + + fun title() = displayName ?: url.lastSegment + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/HomeSetDao.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/HomeSetDao.kt new file mode 100644 index 0000000..adfd60c --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/HomeSetDao.kt @@ -0,0 +1,60 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import kotlinx.coroutines.flow.Flow + +@Dao +interface HomeSetDao { + + @Query("SELECT * FROM homeset WHERE id=:homesetId") + fun getById(homesetId: Long): HomeSet? + + @Query("SELECT * FROM homeset WHERE serviceId=:serviceId AND url=:url") + fun getByUrl(serviceId: Long, url: String): HomeSet? + + @Query("SELECT * FROM homeset WHERE serviceId=:serviceId") + fun getByService(serviceId: Long): List + + @Query("SELECT * FROM homeset WHERE serviceId=(SELECT id FROM service WHERE accountName=:accountName AND type=:serviceType) AND privBind ORDER BY displayName, url COLLATE NOCASE") + fun getBindableByAccountAndServiceTypeFlow(accountName: String, @ServiceType serviceType: String): Flow> + + @Query("SELECT * FROM homeset WHERE serviceId=:serviceId AND privBind") + fun getBindableByServiceFlow(serviceId: Long): Flow> + + @Insert + fun insert(homeSet: HomeSet): Long + + @Update + fun update(homeset: HomeSet) + + /** + * If a homeset with the given service ID and URL already exists, it is updated with the other fields. + * Otherwise, a new homeset is inserted. + * + * This method preserves the primary key, as opposed to using "@Insert(onConflict = OnConflictStrategy.REPLACE)" + * which will create a new row with incremented ID and thus breaks entity relationships! + * + * @param homeSet home set to insert/update + * + * @return ID of the row that has been inserted or updated. -1 If the insert fails due to other reasons. + */ + @Transaction + fun insertOrUpdateByUrlBlocking(homeSet: HomeSet): Long = + getByUrl(homeSet.serviceId, homeSet.url.toString())?.let { existingHomeset -> + update(homeSet.copy(id = existingHomeset.id)) + existingHomeset.id + } ?: insert(homeSet) + + @Delete + fun delete(homeset: HomeSet) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/Principal.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/Principal.kt new file mode 100644 index 0000000..03e1574 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/Principal.kt @@ -0,0 +1,70 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import at.bitfire.dav4jvm.Response +import at.bitfire.dav4jvm.UrlUtils +import at.bitfire.dav4jvm.property.webdav.DisplayName +import at.bitfire.dav4jvm.property.webdav.ResourceType +import at.bitfire.davdroid.util.trimToNull +import okhttp3.HttpUrl + +/** + * A principal entity representing a WebDAV principal (rfc3744). + */ +@Entity(tableName = "principal", + foreignKeys = [ + ForeignKey(entity = Service::class, parentColumns = arrayOf("id"), childColumns = arrayOf("serviceId"), onDelete = ForeignKey.CASCADE) + ], + indices = [ + // index by service, urls are unique + Index("serviceId", "url", unique = true) + ] +) +data class Principal( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + val serviceId: Long, + /** URL of the principal, always without trailing slash */ + val url: HttpUrl, + val displayName: String? = null +) { + + companion object { + + /** + * Generates a principal entity from a WebDAV response. + * @param dav WebDAV response (make sure that you have queried `DAV:resource-type` and `DAV:display-name`) + * @return generated principal data object (with `id`=0), `null` if the response doesn't represent a principal + */ + fun fromDavResponse(serviceId: Long, dav: Response): Principal? { + // Check if response is a principal + val resourceType = dav[ResourceType::class.java] ?: return null + if (!resourceType.types.contains(ResourceType.PRINCIPAL)) + return null + + // Try getting the display name of the principal + val displayName: String? = dav[DisplayName::class.java]?.displayName.trimToNull() + + // Create and return principal - even without it's display name + return Principal( + serviceId = serviceId, + url = UrlUtils.omitTrailingSlash(dav.href), + displayName = displayName + ) + } + + fun fromServiceAndUrl(service: Service, url: HttpUrl) = Principal( + serviceId = service.id, + url = UrlUtils.omitTrailingSlash(url) + ) + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/PrincipalDao.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/PrincipalDao.kt new file mode 100644 index 0000000..8fc344d --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/PrincipalDao.kt @@ -0,0 +1,67 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import okhttp3.HttpUrl + +@Dao +interface PrincipalDao { + + @Query("SELECT * FROM principal WHERE id=:id") + fun get(id: Long): Principal + + @Query("SELECT * FROM principal WHERE id=:id") + suspend fun getAsync(id: Long): Principal + + @Query("SELECT * FROM principal WHERE serviceId=:serviceId") + fun getByService(serviceId: Long): List + + @Query("SELECT * FROM principal WHERE serviceId=:serviceId AND url=:url") + fun getByUrl(serviceId: Long, url: HttpUrl): Principal? + + /** + * Gets all principals who do not own any collections + */ + @Query("SELECT * FROM principal WHERE principal.id NOT IN (SELECT ownerId FROM collection WHERE ownerId IS NOT NULL)") + fun getAllWithoutCollections(): List + + @Insert + fun insert(principal: Principal): Long + + @Update + fun update(principal: Principal) + + @Delete + fun delete(principal: Principal) + + /** + * Inserts, updates or just gets existing principal if its display name has not + * changed (will not update/overwrite with null values). + * + * @param principal Principal to be inserted or updated + * @return ID of the newly inserted or already existing principal + */ + fun insertOrUpdate(serviceId: Long, principal: Principal): Long { + // Try to get existing principal by URL + val oldPrincipal = getByUrl(serviceId, principal.url) + + // Insert new principal if not existing + if (oldPrincipal == null) + return insert(principal) + + // Otherwise update the existing principal + if (principal.displayName != oldPrincipal.displayName) + update(principal.copy(id = oldPrincipal.id)) + + // In any case return the id of the principal + return oldPrincipal.id + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/Service.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/Service.kt new file mode 100644 index 0000000..10bc798 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/Service.kt @@ -0,0 +1,44 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import androidx.annotation.StringDef +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import okhttp3.HttpUrl + +@Retention(AnnotationRetention.SOURCE) +@StringDef(Service.TYPE_CALDAV, Service.TYPE_CARDDAV) +annotation class ServiceType + +/** + * A service entity. + * + * Services represent accounts and are unique. They are of type CardDAV or CalDAV and may have an associated principal. + */ +@Entity(tableName = "service", + indices = [ + // only one service per type and account + Index("accountName", "type", unique = true) + ]) +data class Service( + @PrimaryKey(autoGenerate = true) + val id: Long, + + val accountName: String, + + @ServiceType + val type: String, + + val principal: HttpUrl? = null +) { + + companion object { + const val TYPE_CALDAV = "caldav" + const val TYPE_CARDDAV = "carddav" + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/ServiceDao.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/ServiceDao.kt new file mode 100644 index 0000000..c4938e7 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/ServiceDao.kt @@ -0,0 +1,49 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface ServiceDao { + + @Query("SELECT * FROM service WHERE accountName=:accountName AND type=:type") + suspend fun getByAccountAndType(accountName: String, @ServiceType type: String): Service? + + @Query("SELECT * FROM service WHERE accountName=:accountName AND type=:type") + fun getByAccountAndTypeFlow(accountName: String, @ServiceType type: String): Flow + + @Query("SELECT id FROM service WHERE accountName=:accountName") + suspend fun getIdsByAccountAsync(accountName: String): List + + @Query("SELECT * FROM service WHERE id=:id") + fun get(id: Long): Service? + + @Query("SELECT * FROM service WHERE id=:id") + suspend fun getAsync(id: Long): Service? + + @Query("SELECT * FROM service") + suspend fun getAll(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertOrReplace(service: Service): Long + + @Query("DELETE FROM service") + fun deleteAll() + + @Query("DELETE FROM service WHERE accountName=:accountName") + suspend fun deleteByAccount(accountName: String) + + @Query("DELETE FROM service WHERE accountName NOT IN (:accountNames)") + fun deleteExceptAccounts(accountNames: Array) + + @Query("UPDATE service SET accountName=:newName WHERE accountName=:oldName") + suspend fun renameAccount(oldName: String, newName: String) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/SyncStats.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/SyncStats.kt new file mode 100644 index 0000000..7dfd610 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/SyncStats.kt @@ -0,0 +1,28 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity(tableName = "syncstats", + foreignKeys = [ + ForeignKey(childColumns = arrayOf("collectionId"), entity = Collection::class, parentColumns = arrayOf("id"), onDelete = ForeignKey.CASCADE) + ], + indices = [ + Index(value = ["collectionId", "dataType"], unique = true) + ] +) +data class SyncStats( + @PrimaryKey(autoGenerate = true) + val id: Long, + + val collectionId: Long, + val dataType: String, + + val lastSync: Long +) \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/SyncStatsDao.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/SyncStatsDao.kt new file mode 100644 index 0000000..e7fdcfb --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/SyncStatsDao.kt @@ -0,0 +1,22 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface SyncStatsDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertOrReplace(syncStats: SyncStats) + + @Query("SELECT * FROM syncstats WHERE collectionId=:id") + fun getByCollectionIdFlow(id: Long): Flow> + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/WebDavDocument.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/WebDavDocument.kt new file mode 100644 index 0000000..167d2f2 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/WebDavDocument.kt @@ -0,0 +1,140 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import android.annotation.SuppressLint +import android.os.Bundle +import android.provider.DocumentsContract.Document +import android.webkit.MimeTypeMap +import androidx.core.os.bundleOf +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import at.bitfire.davdroid.util.DavUtils.MEDIA_TYPE_OCTET_STREAM +import at.bitfire.davdroid.webdav.DocumentState +import okhttp3.HttpUrl +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import java.io.FileNotFoundException +import java.time.Instant + +@Entity( + tableName = "webdav_document", + foreignKeys = [ + ForeignKey(entity = WebDavMount::class, parentColumns = ["id"], childColumns = ["mountId"], onDelete = ForeignKey.CASCADE), + ForeignKey(entity = WebDavDocument::class, parentColumns = ["id"], childColumns = ["parentId"], onDelete = ForeignKey.CASCADE) + ], + indices = [ + Index("mountId", "parentId", "name", unique = true), + Index("parentId") + ] +) +// If any column name is modified, also change it in [DavDocumentsProvider$queryChildDocuments] +data class WebDavDocument( + + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + + /** refers to the [WebDavMount] the document belongs to */ + val mountId: Long, + + /** refers to parent document (*null* when this document is a root document) */ + val parentId: Long?, + + /** file name (without any slashes) */ + val name: String, + val isDirectory: Boolean = false, + + val displayName: String? = null, + val mimeType: MediaType? = null, + val eTag: String? = null, + val lastModified: Long? = null, + val size: Long? = null, + + val mayBind: Boolean? = null, + val mayUnbind: Boolean? = null, + val mayWriteContent: Boolean? = null, + + val quotaAvailable: Long? = null, + val quotaUsed: Long? = null + +) { + + fun cacheKey(): CacheKey? { + if (eTag != null || lastModified != null) + return CacheKey(id, DocumentState(eTag, lastModified?.let { ts -> Instant.ofEpochMilli(ts) })) + return null + } + + @SuppressLint("InlinedApi") + fun toBundle(parent: WebDavDocument?): Bundle { + if (parent?.isDirectory == false) + throw IllegalArgumentException("Parent must be a directory") + + val bundle = bundleOf( + Document.COLUMN_DOCUMENT_ID to id.toString(), + Document.COLUMN_DISPLAY_NAME to name + ) + + displayName?.let { bundle.putString(Document.COLUMN_SUMMARY, it) } + size?.let { bundle.putLong(Document.COLUMN_SIZE, it) } + lastModified?.let { bundle.putLong(Document.COLUMN_LAST_MODIFIED, it) } + + // see RFC 3744 appendix B for required privileges for the various operations + var flags = Document.FLAG_SUPPORTS_COPY + if (isDirectory) { + bundle.putString(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR) + if (mayBind != false) + flags += Document.FLAG_DIR_SUPPORTS_CREATE + } else { + val reportedMimeType = mimeType ?: + MimeTypeMap.getSingleton().getMimeTypeFromExtension( + MimeTypeMap.getFileExtensionFromUrl(name) + )?.toMediaTypeOrNull() ?: + MEDIA_TYPE_OCTET_STREAM + + bundle.putString(Document.COLUMN_MIME_TYPE, reportedMimeType.toString()) + if (mimeType?.type == "image") + flags += Document.FLAG_SUPPORTS_THUMBNAIL + if (mayWriteContent != false) + flags += Document.FLAG_SUPPORTS_WRITE + } + if (parent?.mayUnbind != false) + flags += Document.FLAG_SUPPORTS_DELETE or + Document.FLAG_SUPPORTS_MOVE or + Document.FLAG_SUPPORTS_RENAME + bundle.putInt(Document.COLUMN_FLAGS, flags) + + return bundle + } + + suspend fun toHttpUrl(db: AppDatabase): HttpUrl { + val mount = db.webDavMountDao().getById(mountId) + + val segments = mutableListOf(name) + var parentIter = parentId + while (parentIter != null) { + val parent = db.webDavDocumentDao().get(parentIter) ?: throw FileNotFoundException() + segments += parent.name + parentIter = parent.parentId + } + + val builder = mount.url.newBuilder() + for (segment in segments.reversed()) + builder.addPathSegment(segment) + return builder.build() + } + + + /** + * Represents a WebDAV document in a given state (with a given ETag/Last-Modified). + */ + data class CacheKey( + val docId: Long, + val documentState: DocumentState + ) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/WebDavDocumentDao.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/WebDavDocumentDao.kt new file mode 100644 index 0000000..0c0ac43 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/WebDavDocumentDao.kt @@ -0,0 +1,107 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.RawQuery +import androidx.room.RoomRawQuery +import androidx.room.Transaction +import androidx.room.Update + +@Dao +interface WebDavDocumentDao { + + @Query("SELECT * FROM webdav_document WHERE id=:id") + fun get(id: Long): WebDavDocument? + + @Query("SELECT * FROM webdav_document WHERE mountId=:mountId AND (parentId=:parentId OR (parentId IS NULL AND :parentId IS NULL)) AND name=:name") + fun getByParentAndName(mountId: Long, parentId: Long?, name: String): WebDavDocument? + + @RawQuery + fun query(query: RoomRawQuery): List + + /** + * Gets all the child documents from a given parent id. + * + * @param parentId The id of the parent document to get the documents from. + * @param orderBy If desired, a SQL clause to specify how to order the results. + * **The caller is responsible for the correct formatting of this argument. Syntax won't be validated!** + */ + fun getChildren(parentId: Long, orderBy: String = DEFAULT_ORDER): List { + return query( + RoomRawQuery("SELECT * FROM webdav_document WHERE parentId = ? ORDER BY $orderBy") { + it.bindLong(1, parentId) + } + ) + } + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertOrReplace(document: WebDavDocument): Long + + @Query("DELETE FROM webdav_document WHERE parentId=:parentId") + fun removeChildren(parentId: Long) + + @Insert + fun insert(document: WebDavDocument): Long + + @Update + fun update(document: WebDavDocument) + + @Delete + fun delete(document: WebDavDocument) + + + // complex operations + + /** + * Tries to insert new row, but updates existing row if already present. + * This method preserves the primary key, as opposed to using "@Insert(onConflict = OnConflictStrategy.REPLACE)" + * which will create a new row with incremented ID and thus breaks entity relationships! + * + * @return ID of the row, that has been inserted or updated. -1 If the insert fails due to other reasons. + */ + @Transaction + fun insertOrUpdate(document: WebDavDocument): Long { + val parentId = document.parentId + ?: return insert(document) + val existingDocument = getByParentAndName(document.mountId, parentId, document.name) + ?: return insert(document) + update(document.copy(id = existingDocument.id)) + return existingDocument.id + } + + @Transaction + fun getOrCreateRoot(mount: WebDavMount): WebDavDocument { + getByParentAndName(mount.id, null, "")?.let { existing -> + return existing + } + + val newDoc = WebDavDocument( + mountId = mount.id, + parentId = null, + name = "", + isDirectory = true, + displayName = mount.name + ) + val id = insertOrReplace(newDoc) + return newDoc.copy(id = id) + } + + + companion object { + + /** + * Default ORDER BY value to use when content provider doesn't specify a sort order: + * _sort by name (directories first)_ + */ + const val DEFAULT_ORDER = "isDirectory DESC, name ASC" + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/WebDavMount.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/WebDavMount.kt new file mode 100644 index 0000000..c05888b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/WebDavMount.kt @@ -0,0 +1,24 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import androidx.room.Entity +import androidx.room.PrimaryKey +import okhttp3.HttpUrl + +@Entity(tableName = "webdav_mount") +data class WebDavMount( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + + /** display name of the WebDAV mount */ + val name: String, + + /** URL of the WebDAV service, including trailing slash */ + val url: HttpUrl + + // credentials are stored using CredentialsStore + +) \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/WebDavMountDao.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/WebDavMountDao.kt new file mode 100644 index 0000000..8160608 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/WebDavMountDao.kt @@ -0,0 +1,42 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface WebDavMountDao { + + @Delete + suspend fun deleteAsync(mount: WebDavMount) + + @Query("SELECT * FROM webdav_mount ORDER BY name, url") + suspend fun getAll(): List + + @Query("SELECT * FROM webdav_mount ORDER BY name, url") + fun getAllFlow(): Flow> + + @Query("SELECT * FROM webdav_mount WHERE id=:id") + suspend fun getById(id: Long): WebDavMount + + @Insert + suspend fun insert(mount: WebDavMount): Long + + + // complex queries + + /** + * Gets a list of mounts with the quotas of their root document, if available. + */ + @Query("SELECT webdav_mount.*, quotaAvailable, quotaUsed FROM webdav_mount " + + "LEFT JOIN webdav_document ON (webdav_mount.id=webdav_document.mountId AND webdav_document.parentId IS NULL) " + + "ORDER BY webdav_mount.name, webdav_mount.url") + fun getAllWithQuotaFlow(): Flow> + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/WebDavMountWithRootDocument.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/WebDavMountWithRootDocument.kt new file mode 100644 index 0000000..12de039 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/WebDavMountWithRootDocument.kt @@ -0,0 +1,18 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db + +import androidx.room.Embedded + +/** + * A [WebDavMount] with an optional root document (that contains information like quota). + */ +data class WebDavMountWithQuota( + @Embedded + val mount: WebDavMount, + + val quotaAvailable: Long? = null, + val quotaUsed: Long? = null +) \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/AutoMigration12.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/AutoMigration12.kt new file mode 100644 index 0000000..4a60e14 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/AutoMigration12.kt @@ -0,0 +1,47 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db.migration + +import android.content.Context +import androidx.room.DeleteColumn +import androidx.room.ProvidedAutoMigrationSpec +import androidx.room.migration.AutoMigrationSpec +import androidx.sqlite.db.SupportSQLiteDatabase +import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet +import java.util.logging.Logger +import javax.inject.Inject + +@ProvidedAutoMigrationSpec +@DeleteColumn(tableName = "collection", columnName = "owner") +class AutoMigration12 @Inject constructor( + @ApplicationContext val context: Context, + val logger: Logger +): AutoMigrationSpec { + + override fun onPostMigrate(db: SupportSQLiteDatabase) { + logger.info("Database update to v12, refreshing services to get display names of owners") + db.query("SELECT id FROM service", arrayOf()).use { cursor -> + while (cursor.moveToNext()) { + val serviceId = cursor.getLong(0) + RefreshCollectionsWorker.enqueue(context, serviceId) + } + } + } + + + @Module + @InstallIn(SingletonComponent::class) + abstract class AutoMigrationModule { + @Binds @IntoSet + abstract fun provide(impl: AutoMigration12): AutoMigrationSpec + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/AutoMigration16.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/AutoMigration16.kt new file mode 100644 index 0000000..df8be31 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/AutoMigration16.kt @@ -0,0 +1,47 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db.migration + +import androidx.room.ProvidedAutoMigrationSpec +import androidx.room.RenameColumn +import androidx.room.migration.AutoMigrationSpec +import androidx.sqlite.db.SupportSQLiteDatabase +import at.bitfire.ical4android.util.DateUtils +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet +import javax.inject.Inject + +/** + * The timezone column has been renamed to timezoneId, but still contains the VTIMEZONE. + * So we need to parse the VTIMEZONE, extract the timezone ID and save it back. + */ +@ProvidedAutoMigrationSpec +@RenameColumn(tableName = "collection", fromColumnName = "timezone", toColumnName = "timezoneId") +class AutoMigration16 @Inject constructor(): AutoMigrationSpec { + + override fun onPostMigrate(db: SupportSQLiteDatabase) { + db.query("SELECT id, timezoneId FROM collection").use { cursor -> + while (cursor.moveToNext()) { + val id: Long = cursor.getLong(0) + val timezoneDef: String = cursor.getString(1) ?: continue + val vTimeZone = DateUtils.parseVTimeZone(timezoneDef) + val timezoneId = vTimeZone?.timeZoneId?.value + db.execSQL("UPDATE collection SET timezoneId=? WHERE id=?", arrayOf(timezoneId, id)) + } + } + } + + + @Module + @InstallIn(SingletonComponent::class) + abstract class AutoMigrationModule { + @Binds @IntoSet + abstract fun provide(impl: AutoMigration16): AutoMigrationSpec + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/AutoMigration18.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/AutoMigration18.kt new file mode 100644 index 0000000..ac0764f --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/AutoMigration18.kt @@ -0,0 +1,81 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db.migration + +import android.provider.CalendarContract +import android.provider.ContactsContract +import androidx.room.ProvidedAutoMigrationSpec +import androidx.room.RenameColumn +import androidx.room.migration.AutoMigrationSpec +import androidx.sqlite.db.SupportSQLiteDatabase +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.ical4android.TaskProvider +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet +import javax.inject.Inject + +/** + * Renames syncstats.authority to dataType, and maps values to SyncDataType enum names. + */ +@ProvidedAutoMigrationSpec +@RenameColumn(tableName = "syncstats", fromColumnName = "authority", toColumnName = "dataType") +class AutoMigration18 @Inject constructor() : AutoMigrationSpec { + + override fun onPostMigrate(db: SupportSQLiteDatabase) { + // Drop old unique index + db.execSQL("DROP INDEX IF EXISTS index_syncstats_collectionId_authority") + + val seen = mutableSetOf>() // (collectionId, dataType) + db.query( + "SELECT id, collectionId, dataType, lastSync FROM syncstats ORDER BY lastSync DESC" + ).use { cursor -> + val idIndex = cursor.getColumnIndex("id") + val collectionIdIndex = cursor.getColumnIndex("collectionId") + val authorityIndex = cursor.getColumnIndex("dataType") + + while (cursor.moveToNext()) { + val id = cursor.getLong(idIndex) + val collectionId = cursor.getLong(collectionIdIndex) + val authority = cursor.getString(authorityIndex) + + val dataType = when (authority) { + ContactsContract.AUTHORITY -> SyncDataType.CONTACTS.name + CalendarContract.AUTHORITY -> SyncDataType.EVENTS.name + TaskProvider.ProviderName.JtxBoard.authority, + TaskProvider.ProviderName.TasksOrg.authority, + TaskProvider.ProviderName.OpenTasks.authority -> SyncDataType.TASKS.name + else -> { + db.execSQL("DELETE FROM syncstats WHERE id = ?", arrayOf(id)) + continue + } + } + + val keyValue = collectionId to dataType + if (seen.contains(keyValue)) { + db.execSQL("DELETE FROM syncstats WHERE id = ?", arrayOf(id)) + } else { + db.execSQL("UPDATE syncstats SET dataType = ? WHERE id = ?", arrayOf(dataType, id)) + seen.add(keyValue) + } + } + } + + // Create new unique index + db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS index_syncstats_collectionId_dataType ON syncstats (collectionId, dataType)") + } + + + @Module + @InstallIn(SingletonComponent::class) + abstract class AutoMigrationModule { + @Binds + @IntoSet + abstract fun provide(impl: AutoMigration18): AutoMigrationSpec + } + +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration2.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration2.kt new file mode 100644 index 0000000..caae5c2 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration2.kt @@ -0,0 +1,29 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db.migration + +import androidx.room.migration.Migration +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet + +val Migration2 = Migration(1, 2) { db -> + db.execSQL("ALTER TABLE collections ADD COLUMN type TEXT NOT NULL DEFAULT ''") + db.execSQL("ALTER TABLE collections ADD COLUMN source TEXT DEFAULT NULL") + db.execSQL("UPDATE collections SET type=(" + + "SELECT CASE service WHEN ? THEN ? ELSE ? END " + + "FROM services WHERE _id=collections.serviceID" + + ")", + arrayOf("caldav", "CALENDAR", "ADDRESS_BOOK")) +} + +@Module +@InstallIn(SingletonComponent::class) +internal object Migration2Module { + @Provides @IntoSet + fun provide(): Migration = Migration2 +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration3.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration3.kt new file mode 100644 index 0000000..4658ddb --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration3.kt @@ -0,0 +1,26 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db.migration + +import androidx.room.migration.Migration +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet +import java.util.logging.Logger + +val Migration3 = Migration(2, 3) { db -> + // We don't have access to the context in a Room migration now, so + // we will just drop those settings from old DAVx5 versions. + Logger.getGlobal().warning("Dropping settings distrustSystemCerts and overrideProxy*") +} + +@Module +@InstallIn(SingletonComponent::class) +internal object Migration3Module { + @Provides @IntoSet + fun provide(): Migration = Migration3 +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration4.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration4.kt new file mode 100644 index 0000000..b321596 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration4.kt @@ -0,0 +1,23 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db.migration + +import androidx.room.migration.Migration +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet + +val Migration4 = Migration(3, 4) { db -> + db.execSQL("ALTER TABLE collections ADD COLUMN forceReadOnly INTEGER DEFAULT 0 NOT NULL") +} + +@Module +@InstallIn(SingletonComponent::class) +internal object Migration4Module { + @Provides @IntoSet + fun provide(): Migration = Migration4 +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration5.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration5.kt new file mode 100644 index 0000000..d77581f --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration5.kt @@ -0,0 +1,29 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db.migration + +import androidx.room.migration.Migration +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet + +val Migration5 = Migration(4, 5) { db -> + db.execSQL("ALTER TABLE collections ADD COLUMN privWriteContent INTEGER DEFAULT 0 NOT NULL") + db.execSQL("UPDATE collections SET privWriteContent=NOT readOnly") + + db.execSQL("ALTER TABLE collections ADD COLUMN privUnbind INTEGER DEFAULT 0 NOT NULL") + db.execSQL("UPDATE collections SET privUnbind=NOT readOnly") + + // there's no DROP COLUMN in SQLite, so just keep the "readOnly" column +} + +@Module +@InstallIn(SingletonComponent::class) +internal object Migration5Module { + @Provides @IntoSet + fun provide(): Migration = Migration5 +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration6.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration6.kt new file mode 100644 index 0000000..91edacd --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration6.kt @@ -0,0 +1,71 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db.migration + +import androidx.room.migration.Migration +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet + +val Migration6 = Migration(5, 6) { db -> + val sql = arrayOf( + // migrate "services" to "service": rename columns, make id NOT NULL + "CREATE TABLE service(" + + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," + + "accountName TEXT NOT NULL," + + "type TEXT NOT NULL," + + "principal TEXT DEFAULT NULL" + + ")", + "CREATE UNIQUE INDEX index_service_accountName_type ON service(accountName, type)", + "INSERT INTO service(id, accountName, type, principal) SELECT _id, accountName, service, principal FROM services", + "DROP TABLE services", + + // migrate "homesets" to "homeset": rename columns, make id NOT NULL + "CREATE TABLE homeset(" + + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," + + "serviceId INTEGER NOT NULL," + + "url TEXT NOT NULL," + + "FOREIGN KEY (serviceId) REFERENCES service(id) ON DELETE CASCADE" + + ")", + "CREATE UNIQUE INDEX index_homeset_serviceId_url ON homeset(serviceId, url)", + "INSERT INTO homeset(id, serviceId, url) SELECT _id, serviceID, url FROM homesets", + "DROP TABLE homesets", + + // migrate "collections" to "collection": rename columns, make id NOT NULL + "CREATE TABLE collection(" + + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," + + "serviceId INTEGER NOT NULL," + + "type TEXT NOT NULL," + + "url TEXT NOT NULL," + + "privWriteContent INTEGER NOT NULL DEFAULT 1," + + "privUnbind INTEGER NOT NULL DEFAULT 1," + + "forceReadOnly INTEGER NOT NULL DEFAULT 0," + + "displayName TEXT DEFAULT NULL," + + "description TEXT DEFAULT NULL," + + "color INTEGER DEFAULT NULL," + + "timezone TEXT DEFAULT NULL," + + "supportsVEVENT INTEGER DEFAULT NULL," + + "supportsVTODO INTEGER DEFAULT NULL," + + "supportsVJOURNAL INTEGER DEFAULT NULL," + + "source TEXT DEFAULT NULL," + + "sync INTEGER NOT NULL DEFAULT 0," + + "FOREIGN KEY (serviceId) REFERENCES service(id) ON DELETE CASCADE" + + ")", + "CREATE INDEX index_collection_serviceId_type ON collection(serviceId,type)", + "INSERT INTO collection(id, serviceId, type, url, privWriteContent, privUnbind, forceReadOnly, displayName, description, color, timezone, supportsVEVENT, supportsVTODO, source, sync) " + + "SELECT _id, serviceID, type, url, privWriteContent, privUnbind, forceReadOnly, displayName, description, color, timezone, supportsVEVENT, supportsVTODO, source, sync FROM collections", + "DROP TABLE collections" + ) + sql.forEach { db.execSQL(it) } +} + +@Module +@InstallIn(SingletonComponent::class) +internal object Migration6Module { + @Provides @IntoSet + fun provide(): Migration = Migration6 +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration7.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration7.kt new file mode 100644 index 0000000..7f83113 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration7.kt @@ -0,0 +1,24 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db.migration + +import androidx.room.migration.Migration +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet + +val Migration7 = Migration(6, 7) { db -> + db.execSQL("ALTER TABLE homeset ADD COLUMN privBind INTEGER NOT NULL DEFAULT 1") + db.execSQL("ALTER TABLE homeset ADD COLUMN displayName TEXT DEFAULT NULL") +} + +@Module +@InstallIn(SingletonComponent::class) +internal object Migration7Module { + @Provides @IntoSet + fun provide(): Migration = Migration7 +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration8.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration8.kt new file mode 100644 index 0000000..76af39b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration8.kt @@ -0,0 +1,26 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db.migration + +import androidx.room.migration.Migration +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet + +val Migration8 = Migration(7, 8) { db -> + db.execSQL("ALTER TABLE homeset ADD COLUMN personal INTEGER NOT NULL DEFAULT 1") + db.execSQL("ALTER TABLE collection ADD COLUMN homeSetId INTEGER DEFAULT NULL REFERENCES homeset(id) ON DELETE SET NULL") + db.execSQL("ALTER TABLE collection ADD COLUMN owner TEXT DEFAULT NULL") + db.execSQL("CREATE INDEX index_collection_homeSetId_type ON collection(homeSetId, type)") +} + +@Module +@InstallIn(SingletonComponent::class) +internal object Migration8Module { + @Provides @IntoSet + fun provide(): Migration = Migration8 +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration9.kt b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration9.kt new file mode 100644 index 0000000..f066fc0 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/db/migration/Migration9.kt @@ -0,0 +1,30 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.db.migration + +import androidx.room.migration.Migration +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet + +val Migration9 = Migration(8, 9) { db -> + db.execSQL("CREATE TABLE syncstats (" + + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," + + "collectionId INTEGER NOT NULL REFERENCES collection(id) ON DELETE CASCADE," + + "authority TEXT NOT NULL," + + "lastSync INTEGER NOT NULL)") + db.execSQL("CREATE UNIQUE INDEX index_syncstats_collectionId_authority ON syncstats(collectionId, authority)") + + db.execSQL("CREATE INDEX index_collection_url ON collection(url)") +} + +@Module +@InstallIn(SingletonComponent::class) +internal object Migration9Module { + @Provides @IntoSet + fun provide(): Migration = Migration9 +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/di/CoroutineDispatchersModule.kt b/app/src/main/kotlin/at/bitfire/davdroid/di/CoroutineDispatchersModule.kt new file mode 100644 index 0000000..fc33612 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/di/CoroutineDispatchersModule.kt @@ -0,0 +1,61 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import javax.inject.Qualifier +import javax.inject.Singleton + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class DefaultDispatcher + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class IoDispatcher + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class MainDispatcher + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class SyncDispatcher + +@Module +@InstallIn(SingletonComponent::class) +class CoroutineDispatchersModule { + + @Provides + @DefaultDispatcher + fun defaultDispatcher(): CoroutineDispatcher = Dispatchers.Default + + @Provides + @IoDispatcher + fun ioDispatcher(): CoroutineDispatcher = Dispatchers.IO + + @Provides + @MainDispatcher + fun mainDispatcher(): CoroutineDispatcher = Dispatchers.Main + + /** + * A dispatcher for background sync operations. They're not run on [ioDispatcher] because there can + * be many long-blocking operations at the same time which shouldn't never block other I/O operations + * like database access for the UI. + * + * It uses the I/O dispatcher and limits the number of parallel operations to the number of available processors. + */ + @Provides + @SyncDispatcher + @Singleton + fun syncDispatcher(): CoroutineDispatcher = + Dispatchers.IO.limitedParallelism(Runtime.getRuntime().availableProcessors()) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/di/CoroutineScopesModule.kt b/app/src/main/kotlin/at/bitfire/davdroid/di/CoroutineScopesModule.kt new file mode 100644 index 0000000..40c5c23 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/di/CoroutineScopesModule.kt @@ -0,0 +1,30 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import javax.inject.Qualifier +import javax.inject.Singleton + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class ApplicationScope + +@Module +@InstallIn(SingletonComponent::class) +class CoroutineScopesModule { + + @Singleton + @Provides + @ApplicationScope + fun applicationScope(@MainDispatcher mainDispatcher: CoroutineDispatcher): CoroutineScope = CoroutineScope(SupervisorJob() + mainDispatcher) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/di/LoggerModule.kt b/app/src/main/kotlin/at/bitfire/davdroid/di/LoggerModule.kt new file mode 100644 index 0000000..6af6d6f --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/di/LoggerModule.kt @@ -0,0 +1,20 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import java.util.logging.Logger + +@Module +@InstallIn(SingletonComponent::class) +class LoggerModule { + + @Provides + fun globalLogger(): Logger = Logger.getGlobal() + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/log/LogFileHandler.kt b/app/src/main/kotlin/at/bitfire/davdroid/log/LogFileHandler.kt new file mode 100644 index 0000000..4206d00 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/log/LogFileHandler.kt @@ -0,0 +1,177 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.log + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Process +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.TaskStackBuilder +import at.bitfire.davdroid.R +import at.bitfire.davdroid.log.LogFileHandler.Companion.debugDir +import at.bitfire.davdroid.ui.AppSettingsActivity +import at.bitfire.davdroid.ui.DebugInfoActivity +import at.bitfire.davdroid.ui.NotificationRegistry +import at.bitfire.synctools.log.PlainTextFormatter +import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.Closeable +import java.io.File +import java.util.Date +import java.util.logging.FileHandler +import java.util.logging.Handler +import java.util.logging.Level +import java.util.logging.LogRecord +import java.util.logging.Logger +import javax.inject.Inject + +/** + * Logging handler that logs to a debug log file. + * + * Shows a permanent notification as long as it's active (until [close] is called). + * + * Only one [LogFileHandler] should be active at once, because the notification is shared. + */ +class LogFileHandler @Inject constructor( + @ApplicationContext val context: Context, + private val logger: Logger, + private val notificationRegistry: NotificationRegistry +): Handler(), Closeable { + + companion object { + + private const val DEBUG_INFO_DIRECTORY = "debug" + + /** + * Creates (when necessary) and returns the directory where all the debug files (such as log files) are stored. + * Must match the contents of `res/xml/debug.paths.xml`. + * + * @return The directory where all debug info are stored, or `null` if the directory couldn't be created successfully. + */ + fun debugDir(context: Context): File? { + val dir = File(context.filesDir, DEBUG_INFO_DIRECTORY) + if (dir.exists() && dir.isDirectory) + return dir + + if (dir.mkdir()) + return dir + + return null + } + + /** + * The file (in [debugDir]) where verbose logs are stored. + * + * @return The file where verbose logs are stored, or `null` if there's no [debugDir]. + */ + fun getDebugLogFile(context: Context): File? { + val logDir = debugDir(context) ?: return null + return File(logDir, "davx5-log.txt") + } + + } + + private var fileHandler: FileHandler? = null + private val notificationManager = NotificationManagerCompat.from(context) + + private val logFile = getDebugLogFile(context) + + init { + if (logFile != null) { + if (logFile.createNewFile()) + logFile.writeText("Log file created at ${Date()}; PID ${Process.myPid()}; UID ${Process.myUid()}\n") + + // actual logging is handled by a FileHandler + fileHandler = FileHandler(logFile.toString(), true).apply { + formatter = PlainTextFormatter.DEFAULT + } + + showNotification() + } else { + logger.severe("Couldn't create log file in app-private directory $DEBUG_INFO_DIRECTORY/.") + level = Level.OFF + } + } + + + @Synchronized + override fun publish(record: LogRecord) { + fileHandler?.publish(record) + } + + @Synchronized + override fun flush() { + fileHandler?.flush() + } + + @Synchronized + override fun close() { + fileHandler?.close() + fileHandler = null + + // remove all files in debug info directory, may also contain zip files from debug info activity etc. + logFile?.parentFile?.deleteRecursively() + + removeNotification() + } + + + // notifications + + private fun showNotification() { + notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_VERBOSE_LOGGING) { + val builder = NotificationCompat.Builder(context, notificationRegistry.CHANNEL_DEBUG) + builder.setSmallIcon(R.drawable.ic_sd_card_notify) + .setContentTitle(context.getString(R.string.app_settings_logging)) + .setCategory(NotificationCompat.CATEGORY_STATUS) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setContentText( + context.getString( + R.string.logging_notification_text, context.getString( + R.string.app_name + ) + ) + ) + .setOngoing(true) + + // add action to view/share the logs + val shareIntent = DebugInfoActivity.IntentBuilder(context) + .newTask() + .share() + val pendingShare = TaskStackBuilder.create(context) + .addNextIntentWithParentStack(shareIntent) + .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + builder.addAction( + NotificationCompat.Action.Builder( + R.drawable.ic_share, + context.getString(R.string.logging_notification_view_share), + pendingShare + ).build() + ) + + // add action to disable verbose logging + val prefIntent = Intent(context, AppSettingsActivity::class.java) + prefIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val pendingPref = TaskStackBuilder.create(context) + .addNextIntentWithParentStack(prefIntent) + .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + builder.addAction( + NotificationCompat.Action.Builder( + R.drawable.ic_settings, + context.getString(R.string.logging_notification_disable), + pendingPref + ).build() + ) + + builder.build() + } + } + + private fun removeNotification() { + notificationManager.cancel(NotificationRegistry.NOTIFY_VERBOSE_LOGGING) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/log/LogManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/log/LogManager.kt new file mode 100644 index 0000000..3fb285b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/log/LogManager.kt @@ -0,0 +1,90 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.log + +import android.content.Context +import android.util.Log +import at.bitfire.davdroid.BuildConfig +import at.bitfire.davdroid.repository.PreferenceRepository +import at.bitfire.synctools.log.LogcatHandler +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject +import javax.inject.Provider +import javax.inject.Singleton + +/** + * Handles logging configuration and which loggers are active at a moment. + * To initialize, just make sure that the [LogManager] singleton is created. + * + * Configures the root logger like this: + * + * - Always logs to logcat. + * - Watches the "log to file" preference and activates or deactivates file logging accordingly. + * - If "log to file" is enabled, log level is set to [Level.ALL]. + * - Otherwise, log level is set to [Level.INFO]. + * + * Preferred ways to get a [Logger] are: + * + * - `@Inject` [Logger] for a general-purpose logger when injection is possible + * - `Logger.getGlobal()` for a general-purpose logger + * - `Logger.getLogger(javaClass.name)` for a specific logger that can be customized + * + * When using the global logger, the class name of the logging calls will still be logged, so there's + * no need to always get a separate logger for each class (only if the class wants to customize it). + */ +@Singleton +class LogManager @Inject constructor( + @ApplicationContext private val context: Context, + private val logFileHandler: Provider, + private val logger: Logger, + private val prefs: PreferenceRepository +) : AutoCloseable { + + private val scope = CoroutineScope(Dispatchers.Default) + + init { + // observe preference changes + scope.launch { + prefs.logToFileFlow().collect { + reloadConfig() + } + } + + reloadConfig() + } + + override fun close() { + scope.cancel() + } + + @Synchronized + fun reloadConfig() { + val logToFile = prefs.logToFile() + val logVerbose = logToFile || BuildConfig.DEBUG || Log.isLoggable(logger.name, Log.DEBUG) + logger.info("Verbose logging = $logVerbose; log to file = $logToFile") + + // reset existing loggers and initialize from assets/logging.properties + context.assets.open("logging.properties").use { + val javaLogManager = java.util.logging.LogManager.getLogManager() + javaLogManager.readConfiguration(it) + } + + // root logger: set default log level and always log to logcat + val rootLogger = Logger.getLogger("") + rootLogger.level = if (logVerbose) Level.ALL else Level.INFO + rootLogger.addHandler(LogcatHandler(BuildConfig.APPLICATION_ID)) + + // log to file, if requested + if (logToFile) + rootLogger.addHandler(logFileHandler.get()) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/log/StringHandler.kt b/app/src/main/kotlin/at/bitfire/davdroid/log/StringHandler.kt new file mode 100644 index 0000000..1569cf1 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/log/StringHandler.kt @@ -0,0 +1,57 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.log + +import at.bitfire.synctools.log.PlainTextFormatter +import com.google.common.base.Ascii +import java.util.logging.Handler +import java.util.logging.LogRecord + +/** + * Handler that writes log messages to a string buffer. + * + * @param maxSize Maximum size of the buffer. If the buffer exceeds this size, it will be truncated. + */ +class StringHandler( + private val maxSize: Int +): Handler() { + + companion object { + const val TRUNCATION_MARKER = "[...]" + } + + val builder = StringBuilder() + + init { + formatter = PlainTextFormatter.DEFAULT + } + + override fun publish(record: LogRecord) { + var text = formatter.format(record) + + val currentSize = builder.length + val sizeLeft = maxSize - currentSize + + when { + // Append the text if there is enough space + sizeLeft > text.length -> + builder.append(text) + + // Truncate the text if there is not enough space + sizeLeft > TRUNCATION_MARKER.length -> { + text = Ascii.truncate(text, maxSize - currentSize, TRUNCATION_MARKER) + builder.append(text) + } + + // Do nothing if the buffer is already full + } + } + + override fun flush() {} + override fun close() {} + + override fun toString() = builder.toString() + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/Android10Resolver.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/Android10Resolver.kt new file mode 100644 index 0000000..80b90cc --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/Android10Resolver.kt @@ -0,0 +1,73 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.network + +import android.net.DnsResolver +import android.os.Build +import androidx.annotation.RequiresApi +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.runBlocking +import org.xbill.DNS.EDNSOption +import org.xbill.DNS.Message +import org.xbill.DNS.Resolver +import org.xbill.DNS.TSIG +import java.io.IOException +import java.time.Duration + +/** + * dnsjava [Resolver] that uses Android's [DnsResolver] API, which can resolve raw queries and + * is available since Android 10. + */ +@RequiresApi(Build.VERSION_CODES.Q) +class Android10Resolver : Resolver { + + private val executor = Dispatchers.IO.asExecutor() + private val resolver = DnsResolver.getInstance() + + override fun send(query: Message): Message = runBlocking { + val future = CompletableDeferred() + + resolver.rawQuery(null, query.toWire(), DnsResolver.FLAG_EMPTY, executor, null, object: DnsResolver.Callback { + override fun onAnswer(rawAnswer: ByteArray, rcode: Int) { + future.complete(Message((rawAnswer))) + } + + override fun onError(error: DnsResolver.DnsException) { + // wrap into IOException as expected by dnsjava + future.completeExceptionally(IOException(error)) + } + }) + + future.await() + } + + + override fun setPort(port: Int) { + // not applicable + } + + override fun setTCP(flag: Boolean) { + // not applicable + } + + override fun setIgnoreTruncation(flag: Boolean) { + // not applicable + } + + override fun setEDNS(version: Int, payloadSize: Int, flags: Int, options: MutableList?) { + // not applicable + } + + override fun setTSIGKey(key: TSIG?) { + // not applicable + } + + override fun setTimeout(timeout: Duration?) { + // not applicable + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/ClientCertKeyManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/ClientCertKeyManager.kt new file mode 100644 index 0000000..0859849 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/ClientCertKeyManager.kt @@ -0,0 +1,47 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.network + +import android.content.Context +import android.security.KeyChain +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.qualifiers.ApplicationContext +import java.net.Socket +import java.security.Principal +import javax.net.ssl.X509ExtendedKeyManager + +/** + * KeyManager that provides a client certificate and private key from the Android KeyChain. + * + * @throws IllegalArgumentException if the alias doesn't exist or is not accessible + */ +class ClientCertKeyManager @AssistedInject constructor( + @Assisted private val alias: String, + @ApplicationContext private val context: Context +): X509ExtendedKeyManager() { + + @AssistedFactory + interface Factory { + fun create(alias: String): ClientCertKeyManager + } + + val certs = KeyChain.getCertificateChain(context, alias) ?: throw IllegalArgumentException("Alias doesn't exist or not accessible: $alias") + val key = KeyChain.getPrivateKey(context, alias) ?: throw IllegalArgumentException("Alias doesn't exist or not accessible: $alias") + + override fun getServerAliases(p0: String?, p1: Array?): Array? = null + override fun chooseServerAlias(p0: String?, p1: Array?, p2: Socket?) = null + + override fun getClientAliases(p0: String?, p1: Array?) = arrayOf(alias) + override fun chooseClientAlias(p0: Array?, p1: Array?, p2: Socket?) = alias + + override fun getCertificateChain(forAlias: String?) = + certs.takeIf { forAlias == alias } + + override fun getPrivateKey(forAlias: String?) = + key.takeIf { forAlias == alias } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/DnsRecordResolver.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/DnsRecordResolver.kt new file mode 100644 index 0000000..ca20f5c --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/DnsRecordResolver.kt @@ -0,0 +1,166 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.network + +import android.content.Context +import android.net.ConnectivityManager +import android.os.Build +import androidx.core.content.getSystemService +import dagger.hilt.android.qualifiers.ApplicationContext +import org.xbill.DNS.ExtendedResolver +import org.xbill.DNS.Lookup +import org.xbill.DNS.Record +import org.xbill.DNS.Resolver +import org.xbill.DNS.ResolverConfig +import org.xbill.DNS.SRVRecord +import org.xbill.DNS.SimpleResolver +import org.xbill.DNS.TXTRecord +import java.net.InetAddress +import java.util.LinkedList +import java.util.TreeMap +import java.util.logging.Logger +import javax.inject.Inject +import kotlin.random.Random + +/** + * Allows to resolve SRV/TXT records. Chooses the correct resolver, DNS servers etc. + */ +class DnsRecordResolver @Inject constructor( + @ApplicationContext val context: Context, + private val logger: Logger +) { + + // resolving + + /** + * Fallback DNS server that will be used when other DNS are not known or working. + * `9.9.9.9` belongs to Cloudflare who promise good privacy. + */ + private val DNS_FALLBACK = InetAddress.getByAddress(byteArrayOf(9,9,9,9)) + + private val resolver by lazy { chooseResolver() } + + init { + // empty initialization for dnsjava because we set the servers for each request + ResolverConfig.setConfigProviders(listOf()) + } + + /** + * Creates a matching Resolver, depending on the Android version: + * + * Android 10+: Android10Resolver, which uses the raw DNS resolver that comes with Android + * Android <10: ExtendedResolver, which uses the known DNS servers to resolve DNS queries + */ + private fun chooseResolver(): Resolver = + if (Build.VERSION.SDK_INT >= 29) { + /* Since Android 10, there's a native DnsResolver API that allows to send SRV queries without + knowing which DNS servers have to be used. DNS over TLS is now also supported. */ + logger.fine("Using Android 10+ DnsResolver") + Android10Resolver() + + } else { + /* Since Android 8, the system properties net.dns1, net.dns2, ... are not available anymore. + The current version of dnsjava relies on these properties to find the default name servers, + so we have to add the servers explicitly (fortunately, there's an Android API to + get the DNS servers of the network connections). */ + val dnsServers = LinkedList() + + val connectivity = context.getSystemService()!! + @Suppress("DEPRECATION") + connectivity.allNetworks.forEach { network -> + val active = connectivity.getNetworkInfo(network)?.isConnected == true + connectivity.getLinkProperties(network)?.let { link -> + if (active) + // active connection, insert at top of list + dnsServers.addAll(0, link.dnsServers) + else + // inactive connection, insert at end of list + dnsServers.addAll(link.dnsServers) + } + } + + // fallback: add Quad9 DNS in case that no other DNS works + dnsServers.add(DNS_FALLBACK) + + val uniqueDnsServers = LinkedHashSet(dnsServers) + val simpleResolvers = uniqueDnsServers.map { dns -> + logger.fine("Adding DNS server ${dns.hostAddress}") + SimpleResolver(dns) + } + + // combine SimpleResolvers which query one DNS server each to an ExtendedResolver + ExtendedResolver(simpleResolvers.toTypedArray()) + } + + fun resolve(query: String, type: Int): Array { + val lookup = Lookup(query, type) + lookup.setResolver(resolver) + return lookup.run().orEmpty() + } + + + // record selection + + /** + * Selects the best SRV record from a list of records, based on algorithm from RFC 2782. + * + * @param records the records to choose from + * @param randomGenerator a random number generator to use for random selection + * @return the best SRV record, or `null` if no SRV record is available + */ + fun bestSRVRecord(records: Array, randomGenerator: Random = Random.Default): SRVRecord? { + val srvRecords = records.filterIsInstance() + if (srvRecords.size <= 1) + return srvRecords.firstOrNull() + + /* RFC 2782 + Priority + The priority of this target host. A client MUST attempt to + contact the target host with the lowest-numbered priority it can + reach; target hosts with the same priority SHOULD be tried in an + order defined by the weight field. [...] + Weight + A server selection mechanism. The weight field specifies a + relative weight for entries with the same priority. [...] + To select a target to be contacted next, arrange all SRV RRs + (that have not been ordered yet) in any order, except that all + those with weight 0 are placed at the beginning of the list. + Compute the sum of the weights of those RRs, and with each RR + associate the running sum in the selected order. Then choose a + uniform random number between 0 and the sum computed + (inclusive), and select the RR whose running sum value is the + first in the selected order which is greater than or equal to + the random number selected. The target host specified in the + selected SRV RR is the next one to be contacted by the client. + */ + + // Select records which have the minimum priority + val minPriority = srvRecords.minOfOrNull { it.priority } + val usableRecords = srvRecords.filter { it.priority == minPriority } + .sortedBy { it.weight != 0 } // and put those with weight 0 first + + val map = TreeMap() + var runningWeight = 0 + for (record in usableRecords) { + val weight = record.weight + runningWeight += weight + map[runningWeight] = record + } + + val selector = (0..runningWeight).random(randomGenerator) + return map.ceilingEntry(selector)!!.value + } + + fun pathsFromTXTRecords(records: Array): List { + val paths = LinkedList() + records.filterIsInstance().forEach { txt -> + for (segment in txt.strings as List) + if (segment.startsWith("path=")) + paths.add(segment.substring(5)) + } + return paths + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt new file mode 100644 index 0000000..a1f7337 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt @@ -0,0 +1,315 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.network + +import android.accounts.Account +import android.content.Context +import androidx.annotation.WorkerThread +import at.bitfire.cert4android.CustomCertManager +import at.bitfire.dav4jvm.BasicDigestAuthHandler +import at.bitfire.dav4jvm.UrlUtils +import at.bitfire.davdroid.BuildConfig +import at.bitfire.davdroid.di.IoDispatcher +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.settings.Credentials +import at.bitfire.davdroid.settings.Settings +import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.ui.ForegroundTracker +import com.google.common.net.HttpHeaders +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import net.openid.appauth.AuthState +import okhttp3.Authenticator +import okhttp3.Cache +import okhttp3.ConnectionSpec +import okhttp3.CookieJar +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Protocol +import okhttp3.brotli.BrotliInterceptor +import okhttp3.internal.tls.OkHostnameVerifier +import okhttp3.logging.HttpLoggingInterceptor +import java.io.File +import java.net.InetSocketAddress +import java.net.Proxy +import java.util.concurrent.TimeUnit +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject +import javax.net.ssl.KeyManager +import javax.net.ssl.SSLContext + +class HttpClient( + val okHttpClient: OkHttpClient +): AutoCloseable { + + override fun close() { + okHttpClient.cache?.close() + } + + + // builder + + /** + * Builder for the [HttpClient]. + * + * **Attention:** If the builder is injected, it shouldn't be used from multiple locations to generate different clients because then + * there's only one [Builder] object and setting properties from one location would influence the others. + * + * To generate multiple clients, inject and use `Provider` instead. + */ + class Builder @Inject constructor( + private val accountSettingsFactory: AccountSettings.Factory, + @ApplicationContext private val context: Context, + defaultLogger: Logger, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val keyManagerFactory: ClientCertKeyManager.Factory, + private val oAuthInterceptorFactory: OAuthInterceptor.Factory, + private val settingsManager: SettingsManager + ) { + + // property setters/getters + + private var logger: Logger = defaultLogger + fun setLogger(logger: Logger): Builder { + this.logger = logger + return this + } + + private var loggerInterceptorLevel: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.BODY + fun loggerInterceptorLevel(level: HttpLoggingInterceptor.Level): Builder { + loggerInterceptorLevel = level + return this + } + + // default cookie store for non-persistent cookies (some services like Horde use cookies for session tracking) + private var cookieStore: CookieJar = MemoryCookieStore() + fun setCookieStore(cookieStore: CookieJar): Builder { + this.cookieStore = cookieStore + return this + } + + private var authenticationInterceptor: Interceptor? = null + private var authenticator: Authenticator? = null + private var certificateAlias: String? = null + fun authenticate(host: String?, getCredentials: () -> Credentials, updateAuthState: ((AuthState) -> Unit)? = null): Builder { + val credentials = getCredentials() + if (credentials.authState != null) { + // OAuth + authenticationInterceptor = oAuthInterceptorFactory.create( + readAuthState = { + // We don't use the "credentials" object from above because it may contain an outdated access token + // when readAuthState is called. Instead, we fetch the up-to-date auth-state. + getCredentials().authState + }, + writeAuthState = { authState -> + updateAuthState?.invoke(authState) + } + + ) + + } else if (credentials.username != null && credentials.password != null) { + // basic/digest auth + val authHandler = BasicDigestAuthHandler( + domain = UrlUtils.hostToDomain(host), + username = credentials.username, + password = credentials.password.asCharArray(), + insecurePreemptive = true + ) + authenticationInterceptor = authHandler + authenticator = authHandler + } + + // client certificate + if (credentials.certificateAlias != null) + certificateAlias = credentials.certificateAlias + + return this + } + + private var followRedirects = false + fun followRedirects(follow: Boolean): Builder { + followRedirects = follow + return this + } + + private var cache: Cache? = null + @Suppress("unused") + fun withDiskCache(maxSize: Long = 10*1024*1024): Builder { + for (dir in arrayOf(context.externalCacheDir, context.cacheDir).filterNotNull()) { + if (dir.exists() && dir.canWrite()) { + val cacheDir = File(dir, "HttpClient") + cacheDir.mkdir() + logger.fine("Using disk cache: $cacheDir") + cache = Cache(cacheDir, maxSize) + break + } + } + return this + } + + + // convenience builders from other classes + + /** + * Takes authentication (basic/digest or OAuth and client certificate) from a given account. + * + * **Must not be run on main thread, because it creates [AccountSettings]!** Use [fromAccountAsync] if possible. + * + * @param account the account to take authentication from + * @param onlyHost if set: only authenticate for this host name + * + * @throws at.bitfire.davdroid.sync.account.InvalidAccountException when the account doesn't exist + */ + @WorkerThread + fun fromAccount(account: Account, onlyHost: String? = null): Builder { + val accountSettings = accountSettingsFactory.create(account) + authenticate( + host = onlyHost, + getCredentials = { + accountSettings.credentials() + }, + updateAuthState = { authState -> + accountSettings.updateAuthState(authState) + } + ) + return this + } + + /** + * Same as [fromAccount], but can be called on any thread. + * + * @throws at.bitfire.davdroid.sync.account.InvalidAccountException when the account doesn't exist + */ + suspend fun fromAccountAsync(account: Account, onlyHost: String? = null): Builder = withContext(ioDispatcher) { + fromAccount(account, onlyHost) + } + + + // actual builder + + fun build(): HttpClient { + val okBuilder = OkHttpClient.Builder() + // Set timeouts. According to [AbstractThreadedSyncAdapter], when there is no network + // traffic within a minute, a sync will be cancelled. + .connectTimeout(15, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .readTimeout(120, TimeUnit.SECONDS) + .pingInterval(45, TimeUnit.SECONDS) // avoid cancellation because of missing traffic; only works for HTTP/2 + + // don't allow redirects by default because it would break PROPFIND handling + .followRedirects(followRedirects) + + // add User-Agent to every request + .addInterceptor(UserAgentInterceptor) + + // connection-private cookie store + .cookieJar(cookieStore) + + // allow cleartext and TLS 1.2+ + .connectionSpecs(listOf( + ConnectionSpec.CLEARTEXT, + ConnectionSpec.MODERN_TLS + )) + + // offer Brotli and gzip compression (can be disabled per request with `Accept-Encoding: identity`) + .addInterceptor(BrotliInterceptor) + + // add cache, if requested + .cache(cache) + + // app-wide custom proxy support + buildProxy(okBuilder) + + // add authentication + buildAuthentication(okBuilder) + + // add network logging, if requested + if (logger.isLoggable(Level.FINEST)) { + val loggingInterceptor = HttpLoggingInterceptor { message -> logger.finest(message) } + loggingInterceptor.redactHeader(HttpHeaders.AUTHORIZATION) + loggingInterceptor.redactHeader(HttpHeaders.COOKIE) + loggingInterceptor.redactHeader(HttpHeaders.SET_COOKIE) + loggingInterceptor.redactHeader(HttpHeaders.SET_COOKIE2) + loggingInterceptor.level = loggerInterceptorLevel + okBuilder.addNetworkInterceptor(loggingInterceptor) + } + + return HttpClient(okBuilder.build()) + } + + private fun buildAuthentication(okBuilder: OkHttpClient.Builder) { + // basic/digest auth and OAuth + authenticationInterceptor?.let { okBuilder.addInterceptor(it) } + authenticator?.let { okBuilder.authenticator(it) } + + // client certificate + val keyManager: KeyManager? = certificateAlias?.let { alias -> + try { + val manager = keyManagerFactory.create(alias) + logger.fine("Using certificate $alias for authentication") + + // HTTP/2 doesn't support client certificates (yet) + // see https://tools.ietf.org/html/draft-ietf-httpbis-http2-secondary-certs-04 + okBuilder.protocols(listOf(Protocol.HTTP_1_1)) + + manager + } catch (e: IllegalArgumentException) { + logger.log(Level.SEVERE, "Couldn't create KeyManager for certificate $alias", e) + null + } + } + + // cert4android integration + val certManager = CustomCertManager( + context = context, + trustSystemCerts = !settingsManager.getBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES), + appInForeground = if (BuildConfig.customCertsUI) + ForegroundTracker.inForeground // interactive mode + else + null // non-interactive mode + ) + + val sslContext = SSLContext.getInstance("TLS") + sslContext.init( + /* km = */ if (keyManager != null) arrayOf(keyManager) else null, + /* tm = */ arrayOf(certManager), + /* random = */ null + ) + okBuilder + .sslSocketFactory(sslContext.socketFactory, certManager) + .hostnameVerifier(certManager.HostnameVerifier(OkHostnameVerifier)) + } + + private fun buildProxy(okBuilder: OkHttpClient.Builder) { + try { + val proxyTypeValue = settingsManager.getInt(Settings.PROXY_TYPE) + if (proxyTypeValue != Settings.PROXY_TYPE_SYSTEM) { + // we set our own proxy + val address by lazy { // lazy because not required for PROXY_TYPE_NONE + InetSocketAddress( + settingsManager.getString(Settings.PROXY_HOST), + settingsManager.getInt(Settings.PROXY_PORT) + ) + } + val proxy = + when (proxyTypeValue) { + Settings.PROXY_TYPE_NONE -> Proxy.NO_PROXY + Settings.PROXY_TYPE_HTTP -> Proxy(Proxy.Type.HTTP, address) + Settings.PROXY_TYPE_SOCKS -> Proxy(Proxy.Type.SOCKS, address) + else -> throw IllegalArgumentException("Invalid proxy type") + } + okBuilder.proxy(proxy) + logger.log(Level.INFO, "Using proxy setting", proxy) + } + } catch (e: Exception) { + logger.log(Level.SEVERE, "Can't set proxy, ignoring", e) + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/MemoryCookieStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/MemoryCookieStore.kt new file mode 100644 index 0000000..14f9aa6 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/MemoryCookieStore.kt @@ -0,0 +1,81 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.network + +import androidx.annotation.VisibleForTesting +import okhttp3.Cookie +import okhttp3.CookieJar +import okhttp3.HttpUrl +import java.util.LinkedList + +/** + * Primitive cookie store that stores cookies in a (volatile) hash map. + * Will be sufficient for session cookies. + */ +class MemoryCookieStore : CookieJar { + + data class StorageKey( + val domain: String, + val path: String, + val name: String + ) + + private val storage = mutableMapOf() + + override fun saveFromResponse(url: HttpUrl, cookies: List) { + /* [RFC 6265 5.3 Storage Model] + + 11. If the cookie store contains a cookie with the same name, + domain, and path as the newly created cookie: + + 1. Let old-cookie be the existing cookie with the same name, + domain, and path as the newly created cookie. (Notice that + this algorithm maintains the invariant that there is at most + one such cookie.) + + 2. If the newly created cookie was received from a "non-HTTP" + API and the old-cookie's http-only-flag is set, abort these + steps and ignore the newly created cookie entirely. + + 3. Update the creation-time of the newly created cookie to + match the creation-time of the old-cookie. + + 4. Remove the old-cookie from the cookie store. + */ + synchronized(storage) { + storage.putAll(cookies.map { + StorageKey( + domain = it.domain, + path = it.path, + name = it.name + ) to it + }) + } + } + + override fun loadForRequest(url: HttpUrl): List { + val cookies = LinkedList() + + synchronized(storage) { + val iter = storage.iterator() + while (iter.hasNext()) { + val (_, cookie) = iter.next() + + // remove expired cookies + if (cookie.expiresAt <= System.currentTimeMillis()) { + iter.remove() + continue + } + + // add applicable cookies to result + if (cookie.matches(url)) + cookies += cookie + } + } + + return cookies + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/NextcloudLoginFlow.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/NextcloudLoginFlow.kt new file mode 100644 index 0000000..9ed2836 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/NextcloudLoginFlow.kt @@ -0,0 +1,139 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.network + +import at.bitfire.dav4jvm.exception.DavException +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.davdroid.settings.Credentials +import at.bitfire.davdroid.ui.setup.LoginInfo +import at.bitfire.davdroid.util.SensitiveString.Companion.toSensitiveString +import at.bitfire.davdroid.util.withTrailingSlash +import at.bitfire.vcard4android.GroupMethod +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runInterruptible +import kotlinx.coroutines.withContext +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject +import java.net.HttpURLConnection +import java.net.URI +import javax.inject.Inject + +/** + * Implements Nextcloud Login Flow v2. + * + * See https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2 + */ +class NextcloudLoginFlow @Inject constructor( + httpClientBuilder: HttpClient.Builder +): AutoCloseable { + + companion object { + const val FLOW_V1_PATH = "index.php/login/flow" + const val FLOW_V2_PATH = "index.php/login/v2" + + /** Path to DAV endpoint (e.g. `remote.php/dav`). Will be appended to the server URL returned by Login Flow. */ + const val DAV_PATH = "remote.php/dav" + } + + val httpClient = httpClientBuilder + .build() + + override fun close() { + httpClient.close() + } + + + // Login flow state + var loginUrl: HttpUrl? = null + var pollUrl: HttpUrl? = null + var token: String? = null + + + suspend fun initiate(baseUrl: HttpUrl): HttpUrl? { + loginUrl = null + pollUrl = null + token = null + + val json = postForJson(initiateUrl(baseUrl), "".toRequestBody()) + + loginUrl = json.getString("login").toHttpUrlOrNull() + json.getJSONObject("poll").let { poll -> + pollUrl = poll.getString("endpoint").toHttpUrl() + token = poll.getString("token") + } + + return loginUrl + } + + fun initiateUrl(baseUrl: HttpUrl): HttpUrl { + val path = baseUrl.encodedPath + + if (path.endsWith(FLOW_V2_PATH)) + // already a Login Flow v2 URL + return baseUrl + + if (path.endsWith(FLOW_V1_PATH)) + // Login Flow v1 URL, rewrite to v2 + return baseUrl.newBuilder() + .encodedPath(path.replace(FLOW_V1_PATH, FLOW_V2_PATH)) + .build() + + // other URL, make it a Login Flow v2 URL + return baseUrl.newBuilder() + .addPathSegments(FLOW_V2_PATH) + .build() + } + + + suspend fun fetchLoginInfo(): LoginInfo { + val pollUrl = pollUrl ?: throw IllegalArgumentException("Missing pollUrl") + val token = token ?: throw IllegalArgumentException("Missing token") + + // send HTTP request to request server, login name and app password + val json = postForJson(pollUrl, "token=$token".toRequestBody("application/x-www-form-urlencoded".toMediaType())) + + // make sure server URL ends with a slash so that DAV_PATH can be appended + val serverUrl = json.getString("server").withTrailingSlash() + + return LoginInfo( + baseUri = URI(serverUrl).resolve(DAV_PATH), + credentials = Credentials( + username = json.getString("loginName"), + password = json.getString("appPassword").toSensitiveString() + ), + suggestedGroupMethod = GroupMethod.CATEGORIES + ) + } + + + private suspend fun postForJson(url: HttpUrl, requestBody: RequestBody): JSONObject = withContext(Dispatchers.IO) { + val postRq = Request.Builder() + .url(url) + .post(requestBody) + .build() + val response = runInterruptible { + httpClient.okHttpClient.newCall(postRq).execute() + } + + if (response.code != HttpURLConnection.HTTP_OK) + throw HttpException(response) + + response.body.use { body -> + val mimeType = body.contentType() ?: throw DavException("Login Flow response without MIME type") + if (mimeType.type != "application" || mimeType.subtype != "json") + throw DavException("Invalid Login Flow response (not JSON)") + + // decode JSON + return@withContext JSONObject(body.string()) + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthFastmail.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthFastmail.kt new file mode 100644 index 0000000..5ca5df8 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthFastmail.kt @@ -0,0 +1,49 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.network + +import androidx.core.net.toUri +import net.openid.appauth.AuthorizationRequest +import net.openid.appauth.AuthorizationServiceConfiguration +import net.openid.appauth.ResponseTypeValues +import java.net.URI + +object OAuthFastmail { + + // DAVx5 Client ID (issued by Fastmail) + private const val CLIENT_ID = "34ce41ae" + + private val SCOPES = arrayOf( + "https://www.fastmail.com/dev/protocol-caldav", // CalDAV + "https://www.fastmail.com/dev/protocol-carddav" // CardDAV + ) + + /** + * The base URL for Fastmail. Note that this URL is used for both CalDAV and CardDAV; + * the SRV records of the domain are checked to determine the respective service base URL. + */ + val baseUri: URI = URI.create("https://fastmail.com/") + + private val serviceConfig = AuthorizationServiceConfiguration( + "https://api.fastmail.com/oauth/authorize".toUri(), + "https://api.fastmail.com/oauth/refresh".toUri() + ) + + + fun signIn(email: String?, locale: String?): AuthorizationRequest { + val builder = AuthorizationRequest.Builder( + serviceConfig, + CLIENT_ID, + ResponseTypeValues.CODE, + OAuthIntegration.redirectUri + ) + return builder + .setScopes(*SCOPES) + .setLoginHint(email) + .setUiLocales(locale) + .build() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthGoogle.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthGoogle.kt new file mode 100644 index 0000000..8f7c373 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthGoogle.kt @@ -0,0 +1,53 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.network + +import androidx.core.net.toUri +import net.openid.appauth.AuthorizationRequest +import net.openid.appauth.AuthorizationServiceConfiguration +import net.openid.appauth.ResponseTypeValues +import java.net.URI + +object OAuthGoogle { + + // davx5integration@gmail.com (for davx5-ose) + private const val CLIENT_ID = "1069050168830-eg09u4tk1cmboobevhm4k3bj1m4fav9i.apps.googleusercontent.com" + + private val SCOPES = arrayOf( + "https://www.googleapis.com/auth/calendar", // CalDAV + "https://www.googleapis.com/auth/carddav" // CardDAV + ) + + /** + * Gets the Google CalDAV/CardDAV base URI. See https://developers.google.com/calendar/caldav/v2/guide; + * _calid_ of the primary calendar is the account name. + * + * This URL allows CardDAV (over well-known URLs) and CalDAV detection including calendar-homesets and secondary + * calendars. + */ + fun baseUri(googleAccount: String): URI = + URI("https", "apidata.googleusercontent.com", "/caldav/v2/$googleAccount/user", null) + + private val serviceConfig = AuthorizationServiceConfiguration( + "https://accounts.google.com/o/oauth2/v2/auth".toUri(), + "https://oauth2.googleapis.com/token".toUri() + ) + + + fun signIn(email: String?, customClientId: String?, locale: String?): AuthorizationRequest { + val builder = AuthorizationRequest.Builder( + serviceConfig, + customClientId ?: CLIENT_ID, + ResponseTypeValues.CODE, + OAuthIntegration.redirectUri + ) + return builder + .setScopes(*SCOPES) + .setLoginHint(email) + .setUiLocales(locale) + .build() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthIntegration.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthIntegration.kt new file mode 100644 index 0000000..6e92482 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthIntegration.kt @@ -0,0 +1,63 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.network + +import android.content.Context +import android.content.Intent +import androidx.activity.result.contract.ActivityResultContract +import androidx.core.net.toUri +import at.bitfire.davdroid.BuildConfig +import at.bitfire.davdroid.network.OAuthIntegration.redirectUri +import kotlinx.coroutines.CompletableDeferred +import net.openid.appauth.AuthState +import net.openid.appauth.AuthorizationException +import net.openid.appauth.AuthorizationRequest +import net.openid.appauth.AuthorizationResponse +import net.openid.appauth.AuthorizationService +import net.openid.appauth.TokenResponse + +/** + * Integration with OpenID AppAuth (Android) + */ +object OAuthIntegration { + + /** redirect URI, must be registered in Manifest */ + val redirectUri = + (BuildConfig.APPLICATION_ID + ":/oauth2/redirect").toUri() + + /** + * Called by the authorization service when the login is finished and [redirectUri] is launched. + * + * @param authService authorization service + * @param authResponse response from the server (coming over the Intent from the browser / [AuthorizationContract]) + */ + suspend fun authenticate(authService: AuthorizationService, authResponse: AuthorizationResponse): AuthState { + val authState = AuthState(authResponse, null) // authorization code must not be stored; exchange it to refresh token + val authStateFuture = CompletableDeferred() + + authService.performTokenRequest(authResponse.createTokenExchangeRequest()) { tokenResponse: TokenResponse?, refreshTokenException: AuthorizationException? -> + if (tokenResponse != null) { + // success, save authState (= refresh token) + authState.update(tokenResponse, refreshTokenException) + authStateFuture.complete(authState) + } else if (refreshTokenException != null) + authStateFuture.completeExceptionally(refreshTokenException) + } + + return authStateFuture.await() + } + + + class AuthorizationContract( + private val authService: AuthorizationService + ) : ActivityResultContract() { + override fun createIntent(context: Context, input: AuthorizationRequest) = + authService.getAuthorizationRequestIntent(input) + + override fun parseResult(resultCode: Int, intent: Intent?): AuthorizationResponse? = + intent?.let { AuthorizationResponse.fromIntent(it) } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthInterceptor.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthInterceptor.kt new file mode 100644 index 0000000..7cb1dd6 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthInterceptor.kt @@ -0,0 +1,106 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.network + +import at.bitfire.davdroid.BuildConfig +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import net.openid.appauth.AuthState +import net.openid.appauth.AuthorizationException +import net.openid.appauth.AuthorizationService +import okhttp3.Interceptor +import okhttp3.Response +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionException +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Provider + +/** + * Sends an OAuth Bearer token authorization as described in RFC 6750. + * + * @param readAuthState callback that fetches an up-to-date authorization state + * @param writeAuthState callback that persists a new authorization state + */ +class OAuthInterceptor @AssistedInject constructor( + @Assisted private val readAuthState: () -> AuthState?, + @Assisted private val writeAuthState: (AuthState) -> Unit, + private val authServiceProvider: Provider, + private val logger: Logger +): Interceptor { + + @AssistedFactory + interface Factory { + fun create(readAuthState: () -> AuthState?, writeAuthState: (AuthState) -> Unit): OAuthInterceptor + } + + + override fun intercept(chain: Interceptor.Chain): Response { + val rq = chain.request().newBuilder() + + /** Syntax for the "Authorization" header [RFC 6750 2.1]: + * + * b64token = 1*( ALPHA / DIGIT / + * "-" / "." / "_" / "~" / "+" / "/" ) *"=" + * credentials = "Bearer" 1*SP b64token + */ + + val accessToken = provideAccessToken() + if (accessToken != null) + rq.header("Authorization", "Bearer $accessToken") + else + logger.severe("No access token available, won't authenticate") + + return chain.proceed(rq.build()) + } + + /** + * Provides a fresh access token for authorization. Uses the current one if it's still valid, + * or requests a new one if necessary. + * + * This method is synchronized / thread-safe so that it can be called for multiple HTTP requests at the same time. + * + * @return access token or `null` if no valid access token is available (usually because of an error during refresh) + */ + fun provideAccessToken(): String? = synchronized(javaClass) { + // if possible, use cached access token + val authState = readAuthState() ?: return null + + if (authState.isAuthorized && authState.accessToken != null && !authState.needsTokenRefresh) { + if (BuildConfig.DEBUG) // log sensitive information (refresh/access token) only in debug builds + logger.log(Level.FINEST, "Using cached AuthState", authState.jsonSerializeString()) + return authState.accessToken + } + + // request fresh access token + logger.fine("Requesting fresh access token") + val accessTokenFuture = CompletableFuture() + val authService = authServiceProvider.get() + try { + authState.performActionWithFreshTokens(authService) { accessToken: String?, _: String?, ex: AuthorizationException? -> + // appauth internally fetches the new token over HttpURLConnection in an AsyncTask + if (BuildConfig.DEBUG) + logger.log(Level.FINEST, "Got new AuthState", authState.jsonSerializeString()) + + // persist updated AuthState + writeAuthState(authState) + + if (ex != null) + accessTokenFuture.completeExceptionally(ex) + else if (accessToken != null) + accessTokenFuture.complete(accessToken) + } + + accessTokenFuture.join() + } catch (e: CompletionException) { + logger.log(Level.SEVERE, "Couldn't obtain access token", e.cause) + null + } finally { + authService.dispose() + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthModule.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthModule.kt new file mode 100644 index 0000000..8b4e361 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthModule.kt @@ -0,0 +1,40 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.network + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import net.openid.appauth.AppAuthConfiguration +import net.openid.appauth.AuthorizationService +import java.net.HttpURLConnection +import java.net.URL + +@Module +@InstallIn(SingletonComponent::class) +object OAuthModule { + + /** + * Make sure to call [AuthorizationService.dispose] when obtaining an instance. + * + * Creating an instance is expensive (involves CustomTabsManager), so don't create an + * instance if not necessary (use Provider/Lazy). + */ + @Provides + fun authorizationService(@ApplicationContext context: Context): AuthorizationService = + AuthorizationService(context, + AppAuthConfiguration.Builder() + .setConnectionBuilder { uri -> + val url = URL(uri.toString()) + (url.openConnection() as HttpURLConnection).apply { + setRequestProperty("User-Agent", UserAgentInterceptor.userAgent) + } + }.build() + ) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/UserAgentInterceptor.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/UserAgentInterceptor.kt new file mode 100644 index 0000000..88b9791 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/UserAgentInterceptor.kt @@ -0,0 +1,33 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.network + +import android.os.Build +import at.bitfire.davdroid.BuildConfig +import okhttp3.Interceptor +import okhttp3.OkHttp +import okhttp3.Response +import java.util.Locale +import java.util.logging.Logger + +object UserAgentInterceptor: Interceptor { + + val userAgent = "DAVx5/${BuildConfig.VERSION_NAME} (dav4jvm; " + + "okhttp/${OkHttp.VERSION}) Android/${Build.VERSION.RELEASE}" + + init { + Logger.getGlobal().info("Will set User-Agent: $userAgent") + } + + override fun intercept(chain: Interceptor.Chain): Response { + val locale = Locale.getDefault() + val request = chain.request().newBuilder() + .header("User-Agent", userAgent) + .header("Accept-Language", "${locale.language}-${locale.country}, ${locale.language};q=0.7, *;q=0.5") + .build() + return chain.proceed(request) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/push/PushMessageHandler.kt b/app/src/main/kotlin/at/bitfire/davdroid/push/PushMessageHandler.kt new file mode 100644 index 0000000..8c36463 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/push/PushMessageHandler.kt @@ -0,0 +1,119 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.push + +import androidx.annotation.VisibleForTesting +import at.bitfire.dav4jvm.XmlReader +import at.bitfire.dav4jvm.XmlUtils +import at.bitfire.davdroid.db.Collection.Companion.TYPE_ADDRESSBOOK +import at.bitfire.davdroid.repository.AccountRepository +import at.bitfire.davdroid.repository.DavCollectionRepository +import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.sync.TasksAppManager +import at.bitfire.davdroid.sync.worker.SyncWorkerManager +import dagger.Lazy +import org.unifiedpush.android.connector.data.PushMessage +import org.xmlpull.v1.XmlPullParserException +import java.io.StringReader +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject +import at.bitfire.dav4jvm.property.push.PushMessage as DavPushMessage + +/** + * Handles incoming WebDAV-Push messages. + */ +class PushMessageHandler @Inject constructor( + private val accountRepository: AccountRepository, + private val collectionRepository: DavCollectionRepository, + private val logger: Logger, + private val serviceRepository: DavServiceRepository, + private val syncWorkerManager: SyncWorkerManager, + private val tasksAppManager: Lazy +) { + + suspend fun processMessage(message: PushMessage, instance: String) { + if (!message.decrypted) { + logger.severe("Received a push message that could not be decrypted.") + return + } + val messageXml = message.content.toString(Charsets.UTF_8) + logger.log(Level.INFO, "Received push message", messageXml) + + // parse push notification + val topic = parse(messageXml) + + // sync affected collection + if (topic != null) { + logger.info("Got push notification for topic $topic") + + // Sync all authorities of account that the collection belongs to + // Later: only sync affected collection and authorities + collectionRepository.getSyncableByTopic(topic)?.let { collection -> + serviceRepository.get(collection.serviceId)?.let { service -> + val syncDataTypes = mutableSetOf() + // If the type is an address book, add the contacts type + if (collection.type == TYPE_ADDRESSBOOK) + syncDataTypes += SyncDataType.CONTACTS + + // If the collection supports events, add the events type + if (collection.supportsVEVENT != false) + syncDataTypes += SyncDataType.EVENTS + + // If the collection supports tasks, make sure there's a provider installed, + // and add the tasks type + if (collection.supportsVJOURNAL != false || collection.supportsVTODO != false) + if (tasksAppManager.get().currentProvider() != null) + syncDataTypes += SyncDataType.TASKS + + // Schedule sync for all the types identified + val account = accountRepository.fromName(service.accountName) + for (syncDataType in syncDataTypes) + syncWorkerManager.enqueueOneTime(account, syncDataType, fromPush = true) + } + } + + } else { + // fallback when no known topic is present (shouldn't happen) + val service = instance.toLongOrNull()?.let { serviceRepository.getBlocking(it) } + if (service != null) { + logger.warning("Got push message without topic and service, syncing all accounts") + val account = accountRepository.fromName(service.accountName) + syncWorkerManager.enqueueOneTimeAllAuthorities(account, fromPush = true) + + } else { + logger.warning("Got push message without topic, syncing all accounts") + for (account in accountRepository.getAll()) + syncWorkerManager.enqueueOneTimeAllAuthorities(account, fromPush = true) + } + } + } + + /** + * Parses a WebDAV-Push message and returns the `topic` that the message is about. + * + * @return topic of the modified collection, or `null` if the topic couldn't be determined + */ + @VisibleForTesting + internal fun parse(message: String): String? { + var topic: String? = null + + val parser = XmlUtils.newPullParser() + try { + parser.setInput(StringReader(message)) + + XmlReader(parser).processTag(DavPushMessage.NAME) { + val pushMessage = DavPushMessage.Factory.create(parser) + topic = pushMessage.topic?.topic + } + } catch (e: XmlPullParserException) { + logger.log(Level.WARNING, "Couldn't parse push message", e) + } + + return topic + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/push/PushNotificationManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/push/PushNotificationManager.kt new file mode 100644 index 0000000..d8e689f --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/push/PushNotificationManager.kt @@ -0,0 +1,70 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.push + +import android.accounts.Account +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.TaskStackBuilder +import at.bitfire.davdroid.R +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.ui.NotificationRegistry +import at.bitfire.davdroid.ui.account.AccountActivity +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class PushNotificationManager @Inject constructor( + @ApplicationContext private val context: Context, + private val notificationRegistry: NotificationRegistry +) { + + /** + * Generates the notification ID for a push notification. + */ + private fun notificationId(account: Account, dataType: SyncDataType): Int { + return account.name.hashCode() + account.type.hashCode() + dataType.hashCode() + } + + /** + * Sends a notification to inform the user that a push notification has been received, the + * sync has been scheduled, but it still has not run. + */ + fun notify(account: Account, dataType: SyncDataType) { + notificationRegistry.notifyIfPossible(notificationId(account, dataType)) { + NotificationCompat.Builder(context, notificationRegistry.CHANNEL_STATUS) + .setSmallIcon(R.drawable.ic_sync) + .setContentTitle(context.getString(R.string.sync_notification_pending_push_title)) + .setContentText(context.getString(R.string.sync_notification_pending_push_message)) + .setSubText(account.name) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setCategory(NotificationCompat.CATEGORY_STATUS) + .setAutoCancel(true) + .setOnlyAlertOnce(true) + .setContentIntent( + TaskStackBuilder.create(context) + .addNextIntentWithParentStack( + Intent(context, AccountActivity::class.java).apply { + putExtra(AccountActivity.EXTRA_ACCOUNT, account) + } + ) + .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + ) + .build() + } + } + + /** + * Once the sync has been started, the notification is no longer needed and can be dismissed. + * It's safe to call this method even if the notification has not been shown. + */ + fun dismiss(account: Account, dataType: SyncDataType) { + NotificationManagerCompat.from(context) + .cancel(notificationId(account, dataType)) + } + +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/push/PushRegistrationManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/push/PushRegistrationManager.kt new file mode 100644 index 0000000..ad4e882 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/push/PushRegistrationManager.kt @@ -0,0 +1,375 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.push + +import android.content.Context +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager +import at.bitfire.dav4jvm.DavCollection +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.HttpUtils +import at.bitfire.dav4jvm.XmlUtils +import at.bitfire.dav4jvm.XmlUtils.insertTag +import at.bitfire.dav4jvm.exception.DavException +import at.bitfire.dav4jvm.property.push.AuthSecret +import at.bitfire.dav4jvm.property.push.PushRegister +import at.bitfire.dav4jvm.property.push.PushResource +import at.bitfire.dav4jvm.property.push.Subscription +import at.bitfire.dav4jvm.property.push.SubscriptionPublicKey +import at.bitfire.dav4jvm.property.push.WebPushSubscription +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.di.IoDispatcher +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.push.PushRegistrationManager.Companion.mutex +import at.bitfire.davdroid.repository.AccountRepository +import at.bitfire.davdroid.repository.DavCollectionRepository +import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.sync.account.InvalidAccountException +import dagger.Lazy +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.runInterruptible +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.RequestBody.Companion.toRequestBody +import org.unifiedpush.android.connector.UnifiedPush +import org.unifiedpush.android.connector.data.PushEndpoint +import java.io.StringWriter +import java.time.Duration +import java.time.Instant +import java.util.concurrent.TimeUnit +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject +import javax.inject.Provider + +/** + * Manages push registrations and subscriptions. + * + * To update push registrations and subscriptions (for instance after collections have been changed), call [update]. + * + * Public API calls are protected by [mutex] so that there won't be multiple subscribe/unsubscribe operations at the same time. + * If you call other methods than [update], make sure that they don't interfere with other operations. + */ +class PushRegistrationManager @Inject constructor( + private val accountRepository: Lazy, + private val collectionRepository: DavCollectionRepository, + @ApplicationContext private val context: Context, + private val httpClientBuilder: Provider, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val logger: Logger, + private val serviceRepository: DavServiceRepository +) { + + /** + * Sets or removes (disable push) the distributor and updates the subscriptions + worker. + * + * Uses [update] which is protected by [mutex] so creating/deleting subscriptions doesn't + * interfere with other operations. + * + * @param pushDistributor new distributor or `null` to disable Push + */ + suspend fun setPushDistributor(pushDistributor: String?) { + // Disable UnifiedPush and remove all subscriptions + UnifiedPush.removeDistributor(context) + update() + + if (pushDistributor != null) { + // If a distributor was passed, store it and create/register subscriptions + UnifiedPush.saveDistributor(context, pushDistributor) + update() + } + } + + fun getCurrentDistributor() = UnifiedPush.getSavedDistributor(context) + + fun getDistributors() = UnifiedPush.getDistributors(context) + + + /** + * Updates all push registrations and subscriptions so that if Push is available, it's up-to-date and + * working for all database services. If Push is not available, existing subscriptions are unregistered. + * + * Also makes sure that the [PushRegistrationWorker] is enabled if there's a Push-enabled collection. + * + * Acquires [mutex] so that this method can't be called twice at the same time, or at the same time + * with [update(serviceId)]. + */ + suspend fun update() = mutex.withLock { + for (service in serviceRepository.getAll()) + updateService(service.id) + + updatePeriodicWorker() + } + + /** + * Same as [update], but for a specific database service. + * + * Acquires [mutex] so that this method can't be called twice at the same time, or at the same time + * as [update()]. + */ + suspend fun update(serviceId: Long) = mutex.withLock { + updateService(serviceId) + updatePeriodicWorker() + } + + /** + * Registers or unregisters subscriptions depending on whether there is a distributor available. + */ + private suspend fun updateService(serviceId: Long) { + val service = serviceRepository.get(serviceId) ?: return + + // use service ID from database as UnifiedPush instance name + val instance = serviceId.toString() + + val distributorAvailable = getCurrentDistributor() != null + if (distributorAvailable) + try { + val vapid = collectionRepository.getVapidKey(serviceId) + logger.fine("Registering UnifiedPush instance $serviceId (${service.accountName})") + + // message for distributor + val message = "${service.accountName} (${service.type})" + + UnifiedPush.register(context, instance, message, vapid) + } catch (e: UnifiedPush.VapidNotValidException) { + logger.log(Level.WARNING, "Couldn't register invalid VAPID key for service $serviceId", e) + } + else { + logger.fine("Unregistering UnifiedPush instance $serviceId (${service.accountName})") + UnifiedPush.unregister(context, instance) // doesn't call UnifiedPushService.onUnregistered + unsubscribeAll(service) + } + + // UnifiedPush has now been called. It will do its work and then asynchronously call back to UnifiedPushService, which + // will then call processSubscription or removeSubscription. + } + + /** + * Called by [UnifiedPushService] when a subscription (endpoint) is available for the given service. + * + * Uses the subscription to subscribe to syncable collections, and then unsubscribes from non-syncable collections. + */ + suspend fun processSubscription(serviceId: Long, endpoint: PushEndpoint) = mutex.withLock { + val service = serviceRepository.get(serviceId) ?: return + + try { + // subscribe to collections which are selected for synchronization + subscribeSyncable(service, endpoint) + + // unsubscribe from collections which are not selected for synchronization + unsubscribeCollections(service, collectionRepository.getPushRegisteredAndNotSyncable(service.id)) + } catch (_: InvalidAccountException) { + // couldn't create authenticating HTTP client because account is not available + } + } + + private suspend fun subscribeSyncable(service: Service, endpoint: PushEndpoint) { + val subscribeTo = collectionRepository.getPushCapableAndSyncable(service.id) + if (subscribeTo.isEmpty()) + return + + val account = accountRepository.get().fromName(service.accountName) + httpClientBuilder.get() + .fromAccountAsync(account) + .build() + .use { httpClient -> + for (collection in subscribeTo) + try { + val expires = collection.pushSubscriptionExpires + // calculate next run time, but use the duplicate interval for safety (times are not exact) + val nextRun = Instant.now() + Duration.ofDays(2 * WORKER_INTERVAL_DAYS) + if (expires != null && expires >= nextRun.epochSecond) + logger.fine("Push subscription for ${collection.url} is still valid until ${collection.pushSubscriptionExpires}") + else { + // no existing subscription or expiring soon + logger.fine("Registering push subscription for ${collection.url}") + subscribe(httpClient, collection, endpoint) + } + } catch (e: Exception) { + logger.log(Level.WARNING, "Couldn't register subscription at CalDAV/CardDAV server", e) + } + } + } + + /** + * Called when no subscription is available (anymore) for the given service. + * + * Unsubscribes from all subscribed collections. + */ + suspend fun removeSubscription(serviceId: Long) = mutex.withLock { + val service = serviceRepository.get(serviceId) ?: return + unsubscribeAll(service) + } + + private suspend fun unsubscribeAll(service: Service) { + val unsubscribeFrom = collectionRepository.getPushRegistered(service.id) + + try { + unsubscribeCollections(service, unsubscribeFrom) + } catch (_: InvalidAccountException) { + // couldn't create authenticating HTTP client because account is not available + } + } + + + /** + * Registers the subscription to a given collection ("subscribe to a collection"). + * + * @param httpClient HTTP client to use + * @param collection collection to subscribe to + * @param endpoint subscription to register + */ + private suspend fun subscribe(httpClient: HttpClient, collection: Collection, endpoint: PushEndpoint) { + // requested expiration time: 3 days + val requestedExpiration = Instant.now() + Duration.ofDays(3) + + val serializer = XmlUtils.newSerializer() + val writer = StringWriter() + serializer.setOutput(writer) + serializer.startDocument("UTF-8", true) + serializer.insertTag(PushRegister.NAME) { + serializer.insertTag(Subscription.NAME) { + // subscription URL + serializer.insertTag(WebPushSubscription.NAME) { + serializer.insertTag(PushResource.NAME) { + text(endpoint.url) + } + endpoint.pubKeySet?.let { pubKeySet -> + serializer.insertTag(SubscriptionPublicKey.NAME) { + attribute(null, "type", "p256dh") + text(pubKeySet.pubKey) + } + serializer.insertTag(AuthSecret.NAME) { + text(pubKeySet.auth) + } + } + } + } + // requested expiration + serializer.insertTag(PushRegister.EXPIRES) { + text(HttpUtils.formatDate(requestedExpiration)) + } + } + serializer.endDocument() + + runInterruptible(ioDispatcher) { + val xml = writer.toString().toRequestBody(DavResource.MIME_XML) + DavCollection(httpClient.okHttpClient, collection.url).post(xml) { response -> + if (response.isSuccessful) { + // update subscription URL and expiration in DB + val subscriptionUrl = response.header("Location") + val expires = response.header("Expires")?.let { expiresDate -> + HttpUtils.parseDate(expiresDate) + } ?: requestedExpiration + + runBlocking { + collectionRepository.updatePushSubscription( + id = collection.id, + subscriptionUrl = subscriptionUrl, + expires = expires?.epochSecond + ) + } + } else + logger.warning("Couldn't register push for ${collection.url}: $response") + } + } + } + + /** + * Unsubscribe from the given collections. + */ + private suspend fun unsubscribeCollections(service: Service, from: List) { + if (from.isEmpty()) + return + + val account = accountRepository.get().fromName(service.accountName) + httpClientBuilder.get() + .fromAccountAsync(account) + .build() + .use { httpClient -> + for (collection in from) + collection.pushSubscription?.toHttpUrlOrNull()?.let { url -> + logger.info("Unsubscribing Push from ${collection.url}") + unsubscribe(httpClient, collection, url) + } + } + } + + private suspend fun unsubscribe(httpClient: HttpClient, collection: Collection, url: HttpUrl) { + try { + runInterruptible(ioDispatcher) { + DavResource(httpClient.okHttpClient, url).delete { + // deleted + } + } + } catch (e: DavException) { + logger.log(Level.WARNING, "Couldn't unregister push for ${collection.url}", e) + } + + // remove registration URL from DB in any case + collectionRepository.updatePushSubscription( + id = collection.id, + subscriptionUrl = null, + expires = null + ) + } + + + /** + * Determines whether there are any push-capable collections and updates the periodic worker accordingly. + * + * If there are push-capable collections, a unique periodic worker with an initial delay of 5 seconds is enqueued. + * A potentially existing worker is replaced, so that the first run should be soon. + * + * Otherwise, a potentially existing worker is cancelled. + */ + private suspend fun updatePeriodicWorker() { + val workerNeeded = collectionRepository.anyPushCapable() + + val workManager = WorkManager.getInstance(context) + if (workerNeeded) { + logger.info("Enqueuing periodic PushRegistrationWorker") + workManager.enqueueUniquePeriodicWork( + WORKER_UNIQUE_NAME, + ExistingPeriodicWorkPolicy.UPDATE, + PeriodicWorkRequest.Builder(PushRegistrationWorker::class, WORKER_INTERVAL_DAYS, TimeUnit.DAYS) + .setInitialDelay(5, TimeUnit.SECONDS) + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + ) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) + .build() + ) + } else { + logger.info("Cancelling periodic PushRegistrationWorker") + workManager.cancelUniqueWork(WORKER_UNIQUE_NAME) + } + } + + + companion object { + + private const val WORKER_UNIQUE_NAME = "push-registration" + const val WORKER_INTERVAL_DAYS = 1L + + /** + * Mutex to synchronize (un)subscription. + */ + val mutex = Mutex() + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/push/PushRegistrationWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/push/PushRegistrationWorker.kt new file mode 100644 index 0000000..061452b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/push/PushRegistrationWorker.kt @@ -0,0 +1,38 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.push + +import android.content.Context +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import java.util.logging.Logger + +/** + * Worker that runs regularly and initiates push registration updates for all collections. + * + * Managed by [PushRegistrationManager]. + */ +@Suppress("unused") +@HiltWorker +class PushRegistrationWorker @AssistedInject constructor( + @Assisted context: Context, + @Assisted workerParameters: WorkerParameters, + private val logger: Logger, + private val pushRegistrationManager: PushRegistrationManager +) : CoroutineWorker(context, workerParameters) { + + override suspend fun doWork(): Result { + logger.info("Running push registration worker") + + // update registrations for all services + pushRegistrationManager.update() + + return Result.success() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/push/UnifiedPushService.kt b/app/src/main/kotlin/at/bitfire/davdroid/push/UnifiedPushService.kt new file mode 100644 index 0000000..31e0331 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/push/UnifiedPushService.kt @@ -0,0 +1,80 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.push + +import at.bitfire.davdroid.di.ApplicationScope +import dagger.Lazy +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.unifiedpush.android.connector.FailedReason +import org.unifiedpush.android.connector.PushService +import org.unifiedpush.android.connector.data.PushEndpoint +import org.unifiedpush.android.connector.data.PushMessage +import java.util.logging.Logger +import javax.inject.Inject + +/** + * Entry point for UnifiedPush. + * + * Calls [PushRegistrationManager] for most tasks, except incoming push messages, + * which are handled directly. + */ +@AndroidEntryPoint +class UnifiedPushService : PushService() { + + /* Scope to run the requests asynchronously. UnifiedPush binds the service, + * sends the message and unbinds one second later. Our operations may take longer, + * so the scope should not be bound to the service lifecycle. */ + @Inject + @ApplicationScope + lateinit var applicationScope: CoroutineScope + + @Inject + lateinit var logger: Logger + + @Inject + lateinit var pushMessageHandler: Lazy + + @Inject + lateinit var pushRegistrationManager: Lazy + + + override fun onNewEndpoint(endpoint: PushEndpoint, instance: String) { + val serviceId = instance.toLongOrNull() ?: return + logger.warning("Got UnifiedPush endpoint for service $serviceId: ${endpoint.url}") + + // register new endpoint at CalDAV/CardDAV servers + applicationScope.launch { + pushRegistrationManager.get().processSubscription(serviceId, endpoint) + } + } + + override fun onRegistrationFailed(reason: FailedReason, instance: String) { + val serviceId = instance.toLongOrNull() ?: return + logger.warning("UnifiedPush registration failed for service $serviceId: $reason") + + // unregister subscriptions + applicationScope.launch { + pushRegistrationManager.get().removeSubscription(serviceId) + } + } + + override fun onUnregistered(instance: String) { + val serviceId = instance.toLongOrNull() ?: return + logger.warning("UnifiedPush unregistered for service $serviceId") + + applicationScope.launch { + pushRegistrationManager.get().removeSubscription(serviceId) + } + } + + override fun onMessage(message: PushMessage, instance: String) { + applicationScope.launch { + pushMessageHandler.get().processMessage(message, instance) + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt new file mode 100644 index 0000000..080b91c --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt @@ -0,0 +1,269 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.repository + +import android.accounts.Account +import android.accounts.AccountManager +import android.accounts.OnAccountsUpdateListener +import android.content.Context +import androidx.annotation.WorkerThread +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.HomeSet +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.db.ServiceType +import at.bitfire.davdroid.di.DefaultDispatcher +import at.bitfire.davdroid.resource.LocalAddressBookStore +import at.bitfire.davdroid.resource.LocalCalendarStore +import at.bitfire.davdroid.servicedetection.DavResourceFinder +import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.settings.Credentials +import at.bitfire.davdroid.sync.AutomaticSyncManager +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.sync.TasksAppManager +import at.bitfire.davdroid.sync.account.AccountsCleanupWorker +import at.bitfire.davdroid.sync.account.InvalidAccountException +import at.bitfire.davdroid.sync.account.SystemAccountUtils +import at.bitfire.davdroid.sync.worker.SyncWorkerManager +import at.bitfire.vcard4android.GroupMethod +import dagger.Lazy +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.withContext +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject + +/** + * Repository for managing CalDAV/CardDAV accounts. + * + * *Note:* This class is not related to address book accounts, which are managed by + * [at.bitfire.davdroid.resource.LocalAddressBook]. + */ +class AccountRepository @Inject constructor( + private val accountSettingsFactory: AccountSettings.Factory, + private val automaticSyncManager: Lazy, + @ApplicationContext private val context: Context, + private val collectionRepository: DavCollectionRepository, + @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher, + private val homeSetRepository: DavHomeSetRepository, + private val localCalendarStore: Lazy, + private val localAddressBookStore: Lazy, + private val logger: Logger, + private val serviceRepository: DavServiceRepository, + private val syncWorkerManager: Lazy, + private val tasksAppManager: Lazy +) { + + private val accountType = context.getString(R.string.account_type) + private val accountManager = AccountManager.get(context) + + /** + * Creates a new account with discovered services and enables periodic syncs with + * default sync interval times. + * + * @param accountName name of the account + * @param credentials server credentials + * @param config discovered server capabilities for syncable authorities + * @param groupMethod whether CardDAV contact groups are separate VCards or as contact categories + * + * @return account if account creation was successful; null otherwise (for instance because an account with this name already exists) + */ + @WorkerThread + fun createBlocking(accountName: String, credentials: Credentials?, config: DavResourceFinder.Configuration, groupMethod: GroupMethod): Account? { + val account = fromName(accountName) + + // create Android account + val userData = AccountSettings.initialUserData(credentials) + logger.log(Level.INFO, "Creating Android account with initial config", arrayOf(account, userData)) + + if (!SystemAccountUtils.createAccount(context, account, userData, credentials?.password)) + return null + + // add entries for account to database + logger.log(Level.INFO, "Writing account configuration to database", config) + try { + if (config.cardDAV != null) { + // insert CardDAV service + val id = insertService(accountName, Service.TYPE_CARDDAV, config.cardDAV) + + // set initial CardDAV account settings and set sync intervals (enables automatic sync) + val accountSettings = accountSettingsFactory.create(account) + accountSettings.setGroupMethod(groupMethod) + + // start CardDAV service detection (refresh collections) + RefreshCollectionsWorker.enqueue(context, id) + } + + if (config.calDAV != null) { + // insert CalDAV service + val id = insertService(accountName, Service.TYPE_CALDAV, config.calDAV) + + // start CalDAV service detection (refresh collections) + RefreshCollectionsWorker.enqueue(context, id) + } + + // set up automatic sync (processes inserted services) + automaticSyncManager.get().updateAutomaticSync(account) + + } catch(e: InvalidAccountException) { + logger.log(Level.SEVERE, "Couldn't access account settings", e) + return null + } + return account + } + + suspend fun delete(accountName: String): Boolean { + val account = fromName(accountName) + // remove account directly (bypassing the authenticator, which is our own) + return try { + accountManager.removeAccountExplicitly(account) + + // delete address books (= address book accounts) + serviceRepository.getByAccountAndType(accountName, Service.TYPE_CARDDAV)?.let { service -> + collectionRepository.getByService(service.id).forEach { collection -> + localAddressBookStore.get().deleteByCollectionId(collection.id) + } + } + + // delete from database + serviceRepository.deleteByAccount(accountName) + + true + } catch (e: Exception) { + logger.log(Level.WARNING, "Couldn't remove account $accountName", e) + false + } + } + + fun exists(accountName: String): Boolean = + if (accountName.isEmpty()) + false + else + accountManager + .getAccountsByType(accountType) + .any { it.name == accountName } + + fun fromName(accountName: String) = + Account(accountName, accountType) + + fun getAll(): Array = accountManager.getAccountsByType(accountType) + + fun getAllFlow() = callbackFlow> { + val listener = OnAccountsUpdateListener { accounts -> + trySend(accounts.filter { it.type == accountType }.toSet()) + } + withContext(defaultDispatcher) { // causes disk I/O + accountManager.addOnAccountsUpdatedListener(listener, null, true) + } + + awaitClose { + accountManager.removeOnAccountsUpdatedListener(listener) + } + } + + /** + * Renames an account. + * + * **Not**: It is highly advised to re-sync the account after renaming in order to restore + * a consistent state. + * + * @param oldName current name of the account + * @param newName new name the account shall be re named to + * + * @throws InvalidAccountException if the account does not exist + * @throws IllegalArgumentException if the new account name already exists + * @throws Exception (or sub-classes) on other errors + */ + suspend fun rename(oldName: String, newName: String): Unit = withContext(defaultDispatcher) { + val oldAccount = fromName(oldName) + val newAccount = fromName(newName) + + // check whether new account name already exists + if (accountManager.getAccountsByType(context.getString(R.string.account_type)).contains(newAccount)) + throw IllegalArgumentException("Account with name \"$newName\" already exists") + + // rename account + try { + /* https://github.com/bitfireAT/davx5/issues/135 + Lock accounts cleanup so that the AccountsCleanupWorker doesn't run while we rename the account + because this can cause problems when: + 1. The account is renamed. + 2. The AccountsCleanupWorker is called BEFORE the services table is updated. + → AccountsCleanupWorker removes the "orphaned" services because they belong to the old account which doesn't exist anymore + 3. Now the services would be renamed, but they're not here anymore. */ + AccountsCleanupWorker.lockAccountsCleanup() + + // rename account (also moves AccountSettings) + val future = accountManager.renameAccount(oldAccount, newName, null, null) + + // wait for operation to complete (blocks calling thread) + val newNameFromApi: Account = future.result + if (newNameFromApi.name != newName) + throw IllegalStateException("renameAccount returned ${newNameFromApi.name} instead of $newName") + + // account renamed, cancel maybe running synchronization of old account + syncWorkerManager.get().cancelAllWork(oldAccount) + + // disable periodic syncs for old account + for (dataType in SyncDataType.entries) + syncWorkerManager.get().disablePeriodic(oldAccount, dataType) + + // update account name references in database + serviceRepository.renameAccount(oldName, newName) + + try { + // update address books + localAddressBookStore.get().updateAccount(oldAccount, newAccount) + } catch (e: Exception) { + logger.log(Level.WARNING, "Couldn't change address books to renamed account", e) + } + + try { + // update calendar events + localCalendarStore.get().updateAccount(oldAccount, newAccount) + } catch (e: Exception) { + logger.log(Level.WARNING, "Couldn't change calendars to renamed account", e) + } + + try { + // update account_name of local tasks + val dataStore = tasksAppManager.get().getDataStore() + dataStore?.updateAccount(oldAccount, newAccount) + } catch (e: Exception) { + logger.log(Level.WARNING, "Couldn't change task lists to renamed account", e) + } + + // update automatic sync + automaticSyncManager.get().updateAutomaticSync(newAccount) + } finally { + // release AccountsCleanupWorker mutex at the end of this async coroutine + AccountsCleanupWorker.unlockAccountsCleanup() + } + } + + + // helpers + + private fun insertService(accountName: String, @ServiceType type: String, info: DavResourceFinder.Configuration.ServiceInfo): Long { + // insert service + val service = Service(0, accountName, type, info.principal) + val serviceId = serviceRepository.insertOrReplaceBlocking(service) + + // insert home sets + for (homeSet in info.homeSets) + homeSetRepository.insertOrUpdateByUrlBlocking(HomeSet(0, serviceId, true, homeSet)) + + // insert collections + for (collection in info.collections.values) { + collectionRepository.insertOrUpdateByUrl(collection.copy(serviceId = serviceId)) + } + + return serviceId + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt new file mode 100644 index 0000000..ddb4a70 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt @@ -0,0 +1,425 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.repository + +import android.accounts.Account +import android.content.Context +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.XmlUtils +import at.bitfire.dav4jvm.XmlUtils.insertTag +import at.bitfire.dav4jvm.exception.GoneException +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.dav4jvm.exception.NotFoundException +import at.bitfire.dav4jvm.property.caldav.CalendarColor +import at.bitfire.dav4jvm.property.caldav.CalendarDescription +import at.bitfire.dav4jvm.property.caldav.CalendarTimezone +import at.bitfire.dav4jvm.property.caldav.CalendarTimezoneId +import at.bitfire.dav4jvm.property.caldav.NS_CALDAV +import at.bitfire.dav4jvm.property.caldav.SupportedCalendarComponentSet +import at.bitfire.dav4jvm.property.carddav.AddressbookDescription +import at.bitfire.dav4jvm.property.carddav.NS_CARDDAV +import at.bitfire.dav4jvm.property.webdav.DisplayName +import at.bitfire.dav4jvm.property.webdav.NS_WEBDAV +import at.bitfire.dav4jvm.property.webdav.ResourceType +import at.bitfire.davdroid.Constants +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.CollectionType +import at.bitfire.davdroid.db.HomeSet +import at.bitfire.davdroid.di.IoDispatcher +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker +import at.bitfire.davdroid.util.DavUtils +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.runInterruptible +import net.fortuna.ical4j.model.Calendar +import net.fortuna.ical4j.model.Component +import net.fortuna.ical4j.model.ComponentList +import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.PropertyList +import net.fortuna.ical4j.model.TimeZoneRegistryFactory +import net.fortuna.ical4j.model.component.VTimeZone +import net.fortuna.ical4j.model.property.Version +import okhttp3.HttpUrl +import java.io.StringWriter +import java.util.UUID +import java.util.logging.Logger +import javax.inject.Inject +import javax.inject.Provider + +/** + * Repository for managing collections. + */ +class DavCollectionRepository @Inject constructor( + @ApplicationContext private val context: Context, + private val db: AppDatabase, + private val logger: Logger, + private val httpClientBuilder: Provider, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val serviceRepository: DavServiceRepository +) { + + private val dao = db.collectionDao() + + /** + * Whether there are any collections that are registered for push. + */ + suspend fun anyPushCapable() = dao.anyPushCapable() + + /** + * Creates address book collection on server and locally + */ + suspend fun createAddressBook( + account: Account, + homeSet: HomeSet, + displayName: String, + description: String? + ) { + val folderName = UUID.randomUUID().toString() + val url = homeSet.url.newBuilder() + .addPathSegment(folderName) + .addPathSegment("") // trailing slash + .build() + + // create collection on server + createOnServer( + account = account, + url = url, + method = "MKCOL", + xmlBody = generateMkColXml( + addressBook = true, + displayName = displayName, + description = description + ) + ) + + // no HTTP error -> create collection locally + val collection = Collection( + serviceId = homeSet.serviceId, + homeSetId = homeSet.id, + url = url, + type = Collection.TYPE_ADDRESSBOOK, + displayName = displayName, + description = description + ) + dao.insertAsync(collection) + } + + /** + * Create calendar collection on server and locally + */ + suspend fun createCalendar( + account: Account, + homeSet: HomeSet, + color: Int?, + displayName: String, + description: String?, + timeZoneId: String?, + supportVEVENT: Boolean, + supportVTODO: Boolean, + supportVJOURNAL: Boolean + ) { + val folderName = UUID.randomUUID().toString() + val url = homeSet.url.newBuilder() + .addPathSegment(folderName) + .addPathSegment("") // trailing slash + .build() + + // create collection on server + createOnServer( + account = account, + url = url, + method = "MKCALENDAR", + xmlBody = generateMkColXml( + addressBook = false, + displayName = displayName, + description = description, + color = color, + timezoneId = timeZoneId, + supportsVEVENT = supportVEVENT, + supportsVTODO = supportVTODO, + supportsVJOURNAL = supportVJOURNAL + ) + ) + + // no HTTP error -> create collection locally + val collection = Collection( + serviceId = homeSet.serviceId, + homeSetId = homeSet.id, + url = url, + type = Collection.TYPE_CALENDAR, + displayName = displayName, + description = description, + color = color, + timezoneId = timeZoneId, + supportsVEVENT = supportVEVENT, + supportsVTODO = supportVTODO, + supportsVJOURNAL = supportVJOURNAL + ) + dao.insertAsync(collection) + + // Trigger service detection (because the collection may actually have other properties than the ones we have inserted). + // Some servers are known to change the supported components (VEVENT, …) after creation. + RefreshCollectionsWorker.enqueue(context, homeSet.serviceId) + } + + /** Deletes the given collection from the server and the database. */ + suspend fun deleteRemote(collection: Collection) { + val service = serviceRepository.getBlocking(collection.serviceId) ?: throw IllegalArgumentException("Service not found") + val account = Account(service.accountName, context.getString(R.string.account_type)) + + httpClientBuilder.get().fromAccount(account).build().use { httpClient -> + runInterruptible(ioDispatcher) { + try { + DavResource(httpClient.okHttpClient, collection.url).delete { + // success, otherwise an exception would have been thrown → delete locally, too + delete(collection) + } + } catch (e: HttpException) { + if (e is NotFoundException || e is GoneException) { + // HTTP 404 Not Found or 410 Gone (collection is not there anymore) -> delete locally, too + logger.info("Collection ${collection.url} not found on server, deleting locally") + delete(collection) + } else + throw e + } + } + } + } + + suspend fun getSyncableByTopic(topic: String) = dao.getSyncableByPushTopic(topic) + + fun get(id: Long) = dao.get(id) + suspend fun getAsync(id: Long) = dao.getAsync(id) + + fun getFlow(id: Long) = dao.getFlow(id) + + suspend fun getByService(serviceId: Long) = dao.getByService(serviceId) + + fun getByServiceAndUrl(serviceId: Long, url: String) = dao.getByServiceAndUrl(serviceId, url) + + fun getByServiceAndSync(serviceId: Long) = dao.getByServiceAndSync(serviceId) + + fun getSyncCalendars(serviceId: Long) = dao.getSyncCalendars(serviceId) + + fun getSyncJtxCollections(serviceId: Long) = dao.getSyncJtxCollections(serviceId) + + fun getSyncTaskLists(serviceId: Long) = dao.getSyncTaskLists(serviceId) + + /** Returns all collections that are both selected for synchronization and push-capable. */ + suspend fun getPushCapableAndSyncable(serviceId: Long) = dao.getPushCapableSyncCollections(serviceId) + + suspend fun getPushRegistered(serviceId: Long) = dao.getPushRegistered(serviceId) + suspend fun getPushRegisteredAndNotSyncable(serviceId: Long) = dao.getPushRegisteredAndNotSyncable(serviceId) + + suspend fun getVapidKey(serviceId: Long) = dao.getFirstVapidKey(serviceId) + + /** + * Inserts or updates the collection. + * + * On update, it will _not_ update the flags + * - [Collection.sync] and + * - [Collection.forceReadOnly], + * but use the values of the already existing collection. + * + * @param newCollection Collection to be inserted or updated + */ + fun insertOrUpdateByUrlRememberSync(newCollection: Collection) { + db.runInTransaction { + // remember locally set flags + val oldCollection = dao.getByServiceAndUrl(newCollection.serviceId, newCollection.url.toString()) + val newCollectionWithFlags = + if (oldCollection != null) + newCollection.copy(sync = oldCollection.sync, forceReadOnly = oldCollection.forceReadOnly) + else + newCollection + + // commit new collection to database + insertOrUpdateByUrl(newCollectionWithFlags) + } + } + + /** + * Creates or updates the existing collection if it exists (URL) + */ + fun insertOrUpdateByUrl(collection: Collection) { + dao.insertOrUpdateByUrl(collection) + } + + fun pageByServiceAndType(serviceId: Long, @CollectionType type: String) = + dao.pageByServiceAndType(serviceId, type) + + fun pagePersonalByServiceAndType(serviceId: Long, @CollectionType type: String) = + dao.pagePersonalByServiceAndType(serviceId, type) + + /** + * Sets the flag for whether read-only should be enforced on the local collection + */ + suspend fun setForceReadOnly(id: Long, forceReadOnly: Boolean) { + dao.updateForceReadOnly(id, forceReadOnly) + } + + /** + * Whether or not the local collection should be synced with the server + */ + suspend fun setSync(id: Long, forceReadOnly: Boolean) { + dao.updateSync(id, forceReadOnly) + } + + suspend fun updatePushSubscription(id: Long, subscriptionUrl: String?, expires: Long?) { + dao.updatePushSubscription( + id = id, + pushSubscription = subscriptionUrl, + pushSubscriptionExpires = expires + ) + } + + /** + * Deletes the collection locally + */ + fun delete(collection: Collection) { + dao.delete(collection) + } + + + // helpers + + private suspend fun createOnServer(account: Account, url: HttpUrl, method: String, xmlBody: String) { + httpClientBuilder.get() + .fromAccount(account) + .build() + .use { httpClient -> + runInterruptible(ioDispatcher) { + DavResource(httpClient.okHttpClient, url).mkCol( + xmlBody = xmlBody, + method = method + ) { + // success, otherwise an exception would have been thrown + } + } + } + } + + private fun generateMkColXml( + addressBook: Boolean, + displayName: String?, + description: String?, + color: Int? = null, + timezoneId: String? = null, + supportsVEVENT: Boolean = true, + supportsVTODO: Boolean = true, + supportsVJOURNAL: Boolean = true + ): String { + val writer = StringWriter() + val serializer = XmlUtils.newSerializer() + serializer.apply { + setOutput(writer) + + startDocument("UTF-8", null) + setPrefix("", NS_WEBDAV) + setPrefix("CAL", NS_CALDAV) + setPrefix("CARD", NS_CARDDAV) + + if (addressBook) + startTag(NS_WEBDAV, "mkcol") + else + startTag(NS_CALDAV, "mkcalendar") + + insertTag(DavResource.SET) { + insertTag(DavResource.PROP) { + insertTag(ResourceType.NAME) { + insertTag(ResourceType.COLLECTION) + if (addressBook) + insertTag(ResourceType.ADDRESSBOOK) + else + insertTag(ResourceType.CALENDAR) + } + + displayName?.let { + insertTag(DisplayName.NAME) { + text(it) + } + } + + if (addressBook) { + // addressbook-specific properties + description?.let { + insertTag(AddressbookDescription.NAME) { + text(it) + } + } + + } else { + // calendar-specific properties + description?.let { + insertTag(CalendarDescription.NAME) { + text(it) + } + } + color?.let { + insertTag(CalendarColor.NAME) { + text(DavUtils.ARGBtoCalDAVColor(it)) + } + } + timezoneId?.let { id -> + insertTag(CalendarTimezoneId.NAME) { + text(id) + } + getVTimeZone(id)?.let { vTimezone -> + insertTag(CalendarTimezone.NAME) { + text( + // spec requires "an iCalendar object with exactly one VTIMEZONE component" + Calendar( + PropertyList().apply { + add(Version.VERSION_2_0) + add(Constants.iCalProdId) + }, + ComponentList( + listOf(vTimezone) + ) + ).toString() + ) + } + } + } + + if (!supportsVEVENT || !supportsVTODO || !supportsVJOURNAL) { + insertTag(SupportedCalendarComponentSet.NAME) { + // Only if there's at least one not explicitly supported calendar component set, + // otherwise don't include the property, which means "supports everything". + if (supportsVEVENT) + insertTag(SupportedCalendarComponentSet.COMP) { + attribute(null, "name", Component.VEVENT) + } + if (supportsVTODO) + insertTag(SupportedCalendarComponentSet.COMP) { + attribute(null, "name", Component.VTODO) + } + if (supportsVJOURNAL) + insertTag(SupportedCalendarComponentSet.COMP) { + attribute(null, "name", Component.VJOURNAL) + } + } + } + } + } + } + if (addressBook) + endTag(NS_WEBDAV, "mkcol") + else + endTag(NS_CALDAV, "mkcalendar") + endDocument() + } + return writer.toString() + } + + private fun getVTimeZone(tzId: String): VTimeZone? { + val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry() + return tzRegistry.getTimeZone(tzId)?.vTimeZone + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/repository/DavHomeSetRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/repository/DavHomeSetRepository.kt new file mode 100644 index 0000000..67ca542 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/repository/DavHomeSetRepository.kt @@ -0,0 +1,36 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.repository + +import android.accounts.Account +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.HomeSet +import at.bitfire.davdroid.db.Service +import javax.inject.Inject + +class DavHomeSetRepository @Inject constructor( + db: AppDatabase +) { + + private val dao = db.homeSetDao() + + fun getAddressBookHomeSetsFlow(account: Account) = + dao.getBindableByAccountAndServiceTypeFlow(account.name, Service.TYPE_CARDDAV) + + fun getBindableByServiceFlow(serviceId: Long) = dao.getBindableByServiceFlow(serviceId) + + fun getByIdBlocking(id: Long) = dao.getById(id) + + fun getByServiceBlocking(serviceId: Long) = dao.getByService(serviceId) + + fun getCalendarHomeSetsFlow(account: Account) = + dao.getBindableByAccountAndServiceTypeFlow(account.name, Service.TYPE_CALDAV) + + fun insertOrUpdateByUrlBlocking(homeSet: HomeSet): Long = + dao.insertOrUpdateByUrlBlocking(homeSet) + + fun deleteBlocking(homeSet: HomeSet) = dao.delete(homeSet) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/repository/DavServiceRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/repository/DavServiceRepository.kt new file mode 100644 index 0000000..11dcd8e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/repository/DavServiceRepository.kt @@ -0,0 +1,52 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.repository + +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.db.ServiceType +import javax.inject.Inject + +class DavServiceRepository @Inject constructor( + db: AppDatabase +) { + + private val dao = db.serviceDao() + + + // Read + + fun getBlocking(id: Long): Service? = dao.get(id) + suspend fun get(id: Long): Service? = dao.getAsync(id) + + suspend fun getAll(): List = dao.getAll() + + suspend fun getByAccountAndType(name: String, @ServiceType serviceType: String): Service? = + dao.getByAccountAndType(name, serviceType) + + fun getCalDavServiceFlow(accountName: String) = + dao.getByAccountAndTypeFlow(accountName, Service.TYPE_CALDAV) + + fun getCardDavServiceFlow(accountName: String) = + dao.getByAccountAndTypeFlow(accountName, Service.TYPE_CARDDAV) + + + // Create & update + + fun insertOrReplaceBlocking(service: Service) = + dao.insertOrReplace(service) + + suspend fun renameAccount(oldName: String, newName: String) = + dao.renameAccount(oldName, newName) + + + // Delete + + fun deleteAllBlocking() = dao.deleteAll() + + suspend fun deleteByAccount(accountName: String) = + dao.deleteByAccount(accountName) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/repository/DavSyncStatsRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/repository/DavSyncStatsRepository.kt new file mode 100644 index 0000000..1fef68b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/repository/DavSyncStatsRepository.kt @@ -0,0 +1,50 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.repository + +import android.content.Context +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.SyncStats +import at.bitfire.davdroid.sync.SyncDataType +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import java.text.Collator +import javax.inject.Inject + +class DavSyncStatsRepository @Inject constructor( + @ApplicationContext val context: Context, + db: AppDatabase +) { + + private val dao = db.syncStatsDao() + + data class LastSynced( + val dataType: String, + val lastSynced: Long + ) + fun getLastSyncedFlow(collectionId: Long): Flow> = + dao.getByCollectionIdFlow(collectionId).map { list -> + val collator = Collator.getInstance() + list.map { stats -> + LastSynced( + dataType = stats.dataType, + lastSynced = stats.lastSync + ) + }.sortedWith { a, b -> + collator.compare(a.dataType, b.dataType) + } + } + + suspend fun logSyncTime(collectionId: Long, dataType: SyncDataType, lastSync: Long = System.currentTimeMillis()) { + dao.insertOrReplace(SyncStats( + id = 0, + collectionId = collectionId, + dataType = dataType.name, + lastSync = lastSync + )) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/repository/PreferenceRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/repository/PreferenceRepository.kt new file mode 100644 index 0000000..e9efffd --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/repository/PreferenceRepository.kt @@ -0,0 +1,75 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.repository + +import android.content.Context +import android.content.SharedPreferences.OnSharedPreferenceChangeListener +import androidx.core.content.edit +import androidx.preference.PreferenceManager +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import javax.inject.Inject + +/** + * Repository to access preferences. Preferences are stored in a shared preferences file + * and reflect settings that are very low-level and are therefore not covered by + * [at.bitfire.davdroid.settings.SettingsManager]. + */ +class PreferenceRepository @Inject constructor( + @ApplicationContext context: Context +) { + + companion object { + const val LOG_TO_FILE = "log_to_file" + } + + private val preferences = PreferenceManager.getDefaultSharedPreferences(context) + + + /** + * Updates the "log to file" (verbose logging") preference. + */ + fun logToFile(logToFile: Boolean) { + preferences.edit { + putBoolean(LOG_TO_FILE, logToFile) + } + } + + /** + * Gets the "log to file" (verbose logging) preference. + */ + fun logToFile(): Boolean = + preferences.getBoolean(LOG_TO_FILE, false) + + /** + * Gets the "log to file" (verbose logging) preference as a live value. + */ + fun logToFileFlow(): Flow = observeAsFlow(LOG_TO_FILE) { + logToFile() + } + + + // helpers + + private fun observeAsFlow(keyToObserve: String, getValue: () -> T): Flow = + callbackFlow { + val listener = OnSharedPreferenceChangeListener { _, key -> + if (key == keyToObserve) { + trySend(getValue()) + } + } + preferences.registerOnSharedPreferenceChangeListener(listener) + + // Emit the initial value + trySend(getValue()) + + awaitClose { + preferences.unregisterOnSharedPreferenceChangeListener(listener) + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/repository/PrincipalRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/repository/PrincipalRepository.kt new file mode 100644 index 0000000..dc4c511 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/repository/PrincipalRepository.kt @@ -0,0 +1,19 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.repository + +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Principal +import javax.inject.Inject + +class PrincipalRepository @Inject constructor( + db: AppDatabase +) { + + private val dao = db.principalDao() + + fun getBlocking(id: Long): Principal = dao.get(id) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddress.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddress.kt new file mode 100644 index 0000000..7739910 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddress.kt @@ -0,0 +1,9 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import at.bitfire.vcard4android.Contact + +interface LocalAddress: LocalResource \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt new file mode 100644 index 0000000..bf74b54 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt @@ -0,0 +1,360 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ +package at.bitfire.davdroid.resource + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.ContentProviderClient +import android.content.ContentUris +import android.content.Context +import android.os.Bundle +import android.os.RemoteException +import android.provider.ContactsContract +import android.provider.ContactsContract.CommonDataKinds.GroupMembership +import android.provider.ContactsContract.Groups +import android.provider.ContactsContract.RawContacts +import androidx.annotation.OpenForTesting +import androidx.core.content.contentValuesOf +import at.bitfire.davdroid.R +import at.bitfire.davdroid.repository.DavCollectionRepository +import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.resource.LocalAddressBook.Companion.USER_DATA_READ_ONLY +import at.bitfire.davdroid.resource.workaround.ContactDirtyVerifier +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.sync.account.SystemAccountUtils +import at.bitfire.davdroid.sync.account.setAndVerifyUserData +import at.bitfire.davdroid.sync.adapter.SyncFrameworkIntegration +import at.bitfire.synctools.storage.BatchOperation +import at.bitfire.synctools.storage.ContactsBatchOperation +import at.bitfire.vcard4android.AndroidAddressBook +import at.bitfire.vcard4android.AndroidContact +import at.bitfire.vcard4android.AndroidGroup +import at.bitfire.vcard4android.GroupMethod +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.qualifiers.ApplicationContext +import java.util.LinkedList +import java.util.Optional +import java.util.logging.Level +import java.util.logging.Logger + +/** + * A local address book. Requires its own Android account, because Android manages contacts per + * account and there is no such thing as "address books". So, DAVx5 creates a "DAVx5 + * address book" account for every CardDAV address book. + * + * @param account DAVx5 account which "owns" this address book + * @param _addressBookAccount Address book account (not: DAVx5 account) storing the actual Android + * contacts. This is the initial value of [addressBookAccount]. However when the address book is renamed, + * the new name will only be available in [addressBookAccount], so usually that one should be used. + * @param provider Content provider needed to access and modify the address book + */ +@OpenForTesting +open class LocalAddressBook @AssistedInject constructor( + @Assisted("account") val account: Account, + @Assisted("addressBookAccount") _addressBookAccount: Account, + @Assisted provider: ContentProviderClient, + private val accountSettingsFactory: AccountSettings.Factory, + private val collectionRepository: DavCollectionRepository, + @ApplicationContext private val context: Context, + internal val dirtyVerifier: Optional, + private val logger: Logger, + private val serviceRepository: DavServiceRepository, + private val syncFramework: SyncFrameworkIntegration +): AndroidAddressBook(_addressBookAccount, provider, LocalContact.Factory, LocalGroup.Factory), LocalCollection { + + @AssistedFactory + interface Factory { + fun create( + @Assisted("account") account: Account, + @Assisted("addressBookAccount") addressBookAccount: Account, + provider: ContentProviderClient + ): LocalAddressBook + } + + override val tag: String + get() = "contacts-${addressBookAccount.name}" + + override val title + get() = addressBookAccount.name + + private val accountManager by lazy { AccountManager.get(context) } + + /** + * Whether contact groups ([LocalGroup]) are included in query results + * and are affected by updates/deletes on generic members. + * + * For instance, if groupMethod is GROUP_VCARDS, [findDirty] will find only dirty [LocalContact]s, + * but if it is enabled, [findDirty] will find dirty [LocalContact]s and [LocalGroup]s. + */ + open val groupMethod: GroupMethod by lazy { + val account = accountManager.getUserData(addressBookAccount, USER_DATA_COLLECTION_ID)?.toLongOrNull()?.let { collectionId -> + collectionRepository.get(collectionId)?.let { collection -> + serviceRepository.getBlocking(collection.serviceId)?.let { service -> + Account(service.accountName, context.getString(R.string.account_type)) + } + } + } + if (account == null) + throw IllegalArgumentException("Collection of address book account $addressBookAccount does not have an account") + val accountSettings = accountSettingsFactory.create(account) + accountSettings.getGroupMethod() + } + val includeGroups + get() = groupMethod == GroupMethod.GROUP_VCARDS + + override var dbCollectionId: Long? + get() = accountManager.getUserData(addressBookAccount, USER_DATA_COLLECTION_ID)?.toLongOrNull() + set(id) { + accountManager.setAndVerifyUserData(addressBookAccount, USER_DATA_COLLECTION_ID, id.toString()) + } + + /** + * Read-only flag for the address book itself. + * + * Setting this flag: + * + * - stores the new value in [USER_DATA_READ_ONLY] and + * - sets the read-only flag for all contacts and groups in the address book in the content provider, which will + * prevent non-sync-adapter apps from modifying them. However new entries can still be created, so the address book + * is not really read-only. + * + * Reading this flag returns the stored value from [USER_DATA_READ_ONLY]. + */ + override var readOnly: Boolean + get() = accountManager.getUserData(addressBookAccount, USER_DATA_READ_ONLY) != null + set(readOnly) { + // set read-only flag for address book itself + accountManager.setAndVerifyUserData(addressBookAccount, USER_DATA_READ_ONLY, if (readOnly) "1" else null) + + // update raw contacts + val rawContactValues = contentValuesOf(RawContacts.RAW_CONTACT_IS_READ_ONLY to if (readOnly) 1 else 0) + provider!!.update(rawContactsSyncUri(), rawContactValues, null, null) + + // update data rows + val dataValues = contentValuesOf(ContactsContract.Data.IS_READ_ONLY to if (readOnly) 1 else 0) + provider!!.update(syncAdapterURI(ContactsContract.Data.CONTENT_URI), dataValues, null, null) + + // update group rows + val groupValues = contentValuesOf(Groups.GROUP_IS_READ_ONLY to if (readOnly) 1 else 0) + provider!!.update(groupsSyncUri(), groupValues, null, null) + } + + override var lastSyncState: SyncState? + get() = syncState?.let { SyncState.fromString(String(it)) } + set(state) { + syncState = state?.toString()?.toByteArray() + } + + + /* operations on the collection (address book) itself */ + + override fun markNotDirty(flags: Int): Int { + val values = contentValuesOf(LocalContact.COLUMN_FLAGS to flags) + var number = provider!!.update(rawContactsSyncUri(), values, "${RawContacts.DIRTY}=0", null) + + if (includeGroups) { + values.clear() + values.put(LocalGroup.COLUMN_FLAGS, flags) + number += provider!!.update(groupsSyncUri(), values, "NOT ${Groups.DIRTY}", null) + } + + return number + } + + override fun removeNotDirtyMarked(flags: Int): Int { + var number = provider!!.delete(rawContactsSyncUri(), + "NOT ${RawContacts.DIRTY} AND ${LocalContact.COLUMN_FLAGS}=?", arrayOf(flags.toString())) + + if (includeGroups) + number += provider!!.delete(groupsSyncUri(), + "NOT ${Groups.DIRTY} AND ${LocalGroup.COLUMN_FLAGS}=?", arrayOf(flags.toString())) + + return number + } + + /** + * Renames an address book account and moves the contacts and groups (without making them dirty). + * Does not keep user data of the old account, so these have to be set again. + * + * On success, [addressBookAccount] will be updated to the new account name. + * + * _Note:_ Previously, we had used [AccountManager.renameAccount], but then the contacts can't be moved because there's never + * a moment when both accounts are available. + * + * @param newName the new account name (account type is taken from [addressBookAccount]) + * + * @return whether the account was renamed successfully + */ + internal fun renameAccount(newName: String): Boolean { + val oldAccount = addressBookAccount + logger.info("Renaming address book from \"${oldAccount.name}\" to \"$newName\"") + + // create new account + val newAccount = Account(newName, oldAccount.type) + if (!SystemAccountUtils.createAccount(context, newAccount, Bundle())) + return false + + // move contacts and groups to new account + val batch = ContactsBatchOperation(provider!!) + batch += BatchOperation.CpoBuilder + .newUpdate(groupsSyncUri()) + .withSelection(Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?", arrayOf(oldAccount.name, oldAccount.type)) + .withValue(Groups.ACCOUNT_NAME, newAccount.name) + .withValue(Groups.ACCOUNT_TYPE, newAccount.type) + batch += BatchOperation.CpoBuilder + .newUpdate(rawContactsSyncUri()) + .withSelection(RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=?", arrayOf(oldAccount.name, oldAccount.type)) + .withValue(RawContacts.ACCOUNT_NAME, newAccount.name) + .withValue(RawContacts.ACCOUNT_TYPE, newAccount.type) + batch.commit() + + // update AndroidAddressBook.account + addressBookAccount = newAccount + + // delete old account + accountManager.removeAccountExplicitly(oldAccount) + + return true + } + + + /** + * Enables or disables sync on content changes for the address book account based on the current sync + * interval account setting. + */ + fun updateSyncFrameworkSettings() { + val accountSettings = accountSettingsFactory.create(account) + val syncInterval = accountSettings.getSyncInterval(SyncDataType.CONTACTS) + + // Enable/Disable content triggered syncs for the address book account. + if (syncInterval != null) + syncFramework.enableSyncOnContentChange(addressBookAccount, ContactsContract.AUTHORITY) + else + syncFramework.disableSyncOnContentChange(addressBookAccount, ContactsContract.AUTHORITY) + } + + + /* operations on members (contacts/groups) */ + + override fun findByName(name: String): LocalAddress? { + val result = queryContacts("${AndroidContact.COLUMN_FILENAME}=?", arrayOf(name)).firstOrNull() + return if (includeGroups) + result ?: queryGroups("${AndroidGroup.COLUMN_FILENAME}=?", arrayOf(name)).firstOrNull() + else + result + } + + + /** + * Returns an array of local contacts/groups which have been deleted locally. (DELETED != 0). + * @throws RemoteException on content provider errors + */ + override fun findDeleted() = + if (includeGroups) + findDeletedContacts() + findDeletedGroups() + else + findDeletedContacts() + + fun findDeletedContacts() = queryContacts(RawContacts.DELETED, null) + fun findDeletedGroups() = queryGroups(Groups.DELETED, null) + + /** + * Returns an array of local contacts/groups which have been changed locally (DIRTY != 0). + * @throws RemoteException on content provider errors + */ + override fun findDirty() = + if (includeGroups) + findDirtyContacts() + findDirtyGroups() + else + findDirtyContacts() + fun findDirtyContacts() = queryContacts(RawContacts.DIRTY, null) + fun findDirtyGroups() = queryGroups(Groups.DIRTY, null) + + override fun forgetETags() { + if (includeGroups) { + val values = contentValuesOf(AndroidGroup.COLUMN_ETAG to null) + provider!!.update(groupsSyncUri(), values, null, null) + } + val values = contentValuesOf(AndroidContact.COLUMN_ETAG to null) + provider!!.update(rawContactsSyncUri(), values, null, null) + } + + + fun getContactIdsByGroupMembership(groupId: Long): List { + val ids = LinkedList() + provider!!.query(syncAdapterURI(ContactsContract.Data.CONTENT_URI), arrayOf(GroupMembership.RAW_CONTACT_ID), + "(${GroupMembership.MIMETYPE}=? AND ${GroupMembership.GROUP_ROW_ID}=?)", + arrayOf(GroupMembership.CONTENT_ITEM_TYPE, groupId.toString()), null)?.use { cursor -> + while (cursor.moveToNext()) + ids += cursor.getLong(0) + } + return ids + } + + fun getContactUidFromId(contactId: Long): String? { + provider!!.query(rawContactsSyncUri(), arrayOf(AndroidContact.COLUMN_UID), + "${RawContacts._ID}=?", arrayOf(contactId.toString()), null)?.use { cursor -> + if (cursor.moveToNext()) + return cursor.getString(0) + } + return null + } + + + /* special group operations */ + + /** + * Finds the first group with the given title. If there is no group with this + * title, a new group is created. + * @param title title of the group to look for + * @return id of the group with given title + * @throws RemoteException on content provider errors + */ + fun findOrCreateGroup(title: String): Long { + provider!!.query(syncAdapterURI(Groups.CONTENT_URI), arrayOf(Groups._ID), + "${Groups.TITLE}=?", arrayOf(title), null)?.use { cursor -> + if (cursor.moveToNext()) + return cursor.getLong(0) + } + + val values = contentValuesOf(Groups.TITLE to title) + val uri = provider!!.insert(syncAdapterURI(Groups.CONTENT_URI), values) ?: throw RemoteException("Couldn't create contact group") + return ContentUris.parseId(uri) + } + + fun removeEmptyGroups() { + // find groups without members + /** should be done using {@link Groups.SUMMARY_COUNT}, but it's not implemented in Android yet */ + queryGroups(null, null).filter { it.getMembers().isEmpty() }.forEach { group -> + logger.log(Level.FINE, "Deleting group", group) + group.delete() + } + } + + + companion object { + + const val USER_DATA_ACCOUNT_NAME = "account_name" + const val USER_DATA_ACCOUNT_TYPE = "account_type" + + /** + * ID of the corresponding database [at.bitfire.davdroid.db.Collection]. + * + * User data of the address book account (Long). + */ + const val USER_DATA_COLLECTION_ID = "collection_id" + + /** + * Indicates whether the address book is currently set to read-only (i.e. its contacts and groups have the read-only flag). + * + * User data of the address book account (Boolean). + */ + const val USER_DATA_READ_ONLY = "read_only" + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBookStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBookStore.kt new file mode 100644 index 0000000..6fbb821 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBookStore.kt @@ -0,0 +1,269 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.accounts.Account +import android.accounts.AccountManager +import android.accounts.OnAccountsUpdateListener +import android.content.ContentProviderClient +import android.content.Context +import android.provider.ContactsContract +import androidx.annotation.OpenForTesting +import androidx.annotation.VisibleForTesting +import androidx.core.content.contentValuesOf +import androidx.core.os.bundleOf +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.settings.Settings +import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.sync.account.SystemAccountUtils +import at.bitfire.davdroid.sync.account.setAndVerifyUserData +import at.bitfire.davdroid.util.DavUtils.lastSegment +import com.google.common.base.CharMatcher +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject + +class LocalAddressBookStore @Inject constructor( + @ApplicationContext private val context: Context, + private val localAddressBookFactory: LocalAddressBook.Factory, + private val logger: Logger, + private val serviceRepository: DavServiceRepository, + private val settings: SettingsManager +): LocalDataStore { + + override val authority: String + get() = ContactsContract.AUTHORITY + + /** whether a (usually managed) setting wants all address-books to be read-only **/ + val forceAllReadOnly: Boolean + get() = settings.getBoolean(Settings.FORCE_READ_ONLY_ADDRESSBOOKS) + + + /** + * Assembles a name for the address book (account) from its corresponding database [Collection]. + * + * The address book account name contains + * + * - the collection display name or last URL path segment (filtered for dangerous special characters) + * - the actual account name + * - the collection ID, to make it unique. + * + * @param info Collection to take info from + */ + fun accountName(info: Collection): String { + // Name of address book is given collection display name, otherwise the last URL path segment + var name = info.displayName.takeIf { !it.isNullOrEmpty() } ?: info.url.lastSegment + + // Remove ISO control characters + SQL problematic characters + name = CharMatcher + .javaIsoControl() + .or(CharMatcher.anyOf("`'\"")) + .removeFrom(name) + + // Add the actual account name to the address book account name + val sb = StringBuilder(name) + serviceRepository.getBlocking(info.serviceId)?.let { service -> + sb.append(" (${service.accountName})") + } + // Add the collection ID for uniqueness + sb.append(" #${info.id}") + return sb.toString() + } + + override fun acquireContentProvider(throwOnMissingPermissions: Boolean) = try { + context.contentResolver.acquireContentProviderClient(authority) + } catch (e: SecurityException) { + if (throwOnMissingPermissions) + throw e + else + /* return */ null + } + + override fun create(provider: ContentProviderClient, fromCollection: Collection): LocalAddressBook? { + val service = serviceRepository.getBlocking(fromCollection.serviceId) ?: throw IllegalArgumentException("Couldn't fetch DB service from collection") + val account = Account(service.accountName, context.getString(R.string.account_type)) + + val name = accountName(fromCollection) + val addressBookAccount = createAddressBookAccount( + account = account, + name = name, + id = fromCollection.id + ) ?: return null + + val addressBook = localAddressBookFactory.create(account, addressBookAccount, provider) + + // update settings + addressBook.updateSyncFrameworkSettings() + addressBook.settings = contactsProviderSettings + addressBook.readOnly = shouldBeReadOnly(fromCollection, forceAllReadOnly) + + return addressBook + } + + @OpenForTesting + internal fun createAddressBookAccount(account: Account, name: String, id: Long): Account? { + // create address book account with reference to account, collection ID and URL + val addressBookAccount = Account(name, context.getString(R.string.account_type_address_book)) + val userData = bundleOf( + LocalAddressBook.USER_DATA_ACCOUNT_NAME to account.name, + LocalAddressBook.USER_DATA_ACCOUNT_TYPE to account.type, + LocalAddressBook.USER_DATA_COLLECTION_ID to id.toString() + ) + if (!SystemAccountUtils.createAccount(context, addressBookAccount, userData)) { + logger.warning("Couldn't create address book account: $addressBookAccount") + return null + } + + return addressBookAccount + } + + override fun getAll(account: Account, provider: ContentProviderClient): List = + getAddressBookAccounts(account).map { addressBookAccount -> + localAddressBookFactory.create(account, addressBookAccount, provider) + } + + override fun update(provider: ContentProviderClient, localCollection: LocalAddressBook, fromCollection: Collection) { + var currentAccount = localCollection.addressBookAccount + logger.log(Level.INFO, "Updating local address book $currentAccount from collection $fromCollection") + + // Update the account name + val newAccountName = accountName(fromCollection) + if (currentAccount.name != newAccountName) { + // rename, move contacts/groups and update [AndroidAddressBook.]account + localCollection.renameAccount(newAccountName) + currentAccount = Account(newAccountName, currentAccount.type) + } + + // Update the account user data + val accountManager = AccountManager.get(context) + accountManager.setAndVerifyUserData(currentAccount, LocalAddressBook.USER_DATA_ACCOUNT_NAME, localCollection.account.name) + accountManager.setAndVerifyUserData(currentAccount, LocalAddressBook.USER_DATA_ACCOUNT_TYPE, localCollection.account.type) + accountManager.setAndVerifyUserData(currentAccount, LocalAddressBook.USER_DATA_COLLECTION_ID, fromCollection.id.toString()) + + // Set contacts provider settings + localCollection.settings = contactsProviderSettings + + // Update force read only + val nowReadOnly = shouldBeReadOnly(fromCollection, forceAllReadOnly) + if (nowReadOnly != localCollection.readOnly) { + logger.info("Address book has changed to read-only = $nowReadOnly") + localCollection.readOnly = nowReadOnly + } + + // Update automatic synchronization + localCollection.updateSyncFrameworkSettings() + } + + /** + * Updates address books which are assigned to [oldAccount] so that they're assigned to [newAccount] instead. + * + * @param oldAccount The old account + * @param newAccount The new account + */ + override fun updateAccount(oldAccount: Account, newAccount: Account) { + val accountManager = AccountManager.get(context) + accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)) + .filter { addressBookAccount -> + accountManager.getUserData(addressBookAccount, LocalAddressBook.USER_DATA_ACCOUNT_NAME) == oldAccount.name && + accountManager.getUserData(addressBookAccount, LocalAddressBook.USER_DATA_ACCOUNT_TYPE) == oldAccount.type + } + .forEach { addressBookAccount -> + accountManager.setAndVerifyUserData(addressBookAccount, LocalAddressBook.USER_DATA_ACCOUNT_NAME, newAccount.name) + accountManager.setAndVerifyUserData(addressBookAccount, LocalAddressBook.USER_DATA_ACCOUNT_TYPE, newAccount.type) + } + } + + override fun delete(localCollection: LocalAddressBook) { + val accountManager = AccountManager.get(context) + accountManager.removeAccountExplicitly(localCollection.addressBookAccount) + } + + /** + * Deletes a [LocalAddressBook] based on its corresponding database collection. + * + * @param id [Collection.id] to look for + */ + fun deleteByCollectionId(id: Long) { + val accountManager = AccountManager.get(context) + val addressBookAccount = accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)).firstOrNull { account -> + accountManager.getUserData(account, LocalAddressBook.USER_DATA_COLLECTION_ID)?.toLongOrNull() == id + } + if (addressBookAccount != null) + accountManager.removeAccountExplicitly(addressBookAccount) + } + + /** + * Returns all address book accounts that belong to the given account. + * + * @param account Account which has the address books. + * @return List of address book accounts. + */ + fun getAddressBookAccounts(account: Account): List = + AccountManager.get(context).let { accountManager -> + accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)) + .filter { addressBookAccount -> + account.name == accountManager.getUserData( + addressBookAccount, + LocalAddressBook.USER_DATA_ACCOUNT_NAME + ) && account.type == accountManager.getUserData( + addressBookAccount, + LocalAddressBook.USER_DATA_ACCOUNT_TYPE + ) + } + } + + /** + * Returns all address book accounts that belong to the given account in a flow. + * + * @param account Account which has the address books. + * @return List of address book accounts as flow. + */ + fun getAddressBookAccountsFlow(account: Account): Flow> = callbackFlow { + val accountManager = AccountManager.get(context) + val listener = OnAccountsUpdateListener { accounts -> + trySend(getAddressBookAccounts(account)) + } + accountManager.addOnAccountsUpdatedListener( + /* listener = */ listener, + /* handler = */ null, + /* updateImmediately = */ true + ) + awaitClose { accountManager.removeOnAccountsUpdatedListener(listener) } + } + + + companion object { + + /** + * Contacts Provider Settings (equal for every address book) + */ + val contactsProviderSettings + get() = contentValuesOf( + // SHOULD_SYNC is just a hint that an account's contacts (the contacts of this local address book) are syncable. + ContactsContract.Settings.SHOULD_SYNC to 1, + + // UNGROUPED_VISIBLE is required for making contacts work over Bluetooth (especially with some car systems). + ContactsContract.Settings.UNGROUPED_VISIBLE to 1 + ) + + /** + * Determines whether the address book should be set to read-only. + * + * @param forceAllReadOnly Whether (usually managed, app-wide) setting should overwrite local read-only information + * @param info Collection data to determine read-only status from (either user-set read-only flag or missing write privilege) + */ + @VisibleForTesting + internal fun shouldBeReadOnly(info: Collection, forceAllReadOnly: Boolean): Boolean = + info.readOnly() || forceAllReadOnly + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendar.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendar.kt new file mode 100644 index 0000000..2f0318b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendar.kt @@ -0,0 +1,235 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.content.ContentUris +import android.provider.CalendarContract.Calendars +import android.provider.CalendarContract.Events +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Event +import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter +import at.bitfire.synctools.mapping.calendar.LegacyAndroidEventBuilder2 +import at.bitfire.synctools.storage.BatchOperation +import at.bitfire.synctools.storage.calendar.AndroidCalendar +import at.bitfire.synctools.storage.calendar.AndroidEvent2 +import at.bitfire.synctools.storage.calendar.AndroidRecurringCalendar +import at.bitfire.synctools.storage.calendar.CalendarBatchOperation +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import java.util.LinkedList +import java.util.logging.Logger + +/** + * Application-specific subclass of [AndroidCalendar] for local calendars. + * + * [Calendars._SYNC_ID] corresponds to the database collection ID ([at.bitfire.davdroid.db.Collection.id]). + */ +class LocalCalendar @AssistedInject constructor( + @Assisted internal val androidCalendar: AndroidCalendar, + private val logger: Logger +) : LocalCollection { + + @AssistedFactory + interface Factory { + fun create(calendar: AndroidCalendar): LocalCalendar + } + + + // properties + + override val dbCollectionId: Long? + get() = androidCalendar.syncId?.toLongOrNull() + + override val tag: String + get() = "events-${androidCalendar.account.name}-${androidCalendar.id}" + + override val title: String + get() = androidCalendar.displayName ?: androidCalendar.id.toString() + + override val readOnly + get() = androidCalendar.accessLevel <= Calendars.CAL_ACCESS_READ + + override var lastSyncState: SyncState? + get() = androidCalendar.readSyncState()?.let { + SyncState.fromString(it) + } + set(state) { + androidCalendar.writeSyncState(state.toString()) + } + + private val recurringCalendar = AndroidRecurringCalendar(androidCalendar) + + + fun add(event: Event, fileName: String, eTag: String?, scheduleTag: String?, flags: Int) { + val mapped = LegacyAndroidEventBuilder2( + calendar = androidCalendar, + event = event, + syncId = fileName, + eTag = eTag, + scheduleTag = scheduleTag, + flags = flags + ).build() + recurringCalendar.addEventAndExceptions(mapped) + } + + override fun findDeleted(): List { + val result = LinkedList() + androidCalendar.iterateEvents( "${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NULL", null) { entity -> + result += LocalEvent(recurringCalendar, AndroidEvent2(androidCalendar, entity)) + } + return result + } + + override fun findDirty(): List { + val dirty = LinkedList() + + /* + * RFC 5545 3.8.7.4. Sequence Number + * When a calendar component is created, its sequence number is 0. It is monotonically incremented by the "Organizer's" + * CUA each time the "Organizer" makes a significant revision to the calendar component. + */ + androidCalendar.iterateEvents("${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL", null) { values -> + dirty += LocalEvent(recurringCalendar, AndroidEvent2(androidCalendar, values)) + } + + return dirty + } + + override fun findByName(name: String) = + androidCalendar.findEvent("${Events._SYNC_ID}=? AND ${Events.ORIGINAL_SYNC_ID} IS null", arrayOf(name))?.let { + LocalEvent(recurringCalendar, it) + } + + override fun markNotDirty(flags: Int) = + androidCalendar.updateEventRows( + contentValuesOf(AndroidEvent2.COLUMN_FLAGS to flags), + // `dirty` can be 0, 1, or null. "NOT dirty" is not enough. + """ + ${Events.CALENDAR_ID}=? + AND (${Events.DIRTY} IS NULL OR ${Events.DIRTY}=0) + AND ${Events.ORIGINAL_ID} IS NULL + """.trimIndent(), + arrayOf(androidCalendar.id.toString()) + ) + + override fun removeNotDirtyMarked(flags: Int): Int { + // list all non-dirty events with the given flags and delete every row + its exceptions + val batch = CalendarBatchOperation(androidCalendar.client) + androidCalendar.iterateEventRows( + arrayOf(Events._ID), + // `dirty` can be 0, 1, or null. "NOT dirty" is not enough. + """ + ${Events.CALENDAR_ID}=? + AND (${Events.DIRTY} IS NULL OR ${Events.DIRTY}=0) + AND ${Events.ORIGINAL_ID} IS NULL + AND ${AndroidEvent2.COLUMN_FLAGS}=? + """.trimIndent(), + arrayOf(androidCalendar.id.toString(), flags.toString()) + ) { values -> + val id = values.getAsLong(Events._ID) + + // delete event and possible exceptions (content provider doesn't delete exceptions itself) + batch += BatchOperation.CpoBuilder + .newDelete(androidCalendar.eventsUri) + .withSelection("${Events._ID}=? OR ${Events.ORIGINAL_ID}=?", arrayOf(id.toString(), id.toString())) + } + return batch.commit() + } + + override fun forgetETags() { + androidCalendar.updateEventRows( + contentValuesOf(AndroidEvent2.COLUMN_ETAG to null), + "${Events.CALENDAR_ID}=?", arrayOf(androidCalendar.id.toString()) + ) + } + + + fun processDirtyExceptions() { + // process deleted exceptions + logger.info("Processing deleted exceptions") + + androidCalendar.iterateEventRows( + arrayOf(Events._ID, Events.ORIGINAL_ID, AndroidEvent2.COLUMN_SEQUENCE), + "${Events.CALENDAR_ID}=? AND ${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NOT NULL", + arrayOf(androidCalendar.id.toString()) + ) { values -> + logger.fine("Found deleted exception, removing and re-scheduling original event (if available)") + + val id = values.getAsLong(Events._ID) // can't be null (by definition) + val originalID = values.getAsLong(Events.ORIGINAL_ID) // can't be null (by query) + + val batch = CalendarBatchOperation(androidCalendar.client) + + // enqueue: increase sequence of main event + val originalEventValues = androidCalendar.getEventRow(originalID, arrayOf(AndroidEvent2.COLUMN_SEQUENCE)) + val originalSequence = originalEventValues?.getAsInteger(AndroidEvent2.COLUMN_SEQUENCE) ?: 0 + + batch += BatchOperation.CpoBuilder + .newUpdate(ContentUris.withAppendedId(Events.CONTENT_URI, originalID).asSyncAdapter(androidCalendar.account)) + .withValue(AndroidEvent2.COLUMN_SEQUENCE, originalSequence + 1) + .withValue(Events.DIRTY, 1) + + // completely remove deleted exception + batch += BatchOperation.CpoBuilder.newDelete(ContentUris.withAppendedId(Events.CONTENT_URI, id).asSyncAdapter(androidCalendar.account)) + batch.commit() + } + + // process dirty exceptions + logger.info("Processing dirty exceptions") + androidCalendar.iterateEventRows( + arrayOf(Events._ID, Events.ORIGINAL_ID, AndroidEvent2.COLUMN_SEQUENCE), + "${Events.CALENDAR_ID}=? AND ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NOT NULL", + arrayOf(androidCalendar.id.toString()) + ) { values -> + logger.fine("Found dirty exception, increasing SEQUENCE to re-schedule") + + val id = values.getAsLong(Events._ID) // can't be null (by definition) + val originalID = values.getAsLong(Events.ORIGINAL_ID) // can't be null (by query) + val sequence = values.getAsInteger(AndroidEvent2.COLUMN_SEQUENCE) ?: 0 + + val batch = CalendarBatchOperation(androidCalendar.client) + + // enqueue: set original event to DIRTY + batch += BatchOperation.CpoBuilder + .newUpdate(androidCalendar.eventUri(originalID)) + .withValue(Events.DIRTY, 1) + + // enqueue: increase exception SEQUENCE and set DIRTY to 0 + batch += BatchOperation.CpoBuilder + .newUpdate(androidCalendar.eventUri(id)) + .withValue(AndroidEvent2.COLUMN_SEQUENCE, sequence + 1) + .withValue(Events.DIRTY, 0) + + batch.commit() + } + } + + /** + * Marks dirty events (which are not already marked as deleted) which got no valid instances as "deleted" + * + * @return number of affected events + */ + fun deleteDirtyEventsWithoutInstances() { + // Iterate dirty main events without exceptions + androidCalendar.iterateEventRows( + arrayOf(Events._ID), + "${Events.DIRTY} AND NOT ${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NULL", + null + ) { values -> + val eventId = values.getAsLong(Events._ID) + + // get number of instances + val numEventInstances = androidCalendar.numInstances(eventId) + + // delete event if there are no instances + if (numEventInstances == 0) { + logger.fine("Marking event #$eventId without instances as deleted") + androidCalendar.updateEventRow(eventId, contentValuesOf(Events.DELETED to 1)) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendarStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendarStore.kt new file mode 100644 index 0000000..21eb8ed --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendarStore.kt @@ -0,0 +1,154 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentValues +import android.content.Context +import android.provider.CalendarContract +import android.provider.CalendarContract.Attendees +import android.provider.CalendarContract.Calendars +import android.provider.CalendarContract.Events +import android.provider.CalendarContract.Reminders +import androidx.core.content.contentValuesOf +import at.bitfire.davdroid.Constants +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.util.DavUtils.lastSegment +import at.bitfire.ical4android.util.DateUtils +import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter +import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider +import dagger.hilt.android.qualifiers.ApplicationContext +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject + +class LocalCalendarStore @Inject constructor( + @ApplicationContext private val context: Context, + private val accountSettingsFactory: AccountSettings.Factory, + private val localCalendarFactory: LocalCalendar.Factory, + private val logger: Logger, + private val serviceRepository: DavServiceRepository +): LocalDataStore { + + override val authority: String + get() = CalendarContract.AUTHORITY + + override fun acquireContentProvider(throwOnMissingPermissions: Boolean) = try { + context.contentResolver.acquireContentProviderClient(authority) + } catch (e: SecurityException) { + if (throwOnMissingPermissions) + throw e + else + /* return */ null + } + + override fun create(client: ContentProviderClient, fromCollection: Collection): LocalCalendar? { + val service = serviceRepository.getBlocking(fromCollection.serviceId) ?: throw IllegalArgumentException("Couldn't fetch DB service from collection") + val account = Account(service.accountName, context.getString(R.string.account_type)) + + // If the collection doesn't have a color, use a default color. + val collectionWithColor = + if (fromCollection.color != null) + fromCollection + else + fromCollection.copy(color = Constants.DAVDROID_GREEN_RGBA) + + val values = valuesFromCollectionInfo( + info = collectionWithColor, + withColor = true + ).apply { + // ACCOUNT_NAME and ACCOUNT_TYPE are required (see docs)! If it's missing, other apps will crash. + put(Calendars.ACCOUNT_NAME, account.name) + put(Calendars.ACCOUNT_TYPE, account.type) + + // Email address for scheduling. Used by the calendar provider to determine whether the + // user is ORGANIZER/ATTENDEE for a certain event. + put(Calendars.OWNER_ACCOUNT, account.name) + + // flag as visible & syncable at creation, might be changed by user at any time + put(Calendars.VISIBLE, 1) + put(Calendars.SYNC_EVENTS, 1) + } + + logger.log(Level.INFO, "Adding local calendar", values) + val provider = AndroidCalendarProvider(account, client) + return localCalendarFactory.create(provider.createAndGetCalendar(values)) + } + + override fun getAll(account: Account, client: ContentProviderClient) = + AndroidCalendarProvider(account, client) + .findCalendars("${Calendars.SYNC_EVENTS}!=0", null) + .map { localCalendarFactory.create(it) } + + override fun update(client: ContentProviderClient, localCollection: LocalCalendar, fromCollection: Collection) { + val accountSettings = accountSettingsFactory.create(localCollection.androidCalendar.account) + val values = valuesFromCollectionInfo(fromCollection, withColor = accountSettings.getManageCalendarColors()) + + logger.log(Level.FINE, "Updating local calendar ${fromCollection.url}", values) + val androidCalendar = localCollection.androidCalendar + val provider = AndroidCalendarProvider(androidCalendar.account, client) + provider.updateCalendar(androidCalendar.id, values) + } + + private fun valuesFromCollectionInfo(info: Collection, withColor: Boolean): ContentValues { + val values = contentValuesOf( + Calendars._SYNC_ID to info.id, + Calendars.CALENDAR_DISPLAY_NAME to + if (info.displayName.isNullOrBlank()) info.url.lastSegment else info.displayName, + + Calendars.ALLOWED_AVAILABILITY to arrayOf( + Events.AVAILABILITY_BUSY, + Events.AVAILABILITY_FREE + ).joinToString(",") { it.toString() }, + + Calendars.ALLOWED_ATTENDEE_TYPES to arrayOf( + Attendees.TYPE_NONE, + Attendees.TYPE_OPTIONAL, + Attendees.TYPE_REQUIRED, + Attendees.TYPE_RESOURCE + ).joinToString(",") { it.toString() }, + + Calendars.ALLOWED_REMINDERS to arrayOf( + Reminders.METHOD_DEFAULT, + Reminders.METHOD_ALERT, + Reminders.METHOD_EMAIL + ).joinToString(",") { it.toString() }, + ) + + if (withColor && info.color != null) + values.put(Calendars.CALENDAR_COLOR, info.color) + + if (info.privWriteContent && !info.forceReadOnly) { + values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER) + values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1) + values.put(Calendars.CAN_ORGANIZER_RESPOND, 1) + } else + values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ) + + info.timezoneId?.let { tzId -> + values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(tzId)) + } + + return values + } + + override fun updateAccount(oldAccount: Account, newAccount: Account) { + val values = contentValuesOf(Calendars.ACCOUNT_NAME to newAccount.name) + val uri = Calendars.CONTENT_URI.asSyncAdapter(oldAccount) + context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.use { + it.update(uri, values, "${Calendars.ACCOUNT_NAME}=?", arrayOf(oldAccount.name)) + } + } + + override fun delete(localCollection: LocalCalendar) { + logger.log(Level.INFO, "Deleting local calendar", localCollection) + localCollection.androidCalendar.delete() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCollection.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCollection.kt new file mode 100644 index 0000000..5a4e956 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCollection.kt @@ -0,0 +1,76 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +interface LocalCollection> { + + /** a tag that uniquely identifies the collection (DAVx5-wide) */ + val tag: String + + /** ID of the collection in the database (corresponds to [at.bitfire.davdroid.db.Collection.id]) */ + val dbCollectionId: Long? + + /** collection title (used for user notifications etc.) **/ + val title: String + + var lastSyncState: SyncState? + + /** + * Whether the collection should be treated as read-only on sync. + * Stops uploading dirty events (Server side changes are still downloaded). + */ + val readOnly: Boolean + + /** + * Finds local resources of this collection which have been marked as *deleted* by the user + * or an app acting on their behalf. + * + * @return list of resources marked as *deleted* + */ + fun findDeleted(): List + + /** + * Finds local resources of this collection which have been marked as *dirty*, i.e. resources + * which have been modified by the user or an app acting on their behalf. + * + * @return list of resources marked as *dirty* + */ + fun findDirty(): List + + /** + * Finds a local resource of this collection with a given file name. (File names are assigned + * by the sync adapter.) + * + * @param name file name to look for + * @return resource with the given name, or null if none + */ + fun findByName(name: String): T? + + /** + * Updates the flags value for entries which are not dirty. + * + * @param flags value of flags to set (for instance, [LocalResource.FLAG_REMOTELY_PRESENT]]) + * + * @return number of marked entries + */ + fun markNotDirty(flags: Int): Int + + /** + * Removes entries which are not dirty with a given flag combination. + * + * @param flags exact flags value to remove entries with (for instance, if this is [LocalResource.FLAG_REMOTELY_PRESENT]], + * all entries with exactly this flag will be removed) + * + * @return number of removed entries + */ + fun removeNotDirtyMarked(flags: Int): Int + + + /** + * Forgets the ETags of all members so that they will be reloaded from the server during sync. + */ + fun forgetETags() + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalContact.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalContact.kt new file mode 100644 index 0000000..6f7d2b9 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalContact.kt @@ -0,0 +1,239 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import android.os.RemoteException +import android.provider.ContactsContract +import android.provider.ContactsContract.CommonDataKinds.GroupMembership +import android.provider.ContactsContract.RawContacts +import android.provider.ContactsContract.RawContacts.Data +import android.provider.ContactsContract.RawContacts.getContactLookupUri +import androidx.core.content.contentValuesOf +import at.bitfire.davdroid.resource.contactrow.CachedGroupMembershipHandler +import at.bitfire.davdroid.resource.contactrow.GroupMembershipBuilder +import at.bitfire.davdroid.resource.contactrow.GroupMembershipHandler +import at.bitfire.davdroid.resource.contactrow.UnknownPropertiesBuilder +import at.bitfire.davdroid.resource.contactrow.UnknownPropertiesHandler +import at.bitfire.synctools.storage.BatchOperation +import at.bitfire.synctools.storage.ContactsBatchOperation +import at.bitfire.vcard4android.AndroidAddressBook +import at.bitfire.vcard4android.AndroidContact +import at.bitfire.vcard4android.AndroidContactFactory +import at.bitfire.vcard4android.CachedGroupMembership +import at.bitfire.vcard4android.Contact +import com.google.common.base.Ascii +import com.google.common.base.MoreObjects +import java.io.FileNotFoundException +import java.util.Optional +import java.util.UUID +import kotlin.jvm.optionals.getOrNull + +class LocalContact: AndroidContact, LocalAddress { + + companion object { + const val COLUMN_FLAGS = RawContacts.SYNC4 + const val COLUMN_HASHCODE = RawContacts.SYNC3 + } + + override val addressBook: LocalAddressBook + get() = super.addressBook as LocalAddressBook + + internal val cachedGroupMemberships = HashSet() + internal val groupMemberships = HashSet() + + override val scheduleTag: String? + get() = null + + override var flags: Int = 0 + + + constructor(addressBook: LocalAddressBook, values: ContentValues): super(addressBook, values) { + flags = values.getAsInteger(COLUMN_FLAGS) ?: 0 + } + + constructor(addressBook: LocalAddressBook, contact: Contact, fileName: String?, eTag: String?, _flags: Int): super(addressBook, contact, fileName, eTag) { + flags = _flags + } + + init { + processor.registerHandler(CachedGroupMembershipHandler(this)) + processor.registerHandler(GroupMembershipHandler(this)) + processor.registerHandler(UnknownPropertiesHandler) + processor.registerBuilderFactory(GroupMembershipBuilder.Factory(addressBook)) + processor.registerBuilderFactory(UnknownPropertiesBuilder.Factory) + } + + + override fun prepareForUpload(): String { + val contact = getContact() + val uid: String = contact.uid ?: run { + // generate new UID + val newUid = UUID.randomUUID().toString() + + // update in contacts provider + val values = contentValuesOf(COLUMN_UID to newUid) + addressBook.provider!!.update(rawContactSyncURI(), values, null, null) + + // update this event + contact.uid = newUid + + newUid + } + + return "$uid.vcf" + } + + /** + * Clears cached [contact] so that the next read of [contact] will query the content provider again. + */ + fun clearCachedContact() { + _contact = null + } + + override fun clearDirty(fileName: Optional, eTag: String?, scheduleTag: String?) { + if (scheduleTag != null) + throw IllegalArgumentException("Contacts must not have a Schedule-Tag") + + val values = ContentValues(4) + if (fileName.isPresent) + values.put(COLUMN_FILENAME, fileName.get()) + values.put(COLUMN_ETAG, eTag) + values.put(RawContacts.DIRTY, 0) + + // Android 7 workaround + addressBook.dirtyVerifier.getOrNull()?.setHashCodeColumn(this, values) + + addressBook.provider!!.update(rawContactSyncURI(), values, null, null) + + if (fileName.isPresent) + this.fileName = fileName.get() + this.eTag = eTag + } + + fun resetDirty() { + val values = contentValuesOf(RawContacts.DIRTY to 0) + addressBook.provider!!.update(rawContactSyncURI(), values, null, null) + } + + override fun update(data: Contact, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) { + this.fileName = fileName + this.eTag = eTag + this.flags = flags + + // processes this.{fileName, eTag, flags} and resets DIRTY flag + update(data) + } + + override fun updateFlags(flags: Int) { + val values = contentValuesOf(COLUMN_FLAGS to flags) + addressBook.provider!!.update(rawContactSyncURI(), values, null, null) + + this.flags = flags + } + + override fun deleteLocal() { + delete() + } + + override fun resetDeleted() { + val values = contentValuesOf(ContactsContract.Groups.DELETED to 0) + addressBook.provider!!.update(rawContactSyncURI(), values, null, null) + } + + override fun getDebugSummary() = + MoreObjects.toStringHelper(this) + .add("id", id) + .add("fileName", fileName) + .add("eTag", eTag) + .add("flags", flags) + .add("contact", + try { + Ascii.truncate(getContact().toString(), 1000, "…") + } catch (e: Exception) { + e + } + ).toString() + + override fun getViewUri(context: Context): Uri? = + id?.let { idNotNull -> + getContactLookupUri( + context.contentResolver, + ContentUris.withAppendedId(RawContacts.CONTENT_URI, idNotNull) + ) + } + + + fun addToGroup(batch: ContactsBatchOperation, groupID: Long) { + batch += BatchOperation.CpoBuilder + .newInsert(dataSyncURI()) + .withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE) + .withValue(GroupMembership.RAW_CONTACT_ID, id) + .withValue(GroupMembership.GROUP_ROW_ID, groupID) + groupMemberships += groupID + + batch += BatchOperation.CpoBuilder + .newInsert(dataSyncURI()) + .withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE) + .withValue(CachedGroupMembership.RAW_CONTACT_ID, id) + .withValue(CachedGroupMembership.GROUP_ID, groupID) + cachedGroupMemberships += groupID + } + + fun removeGroupMemberships(batch: BatchOperation) { + batch += BatchOperation.CpoBuilder + .newDelete(dataSyncURI()) + .withSelection( + "${Data.RAW_CONTACT_ID}=? AND ${Data.MIMETYPE} IN (?,?)", + arrayOf(id.toString(), GroupMembership.CONTENT_ITEM_TYPE, CachedGroupMembership.CONTENT_ITEM_TYPE) + ) + groupMemberships.clear() + cachedGroupMemberships.clear() + } + + /** + * Returns the IDs of all groups the contact was member of (cached memberships). + * Cached memberships are kept in sync with memberships by DAVx5 and are used to determine + * whether a membership has been deleted/added when a raw contact is dirty. + * @return set of {@link GroupMembership#GROUP_ROW_ID} (may be empty) + * @throws FileNotFoundException if the current contact can't be found + * @throws RemoteException on contacts provider errors + */ + fun getCachedGroupMemberships(): Set { + getContact() + return cachedGroupMemberships + } + + /** + * Returns the IDs of all groups the contact is member of. + * @return set of {@link GroupMembership#GROUP_ROW_ID}s (may be empty) + * @throws FileNotFoundException if the current contact can't be found + * @throws RemoteException on contacts provider errors + */ + fun getGroupMemberships(): Set { + getContact() + return groupMemberships + } + + + // data rows + + override fun buildContact(builder: BatchOperation.CpoBuilder, update: Boolean) { + builder.withValue(COLUMN_FLAGS, flags) + super.buildContact(builder, update) + } + + + // factory + + object Factory: AndroidContactFactory { + override fun fromProvider(addressBook: AndroidAddressBook, values: ContentValues) = + LocalContact(addressBook as LocalAddressBook, values) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalDataStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalDataStore.kt new file mode 100644 index 0000000..8e8a7e4 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalDataStore.kt @@ -0,0 +1,82 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.accounts.Account +import android.content.ContentProviderClient +import at.bitfire.davdroid.db.Collection + +/** + * Represents a local data store for a specific collection type. + * Manages creation, update, and deletion of collections of the given type. + */ +interface LocalDataStore> { + + /** + * Content provider authority for the data store. + */ + val authority: String + + /** + * Acquires a content provider client for the data store. The result of this call + * should be passed to all other methods of this class. + * + * **The caller is responsible for closing the content provider client!** + * + * @param throwOnMissingPermissions If `true`, the function will throw [SecurityException] if permissions are not granted. + * + * @return the content provider client, or `null` if the content provider could not be acquired (or permissions are not + * granted and [throwOnMissingPermissions] is `false`) + * + * @throws SecurityException on missing permissions + */ + fun acquireContentProvider(throwOnMissingPermissions: Boolean = false): ContentProviderClient? + + /** + * Creates a new local collection from the given (remote) collection info. + * + * @param client the content provider client + * @param fromCollection collection info + * + * @return the new local collection, or `null` if creation failed + */ + fun create(client: ContentProviderClient, fromCollection: Collection): T? + + /** + * Returns all local collections of the data store, including those which don't have a corresponding remote + * [Collection] entry. + * + * @param account the account that the data store is associated with + * @param client the content provider client + * + * @return a list of all local collections + */ + fun getAll(account: Account, client: ContentProviderClient): List + + /** + * Updates the local collection with the data from the given (remote) collection info. + * + * @param client the content provider client + * @param localCollection the local collection to update + * @param fromCollection collection info + */ + fun update(client: ContentProviderClient, localCollection: T, fromCollection: Collection) + + /** + * Deletes the local collection. + * + * @param localCollection the local collection to delete + */ + fun delete(localCollection: T) + + /** + * Changes the account assigned to the containing data to another one. + * + * @param oldAccount The old account. + * @param newAccount The new account. + */ + fun updateAccount(oldAccount: Account, newAccount: Account) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalEvent.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalEvent.kt new file mode 100644 index 0000000..549f998 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalEvent.kt @@ -0,0 +1,197 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.content.ContentUris +import android.content.Context +import android.provider.CalendarContract +import android.provider.CalendarContract.Events +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Event +import at.bitfire.ical4android.LegacyAndroidCalendar +import at.bitfire.synctools.mapping.calendar.LegacyAndroidEventBuilder2 +import at.bitfire.synctools.storage.LocalStorageException +import at.bitfire.synctools.storage.calendar.AndroidEvent2 +import at.bitfire.synctools.storage.calendar.AndroidRecurringCalendar +import com.google.common.base.Ascii +import com.google.common.base.MoreObjects +import java.util.Optional +import java.util.UUID + +class LocalEvent( + val recurringCalendar: AndroidRecurringCalendar, + val androidEvent: AndroidEvent2 +) : LocalResource { + + override val id: Long + get() = androidEvent.id + + override val fileName: String? + get() = androidEvent.syncId + + override val eTag: String? + get() = androidEvent.eTag + + override val scheduleTag: String? + get() = androidEvent.scheduleTag + + override val flags: Int + get() = androidEvent.flags + + + override fun update(data: Event, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) { + val eventAndExceptions = LegacyAndroidEventBuilder2( + calendar = androidEvent.calendar, + event = data, + syncId = fileName, + eTag = eTag, + scheduleTag = scheduleTag, + flags = flags + ).build() + recurringCalendar.updateEventAndExceptions(id, eventAndExceptions) + } + + + private var _event: Event? = null + /** + * Retrieves the event from the content provider and converts it to a legacy data object. + * + * Caches the result: the content provider is only queried at the first call and then + * this method always returns the same object. + * + * @throws LocalStorageException if there is no local event with the ID from [androidEvent] + */ + @Synchronized + fun getCachedEvent(): Event { + _event?.let { return it } + + val legacyCalendar = LegacyAndroidCalendar(androidEvent.calendar) + val event = legacyCalendar.getEvent(androidEvent.id) + ?: throw LocalStorageException("Event ${androidEvent.id} not found") + + _event = event + return event + } + + /** + * Generates the [Event] that should actually be uploaded: + * + * 1. Takes the [getCachedEvent]. + * 2. Calculates the new SEQUENCE. + * + * _Note: This method currently modifies the object returned by [getCachedEvent], but + * this may change in the future._ + * + * @return data object that should be used for uploading + */ + fun eventToUpload(): Event { + val event = getCachedEvent() + + val nonGroupScheduled = event.attendees.isEmpty() + val weAreOrganizer = event.isOrganizer == true + + // Increase sequence (event.sequence null/non-null behavior is defined by the Event, see KDoc of event.sequence): + // - If it's null, the event has just been created in the database, so we can start with SEQUENCE:0 (default). + // - If it's non-null, the event already exists on the server, so increase by one. + val sequence = event.sequence + if (sequence != null && (nonGroupScheduled || weAreOrganizer)) + event.sequence = sequence + 1 + + return event + } + + /** + * Updates the SEQUENCE of the event in the content provider. + * + * @param sequence new sequence value + */ + fun updateSequence(sequence: Int?) { + androidEvent.update(contentValuesOf( + AndroidEvent2.COLUMN_SEQUENCE to sequence + )) + } + + + /** + * Creates and sets a new UID in the calendar provider, if no UID is already set. + * It also returns the desired file name for the event for further processing in the sync algorithm. + * + * @return file name to use at upload + */ + override fun prepareForUpload(): String { + // make sure that UID is set + val uid: String = getCachedEvent().uid ?: run { + // generate new UID + val newUid = UUID.randomUUID().toString() + + // persist to calendar provider + val values = contentValuesOf(Events.UID_2445 to newUid) + androidEvent.update(values) + + // update in cached event data object + getCachedEvent().uid = newUid + + newUid + } + + val uidIsGoodFilename = uid.all { char -> + // see RFC 2396 2.2 + char.isLetterOrDigit() || arrayOf( // allow letters and digits + ';', ':', '@', '&', '=', '+', '$', ',', // allow reserved characters except '/' and '?' + '-', '_', '.', '!', '~', '*', '\'', '(', ')' // allow unreserved characters + ).contains(char) + } + return if (uidIsGoodFilename) + "$uid.ics" // use UID as file name + else + "${UUID.randomUUID()}.ics" // UID would be dangerous as file name, use random UUID instead + } + + override fun clearDirty(fileName: Optional, eTag: String?, scheduleTag: String?) { + val values = contentValuesOf( + Events.DIRTY to 0, + AndroidEvent2.COLUMN_ETAG to eTag, + AndroidEvent2.COLUMN_SCHEDULE_TAG to scheduleTag + ) + if (fileName.isPresent) + values.put(Events._SYNC_ID, fileName.get()) + androidEvent.update(values) + } + + override fun updateFlags(flags: Int) { + androidEvent.update(contentValuesOf( + AndroidEvent2.COLUMN_FLAGS to flags + )) + } + + override fun deleteLocal() { + recurringCalendar.deleteEventAndExceptions(id) + } + + override fun resetDeleted() { + androidEvent.update(contentValuesOf( + Events.DELETED to 0 + )) + } + + override fun getDebugSummary() = + MoreObjects.toStringHelper(this) + .add("id", id) + .add("fileName", fileName) + .add("eTag", eTag) + .add("scheduleTag", scheduleTag) + .add("flags", flags) + .add("event", + try { + Ascii.truncate(getCachedEvent().toString(), 1000, "…") + } catch (e: Exception) { + e + } + ).toString() + + override fun getViewUri(context: Context) = + ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalGroup.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalGroup.kt new file mode 100644 index 0000000..9413c2a --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalGroup.kt @@ -0,0 +1,313 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import android.os.RemoteException +import android.provider.ContactsContract +import android.provider.ContactsContract.CommonDataKinds.GroupMembership +import android.provider.ContactsContract.Groups +import android.provider.ContactsContract.RawContacts +import android.provider.ContactsContract.RawContacts.Data +import androidx.core.content.contentValuesOf +import at.bitfire.davdroid.resource.LocalGroup.Companion.COLUMN_PENDING_MEMBERS +import at.bitfire.davdroid.util.trimToNull +import at.bitfire.synctools.storage.BatchOperation +import at.bitfire.synctools.storage.ContactsBatchOperation +import at.bitfire.vcard4android.AndroidAddressBook +import at.bitfire.vcard4android.AndroidContact +import at.bitfire.vcard4android.AndroidGroup +import at.bitfire.vcard4android.AndroidGroupFactory +import at.bitfire.vcard4android.CachedGroupMembership +import at.bitfire.vcard4android.Contact +import com.google.common.base.MoreObjects +import java.util.LinkedList +import java.util.Optional +import java.util.UUID +import java.util.logging.Logger +import kotlin.jvm.optionals.getOrNull + +class LocalGroup: AndroidGroup, LocalAddress { + + companion object { + + private val logger: Logger + get() = Logger.getGlobal() + + const val COLUMN_FLAGS = Groups.SYNC4 + + /** List of member UIDs, as sent by server. This list will be used to establish + * the group memberships when all groups and contacts have been synchronized. + * Use [PendingMemberships] to create/read the list. */ + const val COLUMN_PENDING_MEMBERS = Groups.SYNC3 + + /** + * Processes all groups with non-null [COLUMN_PENDING_MEMBERS]: the pending memberships + * are applied (if possible) to keep cached memberships in sync. + * + * @param addressBook address book to take groups from + */ + fun applyPendingMemberships(addressBook: LocalAddressBook) { + logger.info("Assigning memberships of contact groups") + + addressBook.allGroups { group -> + val groupId = group.id!! + val pendingMemberUids = group.pendingMemberships.toMutableSet() + val batch = ContactsBatchOperation(addressBook.provider!!) + + // required for workaround for Android 7 which sets DIRTY flag when only meta-data is changed + val changeContactIDs = HashSet() + + // process members which are currently in this group, but shouldn't be + for (currentMemberId in addressBook.getContactIdsByGroupMembership(groupId)) { + val uid = addressBook.getContactUidFromId(currentMemberId) ?: continue + + if (!pendingMemberUids.contains(uid)) { + logger.fine("$currentMemberId removed from group $groupId; removing group membership") + val currentMember = addressBook.findContactById(currentMemberId) + currentMember.removeGroupMemberships(batch) + + // Android 7 hack + changeContactIDs += currentMemberId + } + + // UID is processed, remove from pendingMembers + pendingMemberUids -= uid + } + // now pendingMemberUids contains all UIDs which are not assigned yet + + // process members which should be in this group, but aren't + for (missingMemberUid in pendingMemberUids) { + val missingMember = addressBook.findContactByUid(missingMemberUid) + if (missingMember == null) { + logger.warning("Group $groupId has member $missingMemberUid which is not found in the address book; ignoring") + continue + } + + logger.fine("Assigning member $missingMember to group $groupId") + missingMember.addToGroup(batch, groupId) + + // Android 7 hack + changeContactIDs += missingMember.id!! + } + + addressBook.dirtyVerifier.getOrNull()?.let { verifier -> + // workaround for Android 7 which sets DIRTY flag when only meta-data is changed + changeContactIDs + .map { id -> addressBook.findContactById(id) } + .forEach { contact -> + verifier.updateHashCode(contact, batch) + } + } + + batch.commit() + } + } + + } + + + override var scheduleTag: String? + get() = null + set(_) = throw NotImplementedError() + + override var flags: Int = 0 + + var pendingMemberships = setOf() + + + constructor(addressBook: AndroidAddressBook, values: ContentValues) : super(addressBook, values) { + flags = values.getAsInteger(COLUMN_FLAGS) ?: 0 + values.getAsString(COLUMN_PENDING_MEMBERS)?.let { members -> + pendingMemberships = PendingMemberships.fromString(members).uids + } + } + + constructor(addressBook: AndroidAddressBook, contact: Contact, fileName: String?, eTag: String?, flags: Int) + : super(addressBook, contact, fileName, eTag) { + this.flags = flags + } + + + override fun contentValues(): ContentValues { + val values = super.contentValues() + values.put(COLUMN_FLAGS, flags) + values.put(COLUMN_PENDING_MEMBERS, PendingMemberships(getContact().members).toString()) + return values + } + + + override fun prepareForUpload(): String { + var uid: String? = null + addressBook.provider!!.query(groupSyncUri(), arrayOf(AndroidContact.COLUMN_UID), null, null, null)?.use { cursor -> + if (cursor.moveToNext()) + uid = cursor.getString(0).trimToNull() + } + + if (uid == null) { + // generate new UID + uid = UUID.randomUUID().toString() + + val values = contentValuesOf(AndroidContact.COLUMN_UID to uid) + addressBook.provider!!.update(groupSyncUri(), values, null, null) + + _contact?.uid = uid + } + + return "$uid.vcf" + } + + override fun clearDirty(fileName: Optional, eTag: String?, scheduleTag: String?) { + if (scheduleTag != null) + throw IllegalArgumentException("Contact groups must not have a Schedule-Tag") + val id = requireNotNull(id) + + val values = ContentValues(3) + if (fileName.isPresent) + values.put(COLUMN_FILENAME, fileName.get()) + values.putNull(COLUMN_ETAG) // don't save changed ETag but null, so that the group is downloaded again, so that pendingMembers is updated + values.put(Groups.DIRTY, 0) + update(values) + + if (fileName.isPresent) + this.fileName = fileName.get() + this.eTag = null + + // update cached group memberships + val batch = ContactsBatchOperation(addressBook.provider!!) + + // delete old cached group memberships + batch += BatchOperation.CpoBuilder + .newDelete(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI)) + .withSelection( + CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?", + arrayOf(CachedGroupMembership.CONTENT_ITEM_TYPE, id.toString()) + ) + + // insert updated cached group memberships + for (member in getMembers()) + batch += BatchOperation.CpoBuilder + .newInsert(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI)) + .withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE) + .withValue(CachedGroupMembership.RAW_CONTACT_ID, member) + .withValue(CachedGroupMembership.GROUP_ID, id) + + batch.commit() + } + + /** + * Marks all members of the current group as dirty. + */ + fun markMembersDirty() { + val batch = ContactsBatchOperation(addressBook.provider!!) + + for (member in getMembers()) + batch += BatchOperation.CpoBuilder + .newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, member))) + .withValue(RawContacts.DIRTY, 1) + + batch.commit() + } + + override fun update(data: Contact, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) { + this.fileName = fileName + this.eTag = eTag + + // processes this.{fileName, eTag, flags} and resets DIRTY flag + update(data) + } + + override fun updateFlags(flags: Int) { + val values = contentValuesOf(COLUMN_FLAGS to flags) + addressBook.provider!!.update(groupSyncUri(), values, null, null) + + this.flags = flags + } + + override fun deleteLocal() { + delete() + } + + override fun resetDeleted() { + val values = contentValuesOf(Groups.DELETED to 0) + addressBook.provider!!.update(groupSyncUri(), values, null, null) + } + + override fun getDebugSummary() = + MoreObjects.toStringHelper(this) + .add("id", id) + .add("fileName", fileName) + .add("eTag", eTag) + .add("flags", flags) + .add("contact", + try { + getContact().toString() + } catch (e: Exception) { + e + } + ).toString() + + override fun getViewUri(context: Context) = null + + + // helpers + + private fun groupSyncUri(): Uri { + val id = requireNotNull(id) + return ContentUris.withAppendedId(addressBook.groupsSyncUri(), id) + } + + /** + * Lists all members of this group. + * @return list of all members' raw contact IDs + * @throws RemoteException on contact provider errors + */ + internal fun getMembers(): List { + val id = requireNotNull(id) + val members = LinkedList() + addressBook.provider!!.query( + addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI), + arrayOf(Data.RAW_CONTACT_ID), + "${GroupMembership.MIMETYPE}=? AND ${GroupMembership.GROUP_ROW_ID}=?", + arrayOf(GroupMembership.CONTENT_ITEM_TYPE, id.toString()), + null + )?.use { cursor -> + while (cursor.moveToNext()) + members += cursor.getLong(0) + } + return members + } + + + // helper class for COLUMN_PENDING_MEMBERSHIPS blob + + class PendingMemberships( + /** list of member UIDs that shall be assigned **/ + val uids: Set + ) { + + companion object { + const val SEPARATOR = '\n' + + fun fromString(value: String) = + PendingMemberships(value.split(SEPARATOR).toSet()) + } + + override fun toString() = uids.joinToString(SEPARATOR.toString()) + + } + + + // factory + + object Factory: AndroidGroupFactory { + override fun fromProvider(addressBook: AndroidAddressBook, values: ContentValues) = + LocalGroup(addressBook, values) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollection.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollection.kt new file mode 100644 index 0000000..970bf7b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollection.kt @@ -0,0 +1,84 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.accounts.Account +import android.content.ContentProviderClient +import at.bitfire.ical4android.JtxCollection +import at.bitfire.ical4android.JtxCollectionFactory +import at.bitfire.ical4android.JtxICalObject + +/** + * Application-specific implementation for jtx collections. + * + * [at.techbee.jtx.JtxContract.JtxCollection.SYNC_ID] corresponds to the database collection ID ([at.bitfire.davdroid.db.Collection.id]). + */ +class LocalJtxCollection(account: Account, client: ContentProviderClient, id: Long): + JtxCollection(account, client, LocalJtxICalObject.Factory, id), + LocalCollection{ + + override val readOnly: Boolean + get() = throw NotImplementedError() + + override val tag: String + get() = "jtx-${account.name}-$id" + + override val dbCollectionId: Long? + get() = syncId + + override val title: String + get() = displayname ?: id.toString() + + override var lastSyncState: SyncState? + get() = SyncState.fromString(syncstate) + set(value) { syncstate = value.toString() } + + + override fun findDeleted(): List { + val values = queryDeletedICalObjects() + val localJtxICalObjects = mutableListOf() + values.forEach { + localJtxICalObjects.add(LocalJtxICalObject.Factory.fromProvider(this, it)) + } + return localJtxICalObjects + } + + override fun findDirty(): List { + val values = queryDirtyICalObjects() + val localJtxICalObjects = mutableListOf() + values.forEach { + localJtxICalObjects.add(LocalJtxICalObject.Factory.fromProvider(this, it)) + } + return localJtxICalObjects + } + + override fun findByName(name: String): LocalJtxICalObject? { + val values = queryByFilename(name) ?: return null + return LocalJtxICalObject.Factory.fromProvider(this, values) + } + + /** + * Finds and returns a recurrence instance of a [LocalJtxICalObject] + * @param uid UID of the main VTODO + * @param recurid RECURRENCE-ID of the recurrence instance + * @return LocalJtxICalObject or null if none or multiple entries found + */ + fun findRecurInstance(uid: String, recurid: String): LocalJtxICalObject? { + val values = queryRecur(uid, recurid) ?: return null + return LocalJtxICalObject.Factory.fromProvider(this, values) + } + + override fun markNotDirty(flags: Int)= updateSetFlags(flags) + + override fun removeNotDirtyMarked(flags: Int) = deleteByFlags(flags) + + override fun forgetETags() = updateSetETag(null) + + + object Factory: JtxCollectionFactory { + override fun newInstance(account: Account, client: ContentProviderClient, id: Long) = LocalJtxCollection(account, client, id) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollectionStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollectionStore.kt new file mode 100644 index 0000000..856af2d --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollectionStore.kt @@ -0,0 +1,118 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import androidx.core.content.contentValuesOf +import at.bitfire.davdroid.Constants +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.repository.PrincipalRepository +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.util.DavUtils.lastSegment +import at.bitfire.ical4android.JtxCollection +import at.bitfire.ical4android.TaskProvider +import at.techbee.jtx.JtxContract +import at.techbee.jtx.JtxContract.asSyncAdapter +import dagger.hilt.android.qualifiers.ApplicationContext +import java.util.logging.Logger +import javax.inject.Inject + +class LocalJtxCollectionStore @Inject constructor( + @ApplicationContext val context: Context, + val accountSettingsFactory: AccountSettings.Factory, + db: AppDatabase, + val principalRepository: PrincipalRepository +): LocalDataStore { + + private val serviceDao = db.serviceDao() + + override val authority: String + get() = JtxContract.AUTHORITY + + override fun acquireContentProvider(throwOnMissingPermissions: Boolean) = try { + context.contentResolver.acquireContentProviderClient(authority) + } catch (e: SecurityException) { + if (throwOnMissingPermissions) + throw e + else + /* return */ null + } + + override fun create(provider: ContentProviderClient, fromCollection: Collection): LocalJtxCollection? { + val service = serviceDao.get(fromCollection.serviceId) ?: throw IllegalArgumentException("Couldn't fetch DB service from collection") + val account = Account(service.accountName, context.getString(R.string.account_type)) + + // If the collection doesn't have a color, use a default color. + val collectionWithColor = + if (fromCollection.color != null) + fromCollection + else + fromCollection.copy(color = Constants.DAVDROID_GREEN_RGBA) + + val values = valuesFromCollection( + info = collectionWithColor, + account = account, + withColor = true + ) + + val uri = JtxCollection.create(account, provider, values) + return LocalJtxCollection(account, provider, ContentUris.parseId(uri)) + } + + private fun valuesFromCollection(info: Collection, account: Account, withColor: Boolean): ContentValues { + val owner = info.ownerId?.let { principalRepository.getBlocking(it) } + + return ContentValues().apply { + put(JtxContract.JtxCollection.SYNC_ID, info.id) + put(JtxContract.JtxCollection.URL, info.url.toString()) + put( + JtxContract.JtxCollection.DISPLAYNAME, + info.displayName ?: info.url.lastSegment + ) + put(JtxContract.JtxCollection.DESCRIPTION, info.description) + if (owner != null) + put(JtxContract.JtxCollection.OWNER, owner.url.toString()) + else + Logger.getGlobal().warning("No collection owner given. Will create jtx collection without owner") + put(JtxContract.JtxCollection.OWNER_DISPLAYNAME, owner?.displayName) + if (withColor && info.color != null) + put(JtxContract.JtxCollection.COLOR, info.color) + put(JtxContract.JtxCollection.SUPPORTSVEVENT, info.supportsVEVENT) + put(JtxContract.JtxCollection.SUPPORTSVJOURNAL, info.supportsVJOURNAL) + put(JtxContract.JtxCollection.SUPPORTSVTODO, info.supportsVTODO) + put(JtxContract.JtxCollection.ACCOUNT_NAME, account.name) + put(JtxContract.JtxCollection.ACCOUNT_TYPE, account.type) + put(JtxContract.JtxCollection.READONLY, info.forceReadOnly || !info.privWriteContent) + } + } + + override fun getAll(account: Account, provider: ContentProviderClient): List = + JtxCollection.find(account, provider, context, LocalJtxCollection.Factory, null, null) + + override fun update(provider: ContentProviderClient, localCollection: LocalJtxCollection, fromCollection: Collection) { + val accountSettings = accountSettingsFactory.create(localCollection.account) + val values = valuesFromCollection(fromCollection, account = localCollection.account, withColor = accountSettings.getManageCalendarColors()) + localCollection.update(values) + } + + override fun updateAccount(oldAccount: Account, newAccount: Account) { + TaskProvider.acquire(context, TaskProvider.ProviderName.JtxBoard)?.use { provider -> + val values = contentValuesOf(JtxContract.JtxCollection.ACCOUNT_NAME to newAccount.name) + val uri = JtxContract.JtxCollection.CONTENT_URI.asSyncAdapter(oldAccount) + provider.client.update(uri, values, "${JtxContract.JtxCollection.ACCOUNT_NAME}=?", arrayOf(oldAccount.name)) + } + } + + override fun delete(localCollection: LocalJtxCollection) { + localCollection.delete() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxICalObject.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxICalObject.kt new file mode 100644 index 0000000..e7c1606 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxICalObject.kt @@ -0,0 +1,88 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.content.ContentValues +import android.content.Context +import at.bitfire.ical4android.JtxCollection +import at.bitfire.ical4android.JtxICalObject +import at.bitfire.ical4android.JtxICalObjectFactory +import at.techbee.jtx.JtxContract +import com.google.common.base.MoreObjects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class LocalJtxICalObject( + collection: JtxCollection<*>, + fileName: String?, + eTag: String?, + scheduleTag: String?, + flags: Int +) : + JtxICalObject(collection), + LocalResource { + + + init { + this.fileName = fileName + this.eTag = eTag + this.flags = flags + this.scheduleTag = scheduleTag + } + + + object Factory : JtxICalObjectFactory { + + override fun fromProvider( + collection: JtxCollection, + values: ContentValues + ): LocalJtxICalObject { + val fileName = values.getAsString(JtxContract.JtxICalObject.FILENAME) + val eTag = values.getAsString(JtxContract.JtxICalObject.ETAG) + val scheduleTag = values.getAsString(JtxContract.JtxICalObject.SCHEDULETAG) + val flags = values.getAsInteger(JtxContract.JtxICalObject.FLAGS)?: 0 + + val localJtxICalObject = LocalJtxICalObject(collection, fileName, eTag, scheduleTag, flags) + localJtxICalObject.populateFromContentValues(values) + + return localJtxICalObject + } + + } + + override fun update(data: JtxICalObject, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) { + this.fileName = fileName + this.eTag = eTag + this.scheduleTag = scheduleTag + this.flags = flags + + // processes this.{fileName, eTag, scheduleTag, flags} and resets DIRTY flag + update(data) + } + + override fun clearDirty(fileName: Optional, eTag: String?, scheduleTag: String?) { + clearDirty(fileName.getOrNull(), eTag, scheduleTag) + } + + override fun deleteLocal() { + delete() + } + + override fun resetDeleted() { + throw NotImplementedError() + } + + override fun getDebugSummary() = + MoreObjects.toStringHelper(this) + .add("id", id) + .add("fileName", fileName) + .add("eTag", eTag) + .add("scheduleTag", scheduleTag) + .add("flags", flags) + .toString() + + override fun getViewUri(context: Context) = null + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalResource.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalResource.kt new file mode 100644 index 0000000..e57b108 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalResource.kt @@ -0,0 +1,115 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.content.Context +import android.content.Intent +import android.net.Uri +import at.bitfire.davdroid.resource.LocalResource.Companion.FLAG_REMOTELY_PRESENT +import java.util.Optional + +/** + * Defines operations that are used by SyncManager for all sync data types. + */ +interface LocalResource { + + companion object { + /** + * Resource is present on remote server. This flag is used to identify resources + * which are not present on the remote server anymore and can be deleted at the end + * of the synchronization. + */ + const val FLAG_REMOTELY_PRESENT = 1 + } + + + /** + * Unique ID which identifies the resource in the local storage. May be null if the + * resource has not been saved yet. + */ + val id: Long? + + /** + * Remote file name for the resource, for instance `mycontact.vcf`. Also used to determine whether + * a dirty record has just been created (in this case, [fileName] is *null*) or modified + * (in this case, [fileName] is the remote file name). + */ + val fileName: String? + + /** remote ETag for the resource */ + val eTag: String? + + /** remote Schedule-Tag for the resource */ + val scheduleTag: String? + + /** bitfield of flags; currently either [FLAG_REMOTELY_PRESENT] or 0 */ + val flags: Int + + /** + * Prepares the resource for uploading: + * + * 1. If the resource doesn't have an UID yet, this method generates one and writes it to the content provider. + * 2. The new file name which can be used for the upload is derived from the UID and returned, but not + * saved to the content provider. The sync manager is responsible for saving the file name that + * was actually used. + * + * @return suggestion for new file name of the resource (like ".vcf") + */ + fun prepareForUpload(): String + + /** + * Unsets the _dirty_ field of the resource and updates other sync-related fields in the content provider. + * Does not affect `this` object itself (which is immutable). + * + * @param fileName If this optional argument is present, [LocalResource.fileName] will be set to its value. + * @param eTag ETag of the uploaded resource as returned by the server (null if the server didn't return one) + * @param scheduleTag CalDAV only: `Schedule-Tag` of the uploaded resource as returned by the server + * (null if not applicable or if the server didn't return one) + */ + fun clearDirty(fileName: Optional, eTag: String?, scheduleTag: String? = null) + + /** + * Sets (local) flags of the resource in the content provider. + * Does not affect `this` object itself (which is immutable). + * + * At the moment, the only allowed values are 0 and [FLAG_REMOTELY_PRESENT]. + */ + fun updateFlags(flags: Int) + + /** + * Updates the data object in the content provider and ensures that the dirty flag is clear. + * Does not affect `this` or the [data] object (which are both immutable). + * + * @return content URI of the updated row (e.g. event URI) + */ + fun update(data: TData, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) + + /** + * Deletes the data object from the content provider. + */ + fun deleteLocal() + + /** + * Undoes deletion of the data object from the content provider. + */ + fun resetDeleted() + + /** + * User-readable debug summary of this local resource (used in debug info) + */ + fun getDebugSummary(): String + + /** + * Returns the content provider URI that opens the local resource for viewing ([Intent.ACTION_VIEW]) + * in its respective app. + * + * For instance, in case of a local raw contact, this method could return the content provider URI + * that identifies the corresponding contact. + * + * @return content provider URI, or `null` if not available + */ + fun getViewUri(context: Context): Uri? + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTask.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTask.kt new file mode 100644 index 0000000..95dda53 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTask.kt @@ -0,0 +1,159 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.DmfsTask +import at.bitfire.ical4android.DmfsTaskFactory +import at.bitfire.ical4android.DmfsTaskList +import at.bitfire.ical4android.Task +import at.bitfire.ical4android.TaskProvider +import at.bitfire.synctools.storage.BatchOperation +import com.google.common.base.Ascii +import com.google.common.base.MoreObjects +import org.dmfs.tasks.contract.TaskContract.Tasks +import java.util.Optional +import java.util.UUID + +class LocalTask: DmfsTask, LocalResource { + + companion object { + const val COLUMN_ETAG = Tasks.SYNC1 + const val COLUMN_FLAGS = Tasks.SYNC2 + } + + override var fileName: String? = null + + override var scheduleTag: String? = null + override var eTag: String? = null + + override var flags = 0 + private set + + + constructor(taskList: DmfsTaskList<*>, task: Task, fileName: String?, eTag: String?, flags: Int) + : super(taskList, task) { + this.fileName = fileName + this.eTag = eTag + this.flags = flags + } + + private constructor(taskList: DmfsTaskList<*>, values: ContentValues): super(taskList) { + id = values.getAsLong(Tasks._ID) + fileName = values.getAsString(Tasks._SYNC_ID) + eTag = values.getAsString(COLUMN_ETAG) + flags = values.getAsInteger(COLUMN_FLAGS) ?: 0 + } + + + /* process LocalTask-specific fields */ + + override fun buildTask(builder: BatchOperation.CpoBuilder, update: Boolean) { + super.buildTask(builder, update) + + builder .withValue(Tasks._SYNC_ID, fileName) + .withValue(COLUMN_ETAG, eTag) + .withValue(COLUMN_FLAGS, flags) + } + + + /* custom queries */ + + override fun prepareForUpload(): String { + val uid: String = task!!.uid ?: run { + // generate new UID + val newUid = UUID.randomUUID().toString() + + // update in tasks provider + val values = contentValuesOf(Tasks._UID to newUid) + taskList.provider.update(taskSyncURI(), values, null, null) + + // update this task + task!!.uid = newUid + + newUid + } + + return "$uid.ics" + } + + override fun clearDirty(fileName: Optional, eTag: String?, scheduleTag: String?) { + if (scheduleTag != null) + logger.fine("Schedule-Tag for tasks not supported yet, won't save") + + val values = ContentValues(4) + if (fileName.isPresent) + values.put(Tasks._SYNC_ID, fileName.get()) + values.put(COLUMN_ETAG, eTag) + values.put(Tasks.SYNC_VERSION, task!!.sequence) + values.put(Tasks._DIRTY, 0) + taskList.provider.update(taskSyncURI(), values, null, null) + + if (fileName.isPresent) + this.fileName = fileName.get() + this.eTag = eTag + } + + override fun update(data: Task, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) { + this.fileName = fileName + this.eTag = eTag + this.scheduleTag = scheduleTag + this.flags = flags + + // processes this.{fileName, eTag, scheduleTag, flags} and resets DIRTY flag + update(data) + } + + override fun updateFlags(flags: Int) { + if (id != null) { + val values = contentValuesOf(COLUMN_FLAGS to flags) + taskList.provider.update(taskSyncURI(), values, null, null) + } + + this.flags = flags + } + + override fun deleteLocal() { + delete() + } + + override fun resetDeleted() { + throw NotImplementedError() + } + + override fun getDebugSummary() = + MoreObjects.toStringHelper(this) + .add("id", id) + .add("fileName", fileName) + .add("eTag", eTag) + .add("scheduleTag", scheduleTag) + .add("flags", flags) + .add("task", + try { + Ascii.truncate(task.toString(), 1000, "…") + } catch (e: Exception) { + e + } + ).toString() + + override fun getViewUri(context: Context): Uri? { + val idNotNull = id ?: return null + if (taskList.providerName == TaskProvider.ProviderName.OpenTasks) { + val contentUri = Tasks.getContentUri(taskList.providerName.authority) + return ContentUris.withAppendedId(contentUri, idNotNull) + } + return null + } + + + object Factory: DmfsTaskFactory { + override fun fromProvider(taskList: DmfsTaskList<*>, values: ContentValues) = + LocalTask(taskList, values) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskList.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskList.kt new file mode 100644 index 0000000..d87753c --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskList.kt @@ -0,0 +1,129 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentValues +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.DmfsTaskList +import at.bitfire.ical4android.DmfsTaskListFactory +import at.bitfire.ical4android.TaskProvider +import org.dmfs.tasks.contract.TaskContract.TaskListColumns +import org.dmfs.tasks.contract.TaskContract.TaskLists +import org.dmfs.tasks.contract.TaskContract.Tasks +import java.util.logging.Level +import java.util.logging.Logger + +/** + * App-specific implementation of a task list. + * + * [TaskLists._SYNC_ID] corresponds to the database collection ID ([at.bitfire.davdroid.db.Collection.id]). + */ +class LocalTaskList private constructor( + account: Account, + provider: ContentProviderClient, + providerName: TaskProvider.ProviderName, + id: Long +): DmfsTaskList(account, provider, providerName, LocalTask.Factory, id), LocalCollection { + + private val logger = Logger.getGlobal() + + private var accessLevel: Int = TaskListColumns.ACCESS_LEVEL_UNDEFINED + override val readOnly + get() = + accessLevel != TaskListColumns.ACCESS_LEVEL_UNDEFINED && + accessLevel <= TaskListColumns.ACCESS_LEVEL_READ + + override val dbCollectionId: Long? + get() = syncId?.toLongOrNull() + + override val tag: String + get() = "tasks-${account.name}-$id" + + override val title: String + get() = name ?: id.toString() + + override var lastSyncState: SyncState? + get() { + try { + provider.query(taskListSyncUri(), arrayOf(TaskLists.SYNC_VERSION), + null, null, null)?.use { cursor -> + if (cursor.moveToNext()) + cursor.getString(0)?.let { + return SyncState.fromString(it) + } + } + } catch (e: Exception) { + logger.log(Level.WARNING, "Couldn't read sync state", e) + } + return null + } + set(state) { + val values = contentValuesOf(TaskLists.SYNC_VERSION to state?.toString()) + provider.update(taskListSyncUri(), values, null, null) + } + + + override fun populate(values: ContentValues) { + super.populate(values) + accessLevel = values.getAsInteger(TaskListColumns.ACCESS_LEVEL) + } + + + override fun findDeleted() = queryTasks(Tasks._DELETED, null) + + override fun findDirty(): List { + val tasks = queryTasks(Tasks._DIRTY, null) + for (localTask in tasks) { + try { + val task = requireNotNull(localTask.task) + val sequence = task.sequence + if (sequence == null) // sequence has not been assigned yet (i.e. this task was just locally created) + task.sequence = 0 + else // task was modified, increase sequence + task.sequence = sequence + 1 + } catch(e: Exception) { + logger.log(Level.WARNING, "Couldn't check/increase sequence", e) + } + } + return tasks + } + + override fun findByName(name: String) = + queryTasks("${Tasks._SYNC_ID}=?", arrayOf(name)).firstOrNull() + + + override fun markNotDirty(flags: Int): Int { + val values = contentValuesOf(LocalTask.COLUMN_FLAGS to flags) + return provider.update(tasksSyncUri(), values, + "${Tasks.LIST_ID}=? AND ${Tasks._DIRTY}=0", + arrayOf(id.toString())) + } + + override fun removeNotDirtyMarked(flags: Int) = + provider.delete(tasksSyncUri(), + "${Tasks.LIST_ID}=? AND NOT ${Tasks._DIRTY} AND ${LocalTask.COLUMN_FLAGS}=?", + arrayOf(id.toString(), flags.toString())) + + override fun forgetETags() { + val values = contentValuesOf(LocalTask.COLUMN_ETAG to null) + provider.update(tasksSyncUri(), values, "${Tasks.LIST_ID}=?", + arrayOf(id.toString())) + } + + + object Factory: DmfsTaskListFactory { + + override fun newInstance( + account: Account, + provider: ContentProviderClient, + providerName: TaskProvider.ProviderName, + id: Long + ) = LocalTaskList(account, provider, providerName, id) + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskListStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskListStore.kt new file mode 100644 index 0000000..80bd8e1 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskListStore.kt @@ -0,0 +1,124 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import androidx.core.content.contentValuesOf +import at.bitfire.davdroid.Constants +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.util.DavUtils.lastSegment +import at.bitfire.ical4android.DmfsTaskList +import at.bitfire.ical4android.TaskProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.qualifiers.ApplicationContext +import org.dmfs.tasks.contract.TaskContract.TaskListColumns +import org.dmfs.tasks.contract.TaskContract.TaskLists +import org.dmfs.tasks.contract.TaskContract.Tasks +import java.util.logging.Level +import java.util.logging.Logger + +class LocalTaskListStore @AssistedInject constructor( + @Assisted private val providerName: TaskProvider.ProviderName, + val accountSettingsFactory: AccountSettings.Factory, + @ApplicationContext val context: Context, + val db: AppDatabase, + val logger: Logger +): LocalDataStore { + + @AssistedFactory + interface Factory { + fun create(providerName: TaskProvider.ProviderName): LocalTaskListStore + } + + private val serviceDao = db.serviceDao() + + override val authority: String + get() = providerName.authority + + override fun acquireContentProvider(throwOnMissingPermissions: Boolean) = try { + context.contentResolver.acquireContentProviderClient(authority) + } catch (e: SecurityException) { + if (throwOnMissingPermissions) + throw e + else + /* return */ null + } + + override fun create(provider: ContentProviderClient, fromCollection: Collection): LocalTaskList? { + val service = serviceDao.get(fromCollection.serviceId) ?: throw IllegalArgumentException("Couldn't fetch DB service from collection") + val account = Account(service.accountName, context.getString(R.string.account_type)) + + logger.log(Level.INFO, "Adding local task list", fromCollection) + val uri = create(account, provider, providerName, fromCollection) + return DmfsTaskList.findByID(account, provider, providerName, LocalTaskList.Factory, ContentUris.parseId(uri)) + } + + private fun create(account: Account, provider: ContentProviderClient, providerName: TaskProvider.ProviderName, fromCollection: Collection): Uri { + // If the collection doesn't have a color, use a default color. + val collectionWithColor = if (fromCollection.color != null) + fromCollection + else + fromCollection.copy(color = Constants.DAVDROID_GREEN_RGBA) + + val values = valuesFromCollectionInfo( + info = collectionWithColor, + withColor = true + ).apply { + put(TaskLists.OWNER, account.name) + put(TaskLists.SYNC_ENABLED, 1) + put(TaskLists.VISIBLE, 1) + } + return DmfsTaskList.Companion.create(account, provider, providerName, values) + } + + private fun valuesFromCollectionInfo(info: Collection, withColor: Boolean): ContentValues { + val values = ContentValues(3) + values.put(TaskLists._SYNC_ID, info.id.toString()) + values.put(TaskLists.LIST_NAME, + if (info.displayName.isNullOrBlank()) info.url.lastSegment else info.displayName) + + if (withColor && info.color != null) + values.put(TaskLists.LIST_COLOR, info.color) + + if (info.privWriteContent && !info.forceReadOnly) + values.put(TaskListColumns.ACCESS_LEVEL, TaskListColumns.ACCESS_LEVEL_OWNER) + else + values.put(TaskListColumns.ACCESS_LEVEL, TaskListColumns.ACCESS_LEVEL_READ) + + return values + } + + override fun getAll(account: Account, provider: ContentProviderClient) = + DmfsTaskList.find(account, LocalTaskList.Factory, provider, providerName, null, null) + + override fun update(provider: ContentProviderClient, localCollection: LocalTaskList, fromCollection: Collection) { + logger.log(Level.FINE, "Updating local task list ${fromCollection.url}", fromCollection) + val accountSettings = accountSettingsFactory.create(localCollection.account) + localCollection.update(valuesFromCollectionInfo(fromCollection, withColor = accountSettings.getManageCalendarColors())) + } + + override fun updateAccount(oldAccount: Account, newAccount: Account) { + TaskProvider.acquire(context, providerName)?.use { provider -> + val values = contentValuesOf(Tasks.ACCOUNT_NAME to newAccount.name) + val uri = Tasks.getContentUri(providerName.authority) + provider.client.update(uri, values, "${Tasks.ACCOUNT_NAME}=?", arrayOf(oldAccount.name)) + } + } + + override fun delete(localCollection: LocalTaskList) { + localCollection.delete() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/SyncState.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/SyncState.kt new file mode 100644 index 0000000..209b55b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/SyncState.kt @@ -0,0 +1,59 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import at.bitfire.dav4jvm.property.webdav.SyncToken +import org.json.JSONException +import org.json.JSONObject + +data class SyncState( + val type: Type, + val value: String, + + /** + * Whether this sync state occurred during an initial sync as described + * in RFC 6578, which means the initial sync is not complete yet. + */ + var initialSync: Boolean? = null +) { + + companion object { + + private const val KEY_TYPE = "type" + private const val KEY_VALUE = "value" + private const val KEY_INITIAL_SYNC = "initialSync" + + fun fromString(s: String?): SyncState? { + if (s == null) + return null + + return try { + val json = JSONObject(s) + SyncState( + Type.valueOf(json.getString(KEY_TYPE)), + json.getString(KEY_VALUE), + try { json.getBoolean(KEY_INITIAL_SYNC) } catch(e: JSONException) { null } + ) + } catch (e: JSONException) { + null + } + } + + fun fromSyncToken(token: SyncToken, initialSync: Boolean? = null) = + SyncState(Type.SYNC_TOKEN, requireNotNull(token.token), initialSync) + + } + + enum class Type { CTAG, SYNC_TOKEN } + + override fun toString(): String { + val json = JSONObject() + json.put(KEY_TYPE, type.name) + json.put(KEY_VALUE, value) + initialSync?.let { json.put(KEY_INITIAL_SYNC, it) } + return json.toString() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/CachedGroupMembershipHandler.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/CachedGroupMembershipHandler.kt new file mode 100644 index 0000000..f531e83 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/CachedGroupMembershipHandler.kt @@ -0,0 +1,28 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource.contactrow + +import android.content.ContentValues +import at.bitfire.davdroid.resource.LocalContact +import at.bitfire.vcard4android.CachedGroupMembership +import at.bitfire.vcard4android.Contact +import at.bitfire.vcard4android.GroupMethod +import at.bitfire.vcard4android.contactrow.DataRowHandler +import java.util.logging.Logger + +class CachedGroupMembershipHandler(val localContact: LocalContact): DataRowHandler() { + + override fun forMimeType() = CachedGroupMembership.CONTENT_ITEM_TYPE + + override fun handle(values: ContentValues, contact: Contact) { + super.handle(values, contact) + + if (localContact.addressBook.groupMethod == GroupMethod.GROUP_VCARDS) + localContact.cachedGroupMemberships += values.getAsLong(CachedGroupMembership.GROUP_ID) + else + Logger.getGlobal().warning("Ignoring cached group membership for group method CATEGORIES") + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/GroupMembershipBuilder.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/GroupMembershipBuilder.kt new file mode 100644 index 0000000..30b4c99 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/GroupMembershipBuilder.kt @@ -0,0 +1,43 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource.contactrow + +import android.net.Uri +import android.provider.ContactsContract.CommonDataKinds.GroupMembership +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.synctools.storage.BatchOperation +import at.bitfire.vcard4android.Contact +import at.bitfire.vcard4android.GroupMethod +import at.bitfire.vcard4android.contactrow.DataRowBuilder +import java.util.LinkedList + +class GroupMembershipBuilder(dataRowUri: Uri, rawContactId: Long?, contact: Contact, val addressBook: LocalAddressBook, readOnly: Boolean) + : DataRowBuilder(Factory.MIME_TYPE, dataRowUri, rawContactId, contact, readOnly) { + + override fun build(): List { + val result = LinkedList() + + if (addressBook.groupMethod == GroupMethod.CATEGORIES) + for (category in contact.categories) + result += newDataRow().withValue(GroupMembership.GROUP_ROW_ID, addressBook.findOrCreateGroup(category)) + else { + // GroupMethod.GROUP_VCARDS -> memberships are handled by LocalGroups (and not by the members = LocalContacts, which we are processing here) + // TODO: CATEGORIES <-> unknown properties + } + + return result + } + + + class Factory(val addressBook: LocalAddressBook): DataRowBuilder.Factory { + companion object { + const val MIME_TYPE = GroupMembership.CONTENT_ITEM_TYPE + } + override fun mimeType() = MIME_TYPE + override fun newInstance(dataRowUri: Uri, rawContactId: Long?, contact: Contact, readOnly: Boolean) = + GroupMembershipBuilder(dataRowUri, rawContactId, contact, addressBook, readOnly) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/GroupMembershipHandler.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/GroupMembershipHandler.kt new file mode 100644 index 0000000..6bee391 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/GroupMembershipHandler.kt @@ -0,0 +1,39 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource.contactrow + +import android.content.ContentValues +import android.provider.ContactsContract.CommonDataKinds.GroupMembership +import at.bitfire.davdroid.resource.LocalContact +import at.bitfire.davdroid.util.trimToNull +import at.bitfire.vcard4android.Contact +import at.bitfire.vcard4android.GroupMethod +import at.bitfire.vcard4android.contactrow.DataRowHandler +import java.io.FileNotFoundException + +class GroupMembershipHandler(val localContact: LocalContact): DataRowHandler() { + + override fun forMimeType() = GroupMembership.CONTENT_ITEM_TYPE + + override fun handle(values: ContentValues, contact: Contact) { + super.handle(values, contact) + + val groupId = values.getAsLong(GroupMembership.GROUP_ROW_ID) + localContact.groupMemberships += groupId + + if (localContact.addressBook.groupMethod == GroupMethod.CATEGORIES) { + try { + val group = localContact.addressBook.findGroupById(groupId) + group.getContact().displayName.trimToNull()?.let { groupName -> + logger.fine("Adding membership in group $groupName as category") + contact.categories.add(groupName) + } + } catch (ignored: FileNotFoundException) { + logger.warning("Contact is member in group $groupId which doesn't exist anymore") + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownProperties.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownProperties.kt new file mode 100644 index 0000000..593704f --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownProperties.kt @@ -0,0 +1,17 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource.contactrow + +import android.provider.ContactsContract.RawContacts + +object UnknownProperties { + + const val CONTENT_ITEM_TYPE = "x.davdroid/unknown-properties" + + const val MIMETYPE = RawContacts.Data.MIMETYPE + const val RAW_CONTACT_ID = RawContacts.Data.RAW_CONTACT_ID + const val UNKNOWN_PROPERTIES = RawContacts.Data.DATA1 + +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownPropertiesBuilder.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownPropertiesBuilder.kt new file mode 100644 index 0000000..41f8090 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownPropertiesBuilder.kt @@ -0,0 +1,31 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource.contactrow + +import android.net.Uri +import at.bitfire.synctools.storage.BatchOperation +import at.bitfire.vcard4android.Contact +import at.bitfire.vcard4android.contactrow.DataRowBuilder +import java.util.LinkedList + +class UnknownPropertiesBuilder(dataRowUri: Uri, rawContactId: Long?, contact: Contact, readOnly: Boolean) + : DataRowBuilder(Factory.mimeType(), dataRowUri, rawContactId, contact, readOnly) { + + override fun build(): List { + val result = LinkedList() + contact.unknownProperties?.let { unknownProperties -> + result += newDataRow().withValue(UnknownProperties.UNKNOWN_PROPERTIES, unknownProperties) + } + return result + } + + + object Factory: DataRowBuilder.Factory { + override fun mimeType() = UnknownProperties.CONTENT_ITEM_TYPE + override fun newInstance(dataRowUri: Uri, rawContactId: Long?, contact: Contact, readOnly: Boolean) = + UnknownPropertiesBuilder(dataRowUri, rawContactId, contact, readOnly) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownPropertiesHandler.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownPropertiesHandler.kt new file mode 100644 index 0000000..4f0e8fa --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/contactrow/UnknownPropertiesHandler.kt @@ -0,0 +1,21 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource.contactrow + +import android.content.ContentValues +import at.bitfire.vcard4android.Contact +import at.bitfire.vcard4android.contactrow.DataRowHandler + +object UnknownPropertiesHandler: DataRowHandler() { + + override fun forMimeType() = UnknownProperties.CONTENT_ITEM_TYPE + + override fun handle(values: ContentValues, contact: Contact) { + super.handle(values, contact) + + contact.unknownProperties = values.getAsString(UnknownProperties.UNKNOWN_PROPERTIES) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/workaround/Android7DirtyVerifier.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/workaround/Android7DirtyVerifier.kt new file mode 100644 index 0000000..87deebb --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/workaround/Android7DirtyVerifier.kt @@ -0,0 +1,161 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource.workaround + +import android.content.ContentValues +import android.os.Build +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.resource.LocalContact +import at.bitfire.davdroid.resource.LocalContact.Companion.COLUMN_HASHCODE +import at.bitfire.synctools.storage.BatchOperation +import at.bitfire.synctools.storage.ContactsBatchOperation +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import java.util.Optional +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject +import javax.inject.Provider + +/** + * Android 7.x introduced a new behavior in the Contacts provider: when metadata of a contact (like the "last contacted" time) + * changes, the contact is marked as "dirty" (i.e. the [android.provider.ContactsContract.RawContacts.DIRTY] flag is set). + * So, under Android 7.x, every time a user calls a contact or writes an SMS to a contact, the contact is marked as dirty. + * + * **This behavior is not present in Android ≤ 6.x nor in ≥ Android 8.x, where a contact is only marked as dirty + * when its data actually change.** + * + * So, as a dirty workaround for Android 7.x, we need to calculate a hash code from the contact data and group memberships every + * time we change the contact. When then a contact is marked as dirty, we compare the hash code of the current contact data with + * the previous hash code. If the hash code has changed, the contact is "really dirty" and we need to upload it. Otherwise, + * we reset the dirty flag to ignore the meta-data change. + * + * @constructor May only be called on Android 7.x, otherwise an [IllegalStateException] is thrown. + */ +class Android7DirtyVerifier @Inject constructor( + val logger: Logger +): ContactDirtyVerifier { + + init { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + throw IllegalStateException("Android7DirtyVerifier must not be used on Android != 7.x") + } + + + // address-book level functions + + override fun prepareAddressBook(addressBook: LocalAddressBook, isUpload: Boolean): Boolean { + val reallyDirty = verifyDirtyContacts(addressBook) + + val deleted = addressBook.findDeleted().size + if (isUpload && reallyDirty == 0 && deleted == 0) { + logger.info("This sync was called to up-sync dirty/deleted contacts, but no contacts have been changed") + return false + } + + return true + } + + /** + * Queries all contacts with the [android.provider.ContactsContract.RawContacts.DIRTY] flag and checks whether their data + * checksum has changed, i.e. if they're "really dirty" (= data has changed, not only metadata, which is not hashed). + * + * The dirty flag is removed from contacts which are not "really dirty", i.e. from contacts whose contact data + * checksum has not changed. + * + * @return number of "really dirty" contacts + */ + private fun verifyDirtyContacts(addressBook: LocalAddressBook): Int { + var reallyDirty = 0 + for (contact in addressBook.findDirtyContacts()) { + val lastHash = getLastHashCode(addressBook, contact) + val currentHash = contactDataHashCode(contact) + if (lastHash == currentHash) { + // hash is code still the same, contact is not "really dirty" (only metadata been have changed) + logger.log(Level.FINE, "Contact data hash has not changed, resetting dirty flag", contact) + contact.resetDirty() + } else { + logger.log(Level.FINE, "Contact data has changed from hash $lastHash to $currentHash", contact) + reallyDirty++ + } + } + + if (addressBook.includeGroups) + reallyDirty += addressBook.findDirtyGroups().size + + return reallyDirty + } + + private fun getLastHashCode(addressBook: LocalAddressBook, contact: LocalContact): Int { + addressBook.provider!!.query(contact.rawContactSyncURI(), arrayOf(COLUMN_HASHCODE), null, null, null)?.use { c -> + if (c.moveToNext() && !c.isNull(0)) + return c.getInt(0) + } + return 0 + } + + + // contact level functions + + /** + * Calculates a hash code from the [at.bitfire.vcard4android.Contact] data and group memberships. + * Attention: re-reads {@link #contact} from the database, discarding all changes in memory! + * + * @return hash code of contact data (including group memberships) + */ + private fun contactDataHashCode(contact: LocalContact): Int { + contact.clearCachedContact() + + // groupMemberships is filled by getContact() + val dataHash = contact.getContact().hashCode() + val groupHash = contact.groupMemberships.hashCode() + val combinedHash = dataHash xor groupHash + logger.log(Level.FINE, "Calculated data hash = $dataHash, group memberships hash = $groupHash → combined hash = $combinedHash", contact) + return combinedHash + } + + override fun setHashCodeColumn(contact: LocalContact, toValues: ContentValues) { + val hashCode = contactDataHashCode(contact) + toValues.put(COLUMN_HASHCODE, hashCode) + } + + override fun updateHashCode(addressBook: LocalAddressBook, contact: LocalContact) { + val values = ContentValues(1) + setHashCodeColumn(contact, values) + + addressBook.provider!!.update(contact.rawContactSyncURI(), values, null, null) + } + + override fun updateHashCode(contact: LocalContact, batch: ContactsBatchOperation) { + val hashCode = contactDataHashCode(contact) + + batch += BatchOperation.CpoBuilder + .newUpdate(contact.rawContactSyncURI()) + .withValue(COLUMN_HASHCODE, hashCode) + } + + + + // factory + + @Module + @InstallIn(SingletonComponent::class) + object Android7DirtyVerifierModule { + + /** + * Provides an [Android7DirtyVerifier] on Android 7.x, or an empty [Optional] on other versions. + */ + @Provides + fun provide(android7DirtyVerifier: Provider): Optional = + if (/* Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && */ Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + Optional.of(android7DirtyVerifier.get()) + else + Optional.empty() + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/workaround/ContactDirtyVerifier.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/workaround/ContactDirtyVerifier.kt new file mode 100644 index 0000000..b37e487 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/workaround/ContactDirtyVerifier.kt @@ -0,0 +1,54 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource.workaround + +import android.content.ContentValues +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.resource.LocalContact +import at.bitfire.synctools.storage.ContactsBatchOperation + +/** + * Only required for [Android7DirtyVerifier]. If that class is removed because the minimum SDK is raised to Android 8, + * this interface and all calls to it can be removed as well. + */ +interface ContactDirtyVerifier { + + // address-book level functions + + /** + * Checks whether contacts which are marked as "dirty" are really dirty, i.e. their data has changed. + * If contacts are not really dirty (because only the metadata like "last contacted" changed), the "dirty" flag is removed. + * + * Intended to be called by [at.bitfire.davdroid.sync.ContactsSyncManager.prepare]. + * + * @param addressBook the address book + * @param isUpload whether this sync is an upload + * + * @return `true` if the address book should be synced, `false` if the sync is an upload and no contacts have been changed + */ + fun prepareAddressBook(addressBook: LocalAddressBook, isUpload: Boolean): Boolean + + + // contact level functions + + /** + * Sets the [LocalContact.COLUMN_HASHCODE] column in the given [ContentValues] to the hash code of the contact data. + * + * @param contact the contact to calculate the hash code for + * @param toValues set the hash code into these values + */ + fun setHashCodeColumn(contact: LocalContact, toValues: ContentValues) + + /** + * Sets the [LocalContact.COLUMN_HASHCODE] field of the contact to the hash code of the contact data directly in the content provider. + */ + fun updateHashCode(addressBook: LocalAddressBook, contact: LocalContact) + + /** + Sets the [LocalContact.COLUMN_HASHCODE] field of the contact to the hash code of the contact data in a content provider batch operation. + */ + fun updateHashCode(contact: LocalContact, batch: ContactsBatchOperation) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/CollectionsWithoutHomeSetRefresher.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/CollectionsWithoutHomeSetRefresher.kt new file mode 100644 index 0000000..8f8ba9f --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/CollectionsWithoutHomeSetRefresher.kt @@ -0,0 +1,73 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.servicedetection + +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.dav4jvm.property.webdav.Owner +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Principal +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.repository.DavCollectionRepository +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import okhttp3.OkHttpClient + +/** + * Logic for refreshing the list of collections (and their related information) + * which do not belong to a home set. + */ +class CollectionsWithoutHomeSetRefresher @AssistedInject constructor( + @Assisted private val service: Service, + @Assisted private val httpClient: OkHttpClient, + private val db: AppDatabase, + private val collectionRepository: DavCollectionRepository, +) { + + @AssistedFactory + interface Factory { + fun create(service: Service, httpClient: OkHttpClient): CollectionsWithoutHomeSetRefresher + } + + /** + * Refreshes collections which don't have a homeset. + * + * It queries each stored collection with a homeSetId of "null" and either updates or deletes (if inaccessible or unusable) them. + */ + internal fun refreshCollectionsWithoutHomeSet() { + val withoutHomeSet = db.collectionDao().getByServiceAndHomeset(service.id, null).associateBy { it.url }.toMutableMap() + for ((url, localCollection) in withoutHomeSet) try { + val collectionProperties = ServiceDetectionUtils.collectionQueryProperties(service.type) + DavResource(httpClient, url).propfind(0, *collectionProperties) { response, _ -> + if (!response.isSuccess()) { + collectionRepository.delete(localCollection) + return@propfind + } + + // Save or update the collection, if usable, otherwise delete it + Collection.fromDavResponse(response)?.let { collection -> + if (!ServiceDetectionUtils.isUsableCollection(service, collection)) + return@let + collectionRepository.insertOrUpdateByUrlRememberSync(collection.copy( + serviceId = localCollection.serviceId, // use same service ID as previous entry + ownerId = response[Owner::class.java]?.href // save the principal id (collection owner) + ?.let { response.href.resolve(it) } + ?.let { principalUrl -> Principal.fromServiceAndUrl(service, principalUrl) } + ?.let { principal -> db.principalDao().insertOrUpdate(service.id, principal) } + )) + } ?: collectionRepository.delete(localCollection) + } + } catch (e: HttpException) { + // delete collection locally if it was not accessible (40x) + if (e.statusCode in arrayOf(403, 404, 410)) + collectionRepository.delete(localCollection) + else + throw e + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt new file mode 100644 index 0000000..494d9c1 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt @@ -0,0 +1,506 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ +package at.bitfire.davdroid.servicedetection + +import android.app.ActivityManager +import android.content.Context +import androidx.core.content.getSystemService +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.Property +import at.bitfire.dav4jvm.Response +import at.bitfire.dav4jvm.UrlUtils +import at.bitfire.dav4jvm.exception.DavException +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.dav4jvm.exception.UnauthorizedException +import at.bitfire.dav4jvm.property.caldav.CalendarColor +import at.bitfire.dav4jvm.property.caldav.CalendarDescription +import at.bitfire.dav4jvm.property.caldav.CalendarHomeSet +import at.bitfire.dav4jvm.property.caldav.CalendarTimezone +import at.bitfire.dav4jvm.property.caldav.CalendarUserAddressSet +import at.bitfire.dav4jvm.property.caldav.SupportedCalendarComponentSet +import at.bitfire.dav4jvm.property.carddav.AddressbookDescription +import at.bitfire.dav4jvm.property.carddav.AddressbookHomeSet +import at.bitfire.dav4jvm.property.common.HrefListProperty +import at.bitfire.dav4jvm.property.webdav.CurrentUserPrincipal +import at.bitfire.dav4jvm.property.webdav.CurrentUserPrivilegeSet +import at.bitfire.dav4jvm.property.webdav.DisplayName +import at.bitfire.dav4jvm.property.webdav.ResourceType +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.log.StringHandler +import at.bitfire.davdroid.network.DnsRecordResolver +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.settings.Credentials +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.qualifiers.ApplicationContext +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import org.xbill.DNS.Type +import java.io.InterruptedIOException +import java.net.SocketTimeoutException +import java.net.URI +import java.net.URISyntaxException +import java.util.LinkedList +import java.util.logging.Level +import java.util.logging.Logger + +/** + * Does initial resource detection when an account is added. It uses the (user given) base URL to find + * + * - services (CalDAV and/or CardDAV), + * - principal, + * - homeset/collections (multistatus responses are handled through dav4jvm). + * + * @param context to build the HTTP client + * @param baseURI user-given base URI (either mailto: URI or http(s):// URL) + * @param credentials optional login credentials (username/password, client certificate, OAuth state) + */ +class DavResourceFinder @AssistedInject constructor( + @Assisted private val baseURI: URI, + @Assisted private val credentials: Credentials? = null, + @ApplicationContext val context: Context, + private val dnsRecordResolver: DnsRecordResolver, + httpClientBuilder: HttpClient.Builder +): AutoCloseable { + + @AssistedFactory + interface Factory { + fun create(baseURI: URI, credentials: Credentials?): DavResourceFinder + } + + enum class Service(val wellKnownName: String) { + CALDAV("caldav"), + CARDDAV("carddav"); + + override fun toString() = wellKnownName + } + + val log: Logger = Logger.getLogger(javaClass.name) + private val logBuffer: StringHandler = initLogging() + + private var encountered401 = false + + private val httpClient = httpClientBuilder + .setLogger(log) + .apply { + if (credentials != null) + authenticate( + host = null, + getCredentials = { credentials } + ) + } + .build() + + override fun close() { + httpClient.close() + } + + private fun initLogging(): StringHandler { + // don't use more than 1/4 of the available memory for a log string + val activityManager = context.getSystemService()!! + val maxLogSize = activityManager.memoryClass * (1024 * 1024 / 8) + val handler = StringHandler(maxLogSize) + + // add StringHandler to logger + log.level = Level.ALL + log.addHandler(handler) + + return handler + } + + + /** + * Finds the initial configuration (= runs the service detection process). + * + * In case of an error, it returns an empty [Configuration] with error logs + * instead of throwing an [Exception]. + * + * @return service information – if there's neither a CalDAV service nor a CardDAV service, + * service detection was not successful + */ + fun findInitialConfiguration(): Configuration { + var cardDavConfig: Configuration.ServiceInfo? = null + var calDavConfig: Configuration.ServiceInfo? = null + + try { + try { + cardDavConfig = findInitialConfiguration(Service.CARDDAV) + } catch (e: Exception) { + log.log(Level.INFO, "CardDAV service detection failed", e) + processException(e) + } + + try { + calDavConfig = findInitialConfiguration(Service.CALDAV) + } catch (e: Exception) { + log.log(Level.INFO, "CalDAV service detection failed", e) + processException(e) + } + } catch(_: Exception) { + // we have been interrupted; reset results so that an error message will be shown + cardDavConfig = null + calDavConfig = null + } + + return Configuration( + cardDAV = cardDavConfig, + calDAV = calDavConfig, + encountered401 = encountered401, + logs = logBuffer.toString() + ) + } + + private fun findInitialConfiguration(service: Service): Configuration.ServiceInfo? { + // domain for service discovery + var discoveryFQDN: String? = null + + // discovered information goes into this config + val config = Configuration.ServiceInfo() + + // Start discovering + log.info("Finding initial ${service.wellKnownName} service configuration") + when (baseURI.scheme.lowercase()) { + "http", "https" -> + baseURI.toHttpUrlOrNull()?.let { baseURL -> + // remember domain for service discovery + if (baseURL.scheme.equals("https", true)) + // service discovery will only be tried for https URLs, because only secure service discovery is implemented + discoveryFQDN = baseURL.host + + // Actual discovery process + checkBaseURL(baseURL, service, config) + + // If principal was not found already, try well known URI + if (config.principal == null) + try { + config.principal = getCurrentUserPrincipal(baseURL.resolve("/.well-known/" + service.wellKnownName)!!, service) + } catch(e: Exception) { + log.log(Level.FINE, "Well-known URL detection failed", e) + processException(e) + } + } + "mailto" -> { + val mailbox = baseURI.schemeSpecificPart + val posAt = mailbox.lastIndexOf("@") + if (posAt != -1) + discoveryFQDN = mailbox.substring(posAt + 1) + } + } + + // Second try: If user-given URL didn't reveal a principal, search for it (SERVICE DISCOVERY) + if (config.principal == null) + discoveryFQDN?.let { fqdn -> + log.info("No principal found at user-given URL, trying to discover for domain $fqdn") + try { + config.principal = discoverPrincipalUrl(fqdn, service) + } catch(e: Exception) { + log.log(Level.FINE, "$service service discovery failed", e) + processException(e) + } + } + + // detect email address + if (service == Service.CALDAV) + config.principal?.let { principal -> + config.emails.addAll(queryEmailAddress(principal)) + } + + // return config or null if config doesn't contain useful information + val serviceAvailable = config.principal != null || config.homeSets.isNotEmpty() || config.collections.isNotEmpty() + return if (serviceAvailable) + config + else + null + } + + /** + * Entry point of the actual discovery process. + * + * Queries the user-given URL (= base URL) to detect whether it contains a current-user-principal + * or whether it is a homeset or collection. + * + * @param baseURL base URL provided by the user + * @param service service to detect configuration for + * @param config found configuration will be written to this object + */ + private fun checkBaseURL(baseURL: HttpUrl, service: Service, config: Configuration.ServiceInfo) { + log.info("Checking user-given URL: $baseURL") + + val davBaseURL = DavResource(httpClient.okHttpClient, baseURL, log) + try { + when (service) { + Service.CARDDAV -> { + davBaseURL.propfind( + 0, + ResourceType.NAME, DisplayName.NAME, AddressbookDescription.NAME, + AddressbookHomeSet.NAME, + CurrentUserPrincipal.NAME + ) { response, _ -> + scanResponse(ResourceType.ADDRESSBOOK, response, config) + } + } + Service.CALDAV -> { + davBaseURL.propfind( + 0, + ResourceType.NAME, DisplayName.NAME, CalendarColor.NAME, CalendarDescription.NAME, CalendarTimezone.NAME, CurrentUserPrivilegeSet.NAME, SupportedCalendarComponentSet.NAME, + CalendarHomeSet.NAME, + CurrentUserPrincipal.NAME + ) { response, _ -> + scanResponse(ResourceType.CALENDAR, response, config) + } + } + } + } catch(e: Exception) { + log.log(Level.FINE, "PROPFIND/OPTIONS on user-given URL failed", e) + processException(e) + } + } + + /** + * Queries a user's email address using CalDAV scheduling: calendar-user-address-set. + * @param principal principal URL of the user + * @return list of found email addresses (empty if none) + */ + fun queryEmailAddress(principal: HttpUrl): List { + val mailboxes = LinkedList() + try { + DavResource(httpClient.okHttpClient, principal, log).propfind(0, CalendarUserAddressSet.NAME) { response, _ -> + response[CalendarUserAddressSet::class.java]?.let { addressSet -> + for (href in addressSet.hrefs) + try { + val uri = URI(href) + if (uri.scheme.equals("mailto", true)) + mailboxes.add(uri.schemeSpecificPart) + } catch(e: URISyntaxException) { + log.log(Level.WARNING, "Couldn't parse user address", e) + } + } + } + } catch(e: Exception) { + log.log(Level.WARNING, "Couldn't query user email address", e) + processException(e) + } + return mailboxes + } + + /** + * Depending on [resourceType] (CalDAV or CardDAV), this method checks whether [davResponse] references + * - an address book or calendar (actual resource), and/or + * - an "address book home set" or a "calendar home set", and/or + * - whether it's a principal. + * + * Respectively, this method will add the response to [config.collections], [config.homesets] and/or [config.principal]. + * Collection URLs will be stored with trailing "/". + * + * @param resourceType type of service to search for in the response + * @param davResponse response whose properties are evaluated + * @param config structure storing the references + */ + fun scanResponse(resourceType: Property.Name, davResponse: Response, config: Configuration.ServiceInfo) { + var principal: HttpUrl? = null + + // Type mapping + val homeSetClass: Class + val serviceType: Service + when (resourceType) { + ResourceType.ADDRESSBOOK -> { + homeSetClass = AddressbookHomeSet::class.java + serviceType = Service.CARDDAV + } + ResourceType.CALENDAR -> { + homeSetClass = CalendarHomeSet::class.java + serviceType = Service.CALDAV + } + else -> throw IllegalArgumentException() + } + + // check for current-user-principal + davResponse[CurrentUserPrincipal::class.java]?.href?.let { currentUserPrincipal -> + principal = davResponse.requestedUrl.resolve(currentUserPrincipal) + } + + davResponse[ResourceType::class.java]?.let { + // Is it a calendar or an address book, ... + if (it.types.contains(resourceType)) + Collection.fromDavResponse(davResponse)?.let { info -> + log.info("Found resource of type $resourceType at ${info.url}") + config.collections[info.url] = info + } + + // ... and/or a principal? + if (it.types.contains(ResourceType.PRINCIPAL)) + principal = davResponse.href + } + + // Is it an addressbook-home-set or calendar-home-set? + davResponse[homeSetClass]?.let { homeSet -> + for (href in homeSet.hrefs) { + davResponse.requestedUrl.resolve(href)?.let { + val location = UrlUtils.withTrailingSlash(it) + log.info("Found home-set of type $resourceType at $location") + config.homeSets += location + } + } + } + + // Is there a principal too? + principal?.let { + if (providesService(it, serviceType)) + config.principal = principal + else + log.warning("Principal $principal doesn't provide $serviceType service") + } + } + + /** + * Sends an OPTIONS request to determine whether a URL provides a given service. + * + * @param url URL to check; often a principal URL + * @param service service to check for + * + * @return whether the URL provides the given service + */ + fun providesService(url: HttpUrl, service: Service): Boolean { + var provided = false + try { + DavResource(httpClient.okHttpClient, url, log).options { capabilities, _ -> + if ((service == Service.CARDDAV && capabilities.contains("addressbook")) || + (service == Service.CALDAV && capabilities.contains("calendar-access"))) + provided = true + } + } catch(e: Exception) { + log.log(Level.SEVERE, "Couldn't detect services on $url", e) + if (e !is HttpException && e !is DavException) + throw e + } + return provided + } + + + /** + * Try to find the principal URL by performing service discovery on a given domain name. + * Only secure services (caldavs, carddavs) will be discovered! + * + * @param domain domain name, e.g. "icloud.com" + * @param service service to discover (CALDAV or CARDDAV) + * @return principal URL, or null if none found + */ + fun discoverPrincipalUrl(domain: String, service: Service): HttpUrl? { + val scheme: String + val fqdn: String + var port = 443 + val paths = LinkedList() // there may be multiple paths to try + + val query = "_${service.wellKnownName}s._tcp.$domain" + log.fine("Looking up SRV records for $query") + + val srvRecords = dnsRecordResolver.resolve(query, Type.SRV) + val srv = dnsRecordResolver.bestSRVRecord(srvRecords) + + if (srv != null) { + // choose SRV record to use (query may return multiple SRV records) + scheme = "https" + fqdn = srv.target.toString(true) + port = srv.port + log.info("Found $service service at https://$fqdn:$port") + } else { + // no SRV records, try domain name as FQDN + log.info("Didn't find $service service, trying at https://$domain:$port") + + scheme = "https" + fqdn = domain + } + + // look for TXT record too (for initial context path) + val txtRecords = dnsRecordResolver.resolve(query, Type.TXT) + paths.addAll(dnsRecordResolver.pathsFromTXTRecords(txtRecords)) + + // in case there's a TXT record, but it's wrong, try well-known + paths.add("/.well-known/" + service.wellKnownName) + // if this fails too, try "/" + paths.add("/") + + for (path in paths) + try { + val initialContextPath = HttpUrl.Builder() + .scheme(scheme) + .host(fqdn).port(port) + .encodedPath(path) + .build() + + log.info("Trying to determine principal from initial context path=$initialContextPath") + val principal = getCurrentUserPrincipal(initialContextPath, service) + + principal?.let { return it } + } catch(e: Exception) { + log.log(Level.WARNING, "No resource found", e) + processException(e) + } + return null + } + + /** + * Queries a given URL for current-user-principal + * + * @param url URL to query with PROPFIND (Depth: 0) + * @param service required service (may be null, in which case no service check is done) + * @return current-user-principal URL that provides required service, or null if none + */ + fun getCurrentUserPrincipal(url: HttpUrl, service: Service?): HttpUrl? { + var principal: HttpUrl? = null + DavResource(httpClient.okHttpClient, url, log).propfind(0, CurrentUserPrincipal.NAME) { response, _ -> + response[CurrentUserPrincipal::class.java]?.href?.let { href -> + response.requestedUrl.resolve(href)?.let { + log.info("Found current-user-principal: $it") + + // service check + if (service != null && !providesService(it, service)) + log.warning("Principal $it doesn't provide $service service") + else + principal = it + } + } + } + return principal + } + + /** + * Processes a thrown exception like this: + * + * - If the Exception is an [UnauthorizedException] (HTTP 401), [encountered401] is set to *true*. + * - Re-throws the exception if it signals that the current thread was interrupted to stop the current operation. + */ + private fun processException(e: Exception) { + if (e is UnauthorizedException) + encountered401 = true + else if ((e is InterruptedIOException && e !is SocketTimeoutException) || e is InterruptedException) + throw e + } + + + // data classes + + class Configuration( + val cardDAV: ServiceInfo?, + val calDAV: ServiceInfo?, + + val encountered401: Boolean, + val logs: String + ) { + + data class ServiceInfo( + var principal: HttpUrl? = null, + val homeSets: MutableSet = HashSet(), + val collections: MutableMap = HashMap(), + + val emails: MutableList = LinkedList() + ) + + override fun toString() = + "DavResourceFinder.Configuration(cardDAV=$cardDAV, calDAV=$calDAV, encountered401=$encountered401, logs=(${logs.length} chars))" + + } + +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/HomeSetRefresher.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/HomeSetRefresher.kt new file mode 100644 index 0000000..7788cf6 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/HomeSetRefresher.kt @@ -0,0 +1,162 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.servicedetection + +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.Response +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.dav4jvm.property.webdav.CurrentUserPrivilegeSet +import at.bitfire.dav4jvm.property.webdav.DisplayName +import at.bitfire.dav4jvm.property.webdav.Owner +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.HomeSet +import at.bitfire.davdroid.db.Principal +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.repository.DavCollectionRepository +import at.bitfire.davdroid.repository.DavHomeSetRepository +import at.bitfire.davdroid.settings.Settings +import at.bitfire.davdroid.settings.SettingsManager +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import okhttp3.OkHttpClient +import java.util.logging.Level +import java.util.logging.Logger + +/** + * Used to update the list of synchronizable collections + */ +class HomeSetRefresher @AssistedInject constructor( + @Assisted private val service: Service, + @Assisted private val httpClient: OkHttpClient, + private val db: AppDatabase, + private val logger: Logger, + private val collectionRepository: DavCollectionRepository, + private val homeSetRepository: DavHomeSetRepository, + private val settings: SettingsManager +) { + + @AssistedFactory + interface Factory { + fun create(service: Service, httpClient: OkHttpClient): HomeSetRefresher + } + + /** + * Refreshes home-sets and their collections. + * + * Each stored home-set URL is queried (`PROPFIND`) and its collections are either saved, updated + * or marked as "without home-set" - in case a collection was removed from its home-set. + * + * If a home-set URL in fact points to a collection directly, the collection will be saved with this URL, + * and a null value for it's home-set. Refreshing of collections without home-sets is then handled by [CollectionsWithoutHomeSetRefresher.refreshCollectionsWithoutHomeSet]. + */ + internal fun refreshHomesetsAndTheirCollections() { + val homesets = homeSetRepository.getByServiceBlocking(service.id).associateBy { it.url }.toMutableMap() + for ((homeSetUrl, localHomeset) in homesets) { + logger.fine("Listing home set $homeSetUrl") + + // To find removed collections in this homeset: create a queue from existing collections and remove every collection that + // is successfully rediscovered. If there are collections left, after processing is done, these are marked as "without home-set". + val localHomesetCollections = db.collectionDao() + .getByServiceAndHomeset(service.id, localHomeset.id) + .associateBy { it.url } + .toMutableMap() + + try { + val collectionProperties = ServiceDetectionUtils.collectionQueryProperties(service.type) + DavResource(httpClient, homeSetUrl).propfind(1, *collectionProperties) { response, relation -> + // Note: This callback may be called multiple times ([MultiResponseCallback]) + if (!response.isSuccess()) + return@propfind + + if (relation == Response.HrefRelation.SELF) + // this response is about the home set itself + homeSetRepository.insertOrUpdateByUrlBlocking( + localHomeset.copy( + displayName = response[DisplayName::class.java]?.displayName, + privBind = response[CurrentUserPrivilegeSet::class.java]?.mayBind != false + ) + ) + + // in any case, check whether the response is about a usable collection + var collection = Collection.fromDavResponse(response) ?: return@propfind + collection = collection.copy( + serviceId = service.id, + homeSetId = localHomeset.id, + sync = shouldPreselect(collection, homesets.values), + ownerId = response[Owner::class.java]?.href // save the principal id (collection owner) + ?.let { response.href.resolve(it) } + ?.let { principalUrl -> Principal.fromServiceAndUrl(service, principalUrl) } + ?.let { principal -> db.principalDao().insertOrUpdate(service.id, principal) } + ) + logger.log(Level.FINE, "Found collection", collection) + + // save or update collection if usable (ignore it otherwise) + if (ServiceDetectionUtils.isUsableCollection(service, collection)) + collectionRepository.insertOrUpdateByUrlRememberSync(collection) + + // Remove this collection from queue - because it was found in the home set + localHomesetCollections.remove(collection.url) + } + } catch (e: HttpException) { + // delete home set locally if it was not accessible (40x) + if (e.statusCode in arrayOf(403, 404, 410)) + homeSetRepository.deleteBlocking(localHomeset) + } + + // Mark leftover (not rediscovered) collections from queue as "without home-set" (remove association) + for ((_, collection) in localHomesetCollections) + collectionRepository.insertOrUpdateByUrlRememberSync( + collection.copy(homeSetId = null) + ) + + } + } + + /** + * Whether to preselect the given collection for synchronisation, according to the + * settings [Settings.PRESELECT_COLLECTIONS] (see there for allowed values) and + * [Settings.PRESELECT_COLLECTIONS_EXCLUDED]. + * + * A collection is considered _personal_ if it is found in one of the current-user-principal's home-sets. + * + * Before a collection is pre-selected, we check whether its URL matches the regexp in + * [Settings.PRESELECT_COLLECTIONS_EXCLUDED], in which case *false* is returned. + * + * @param collection the collection to check + * @param homeSets list of personal home-sets + * @return *true* if the collection should be preselected for synchronization; *false* otherwise + */ + internal fun shouldPreselect(collection: Collection, homeSets: Iterable): Boolean { + val shouldPreselect = settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) + + val excluded by lazy { + val excludedRegex = settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) + if (!excludedRegex.isNullOrEmpty()) + Regex(excludedRegex).containsMatchIn(collection.url.toString()) + else + false + } + + return when (shouldPreselect) { + Settings.PRESELECT_COLLECTIONS_ALL -> + // preselect if collection url is not excluded + !excluded + + Settings.PRESELECT_COLLECTIONS_PERSONAL -> + // preselect if is personal (in a personal home-set), but not excluded + homeSets + .filter { homeset -> homeset.personal } + .map { homeset -> homeset.id } + .contains(collection.homeSetId) + && !excluded + + else -> // don't preselect + false + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/PrincipalsRefresher.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/PrincipalsRefresher.kt new file mode 100644 index 0000000..af56b68 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/PrincipalsRefresher.kt @@ -0,0 +1,73 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.servicedetection + +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.dav4jvm.property.webdav.DisplayName +import at.bitfire.dav4jvm.property.webdav.ResourceType +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Principal +import at.bitfire.davdroid.db.Service +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import okhttp3.OkHttpClient +import java.util.logging.Logger + +/** + * Used to update the principals (their current display names) and delete those without collections. + */ +class PrincipalsRefresher @AssistedInject constructor( + @Assisted private val service: Service, + @Assisted private val httpClient: OkHttpClient, + private val db: AppDatabase, + private val logger: Logger +) { + + @AssistedFactory + interface Factory { + fun create(service: Service, httpClient: OkHttpClient): PrincipalsRefresher + } + + /** + * Principal properties to ask the server for. + */ + private val principalProperties = arrayOf( + DisplayName.NAME, + ResourceType.NAME + ) + + /** + * Refreshes the principals (get their current display names). + * Also removes principals which do not own any collections anymore. + */ + fun refreshPrincipals() { + // Refresh principals (collection owner urls) + val principals = db.principalDao().getByService(service.id) + for (oldPrincipal in principals) { + val principalUrl = oldPrincipal.url + logger.fine("Querying principal $principalUrl") + try { + DavResource(httpClient, principalUrl).propfind(0, *principalProperties) { response, _ -> + if (!response.isSuccess()) + return@propfind + Principal.fromDavResponse(service.id, response)?.let { principal -> + logger.fine("Got principal: $principal") + db.principalDao().insertOrUpdate(service.id, principal) + } + } + } catch (e: HttpException) { + logger.info("Principal update failed with response code ${e.statusCode}. principalUrl=$principalUrl") + } + } + + // Delete principals which don't own any collections + db.principalDao().getAllWithoutCollections().forEach { principal -> + db.principalDao().delete(principal) + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt new file mode 100644 index 0000000..a2a7a65 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt @@ -0,0 +1,251 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.servicedetection + +import android.accounts.Account +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.TaskStackBuilder +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.ExistingWorkPolicy +import androidx.work.ForegroundInfo +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.Operation +import androidx.work.OutOfQuotaPolicy +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import at.bitfire.dav4jvm.exception.UnauthorizedException +import at.bitfire.davdroid.R +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.push.PushRegistrationManager +import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker.Companion.ARG_SERVICE_ID +import at.bitfire.davdroid.sync.account.InvalidAccountException +import at.bitfire.davdroid.ui.DebugInfoActivity +import at.bitfire.davdroid.ui.NotificationRegistry +import at.bitfire.davdroid.ui.account.AccountSettingsActivity +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runInterruptible +import java.util.logging.Level +import java.util.logging.Logger + +/** + * Refreshes list of home sets and their respective collections of a service type (CardDAV or CalDAV). + * Called from UI, when user wants to refresh all collections of a service. + * + * Input data: + * + * - [ARG_SERVICE_ID]: service ID + * + * It queries all existing homesets and/or collections and then: + * - updates resources with found properties (overwrites without comparing) + * - adds resources if new ones are detected + * - removes resources if not found 40x (delete locally) + * + * Expedited: yes (always initiated by user) + * + * Long-running: no + * + * @throws IllegalArgumentException when there's no service with the given service ID + */ +@HiltWorker +class RefreshCollectionsWorker @AssistedInject constructor( + @Assisted appContext: Context, + @Assisted workerParams: WorkerParameters, + private val collectionsWithoutHomeSetRefresherFactory: CollectionsWithoutHomeSetRefresher.Factory, + private val homeSetRefresherFactory: HomeSetRefresher.Factory, + private val httpClientBuilder: HttpClient.Builder, + private val logger: Logger, + private val notificationRegistry: NotificationRegistry, + private val principalsRefresherFactory: PrincipalsRefresher.Factory, + private val pushRegistrationManager: PushRegistrationManager, + private val serviceRefresherFactory: ServiceRefresher.Factory, + serviceRepository: DavServiceRepository +): CoroutineWorker(appContext, workerParams) { + + companion object { + + const val ARG_SERVICE_ID = "serviceId" + const val WORKER_TAG = "refreshCollectionsWorker" + + /** + * Uniquely identifies a refresh worker. Useful for stopping work, or querying its state. + * + * @param serviceId what service (CalDAV/CardDAV) the worker is running for + */ + fun workerName(serviceId: Long): String = "$WORKER_TAG-$serviceId" + + /** + * Requests immediate refresh of a given service. If not running already. this will enqueue + * a [RefreshCollectionsWorker]. + * + * @param serviceId serviceId which is to be refreshed + * @return Pair with + * + * 1. worker name, + * 2. operation of [WorkManager.enqueueUniqueWork] (can be used to wait for completion) + * + * @throws IllegalArgumentException when there's no service with this ID + */ + fun enqueue(context: Context, serviceId: Long): Pair { + val name = workerName(serviceId) + val arguments = Data.Builder() + .putLong(ARG_SERVICE_ID, serviceId) + .build() + val workRequest = OneTimeWorkRequestBuilder() + .addTag(name) + .setInputData(arguments) + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .build() + + return Pair( + name, + WorkManager.getInstance(context).enqueueUniqueWork( + name, + ExistingWorkPolicy.KEEP, // if refresh is already running, just continue that one + workRequest + ) + ) + } + + /** + * Observes whether a refresh worker with given service id and state exists. + * + * @param workerName name of worker to find + * @param workState state of worker to match + * + * @return flow that emits `true` if worker with matching state was found (otherwise `false`) + */ + fun existsFlow(context: Context, workerName: String, workState: WorkInfo.State = WorkInfo.State.RUNNING) = + WorkManager.getInstance(context).getWorkInfosForUniqueWorkFlow(workerName).map { workInfoList -> + workInfoList.any { workInfo -> workInfo.state == workState } + } + + } + + val serviceId: Long = inputData.getLong(ARG_SERVICE_ID, -1) + val service = serviceRepository.getBlocking(serviceId) + val account = service?.let { service -> + Account(service.accountName, applicationContext.getString(R.string.account_type)) + } + + override suspend fun doWork(): Result { + if (service == null || account == null) { + logger.warning("Missing service or account with service ID: $serviceId") + return Result.failure() + } + + try { + logger.info("Refreshing ${service.type} collections of service #$service") + + // cancel previous notification + NotificationManagerCompat.from(applicationContext) + .cancel(serviceId.toString(), NotificationRegistry.NOTIFY_REFRESH_COLLECTIONS) + + // create authenticating OkHttpClient (credentials taken from account settings) + httpClientBuilder + .fromAccount(account) + .build() + .use { httpClient -> + runInterruptible { + val httpClient = httpClient.okHttpClient + val refresher = collectionsWithoutHomeSetRefresherFactory.create(service, httpClient) + + // refresh home set list (from principal url) + service.principal?.let { principalUrl -> + logger.fine("Querying principal $principalUrl for home sets") + val serviceRefresher = serviceRefresherFactory.create(service, httpClient) + serviceRefresher.discoverHomesets(principalUrl) + } + + // refresh home sets and their member collections + homeSetRefresherFactory.create(service, httpClient) + .refreshHomesetsAndTheirCollections() + + // also refresh collections without a home set + refresher.refreshCollectionsWithoutHomeSet() + + // Lastly, refresh the principals (collection owners) + val principalsRefresher = principalsRefresherFactory.create(service, httpClient) + principalsRefresher.refreshPrincipals() + } + } + + } catch(e: InvalidAccountException) { + logger.log(Level.SEVERE, "Invalid account", e) + return Result.failure() + } catch (e: UnauthorizedException) { + logger.log(Level.SEVERE, "Not authorized (anymore)", e) + // notify that we need to re-authenticate in the account settings + val settingsIntent = Intent(applicationContext, AccountSettingsActivity::class.java) + .putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account) + notifyRefreshError( + applicationContext.getString(R.string.sync_error_authentication_failed), + settingsIntent + ) + return Result.failure() + } catch(e: Exception) { + logger.log(Level.SEVERE, "Couldn't refresh collection list", e) + + val debugIntent = DebugInfoActivity.IntentBuilder(applicationContext) + .withCause(e) + .withAccount(account) + .build() + notifyRefreshError( + applicationContext.getString(R.string.refresh_collections_worker_refresh_couldnt_refresh), + debugIntent + ) + return Result.failure() + } + + // update push registrations + pushRegistrationManager.update(serviceId) + + // Success + return Result.success() + } + + /** + * Used by WorkManager to show a foreground service notification for expedited jobs on Android <12. + */ + override suspend fun getForegroundInfo(): ForegroundInfo { + val notification = NotificationCompat.Builder(applicationContext, notificationRegistry.CHANNEL_STATUS) + .setSmallIcon(R.drawable.ic_foreground_notify) + .setContentTitle(applicationContext.getString(R.string.foreground_service_notify_title)) + .setContentText(applicationContext.getString(R.string.foreground_service_notify_text)) + .setStyle(NotificationCompat.BigTextStyle()) + .setCategory(NotificationCompat.CATEGORY_STATUS) + .setOngoing(true) + .setPriority(NotificationCompat.PRIORITY_LOW) + .build() + return ForegroundInfo(NotificationRegistry.NOTIFY_SYNC_EXPEDITED, notification) + } + + private fun notifyRefreshError(contentText: String, contentIntent: Intent) { + notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_REFRESH_COLLECTIONS, tag = serviceId.toString()) { + NotificationCompat.Builder(applicationContext, notificationRegistry.CHANNEL_GENERAL) + .setSmallIcon(R.drawable.ic_sync_problem_notify) + .setContentTitle(applicationContext.getString(R.string.refresh_collections_worker_refresh_failed)) + .setContentText(contentText) + .setContentIntent( + TaskStackBuilder.create(applicationContext) + .addNextIntentWithParentStack(contentIntent) + .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + ) + .setSubText(account?.name) + .setCategory(NotificationCompat.CATEGORY_ERROR) + .build() + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/ServiceDetectionUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/ServiceDetectionUtils.kt new file mode 100644 index 0000000..b6f4078 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/ServiceDetectionUtils.kt @@ -0,0 +1,66 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.servicedetection + +import at.bitfire.dav4jvm.Property +import at.bitfire.dav4jvm.property.caldav.CalendarColor +import at.bitfire.dav4jvm.property.caldav.CalendarDescription +import at.bitfire.dav4jvm.property.caldav.CalendarTimezone +import at.bitfire.dav4jvm.property.caldav.CalendarTimezoneId +import at.bitfire.dav4jvm.property.caldav.Source +import at.bitfire.dav4jvm.property.caldav.SupportedCalendarComponentSet +import at.bitfire.dav4jvm.property.carddav.AddressbookDescription +import at.bitfire.dav4jvm.property.push.PushTransports +import at.bitfire.dav4jvm.property.push.Topic +import at.bitfire.dav4jvm.property.webdav.CurrentUserPrivilegeSet +import at.bitfire.dav4jvm.property.webdav.DisplayName +import at.bitfire.dav4jvm.property.webdav.Owner +import at.bitfire.dav4jvm.property.webdav.ResourceType +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.db.ServiceType + +object ServiceDetectionUtils { + + /** + * WebDAV properties to ask for in a PROPFIND request on a collection. + */ + fun collectionQueryProperties(@ServiceType serviceType: String): Array = + arrayOf( // generic WebDAV properties + CurrentUserPrivilegeSet.NAME, + DisplayName.NAME, + Owner.NAME, + ResourceType.NAME, + PushTransports.NAME, // WebDAV-Push + Topic.NAME + ) + when (serviceType) { // service-specific CalDAV/CardDAV properties + Service.TYPE_CARDDAV -> arrayOf( + AddressbookDescription.NAME + ) + + Service.TYPE_CALDAV -> arrayOf( + CalendarColor.NAME, + CalendarDescription.NAME, + CalendarTimezone.NAME, + CalendarTimezoneId.NAME, + SupportedCalendarComponentSet.NAME, + Source.NAME + ) + + else -> throw IllegalArgumentException() + } + + /** + * Finds out whether given collection is usable for synchronization, by checking that either + * + * - CalDAV/CardDAV: service and collection type match, or + * - WebCal: subscription source URL is not empty. + */ + fun isUsableCollection(service: Service, collection: Collection) = + (service.type == Service.TYPE_CARDDAV && collection.type == Collection.TYPE_ADDRESSBOOK) || + (service.type == Service.TYPE_CALDAV && arrayOf(Collection.TYPE_CALENDAR, Collection.TYPE_WEBCAL).contains(collection.type)) || + (collection.type == Collection.TYPE_WEBCAL && collection.source != null) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/ServiceRefresher.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/ServiceRefresher.kt new file mode 100644 index 0000000..3398fb9 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/ServiceRefresher.kt @@ -0,0 +1,178 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.servicedetection + +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.Property +import at.bitfire.dav4jvm.UrlUtils +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.dav4jvm.property.caldav.CalendarHomeSet +import at.bitfire.dav4jvm.property.caldav.CalendarProxyReadFor +import at.bitfire.dav4jvm.property.caldav.CalendarProxyWriteFor +import at.bitfire.dav4jvm.property.carddav.AddressbookHomeSet +import at.bitfire.dav4jvm.property.common.HrefListProperty +import at.bitfire.dav4jvm.property.webdav.DisplayName +import at.bitfire.dav4jvm.property.webdav.GroupMembership +import at.bitfire.dav4jvm.property.webdav.ResourceType +import at.bitfire.davdroid.db.HomeSet +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.repository.DavHomeSetRepository +import at.bitfire.davdroid.util.DavUtils.parent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import java.util.logging.Level +import java.util.logging.Logger + +/** + * ServiceRefresher is used to discover and save home sets of a given service. + */ +class ServiceRefresher @AssistedInject constructor( + @Assisted private val service: Service, + @Assisted private val httpClient: OkHttpClient, + private val logger: Logger, + private val homeSetRepository: DavHomeSetRepository +) { + + @AssistedFactory + interface Factory { + fun create(service: Service, httpClient: OkHttpClient): ServiceRefresher + } + + /** + * Home-set class to use depending on the given service type. + */ + private val homeSetClass: Class = + when (service.type) { + Service.TYPE_CARDDAV -> AddressbookHomeSet::class.java + Service.TYPE_CALDAV -> CalendarHomeSet::class.java + else -> throw IllegalArgumentException() + } + + /** + * Home-set properties to ask for in a PROPFIND request to the principal URL, + * depending on the given service type. + */ + private val homeSetProperties: Array = + arrayOf( // generic WebDAV properties + DisplayName.NAME, + GroupMembership.NAME, + ResourceType.NAME + ) + when (service.type) { // service-specific CalDAV/CardDAV properties + Service.TYPE_CARDDAV -> arrayOf( + AddressbookHomeSet.NAME, + ) + + Service.TYPE_CALDAV -> arrayOf( + CalendarHomeSet.NAME, + CalendarProxyReadFor.NAME, + CalendarProxyWriteFor.NAME + ) + + else -> throw IllegalArgumentException() + } + + /** + * Starting at given principal URL, tries to recursively find and save all user relevant home sets. + * + * @param principalUrl URL of principal to query (user-provided principal or current-user-principal) + * @param level Current recursion level (limited to 0, 1 or 2): + * - 0: We assume found home sets belong to the current-user-principal + * - 1 or 2: We assume found home sets don't directly belong to the current-user-principal + * @param alreadyQueriedPrincipals The HttpUrls of principals which have been queried already, to avoid querying principals more than once. + * @param alreadySavedHomeSets The HttpUrls of home sets which have been saved to database already, to avoid saving home sets + * more than once, which could overwrite the already set "personal" flag with `false`. + * + * @throws java.io.IOException on I/O errors + * @throws HttpException on HTTP errors + * @throws at.bitfire.dav4jvm.exception.DavException on application-level or logical errors + */ + internal fun discoverHomesets( + principalUrl: HttpUrl, + level: Int = 0, + alreadyQueriedPrincipals: MutableSet = mutableSetOf(), + alreadySavedHomeSets: MutableSet = mutableSetOf() + ) { + logger.fine("Discovering homesets of $principalUrl") + val relatedResources = mutableSetOf() + + // Query the URL + val principal = DavResource(httpClient, principalUrl) + val personal = level == 0 + try { + principal.propfind(0, *homeSetProperties) { davResponse, _ -> + alreadyQueriedPrincipals += davResponse.href + + // If response holds home sets, save them + davResponse[homeSetClass]?.let { homeSets -> + for (homeSetHref in homeSets.hrefs) + principal.location.resolve(homeSetHref)?.let { homesetUrl -> + val resolvedHomeSetUrl = UrlUtils.withTrailingSlash(homesetUrl) + if (!alreadySavedHomeSets.contains(resolvedHomeSetUrl)) { + homeSetRepository.insertOrUpdateByUrlBlocking( + // HomeSet is considered personal if this is the outer recursion call, + // This is because we assume the first call to query the current-user-principal + // Note: This is not be be confused with the DAV:owner attribute. Home sets can be owned by + // other principals while still being considered "personal" (belonging to the current-user-principal) + // and an owned home set need not always be personal either. + HomeSet(0, service.id, personal, resolvedHomeSetUrl) + ) + alreadySavedHomeSets += resolvedHomeSetUrl + } + } + } + + // Add related principals to be queried afterwards + if (personal) { + val relatedResourcesTypes = listOf( + // current resource is a read/write-proxy for other principals + CalendarProxyReadFor::class.java, + CalendarProxyWriteFor::class.java, + // current resource is a member of a group (principal that can also have proxies) + GroupMembership::class.java + ) + for (type in relatedResourcesTypes) + davResponse[type]?.let { + for (href in it.hrefs) + principal.location.resolve(href)?.let { url -> + relatedResources += url + } + } + } + + // If current resource is a calendar-proxy-read/write, it's likely that its parent is a principal, too. + davResponse[ResourceType::class.java]?.let { resourceType -> + val proxyProperties = arrayOf( + ResourceType.CALENDAR_PROXY_READ, + ResourceType.CALENDAR_PROXY_WRITE, + ) + if (proxyProperties.any { resourceType.types.contains(it) }) + relatedResources += davResponse.href.parent() + } + } + } catch (e: HttpException) { + if (e.isClientError) + logger.log(Level.INFO, "Ignoring Client Error 4xx while looking for ${service.type} home sets", e) + else + throw e + } + + // query related resources + if (level <= 1) + for (resource in relatedResources) + if (alreadyQueriedPrincipals.contains(resource)) + logger.warning("$resource already queried, skipping") + else + discoverHomesets( + principalUrl = resource, + level = level + 1, + alreadyQueriedPrincipals = alreadyQueriedPrincipals, + alreadySavedHomeSets = alreadySavedHomeSets + ) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt new file mode 100644 index 0000000..52ac7bb --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt @@ -0,0 +1,443 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ +package at.bitfire.davdroid.settings + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import android.os.Bundle +import android.os.Looper +import androidx.annotation.WorkerThread +import androidx.core.os.bundleOf +import at.bitfire.davdroid.R +import at.bitfire.davdroid.settings.AccountSettings.Companion.CREDENTIALS_LOCK +import at.bitfire.davdroid.settings.AccountSettings.Companion.CREDENTIALS_LOCK_AT_LOGIN_AND_SETTINGS +import at.bitfire.davdroid.settings.migration.AccountSettingsMigration +import at.bitfire.davdroid.sync.AutomaticSyncManager +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.sync.account.InvalidAccountException +import at.bitfire.davdroid.sync.account.setAndVerifyUserData +import at.bitfire.davdroid.util.SensitiveString.Companion.toSensitiveString +import at.bitfire.davdroid.util.trimToNull +import at.bitfire.vcard4android.GroupMethod +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.qualifiers.ApplicationContext +import net.openid.appauth.AuthState +import java.util.Collections +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Provider + +/** + * Manages settings of an account. + * + * **Must not be called from main thread as it uses blocking I/O and may run migrations.** + * + * @param account account to take settings from + * @param abortOnMissingMigration whether to throw an [IllegalArgumentException] when migrations are missing (useful for testing) + * + * @throws InvalidAccountException on construction when the account doesn't exist (anymore) + * @throws IllegalArgumentException when the account is not a DAVx5 account or migrations are missing and [abortOnMissingMigration] is set + */ +@WorkerThread +class AccountSettings @AssistedInject constructor( + @Assisted val account: Account, + @Assisted val abortOnMissingMigration: Boolean, + private val automaticSyncManager: AutomaticSyncManager, + @ApplicationContext private val context: Context, + private val logger: Logger, + private val migrations: Map>, + private val settingsManager: SettingsManager +) { + + @AssistedFactory + interface Factory { + /** + * **Must not be called on main thread. Throws exceptions!** See [AccountSettings] for details. + */ + @WorkerThread + fun create(account: Account, abortOnMissingMigration: Boolean = false): AccountSettings + } + + init { + if (Looper.getMainLooper() == Looper.myLooper()) + throw IllegalThreadStateException("AccountSettings may not be used on main thread") + } + + val accountManager: AccountManager = AccountManager.get(context) + init { + val allowedAccountTypes = arrayOf( + context.getString(R.string.account_type), + "at.bitfire.davdroid.test" // R.strings.account_type_test in androidTest + ) + if (!allowedAccountTypes.contains(account.type)) + throw IllegalArgumentException("Invalid account type for AccountSettings(): ${account.type}") + + // synchronize because account migration must only be run one time + synchronized(currentlyUpdating) { + if (currentlyUpdating.contains(account)) + logger.warning("AccountSettings created during migration of $account – not running update()") + else { + val versionStr = accountManager.getUserData(account, KEY_SETTINGS_VERSION) ?: throw InvalidAccountException(account) + var version = 0 + try { + version = Integer.parseInt(versionStr) + } catch (e: NumberFormatException) { + logger.log(Level.SEVERE, "Invalid account version: $versionStr", e) + } + logger.fine("Account ${account.name} has version $version, current version: $CURRENT_VERSION") + + if (version < CURRENT_VERSION) { + currentlyUpdating += account + try { + update(version, abortOnMissingMigration) + } finally { + currentlyUpdating -= account + } + } + } + } + } + + + // authentication settings + + fun credentials() = Credentials( + accountManager.getUserData(account, KEY_USERNAME), + accountManager.getPassword(account)?.toSensitiveString(), + + accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS), + + accountManager.getUserData(account, KEY_AUTH_STATE)?.let { json -> + AuthState.jsonDeserialize(json) + } + ) + + fun credentials(credentials: Credentials) { + // Basic/Digest auth + accountManager.setAndVerifyUserData(account, KEY_USERNAME, credentials.username) + accountManager.setPassword(account, credentials.password?.asString()) + + // client certificate + accountManager.setAndVerifyUserData(account, KEY_CERTIFICATE_ALIAS, credentials.certificateAlias) + + // OAuth + credentials.authState?.let { authState -> + updateAuthState(authState) + } + } + + fun updateAuthState(authState: AuthState) { + accountManager.setAndVerifyUserData(account, KEY_AUTH_STATE, authState.jsonSerializeString()) + } + + /** + * Returns whether users can modify credentials from the account settings screen. + * Checks the value of [CREDENTIALS_LOCK] to be `0` or not equal to [CREDENTIALS_LOCK_AT_LOGIN_AND_SETTINGS]. + */ + fun changingCredentialsAllowed(): Boolean { + val credentialsLock = settingsManager.getIntOrNull(CREDENTIALS_LOCK) + return credentialsLock == null || credentialsLock != CREDENTIALS_LOCK_AT_LOGIN_AND_SETTINGS + } + + + // sync. settings + + /** + * Gets the currently set sync interval for this account and data type in seconds. + * + * @param dataType data type of desired sync interval + * @return sync interval in seconds, or `null` if not set (not applicable or only manual sync) + */ + fun getSyncInterval(dataType: SyncDataType): Long? { + val key = when (dataType) { + SyncDataType.CONTACTS -> KEY_SYNC_INTERVAL_ADDRESSBOOKS + SyncDataType.EVENTS -> KEY_SYNC_INTERVAL_CALENDARS + SyncDataType.TASKS -> KEY_SYNC_INTERVAL_TASKS + } + val seconds = accountManager.getUserData(account, key)?.toLong() + return when (seconds) { + null -> settingsManager.getLongOrNull(Settings.DEFAULT_SYNC_INTERVAL) // no setting → default value + SYNC_INTERVAL_MANUALLY -> null // manual sync + else -> seconds + } + } + + /** + * Sets the sync interval for the given data type and updates the automatic sync. + * + * @param dataType data type of the sync interval to set + * @param seconds sync interval in seconds; _null_ for no periodic sync + */ + fun setSyncInterval(dataType: SyncDataType, seconds: Long?) { + val key = when (dataType) { + SyncDataType.CONTACTS -> KEY_SYNC_INTERVAL_ADDRESSBOOKS + SyncDataType.EVENTS -> KEY_SYNC_INTERVAL_CALENDARS + SyncDataType.TASKS -> KEY_SYNC_INTERVAL_TASKS + } + val newValue = seconds ?: SYNC_INTERVAL_MANUALLY + accountManager.setAndVerifyUserData(account, key, newValue.toString()) + + automaticSyncManager.updateAutomaticSync(account, dataType) + } + + fun getSyncWifiOnly() = + if (settingsManager.containsKey(KEY_WIFI_ONLY)) + settingsManager.getBoolean(KEY_WIFI_ONLY) + else + accountManager.getUserData(account, KEY_WIFI_ONLY) != null + + fun setSyncWiFiOnly(wiFiOnly: Boolean) { + accountManager.setAndVerifyUserData(account, KEY_WIFI_ONLY, if (wiFiOnly) "1" else null) + automaticSyncManager.updateAutomaticSync(account) + } + + fun getSyncWifiOnlySSIDs(): List? = + if (getSyncWifiOnly()) { + val strSsids = if (settingsManager.containsKey(KEY_WIFI_ONLY_SSIDS)) + settingsManager.getString(KEY_WIFI_ONLY_SSIDS) + else + accountManager.getUserData(account, KEY_WIFI_ONLY_SSIDS) + strSsids?.split(',') + } else + null + fun setSyncWifiOnlySSIDs(ssids: List?) = + accountManager.setAndVerifyUserData(account, KEY_WIFI_ONLY_SSIDS, ssids?.joinToString(",").trimToNull()) + + fun getIgnoreVpns(): Boolean = + when (accountManager.getUserData(account, KEY_IGNORE_VPNS)) { + null -> settingsManager.getBoolean(KEY_IGNORE_VPNS) + "0" -> false + else -> true + } + + fun setIgnoreVpns(ignoreVpns: Boolean) = + accountManager.setAndVerifyUserData(account, KEY_IGNORE_VPNS, if (ignoreVpns) "1" else "0") + + + // CalDAV settings + + fun getTimeRangePastDays(): Int? { + val strDays = accountManager.getUserData(account, KEY_TIME_RANGE_PAST_DAYS) + return if (strDays != null) { + val days = strDays.toInt() + if (days < 0) + null + else + days + } else + DEFAULT_TIME_RANGE_PAST_DAYS + } + + fun setTimeRangePastDays(days: Int?) = + accountManager.setAndVerifyUserData(account, KEY_TIME_RANGE_PAST_DAYS, (days ?: -1).toString()) + + /** + * Takes the default alarm setting (in this order) from + * + * 1. the local account settings + * 2. the settings provider (unless the value is -1 there). + * + * @return A default reminder shall be created this number of minutes before the start of every + * non-full-day event without reminder. *null*: No default reminders shall be created. + */ + fun getDefaultAlarm() = + accountManager.getUserData(account, KEY_DEFAULT_ALARM)?.toInt() ?: + settingsManager.getIntOrNull(KEY_DEFAULT_ALARM)?.takeIf { it != -1 } + + /** + * Sets the default alarm value in the local account settings, if the new value differs + * from the value of the settings provider. If the new value is the same as the value of + * the settings provider, the local setting will be deleted, so that the settings provider + * value applies. + * + * @param minBefore The number of minutes a default reminder shall be created before the + * start of every non-full-day event without reminder. *null*: No default reminders shall be created. + */ + fun setDefaultAlarm(minBefore: Int?) = + accountManager.setAndVerifyUserData(account, KEY_DEFAULT_ALARM, + if (minBefore == settingsManager.getIntOrNull(KEY_DEFAULT_ALARM)?.takeIf { it != -1 }) + null + else + minBefore?.toString()) + + fun getManageCalendarColors() = + if (settingsManager.containsKey(KEY_MANAGE_CALENDAR_COLORS)) + settingsManager.getBoolean(KEY_MANAGE_CALENDAR_COLORS) + else + accountManager.getUserData(account, KEY_MANAGE_CALENDAR_COLORS) == null + fun setManageCalendarColors(manage: Boolean) = + accountManager.setAndVerifyUserData(account, KEY_MANAGE_CALENDAR_COLORS, if (manage) null else "0") + + fun getEventColors() = + if (settingsManager.containsKey(KEY_EVENT_COLORS)) + settingsManager.getBoolean(KEY_EVENT_COLORS) + else + accountManager.getUserData(account, KEY_EVENT_COLORS) != null + fun setEventColors(useColors: Boolean) = + accountManager.setAndVerifyUserData(account, KEY_EVENT_COLORS, if (useColors) "1" else null) + + // CardDAV settings + + fun getGroupMethod(): GroupMethod { + val name = settingsManager.getString(KEY_CONTACT_GROUP_METHOD) ?: + accountManager.getUserData(account, KEY_CONTACT_GROUP_METHOD) + if (name != null) + try { + return GroupMethod.valueOf(name) + } + catch (_: IllegalArgumentException) { + } + return GroupMethod.GROUP_VCARDS + } + + fun setGroupMethod(method: GroupMethod) { + accountManager.setAndVerifyUserData(account, KEY_CONTACT_GROUP_METHOD, method.name) + } + + + // UI settings + + /** + * Whether to show only personal collections in the UI + * + * @return *true* if only personal collections shall be shown; *false* otherwise + */ + fun getShowOnlyPersonal(): Boolean = when (settingsManager.getIntOrNull(KEY_SHOW_ONLY_PERSONAL)) { + 0 -> false + 1 -> true + else /* including -1 */ -> accountManager.getUserData(account, KEY_SHOW_ONLY_PERSONAL) != null + } + + /** + * Whether the user shall be able to change the setting (= setting not locked) + * + * @return *true* if the setting is locked; *false* otherwise + */ + fun getShowOnlyPersonalLocked(): Boolean = when (settingsManager.getIntOrNull(KEY_SHOW_ONLY_PERSONAL)) { + 0, 1 -> true + else /* including -1 */ -> false + } + + fun setShowOnlyPersonal(showOnlyPersonal: Boolean) { + accountManager.setAndVerifyUserData(account, KEY_SHOW_ONLY_PERSONAL, if (showOnlyPersonal) "1" else null) + } + + + // update from previous account settings + + private fun update(baseVersion: Int, abortOnMissingMigration: Boolean) { + for (toVersion in baseVersion+1 ..CURRENT_VERSION) { + val fromVersion = toVersion - 1 + logger.info("Updating account ${account.name} settings version $fromVersion → $toVersion") + + val migration = migrations[toVersion] + if (migration == null) { + logger.severe("No AccountSettings migration $fromVersion → $toVersion") + if (abortOnMissingMigration) + throw IllegalArgumentException("Missing AccountSettings migration $fromVersion → $toVersion") + } else { + try { + migration.get().migrate(account) + + logger.info("Account settings version update to $toVersion successful") + accountManager.setAndVerifyUserData(account, KEY_SETTINGS_VERSION, toVersion.toString()) + } catch (e: Exception) { + logger.log(Level.SEVERE, "Couldn't run AccountSettings migration $fromVersion → $toVersion", e) + } + } + } + } + + + companion object { + + const val CURRENT_VERSION = 20 + const val KEY_SETTINGS_VERSION = "version" + + const val KEY_SYNC_INTERVAL_ADDRESSBOOKS = "sync_interval_addressbooks" + const val KEY_SYNC_INTERVAL_CALENDARS = "sync_interval_calendars" + + /** Stores the tasks sync interval (in seconds) so that it can be set again when the provider is switched */ + const val KEY_SYNC_INTERVAL_TASKS = "sync_interval_tasks" + + const val KEY_USERNAME = "user_name" + const val KEY_CERTIFICATE_ALIAS = "certificate_alias" + + const val CREDENTIALS_LOCK = "login_credentials_lock" + const val CREDENTIALS_LOCK_NO_LOCK = 0 + const val CREDENTIALS_LOCK_AT_LOGIN = 1 + const val CREDENTIALS_LOCK_AT_LOGIN_AND_SETTINGS = 2 + + /** OAuth [AuthState] (serialized as JSON) */ + const val KEY_AUTH_STATE = "auth_state" + + const val KEY_WIFI_ONLY = "wifi_only" // sync on WiFi only (default: false) + const val KEY_WIFI_ONLY_SSIDS = "wifi_only_ssids" // restrict sync to specific WiFi SSIDs + const val KEY_IGNORE_VPNS = "ignore_vpns" // ignore vpns at connection detection + + /** Time range limitation to the past [in days]. Values: + * + * - null: default value (DEFAULT_TIME_RANGE_PAST_DAYS) + * - <0 (typically -1): no limit + * - n>0: entries more than n days in the past won't be synchronized + */ + const val KEY_TIME_RANGE_PAST_DAYS = "time_range_past_days" + const val DEFAULT_TIME_RANGE_PAST_DAYS = 90 + + /** + * Whether a default alarm shall be assigned to received events/tasks which don't have an alarm. + * Value can be null (no default alarm) or an integer (default alarm shall be created this + * number of minutes before the event/task). + */ + const val KEY_DEFAULT_ALARM = "default_alarm" + + /** Whether DAVx5 sets the local calendar color to the value from service DB at every sync + value = *null* (not existing): true (default); + "0" false */ + const val KEY_MANAGE_CALENDAR_COLORS = "manage_calendar_colors" + + /** Whether DAVx5 populates and uses CalendarContract.Colors + value = *null* (not existing) false (default); + "1" true */ + const val KEY_EVENT_COLORS = "event_colors" + + /** Contact group method: + *null (not existing)* groups as separate vCards (default); + "CATEGORIES" groups are per-contact CATEGORIES + */ + const val KEY_CONTACT_GROUP_METHOD = "contact_group_method" + + /** UI preference: Show only personal collections + value = *null* (not existing) show all collections (default); + "1" show only personal collections */ + const val KEY_SHOW_ONLY_PERSONAL = "show_only_personal" + + internal const val SYNC_INTERVAL_MANUALLY = -1L + + /** Static property to remember which AccountSettings updates/migrations are currently running */ + val currentlyUpdating = Collections.synchronizedSet(mutableSetOf()) + + fun initialUserData(credentials: Credentials?): Bundle { + val bundle = bundleOf(KEY_SETTINGS_VERSION to CURRENT_VERSION.toString()) + + if (credentials != null) { + if (credentials.username != null) + bundle.putString(KEY_USERNAME, credentials.username) + + if (credentials.certificateAlias != null) + bundle.putString(KEY_CERTIFICATE_ALIAS, credentials.certificateAlias) + + if (credentials.authState != null) + bundle.putString(KEY_AUTH_STATE, credentials.authState.jsonSerializeString()) + } + + return bundle + } + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/Credentials.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/Credentials.kt new file mode 100644 index 0000000..5278419 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/Credentials.kt @@ -0,0 +1,46 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings + +import at.bitfire.davdroid.util.SensitiveString +import net.openid.appauth.AuthState + +/** + * Represents credentials that are used to authenticate against a CalDAV/CardDAV/WebDAV server. + * + * Note: [authState] can change from request to request, so make sure that you have an up-to-date + * copy when using it. + */ +data class Credentials( + /** username for Basic / Digest auth */ + val username: String? = null, + /** password for Basic / Digest auth */ + val password: SensitiveString? = null, + + /** alias of an client certificate that is present on the system */ + val certificateAlias: String? = null, + + /** OAuth authorization state */ + val authState: AuthState? = null +) { + + override fun toString(): String { + val s = mutableListOf() + + if (username != null) + s += "userName=$username" + if (password != null) + s += "password=*****" + + if (certificateAlias != null) + s += "certificateAlias=$certificateAlias" + + if (authState != null) // contains sensitive information (refresh token, access token) + s += "authState=${authState.jsonSerializeString()}" + + return "Credentials(" + s.joinToString(", ") + ")" + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/DefaultsProvider.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/DefaultsProvider.kt new file mode 100644 index 0000000..4755bcc --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/DefaultsProvider.kt @@ -0,0 +1,98 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings + +import at.bitfire.davdroid.TextTable +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntKey +import dagger.multibindings.IntoMap +import java.io.Writer +import javax.inject.Inject + +class DefaultsProvider @Inject constructor(): SettingsProvider { + + val booleanDefaults = mutableMapOf( + Pair(Settings.DISTRUST_SYSTEM_CERTIFICATES, false), + Pair(Settings.FORCE_READ_ONLY_ADDRESSBOOKS, false), + Pair(Settings.IGNORE_VPN_NETWORK_CAPABILITY, true) + ) + + val intDefaults = mapOf( + Pair(Settings.PRESELECT_COLLECTIONS, Settings.PRESELECT_COLLECTIONS_NONE), + Pair(Settings.PROXY_TYPE, Settings.PROXY_TYPE_SYSTEM), + Pair(Settings.PROXY_PORT, 9050) // Orbot SOCKS + ) + + val longDefaults = mapOf( + Pair(Settings.DEFAULT_SYNC_INTERVAL, 4*3600) /* 4 hours */ + ) + + val stringDefaults = mapOf( + Pair(Settings.PROXY_HOST, "localhost"), + Pair(Settings.PRESELECT_COLLECTIONS_EXCLUDED, "/z-app-generated--contactsinteraction--recent/") // Nextcloud "Recently Contacted" address book + ) + + + override fun canWrite() = false + + override fun close() { + // no resources to close + } + + override fun setOnChangeListener(listener: SettingsProvider.OnChangeListener) { + // default settings never change + } + + override fun forceReload() { + // default settings never change + } + + + override fun contains(key: String) = + booleanDefaults.containsKey(key) || + intDefaults.containsKey(key) || + longDefaults.containsKey(key) || + stringDefaults.containsKey(key) + + override fun getBoolean(key: String) = booleanDefaults[key] + override fun getInt(key: String) = intDefaults[key] + override fun getLong(key: String) = longDefaults[key] + override fun getString(key: String) = stringDefaults[key] + + override fun putBoolean(key: String, value: Boolean?) = throw NotImplementedError() + override fun putInt(key: String, value: Int?) = throw NotImplementedError() + override fun putLong(key: String, value: Long?) = throw NotImplementedError() + override fun putString(key: String, value: String?) = throw NotImplementedError() + + override fun remove(key: String) = throw NotImplementedError() + + + override fun dump(writer: Writer) { + val strValues = mutableMapOf() + strValues.putAll(booleanDefaults.mapValues { (_, value) -> value.toString() }) + strValues.putAll(intDefaults.mapValues { (_, value) -> value.toString() }) + strValues.putAll(longDefaults.mapValues { (_, value) -> value.toString() }) + strValues.putAll(stringDefaults) + + val table = TextTable("Setting", "Value") + for ((key, value) in strValues.toSortedMap()) + table.addLine(key, value) + writer.write(table.toString()) + } + + + @Module + @InstallIn(SingletonComponent::class) + abstract class DefaultsProviderModule { + @Binds + @IntoMap + @IntKey(/* priority */ 0) + abstract fun defaultsProvider(impl: DefaultsProvider): SettingsProvider + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/Settings.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/Settings.kt new file mode 100644 index 0000000..985ee85 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/Settings.kt @@ -0,0 +1,69 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings + +import androidx.appcompat.app.AppCompatDelegate +import at.bitfire.davdroid.settings.Settings.PRESELECT_COLLECTIONS_EXCLUDED + +object Settings { + + const val DISTRUST_SYSTEM_CERTIFICATES = "distrust_system_certs" + + const val PROXY_TYPE = "proxy_type" // Integer + const val PROXY_TYPE_SYSTEM = -1 + const val PROXY_TYPE_NONE = 0 + const val PROXY_TYPE_HTTP = 1 + const val PROXY_TYPE_SOCKS = 2 + const val PROXY_HOST = "proxy_host" // String + const val PROXY_PORT = "proxy_port" // Integer + + /** + * Whether to ignore VPNs at internet connection detection, true by default because VPN connections + * seem to include "VALIDATED" by default even without actual internet connection + */ + const val IGNORE_VPN_NETWORK_CAPABILITY = "ignore_vpns" // Boolean + + /** + * Default sync interval (Long), in seconds. + * Used to initialize an account. + */ + const val DEFAULT_SYNC_INTERVAL = "default_sync_interval" + + /** + * Preferred theme (light/dark). Value must be one of [AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM] + * (default if setting is missing), [AppCompatDelegate.MODE_NIGHT_NO] or [AppCompatDelegate.MODE_NIGHT_YES]. + */ + const val PREFERRED_THEME = "preferred_theme" + const val PREFERRED_THEME_DEFAULT = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + + /** + * Selected tasks app. When at least one tasks app is installed, this setting is set to its sync authority. + * In case of multiple available tasks app, the user can choose one and this setting will reflect the selected one. + * + * This setting may even be set if the corresponding tasks app is not installed because it only reflects the user's choice. + */ + const val SELECTED_TASKS_PROVIDER = "preferred_tasks_provider" + + /** whether collections are automatically selected for synchronization after their initial detection */ + const val PRESELECT_COLLECTIONS = "preselect_collections" + /** collections are not automatically selected for synchronization */ + const val PRESELECT_COLLECTIONS_NONE = 0 + /** all collections (except those matching [PRESELECT_COLLECTIONS_EXCLUDED]) are automatically selected for synchronization */ + const val PRESELECT_COLLECTIONS_ALL = 1 + /** personal collections (except those matching [PRESELECT_COLLECTIONS_EXCLUDED]) are automatically selected for synchronization */ + const val PRESELECT_COLLECTIONS_PERSONAL = 2 + + /** regular expression to match URLs of collections to be excluded from pre-selection */ + const val PRESELECT_COLLECTIONS_EXCLUDED = "preselect_collections_excluded" + + + /** whether all address books are forced to be read-only */ + const val FORCE_READ_ONLY_ADDRESSBOOKS = "force_read_only_addressbooks" + + + /** max. number of accounts */ + const val MAX_ACCOUNTS = "max_accounts" + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/SettingsManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/SettingsManager.kt new file mode 100644 index 0000000..72cff51 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/SettingsManager.kt @@ -0,0 +1,213 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings + +import android.util.NoSuchPropertyException +import androidx.annotation.AnyThread +import androidx.annotation.VisibleForTesting +import at.bitfire.davdroid.settings.SettingsManager.OnChangeListener +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import java.io.Writer +import java.lang.ref.WeakReference +import java.util.LinkedList +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Settings manager which coordinates [SettingsProvider]s to read/write + * application settings. + */ +@Singleton +class SettingsManager @Inject constructor( + private val logger: Logger, + providerMap: Map +): SettingsProvider.OnChangeListener { + + private val providers = LinkedList() + private var writeProvider: SettingsProvider? = null + + private val observers = LinkedList>() + + init { + providerMap // get providers from Hilt + .toSortedMap() // sort by Int key + .values.reversed() // take reverse-sorted values (because high priority numbers shall be processed first) + .forEach { provider -> + logger.info("Loading settings provider: ${provider.javaClass.name}") + + // register for changes + provider.setOnChangeListener(this) + + // add to list of available providers + providers += provider + } + + // settings will be written to the first writable provider + writeProvider = providers.firstOrNull { it.canWrite() } + logger.info("Changed settings are handled by $writeProvider") + } + + /** + * Requests all providers to reload their settings. + */ + @AnyThread + fun forceReload() { + for (provider in providers) + provider.forceReload() + + // notify possible listeners + onSettingsChanged(null) + } + + + /*** OBSERVERS ***/ + + fun addOnChangeListener(observer: OnChangeListener) { + synchronized(observers) { + observers += WeakReference(observer) + } + } + + fun removeOnChangeListener(observer: OnChangeListener) { + synchronized(observers) { + observers.removeAll { it.get() == null || it.get() == observer } + } + } + + /** + * Notifies registered listeners about changes in the configuration. + * Called by config providers when settings have changed. + */ + @AnyThread + override fun onSettingsChanged(key: String?) { + synchronized(observers) { + for (observer in observers.mapNotNull { it.get() }) + observer.onSettingsChanged() + } + } + + /** + * Returns a Flow that + * + * - always emits the initial value of the setting, and then + * - emits the new value whenever the setting changes. + * + * @param getValue used to determine the current value of the setting + */ + @VisibleForTesting + internal fun observerFlow(getValue: () -> T): Flow = callbackFlow { + // emit value on changes + val listener = OnChangeListener { + trySend(getValue()) + } + addOnChangeListener(listener) + + // get current value and emit it as first state + trySend(getValue()) + + // wait and clean up + awaitClose { removeOnChangeListener(listener) } + } + + + /*** SETTINGS ACCESS ***/ + + fun containsKey(key: String) = providers.any { it.contains(key) } + fun containsKeyFlow(key: String): Flow = observerFlow { containsKey(key) } + + private fun getValue(key: String, reader: (SettingsProvider) -> T?): T? { + logger.fine("Looking up setting $key") + val result: T? = null + for (provider in providers) + try { + val value = reader(provider) + logger.finer("${provider::class.java.simpleName}: $key = $value") + if (value != null) { + logger.fine("Looked up setting $key -> $value") + return value + } + } catch(e: Exception) { + logger.log(Level.SEVERE, "Couldn't read setting from $provider", e) + } + logger.fine("Looked up setting $key -> no result") + return result + } + + fun getBooleanOrNull(key: String): Boolean? = getValue(key) { provider -> provider.getBoolean(key) } + fun getBoolean(key: String): Boolean = getBooleanOrNull(key) ?: throw NoSuchPropertyException(key) + fun getBooleanFlow(key: String): Flow = observerFlow { getBooleanOrNull(key) } + fun getBooleanFlow(key: String, defaultValue: Boolean): Flow = observerFlow { getBooleanOrNull(key) ?: defaultValue } + + fun getIntOrNull(key: String): Int? = getValue(key) { provider -> provider.getInt(key) } + fun getInt(key: String): Int = getIntOrNull(key) ?: throw NoSuchPropertyException(key) + fun getIntFlow(key: String): Flow = observerFlow { getIntOrNull(key) } + + fun getLongOrNull(key: String): Long? = getValue(key) { provider -> provider.getLong(key) } + fun getLong(key: String) = getLongOrNull(key) ?: throw NoSuchPropertyException(key) + + fun getString(key: String) = getValue(key) { provider -> provider.getString(key) } + fun getStringFlow(key: String): Flow = observerFlow { getString(key) } + + + fun isWritable(key: String): Boolean { + for (provider in providers) { + if (provider.canWrite()) + return true + else if (provider.contains(key)) + // non-writeable provider contains this key -> setting will always be provided by this read-only provider + return false + } + return false + } + + private fun putValue(key: String, value: T?, writer: (SettingsProvider) -> Unit) { + logger.fine("Trying to write setting $key = $value") + val provider = writeProvider ?: return + try { + writer(provider) + } catch (e: Exception) { + logger.log(Level.SEVERE, "Couldn't write setting to $writeProvider", e) + } + } + + fun putBoolean(key: String, value: Boolean?) = + putValue(key, value) { provider -> provider.putBoolean(key, value) } + + fun putInt(key: String, value: Int?) = + putValue(key, value) { provider -> provider.putInt(key, value) } + + fun putLong(key: String, value: Long?) = + putValue(key, value) { provider -> provider.putLong(key, value) } + + fun putString(key: String, value: String?) = + putValue(key, value) { provider -> provider.putString(key, value) } + + fun remove(key: String) = putString(key, null) + + + /*** HELPERS ***/ + + fun dump(writer: Writer) { + for ((idx, provider) in providers.withIndex()) { + writer.write("${idx + 1}. ${provider::class.java.simpleName} canWrite=${provider.canWrite()}\n") + provider.dump(writer) + } + } + + + fun interface OnChangeListener { + /** + * Will be called when something has changed in a [SettingsProvider]. + * May run in worker thread! + */ + @AnyThread + fun onSettingsChanged() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/SettingsProvider.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/SettingsProvider.kt new file mode 100644 index 0000000..b43405c --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/SettingsProvider.kt @@ -0,0 +1,71 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings + +import androidx.annotation.AnyThread +import java.io.Writer + +/** + * Defines a settings provider, which provides settings from a certain source + * to the [SettingsManager]. + * + * Implementations must be thread-safe and synchronize get/put operations on their own. + */ +interface SettingsProvider { + + fun interface OnChangeListener { + /** + * Called when a setting has changed. + * + * @param key The key of the setting that has changed, or null if the key is not + * available. In this case, the listener should reload all settings. + */ + fun onSettingsChanged(key: String?) + } + + + /** + * Whether this provider can write settings. + * + * If this method returns false, the put...() methods will never be called for this provider. + * + * @return true = this provider provides read/write settings; + * false = this provider provides read-only settings + */ + fun canWrite(): Boolean + + /** + * Closes the provider and releases resources. + */ + fun close() + + /** + * Sets an on-changed listener. The provider calls the listener whenever a setting + * has changed. + */ + fun setOnChangeListener(listener: OnChangeListener) + + @AnyThread + fun forceReload() + + + fun contains(key: String): Boolean + + fun getBoolean(key: String): Boolean? + fun getInt(key: String): Int? + fun getLong(key: String): Long? + fun getString(key: String): String? + + fun putBoolean(key: String, value: Boolean?) + fun putInt(key: String, value: Int?) + fun putLong(key: String, value: Long?) + fun putString(key: String, value: String?) + + fun remove(key: String) + + + fun dump(writer: Writer) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/SharedPreferencesProvider.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/SharedPreferencesProvider.kt new file mode 100644 index 0000000..aa5a8b9 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/SharedPreferencesProvider.kt @@ -0,0 +1,155 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings + +import android.content.Context +import android.content.Context.MODE_PRIVATE +import android.content.SharedPreferences +import androidx.core.content.edit +import androidx.preference.PreferenceManager +import at.bitfire.davdroid.TextTable +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntKey +import dagger.multibindings.IntoMap +import java.io.Writer +import java.util.logging.Logger +import javax.inject.Inject + +class SharedPreferencesProvider @Inject constructor( + @ApplicationContext val context: Context, + private val logger: Logger +): SettingsProvider, SharedPreferences.OnSharedPreferenceChangeListener { + + companion object { + private const val META_VERSION = "version" + private const val CURRENT_VERSION = 0 + } + + private var onChangeListener: SettingsProvider.OnChangeListener? = null + private val preferences = PreferenceManager.getDefaultSharedPreferences(context) + + init { + val meta = context.getSharedPreferences("meta", MODE_PRIVATE) + val version = meta.getInt(META_VERSION, -1) + if (version == -1) { + // first call, check whether to migrate from SQLite database (DAVdroid <1.9) + firstCall() + meta.edit { + putInt(META_VERSION, CURRENT_VERSION) + } + } + + preferences.registerOnSharedPreferenceChangeListener(this) + } + + override fun forceReload() { + } + + override fun close() { + preferences.unregisterOnSharedPreferenceChangeListener(this) + } + + override fun setOnChangeListener(listener: SettingsProvider.OnChangeListener) { + onChangeListener = listener + } + + override fun canWrite() = true + + + + override fun contains(key: String) = preferences.contains(key) + + private fun getValue(key: String, reader: (SharedPreferences) -> T): T? = + try { + if (preferences.contains(key)) + reader(preferences) + else + null + } catch(e: ClassCastException) { + null + } + + override fun getBoolean(key: String) = + getValue(key) { preferences -> preferences.getBoolean(key, /* will never be used: */ false) } + + override fun getInt(key: String) = + getValue(key) { preferences -> preferences.getInt(key, /* will never be used: */ -1) } + + override fun getLong(key: String) = + getValue(key) { preferences -> preferences.getLong(key, /* will never be used: */ -1) } + + override fun getString(key: String): String? = + preferences.getString(key, /* will never be used: */ null) + + + private fun putValue(key: String, value: T?, writer: (SharedPreferences.Editor, T) -> Unit) { + if (value == null) + remove(key) + else { + logger.fine("Writing setting $key = $value") + preferences.edit { + writer(this, value) + } + } + } + + override fun putBoolean(key: String, value: Boolean?) = + putValue(key, value) { editor, v -> editor.putBoolean(key, v) } + + override fun putInt(key: String, value: Int?) = + putValue(key, value) { editor, v -> editor.putInt(key, v) } + + override fun putLong(key: String, value: Long?) = + putValue(key, value) { editor, v -> editor.putLong(key, v) } + + override fun putString(key: String, value: String?) = + putValue(key, value) { editor, v -> editor.putString(key, v) } + + override fun remove(key: String) { + logger.fine("Removing setting $key") + preferences.edit { + remove(key) + } + } + + + override fun dump(writer: Writer) { + val table = TextTable("Setting", "Value") + for ((key, value) in preferences.all.toSortedMap()) + table.addLine(key, value) + writer.write(table.toString()) + } + + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { + onChangeListener?.onSettingsChanged(key) + } + + + private fun firstCall() { + // remove possible artifacts from DAVdroid <1.9 + preferences.edit { + remove("override_proxy") + remove("proxy_host") + remove("proxy_port") + remove("log_to_external_storage") + } + } + + + @Module + @InstallIn(SingletonComponent::class) + abstract class SharedPreferencesProviderModule { + @Binds + @IntoMap + @IntKey(/* priority */ 10) + abstract fun sharedPreferencesProvider(impl: SharedPreferencesProvider): SettingsProvider + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration.kt new file mode 100644 index 0000000..208a473 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration.kt @@ -0,0 +1,25 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import at.bitfire.davdroid.settings.AccountSettings + +interface AccountSettingsMigration { + + /** + * Migrate the account settings from the old version to the new version. + * + * **The new (target) version number is registered in the Hilt module as [Int] key of the multi-binding of [AccountSettings].** + * + * This method should depend on current architecture of [AccountSettings] as little as possible. Methods of [AccountSettings] + * may change in future and it shouldn't be necessary to change migrations as well. So it's better to operate "low-level" + * directly on the account user-data – which is also better testable. + * + * @param account The account to migrate + */ + fun migrate(account: Account) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration10.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration10.kt new file mode 100644 index 0000000..fb561c7 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration10.kt @@ -0,0 +1,69 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import android.content.Context +import android.content.pm.PackageManager +import android.provider.CalendarContract +import android.provider.CalendarContract.Calendars +import android.provider.CalendarContract.Reminders +import androidx.core.content.ContextCompat +import androidx.core.content.contentValuesOf +import at.bitfire.davdroid.resource.LocalTask +import at.bitfire.ical4android.TaskProvider +import at.techbee.jtx.JtxContract.asSyncAdapter +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntKey +import dagger.multibindings.IntoMap +import org.dmfs.tasks.contract.TaskContract +import javax.inject.Inject +import kotlin.use + +/** + * Task synchronization now handles alarms, categories, relations and unknown properties. + * Setting task ETags to null will cause them to be downloaded (and parsed) again. + * + * Also update the allowed reminder types for calendars. + */ +class AccountSettingsMigration10 @Inject constructor( + @ApplicationContext private val context: Context +): AccountSettingsMigration { + + override fun migrate(account: Account) { + TaskProvider.acquire(context, TaskProvider.ProviderName.OpenTasks)?.use { provider -> + val tasksUri = provider.tasksUri().asSyncAdapter(account) + val emptyETag = contentValuesOf(LocalTask.COLUMN_ETAG to null) + provider.client.update(tasksUri, emptyETag, "${TaskContract.Tasks._DIRTY}=0 AND ${TaskContract.Tasks._DELETED}=0", null) + } + + if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED) + context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.use { provider -> + provider.update( + Calendars.CONTENT_URI.asSyncAdapter(account), + contentValuesOf( + Calendars.ALLOWED_REMINDERS to arrayOf( + Reminders.METHOD_DEFAULT, + Reminders.METHOD_ALERT, + Reminders.METHOD_EMAIL + ).joinToString(",") { it.toString() } + ), null, null) + } + } + + + @Module + @InstallIn(SingletonComponent::class) + abstract class AccountSettingsMigrationModule { + @Binds @IntoMap + @IntKey(10) + abstract fun provide(impl: AccountSettingsMigration10): AccountSettingsMigration + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration11.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration11.kt new file mode 100644 index 0000000..530c118 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration11.kt @@ -0,0 +1,61 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.ContentResolver +import android.content.Context +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.settings.AccountSettings.Companion.SYNC_INTERVAL_MANUALLY +import at.bitfire.davdroid.sync.TasksAppManager +import at.bitfire.davdroid.sync.account.setAndVerifyUserData +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntKey +import dagger.multibindings.IntoMap +import javax.inject.Inject + +/** + * The tasks sync interval should be stored in account settings. It's used to set the sync interval + * again when the tasks provider is switched. + */ +class AccountSettingsMigration11 @Inject constructor( + @ApplicationContext private val context: Context, + private val tasksAppManager: TasksAppManager +): AccountSettingsMigration { + + override fun migrate(account: Account) { + val accountManager: AccountManager = AccountManager.get(context) + tasksAppManager.currentProvider()?.let { provider -> + val interval = getSyncFrameworkInterval(account, provider.authority) + if (interval != null) + accountManager.setAndVerifyUserData(account, AccountSettings.KEY_SYNC_INTERVAL_TASKS, interval.toString()) + } + } + + private fun getSyncFrameworkInterval(account: Account, authority: String): Long? { + if (ContentResolver.getIsSyncable(account, authority) <= 0) + return null + + return if (ContentResolver.getSyncAutomatically(account, authority)) + ContentResolver.getPeriodicSyncs(account, authority).firstOrNull()?.period ?: SYNC_INTERVAL_MANUALLY + else + SYNC_INTERVAL_MANUALLY + } + + + @Module + @InstallIn(SingletonComponent::class) + abstract class AccountSettingsMigrationModule { + @Binds @IntoMap + @IntKey(11) + abstract fun provide(impl: AccountSettingsMigration11): AccountSettingsMigration + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration12.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration12.kt new file mode 100644 index 0000000..684b1c4 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration12.kt @@ -0,0 +1,126 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import android.content.ContentUris +import android.content.Context +import android.content.pm.PackageManager +import android.provider.CalendarContract +import android.util.Base64 +import androidx.core.content.ContextCompat +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.UnknownProperty +import at.bitfire.synctools.storage.calendar.AndroidEvent2 +import at.techbee.jtx.JtxContract.asSyncAdapter +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntKey +import dagger.multibindings.IntoMap +import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.property.Url +import java.io.ByteArrayInputStream +import java.io.ObjectInputStream +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject +import kotlin.use + +/** + * Store event URLs as URL (extended property) instead of unknown property. At the same time, + * convert legacy unknown properties to the current format. + */ +class AccountSettingsMigration12 @Inject constructor( + @ApplicationContext private val context: Context, + private val logger: Logger +): AccountSettingsMigration { + + override fun migrate(account: Account) { + if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED) { + context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.use { provider -> + // Attention: CalendarProvider does NOT limit the results of the ExtendedProperties query + // to the given account! So all extended properties will be processed number-of-accounts times. + val extUri = CalendarContract.ExtendedProperties.CONTENT_URI.asSyncAdapter(account) + + provider.query( + extUri, arrayOf( + CalendarContract.ExtendedProperties._ID, // idx 0 + CalendarContract.ExtendedProperties.NAME, // idx 1 + CalendarContract.ExtendedProperties.VALUE // idx 2 + ), null, null, null + )?.use { cursor -> + while (cursor.moveToNext()) { + val id = cursor.getLong(0) + val rawValue = cursor.getString(2) + + val uri by lazy { + ContentUris.withAppendedId(CalendarContract.ExtendedProperties.CONTENT_URI, id).asSyncAdapter(account) + } + + when (cursor.getString(1)) { + UnknownProperty.CONTENT_ITEM_TYPE -> { + // unknown property; check whether it's a URL + try { + val property = UnknownProperty.fromJsonString(rawValue) + if (property is Url) { // rewrite to MIMETYPE_URL + val newValues = contentValuesOf( + CalendarContract.ExtendedProperties.NAME to AndroidEvent2.EXTNAME_URL, + CalendarContract.ExtendedProperties.VALUE to property.value + ) + provider.update(uri, newValues, null, null) + } + } catch (e: Exception) { + logger.log( + Level.WARNING, + "Couldn't rewrite URL from unknown property to ${AndroidEvent2.EXTNAME_URL}", + e + ) + } + } + + "unknown-property" -> { + // unknown property (deprecated format); convert to current format + try { + val stream = ByteArrayInputStream(Base64.decode(rawValue, Base64.NO_WRAP)) + ObjectInputStream(stream).use { + (it.readObject() as? Property)?.let { property -> + // rewrite to current format + val newValues = contentValuesOf( + CalendarContract.ExtendedProperties.NAME to UnknownProperty.CONTENT_ITEM_TYPE, + CalendarContract.ExtendedProperties.VALUE to UnknownProperty.toJsonString(property) + ) + provider.update(uri, newValues, null, null) + } + } + } catch (e: Exception) { + logger.log(Level.WARNING, "Couldn't rewrite deprecated unknown property to current format", e) + } + } + + "unknown-property.v2" -> { + // unknown property (deprecated MIME type); rewrite to current MIME type + val newValues = contentValuesOf(CalendarContract.ExtendedProperties.NAME to UnknownProperty.CONTENT_ITEM_TYPE) + provider.update(uri, newValues, null, null) + } + } + } + } + } + } + } + + + @Module + @InstallIn(SingletonComponent::class) + abstract class AccountSettingsMigrationModule { + @Binds @IntoMap + @IntKey(12) + abstract fun provide(impl: AccountSettingsMigration12): AccountSettingsMigration + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration13.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration13.kt new file mode 100644 index 0000000..a4bbd69 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration13.kt @@ -0,0 +1,71 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import android.content.Context +import androidx.core.content.edit +import androidx.preference.PreferenceManager +import at.bitfire.davdroid.settings.Settings +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntKey +import dagger.multibindings.IntoMap +import javax.inject.Inject + +/** + * Not a per-account migration, but not a database migration, too, so it fits best there. + * Best future solution would be that SettingsManager manages versions and migrations. + * + * Updates proxy settings from override_proxy_* to proxy_type, proxy_host, proxy_port. + */ +class AccountSettingsMigration13 @Inject constructor( + @ApplicationContext private val context: Context +): AccountSettingsMigration { + + override fun migrate(account: Account) { + // proxy settings are managed by SharedPreferencesProvider + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + + // old setting names + val overrideProxy = "override_proxy" + val overrideProxyHost = "override_proxy_host" + val overrideProxyPort = "override_proxy_port" + + preferences.edit { + if (preferences.contains(overrideProxy)) { + if (preferences.getBoolean(overrideProxy, false)) + // override_proxy set, migrate to proxy_type = HTTP + putInt(Settings.PROXY_TYPE, Settings.PROXY_TYPE_HTTP) + remove(overrideProxy) + } + if (preferences.contains(overrideProxyHost)) { + preferences.getString(overrideProxyHost, null)?.let { host -> + putString(Settings.PROXY_HOST, host) + } + remove(overrideProxyHost) + } + if (preferences.contains(overrideProxyPort)) { + val port = preferences.getInt(overrideProxyPort, 0) + if (port != 0) + putInt(Settings.PROXY_PORT, port) + remove(overrideProxyPort) + } + } + } + + + @Module + @InstallIn(SingletonComponent::class) + abstract class AccountSettingsMigrationModule { + @Binds @IntoMap + @IntKey(13) + abstract fun provide(impl: AccountSettingsMigration13): AccountSettingsMigration + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration14.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration14.kt new file mode 100644 index 0000000..d68614a --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration14.kt @@ -0,0 +1,96 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import android.content.ContentResolver +import android.provider.CalendarContract +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.ical4android.TaskProvider +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntKey +import dagger.multibindings.IntoMap +import java.util.logging.Logger +import javax.inject.Inject + +/** + * Disables all sync adapter periodic syncs for every authority. Then enables corresponding periodic sync workers. + */ +class AccountSettingsMigration14 @Inject constructor( + private val accountSettingsFactory: AccountSettings.Factory, + private val logger: Logger +): AccountSettingsMigration { + + override fun migrate(account: Account) { + // Cancel any potentially running syncs for this account (sync framework) + ContentResolver.cancelSync(account, null) + + val authorities = listOf( + "at.bitfire.davdroid.addressbooks", + CalendarContract.AUTHORITY, + TaskProvider.ProviderName.JtxBoard.authority, + TaskProvider.ProviderName.OpenTasks.authority, + TaskProvider.ProviderName.TasksOrg.authority + ) + + // Disable periodic syncs (sync adapter framework) + for (authority in authorities) + disableSyncFramework(account, authority) + + // Enable PeriodicSyncWorker (WorkManager), with known intervals + for (dataType in SyncDataType.entries) + enableWorkManager(account, dataType) + } + + private fun enableWorkManager(account: Account, dataType: SyncDataType) { + val accountSettings = accountSettingsFactory.create(account) + val enabled: Boolean = accountSettings.getSyncInterval(dataType)?.let { syncInterval -> + accountSettings.setSyncInterval(dataType, syncInterval) + true + } == true + logger.info("PeriodicSyncWorker for $account/$dataType enabled=$enabled") + } + + private fun disableSyncFramework(account: Account, authority: String) { + // Disable periodic syncs (sync adapter framework) + val disable: () -> Boolean = { + /* Ugly hack: because there is no callback for when the sync status/interval has been + updated, we need to make this call blocking. */ + for (sync in ContentResolver.getPeriodicSyncs(account, authority)) + ContentResolver.removePeriodicSync(sync.account, sync.authority, sync.extras) + + // check whether syncs are really disabled + var result = true + for (sync in ContentResolver.getPeriodicSyncs(account, authority)) { + logger.info("Sync framework still has a periodic sync for $account/$authority: $sync") + result = false + } + result + } + // try up to 10 times with 100 ms pause + var success = false + for (idxTry in 0 until 10) { + success = disable() + if (success) + break + Thread.sleep(200) + } + logger.info("Sync framework periodic syncs for $account/$authority disabled=$success") + } + + + @Module + @InstallIn(SingletonComponent::class) + abstract class AccountSettingsMigrationModule { + @Binds @IntoMap + @IntKey(14) + abstract fun provide(impl: AccountSettingsMigration14): AccountSettingsMigration + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration15.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration15.kt new file mode 100644 index 0000000..fa654e9 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration15.kt @@ -0,0 +1,40 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import at.bitfire.davdroid.sync.AutomaticSyncManager +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntKey +import dagger.multibindings.IntoMap +import javax.inject.Inject + +/** + * Updates the periodic sync workers by re-setting the same sync interval. + * + * The goal is to add the [at.bitfire.davdroid.sync.worker.BaseSyncWorker.commonTag] to all existing periodic sync workers so that they + * can be detected correctly. + */ +class AccountSettingsMigration15 @Inject constructor( + private val automaticSyncManager: AutomaticSyncManager +): AccountSettingsMigration { + + override fun migrate(account: Account) { + automaticSyncManager.updateAutomaticSync(account) + } + + + @Module + @InstallIn(SingletonComponent::class) + abstract class AccountSettingsMigrationModule { + @Binds @IntoMap + @IntKey(15) + abstract fun provide(impl: AccountSettingsMigration15): AccountSettingsMigration + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration16.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration16.kt new file mode 100644 index 0000000..1d77d7b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration16.kt @@ -0,0 +1,67 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import android.content.Context +import androidx.work.WorkManager +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.sync.worker.SyncWorkerManager +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntKey +import dagger.multibindings.IntoMap +import java.util.logging.Logger +import javax.inject.Inject + +/** + * Between DAVx5 4.4.1-beta.1 and 4.4.1-rc.1 (both v15), the periodic sync workers were renamed (moved to another + * package) and thus automatic synchronization stopped (because the enqueued workers rely on the full class + * name and no new workers were enqueued). Here we enqueue all periodic sync workers again with the correct class name. + */ +class AccountSettingsMigration16 @Inject constructor( + private val accountSettingsFactory: AccountSettings.Factory, + @ApplicationContext private val context: Context, + private val logger: Logger, + private val syncWorkerManager: SyncWorkerManager +): AccountSettingsMigration { + + override fun migrate(account: Account) { + for (dataType in SyncDataType.entries) { + logger.info("Re-enqueuing periodic sync workers for $account/$dataType, if necessary") + + /* A maybe existing periodic worker references the old class name (even if it failed and/or is not active). So + we need to explicitly disable and prune all workers. Just updating the worker is not enough – WorkManager will update + the work details, but not the class name. */ + val disableOp = syncWorkerManager.disablePeriodic(account, dataType) + disableOp.result.get() // block until worker with old name is disabled + + val pruneOp = WorkManager.getInstance(context).pruneWork() + pruneOp.result.get() // block until worker with old name is removed from DB + + val accountSettings = accountSettingsFactory.create(account) + val interval = accountSettings.getSyncInterval(dataType) + if (interval != null) { + // There's a sync interval for this account/authority; a periodic sync worker should be there, too. + val onlyWifi = accountSettings.getSyncWifiOnly() + syncWorkerManager.enablePeriodic(account, dataType, interval, onlyWifi) + } + } + } + + + @Module + @InstallIn(SingletonComponent::class) + abstract class AccountSettingsMigrationModule { + @Binds @IntoMap + @IntKey(16) + abstract fun provide(impl: AccountSettingsMigration16): AccountSettingsMigration + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration17.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration17.kt new file mode 100644 index 0000000..7c255dd --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration17.kt @@ -0,0 +1,96 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import android.provider.ContactsContract +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.repository.DavCollectionRepository +import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.resource.LocalAddressBookStore +import at.bitfire.davdroid.sync.account.setAndVerifyUserData +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntKey +import dagger.multibindings.IntoMap +import kotlinx.coroutines.runBlocking +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject + +/** + * With DAVx5 4.4.3 address book account names now contain the collection ID as a unique + * identifier. We need to update the address book account names. + */ +class AccountSettingsMigration17 @Inject constructor( + private val collectionRepository: DavCollectionRepository, + @ApplicationContext private val context: Context, + private val localAddressBookFactory: LocalAddressBook.Factory, + private val localAddressBookStore: LocalAddressBookStore, + private val logger: Logger, + private val serviceRepository: DavServiceRepository +): AccountSettingsMigration { + + override fun migrate(account: Account) { + val addressBookAccountType = context.getString(R.string.account_type_address_book) + try { + context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY) + } catch (e: SecurityException) { + // Not setting the collection ID will cause the address books to removed and fully re-synced as soon as there are permissions. + logger.log(Level.WARNING, "Missing permissions for contacts authority, won't set collection ID for address books", e) + null + }?.use { provider -> + runBlocking { + val service = serviceRepository.getByAccountAndType(account.name, Service.TYPE_CARDDAV) ?: return@runBlocking + + val accountManager = AccountManager.get(context) + // Get all old address books of this account, i.e. the ones which have a "real_account_name" of this account. + // After this migration is run, address books won't be associated to accounts anymore but only to their respective collection/URL. + val oldAddressBookAccounts = accountManager.getAccountsByType(addressBookAccountType) + .filter { addressBookAccount -> + account.name == accountManager.getUserData(addressBookAccount, "real_account_name") + } + + for (oldAddressBookAccount in oldAddressBookAccounts) { + // Old address books only have a URL, so use it to determine the collection ID + logger.info("Migrating address book ${oldAddressBookAccount.name}") + val oldAddressBook = localAddressBookFactory.create(account, oldAddressBookAccount, provider) + val url = accountManager.getUserData(oldAddressBookAccount, LOCAL_ADDRESS_BOOK_ACCOUNT_USER_DATA_URL) + collectionRepository.getByServiceAndUrl(service.id, url)?.let { collection -> + // Set collection ID and rename the account + localAddressBookStore.update(provider, oldAddressBook, collection) + // The user-data-url is not being set in localAddressBookStore.update() anymore, + // but we need to keep it for the migration + accountManager.setAndVerifyUserData( + oldAddressBook.addressBookAccount, + LOCAL_ADDRESS_BOOK_ACCOUNT_USER_DATA_URL, + collection.url.toString() + ) + } + } + } + } + } + + companion object { + private const val LOCAL_ADDRESS_BOOK_ACCOUNT_USER_DATA_URL = "url" + } + + @Module + @InstallIn(SingletonComponent::class) + abstract class AccountSettingsMigrationModule { + @Binds @IntoMap + @IntKey(17) + abstract fun provide(impl: AccountSettingsMigration17): AccountSettingsMigration + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration18.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration18.kt new file mode 100644 index 0000000..a855460 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration18.kt @@ -0,0 +1,77 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.sync.account.setAndVerifyUserData +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntKey +import dagger.multibindings.IntoMap +import kotlinx.coroutines.runBlocking +import javax.inject.Inject + +/** + * v17 had removed the binding between address book accounts and accounts and introduced + * the binding to collection IDs instead. + * + * However, it turned out that the account binding is needed even with collection IDs for the case + * that the collection is not available in the database anymore (for instance, because it has been + * removed on the server). In that case, the [at.bitfire.davdroid.sync.Syncer] still needs to get + * a list of all address book accounts that belong to the account, and not _all_ address books. + * + * So this migration again assigns address book accounts to accounts. + */ +class AccountSettingsMigration18 @Inject constructor( + @ApplicationContext private val context: Context, + private val db: AppDatabase +): AccountSettingsMigration { + + override fun migrate(account: Account) { + val accountManager = AccountManager.get(context) + runBlocking { + db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CARDDAV)?.let { service -> + db.collectionDao().getByService(service.id).forEach { collection -> + // Find associated address book account by collection ID (if it exists) + val addressBookAccount = accountManager + .getAccountsByType(context.getString(R.string.account_type_address_book)) + .firstOrNull { + accountManager.getUserData( + it, + LocalAddressBook.USER_DATA_COLLECTION_ID + ) == collection.id.toString() + } + + if (addressBookAccount != null) { + // (Re-)assign address book to account + accountManager.setAndVerifyUserData(addressBookAccount, LocalAddressBook.USER_DATA_ACCOUNT_NAME, account.name) + accountManager.setAndVerifyUserData(addressBookAccount, LocalAddressBook.USER_DATA_ACCOUNT_TYPE, account.type) + } + } + } + } + + // Address books without an assigned account will be removed by AccountsCleanupWorker + } + + + @Module + @InstallIn(SingletonComponent::class) + abstract class AccountSettingsMigrationModule { + @Binds @IntoMap + @IntKey(18) + abstract fun provide(impl: AccountSettingsMigration18): AccountSettingsMigration + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration19.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration19.kt new file mode 100644 index 0000000..344629e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration19.kt @@ -0,0 +1,59 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import android.content.Context +import android.provider.CalendarContract +import androidx.work.WorkManager +import at.bitfire.davdroid.sync.AutomaticSyncManager +import at.bitfire.ical4android.TaskProvider +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntKey +import dagger.multibindings.IntoMap +import javax.inject.Inject + +/** + * Sync workers are now not per authority anymore, but per [at.bitfire.davdroid.sync.SyncDataType]. So we have to + * + * 1. cancel all current periodic sync workers (which have "authority" input data), + * 2. re-enqueue periodic sync workers (now with "data type" input data), if applicable. + */ +class AccountSettingsMigration19 @Inject constructor( + @ApplicationContext private val context: Context, + private val automaticSyncManager: AutomaticSyncManager +): AccountSettingsMigration { + + override fun migrate(account: Account) { + // cancel old workers + val workManager = WorkManager.getInstance(context) + val authorities = listOf( + "at.bitfire.davdroid.addressbooks", + CalendarContract.AUTHORITY, + *TaskProvider.TASK_PROVIDERS.map { it.authority }.toTypedArray() + ) + for (authority in authorities) { + val oldWorkerName = "periodic-sync $authority ${account.type}/${account.name}" + workManager.cancelUniqueWork(oldWorkerName) + } + + // enqueue new workers + automaticSyncManager.updateAutomaticSync(account) + } + + + @Module + @InstallIn(SingletonComponent::class) + abstract class AccountSettingsMigrationModule { + @Binds @IntoMap + @IntKey(19) + abstract fun provide(impl: AccountSettingsMigration19): AccountSettingsMigration + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration20.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration20.kt new file mode 100644 index 0000000..b2134e7 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration20.kt @@ -0,0 +1,133 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import android.provider.CalendarContract +import androidx.annotation.OpenForTesting +import androidx.core.content.contentValuesOf +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.repository.DavCollectionRepository +import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.resource.LocalAddressBookStore +import at.bitfire.davdroid.resource.LocalCalendarStore +import at.bitfire.davdroid.resource.LocalTaskList +import at.bitfire.davdroid.sync.TasksAppManager +import at.bitfire.ical4android.JtxCollection +import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider +import at.techbee.jtx.JtxContract +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntKey +import dagger.multibindings.IntoMap +import kotlinx.coroutines.runBlocking +import org.dmfs.tasks.contract.TaskContract.TaskLists +import javax.inject.Inject + +/** + * [at.bitfire.davdroid.sync.Syncer] now users collection IDs instead of URLs to match + * local and remote (database) collections. + * + * This migration writes the database collection IDs to the local collections. If we wouldn't do that, + * the syncer would not be able to find the correct local collection for a remote collection and + * all local collections would be deleted and re-created. + */ +class AccountSettingsMigration20 @Inject constructor( + @ApplicationContext context: Context, + private val addressBookStore: LocalAddressBookStore, + private val calendarStore: LocalCalendarStore, + private val collectionRepository: DavCollectionRepository, + private val serviceRepository: DavServiceRepository, + private val tasksAppManager: TasksAppManager +): AccountSettingsMigration { + + val accountManager = AccountManager.get(context) + + override fun migrate(account: Account) { + runBlocking { + serviceRepository.getByAccountAndType(account.name, Service.TYPE_CARDDAV)?.let { cardDavService -> + migrateAddressBooks(account, cardDavService.id) + } + + serviceRepository.getByAccountAndType(account.name, Service.TYPE_CALDAV)?.let { calDavService -> + migrateCalendars(account, calDavService.id) + migrateTaskLists(account, calDavService.id) + } + } + } + + @OpenForTesting + internal fun migrateAddressBooks(account: Account, cardDavServiceId: Long) { + addressBookStore.acquireContentProvider()?.use { provider -> + for (addressBook in addressBookStore.getAll(account, provider)) { + val url = accountManager.getUserData(addressBook.addressBookAccount, ADDRESS_BOOK_USER_DATA_URL) ?: continue + val collection = collectionRepository.getByServiceAndUrl(cardDavServiceId, url) ?: continue + addressBook.dbCollectionId = collection.id + } + } + } + + @OpenForTesting + internal fun migrateCalendars(account: Account, calDavServiceId: Long) { + calendarStore.acquireContentProvider()?.use { client -> + val calendarProvider = AndroidCalendarProvider(account, client) + // for each calendar, assign _SYNC_ID := ID if collection (identified by NAME field = URL) + for (calendar in calendarProvider.findCalendars()) { + val url = calendar.name ?: continue + collectionRepository.getByServiceAndUrl(calDavServiceId, url)?.let { collection -> + calendar.update(contentValuesOf( + CalendarContract.Calendars._SYNC_ID to collection.id + )) + } + + } + } + } + + @OpenForTesting + internal fun migrateTaskLists(account: Account, calDavServiceId: Long) { + val taskListStore = tasksAppManager.getDataStore() ?: /* no tasks app */ return + taskListStore.acquireContentProvider()?.use { provider -> + for (taskList in taskListStore.getAll(account, provider)) { + when (taskList) { + is LocalTaskList -> { // tasks.org, OpenTasks + val url = taskList.syncId ?: continue + collectionRepository.getByServiceAndUrl(calDavServiceId, url)?.let { collection -> + taskList.update(contentValuesOf( + TaskLists._SYNC_ID to collection.id.toString() + )) + } + } + is JtxCollection<*> -> { // jtxBoard + val url = taskList.url ?: continue + collectionRepository.getByServiceAndUrl(calDavServiceId, url)?.let { collection -> + taskList.update(contentValuesOf( + JtxContract.JtxCollection.SYNC_ID to collection.id + )) + } + } + } + } + } + } + + companion object { + internal const val ADDRESS_BOOK_USER_DATA_URL = "url" + } + + @Module + @InstallIn(SingletonComponent::class) + abstract class AccountSettingsMigrationModule { + @Binds @IntoMap + @IntKey(20) + abstract fun provide(impl: AccountSettingsMigration20): AccountSettingsMigration + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration7.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration7.kt new file mode 100644 index 0000000..a6bddcf --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration7.kt @@ -0,0 +1,50 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import android.provider.CalendarContract +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.sync.account.setAndVerifyUserData +import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntKey +import dagger.multibindings.IntoMap +import javax.inject.Inject + +class AccountSettingsMigration7 @Inject constructor( + @ApplicationContext private val context: Context +): AccountSettingsMigration { + + override fun migrate(account: Account) { + // add calendar colors + context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.use { client -> + val provider = AndroidCalendarProvider(account, client) + provider.provideCss3ColorIndices() + } + + // update allowed WiFi settings key + val accountManager = AccountManager.get(context) + val onlySSID = accountManager.getUserData(account, "wifi_only_ssid") + accountManager.setAndVerifyUserData(account, AccountSettings.KEY_WIFI_ONLY_SSIDS, onlySSID) + accountManager.setAndVerifyUserData(account, "wifi_only_ssid", null) + } + + + @Module + @InstallIn(SingletonComponent::class) + abstract class AccountSettingsMigrationModule { + @Binds @IntoMap + @IntKey(7) + abstract fun provide(impl: AccountSettingsMigration7): AccountSettingsMigration + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration8.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration8.kt new file mode 100644 index 0000000..e29aeb2 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration8.kt @@ -0,0 +1,71 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import android.content.ContentUris +import android.content.Context +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.TaskProvider +import at.techbee.jtx.JtxContract.asSyncAdapter +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntKey +import dagger.multibindings.IntoMap +import org.dmfs.tasks.contract.TaskContract +import org.dmfs.tasks.contract.TaskContract.CommonSyncColumns +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject + +class AccountSettingsMigration8 @Inject constructor( + @ApplicationContext private val context: Context, + private val logger: Logger +): AccountSettingsMigration { + + /** + * There is a mistake in this method. [TaskContract.Tasks.SYNC_VERSION] is used to store the + * SEQUENCE and should not be used for the eTag. + */ + override fun migrate(account: Account) { + TaskProvider.acquire(context, TaskProvider.ProviderName.OpenTasks)?.use { provider -> + // ETag is now in sync_version instead of sync1 + // UID is now in _uid instead of sync2 + provider.client.query(provider.tasksUri().asSyncAdapter(account), + arrayOf(TaskContract.Tasks._ID, TaskContract.Tasks.SYNC1, TaskContract.Tasks.SYNC2), + "${TaskContract.Tasks.ACCOUNT_TYPE}=? AND ${TaskContract.Tasks.ACCOUNT_NAME}=?", + arrayOf(account.type, account.name), null)!!.use { cursor -> + while (cursor.moveToNext()) { + val id = cursor.getLong(0) + val eTag = cursor.getString(1) + val uid = cursor.getString(2) + val values = contentValuesOf( + TaskContract.Tasks._UID to uid, + TaskContract.Tasks.SYNC_VERSION to eTag, + TaskContract.Tasks.SYNC1 to null, + TaskContract.Tasks.SYNC2 to null + ) + logger.log(Level.FINER, "Updating task $id", values) + provider.client.update( + ContentUris.withAppendedId(provider.tasksUri(), id).asSyncAdapter(account), + values, null, null) + } + } + } + } + + + @Module + @InstallIn(SingletonComponent::class) + abstract class AccountSettingsMigrationModule { + @Binds @IntoMap + @IntKey(8) + abstract fun provide(impl: AccountSettingsMigration8): AccountSettingsMigration + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration9.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration9.kt new file mode 100644 index 0000000..b3b2275 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration9.kt @@ -0,0 +1,48 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import android.content.ContentResolver +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Service +import at.bitfire.ical4android.TaskProvider +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntKey +import dagger.multibindings.IntoMap +import kotlinx.coroutines.runBlocking +import java.util.logging.Logger +import javax.inject.Inject + +/** + * It seems that somehow some non-CalDAV accounts got OpenTasks syncable, which caused battery problems. + * Disable it on those accounts for the future. + */ +class AccountSettingsMigration9 @Inject constructor( + private val db: AppDatabase, + private val logger: Logger +): AccountSettingsMigration { + + override fun migrate(account: Account) = runBlocking { + val hasCalDAV = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV) != null + if (!hasCalDAV && ContentResolver.getIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority) != 0) { + logger.info("Disabling OpenTasks sync for $account") + ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 0) + } + } + + + @Module + @InstallIn(SingletonComponent::class) + abstract class AccountSettingsMigrationModule { + @Binds @IntoMap + @IntKey(9) + abstract fun provide(impl: AccountSettingsMigration9): AccountSettingsMigration + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/startup/CrashHandlerSetup.kt b/app/src/main/kotlin/at/bitfire/davdroid/startup/CrashHandlerSetup.kt new file mode 100644 index 0000000..643f1aa --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/startup/CrashHandlerSetup.kt @@ -0,0 +1,90 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.startup + +import android.content.Context +import android.os.Build +import android.os.StrictMode +import at.bitfire.davdroid.BuildConfig +import at.bitfire.davdroid.startup.StartupPlugin.Companion.PRIORITY_DEFAULT +import at.bitfire.davdroid.startup.StartupPlugin.Companion.PRIORITY_HIGHEST +import dagger.Binds +import dagger.BindsOptionalOf +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet +import java.util.Optional +import java.util.logging.Logger +import javax.inject.Inject +import kotlin.jvm.optionals.getOrNull + +/** + * Sets up the uncaught exception (crash) handler and enables StrictMode in debug builds. + */ +class CrashHandlerSetup @Inject constructor( + @ApplicationContext private val context: Context, + private val logger: Logger, + private val crashHandler: Optional +): StartupPlugin { + + @Module + @InstallIn(SingletonComponent::class) + interface CrashHandlerSetupModule { + // allows to inject Optional + @BindsOptionalOf + fun optionalDebugInfoCrashHandler(): Thread.UncaughtExceptionHandler + + @Binds + @IntoSet + fun crashHandlerSetup(impl: CrashHandlerSetup): StartupPlugin + } + + + override fun onAppCreate() { + if (BuildConfig.DEBUG) { + logger.info("Debug build, enabling StrictMode with logging") + + StrictMode.setThreadPolicy( + StrictMode.ThreadPolicy.Builder() + .detectAll() + .penaltyLog() + .build() + ) + + val builder = StrictMode.VmPolicy.Builder() // don't use detectAll() because it causes "untagged socket" warnings + .detectActivityLeaks() + .detectFileUriExposure() + .detectLeakedClosableObjects() + .detectLeakedRegistrationObjects() + .detectLeakedSqlLiteObjects() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + builder.detectContentUriWithoutPermission() + /*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) // often triggered by Conscrypt + builder.detectNonSdkApiUsage()*/ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + builder.detectUnsafeIntentLaunch() + StrictMode.setVmPolicy(builder.penaltyLog().build()) + + } else { + // release build + val handler = crashHandler.getOrNull() + if (handler != null) { + logger.info("Setting uncaught exception handler: ${handler.javaClass.name}") + Thread.setDefaultUncaughtExceptionHandler(handler) + } else + logger.info("Using default uncaught exception handler") + } + } + + override fun priority() = PRIORITY_HIGHEST + + override suspend fun onAppCreateAsync() { + } + + override fun priorityAsync(): Int = PRIORITY_DEFAULT + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/startup/StartupPlugin.kt b/app/src/main/kotlin/at/bitfire/davdroid/startup/StartupPlugin.kt new file mode 100644 index 0000000..a5b0ad3 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/startup/StartupPlugin.kt @@ -0,0 +1,44 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.startup + +interface StartupPlugin { + + companion object { + const val PRIORITY_DEFAULT = 100 + const val PRIORITY_HIGHEST = 0 + } + + /** + * Runs synchronously during [at.bitfire.davdroid.App.onCreate]. Use only for tasks that must be completed before + * the app can run. Causes the app to start slower. + * + * Will be run before [onAppCreateAsync]. + */ + fun onAppCreate() + + /** + * Priority of this plugin's [onAppCreate]. Lower values are executed first. + */ + fun priority(): Int + + + /** + * Runs asynchronously after [at.bitfire.davdroid.App.onCreate]. Use for tasks that can be run in the background. + * + * Will be run after [onAppCreate]. + * + * The coroutine scope will usually be `GlobalScope` (which has no end because we don't get + * a signal before the app is terminated) on the default dispatcher, but can be a custom scope + * for testing. + */ + suspend fun onAppCreateAsync() + + /** + * Priority of this plugin's [onAppCreateAsync]. Lower values are executed first. + */ + fun priorityAsync(): Int + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/startup/TasksAppWatcher.kt b/app/src/main/kotlin/at/bitfire/davdroid/startup/TasksAppWatcher.kt new file mode 100644 index 0000000..e9ea631 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/startup/TasksAppWatcher.kt @@ -0,0 +1,74 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.startup + +import android.content.Context +import at.bitfire.davdroid.startup.StartupPlugin.Companion.PRIORITY_DEFAULT +import at.bitfire.davdroid.sync.TasksAppManager +import at.bitfire.davdroid.util.packageChangedFlow +import at.bitfire.ical4android.TaskProvider +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet +import java.util.logging.Logger +import javax.inject.Inject +import javax.inject.Provider + +/** + * Watches whether a tasks app has been installed or uninstalled and updates + * the selected tasks app and task sync settings accordingly. + */ +class TasksAppWatcher @Inject constructor( + @ApplicationContext private val context: Context, + private val logger: Logger, + private val tasksAppManager: Provider +): StartupPlugin { + + @Module + @InstallIn(SingletonComponent::class) + interface TasksAppWatcherModule { + @Binds + @IntoSet + fun tasksAppWatcher(impl: TasksAppWatcher): StartupPlugin + } + + + override fun onAppCreate() { + } + + override fun priority() = PRIORITY_DEFAULT + + override suspend fun onAppCreateAsync() { + logger.info("Watching for package changes in order to detect tasks app changes") + packageChangedFlow(context).collect { + onPackageChanged() + } + } + + override fun priorityAsync() = PRIORITY_DEFAULT + + + private fun onPackageChanged() { + val manager = tasksAppManager.get() + val currentProvider = manager.currentProvider() + logger.info("App launched or package (un)installed; current tasks provider = $currentProvider") + + if (currentProvider == null) { + // Iterate through all supported providers and select one, if available. + var newProvider = TaskProvider.ProviderName.entries + .firstOrNull { provider -> + context.packageManager.resolveContentProvider(provider.authority, 0) != null + } + + // Select provider or clear setting and sync if now provider available + logger.info("Selecting new tasks provider: $newProvider") + manager.selectProvider(newProvider) + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/AddressBookSyncer.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/AddressBookSyncer.kt new file mode 100644 index 0000000..f6deea9 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/AddressBookSyncer.kt @@ -0,0 +1,132 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.ContentProviderClient +import android.provider.ContactsContract +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.resource.LocalAddressBookStore +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.sync.account.setAndVerifyUserData +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.runBlocking +import java.util.logging.Level + +/** + * Sync logic for address books + */ +class AddressBookSyncer @AssistedInject constructor( + @Assisted account: Account, + @Assisted resync: ResyncType?, + @Assisted val syncFrameworkUpload: Boolean, + @Assisted syncResult: SyncResult, + addressBookStore: LocalAddressBookStore, + private val accountSettingsFactory: AccountSettings.Factory, + private val contactsSyncManagerFactory: ContactsSyncManager.Factory +): Syncer(account, resync, syncResult) { + + @AssistedFactory + interface Factory { + fun create( + account: Account, + resyncType: ResyncType?, + syncFrameworkUpload: Boolean, + syncResult: SyncResult + ): AddressBookSyncer + } + + override val dataStore = addressBookStore + + override val serviceType: String + get() = Service.TYPE_CARDDAV + + + override fun getDbSyncCollections(serviceId: Long): List = + collectionRepository.getByServiceAndSync(serviceId) + + override fun syncCollection(provider: ContentProviderClient, localCollection: LocalAddressBook, remoteCollection: Collection) { + logger.info("Synchronizing address book: ${localCollection.addressBookAccount.name}") + syncAddressBook( + account = account, + addressBook = localCollection, + httpClient = httpClient, + provider = provider, + syncResult = syncResult, + collection = remoteCollection + ) + } + + /** + * Synchronizes an address book + * + * @param addressBook local address book + * @param provider Content provider to access android contacts + * @param syncResult Stores hard and soft sync errors + * @param collection The database collection associated with this address book + */ + private fun syncAddressBook( + account: Account, + addressBook: LocalAddressBook, + httpClient: Lazy, + provider: ContentProviderClient, + syncResult: SyncResult, + collection: Collection + ) { + try { + // handle group method change + val accountSettings = accountSettingsFactory.create(account) + val groupMethod = accountSettings.getGroupMethod().name + + val accountManager = AccountManager.get(context) + accountManager.getUserData(addressBook.addressBookAccount, PREVIOUS_GROUP_METHOD)?.let { previousGroupMethod -> + if (previousGroupMethod != groupMethod) { + logger.info("Group method changed, deleting all local contacts/groups") + + // delete all local contacts and groups so that they will be downloaded again + provider.delete(addressBook.syncAdapterURI(ContactsContract.RawContacts.CONTENT_URI), null, null) + provider.delete(addressBook.syncAdapterURI(ContactsContract.Groups.CONTENT_URI), null, null) + + // reset sync state + addressBook.syncState = null + } + } + accountManager.setAndVerifyUserData(addressBook.addressBookAccount, PREVIOUS_GROUP_METHOD, groupMethod) + + val syncManager = contactsSyncManagerFactory.contactsSyncManager( + account, + httpClient.value, + syncResult, + provider, + addressBook, + collection, + resync, + syncFrameworkUpload + ) + runBlocking { + syncManager.performSync() + } + + } catch(e: Exception) { + logger.log(Level.SEVERE, "Couldn't sync contacts", e) + } + + logger.info("Contacts sync complete") + } + + + companion object { + + const val PREVIOUS_GROUP_METHOD = "previous_group_method" + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/AutomaticSyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/AutomaticSyncManager.kt new file mode 100644 index 0000000..c8e8912 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/AutomaticSyncManager.kt @@ -0,0 +1,154 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.accounts.Account +import android.provider.CalendarContract +import android.provider.ContactsContract +import androidx.annotation.WorkerThread +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.resource.LocalAddressBookStore +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.sync.adapter.SyncFrameworkIntegration +import at.bitfire.davdroid.sync.worker.SyncWorkerManager +import kotlinx.coroutines.runBlocking +import javax.inject.Inject +import javax.inject.Provider + +/** + * Manages automatic synchronization, that is: + * + * - synchronization in given intervals, and + * - synchronization on local data changes. + * + * Integrates with both the periodic sync workers and the sync framework. So this class should be used when + * the caller just wants to update the automatic sync, without needing to know about the underlying details. + * + * Automatic synchronization stands in contrast to manual synchronization, which is only triggered by the user. + */ +class AutomaticSyncManager @Inject constructor( + private val accountSettingsFactory: AccountSettings.Factory, + private val localAddressBookStore: LocalAddressBookStore, + private val serviceRepository: DavServiceRepository, + private val syncFramework: SyncFrameworkIntegration, + private val tasksAppManager: Provider, + private val workerManager: SyncWorkerManager +) { + + /** + * Disable automatic synchronization for the given account and data type. + */ + private fun disableAutomaticSync(account: Account, dataType: SyncDataType) { + workerManager.disablePeriodic(account, dataType) + + for (authority in dataType.possibleAuthorities()) { + syncFramework.disableSyncAbility(account, authority) + // no need to disable content-triggered sync, as it can't be active when sync-ability is disabled + } + } + + /** + * Enables/Disables automatic synchronization for the given account and data type and sets it to the given interval, + * based on sync interval setting in account settings: + * + * 1. Enables/Disables periodic sync worker for the given data type with the given interval. + * 2. Enables/Disables sync in the sync framework and enables or disables content-triggered syncs for the given data type + * + * @param account the account to synchronize + * @param dataType the data type to synchronize + */ + @WorkerThread + private fun enableAutomaticSync( + account: Account, + dataType: SyncDataType + ) { + val accountSettings = accountSettingsFactory.create(account) + val syncInterval = accountSettings.getSyncInterval(dataType) + + // 1. Update sync workers (needs already updated sync interval in AccountSettings). + if (syncInterval != null) { + val wifiOnly = accountSettings.getSyncWifiOnly() + workerManager.enablePeriodic(account, dataType, syncInterval, wifiOnly) + } else + workerManager.disablePeriodic(account, dataType) + + // 2. Enable/disable content-triggered syncs. + if (dataType == SyncDataType.CONTACTS) { + // Contact updates are handled by their respective address book accounts, so we must always + // disable the content-triggered sync for the main account. + syncFramework.disableSyncAbility(account, ContactsContract.AUTHORITY) + + // pass through request to update all existing address books + localAddressBookStore.acquireContentProvider()?.use { provider -> + for (addressBookAccount in localAddressBookStore.getAll(account, provider)) + addressBookAccount.updateSyncFrameworkSettings() + } + + } else { + // everything but contacts + val possibleAuthorities = dataType.possibleAuthorities() + val authority: String? = when (dataType) { + SyncDataType.CONTACTS -> throw IllegalStateException() // handled above + SyncDataType.EVENTS -> CalendarContract.AUTHORITY + SyncDataType.TASKS -> tasksAppManager.get().currentProvider()?.authority + } + if (authority != null && syncInterval != null) { + // enable given authority, but completely disable all other possible authorities + // (for instance, tasks apps which are not the current task app) + syncFramework.enableSyncOnContentChange(account, authority) + for (disableAuthority in possibleAuthorities - authority) + syncFramework.disableSyncAbility(account, disableAuthority) + } else + for (authority in possibleAuthorities) + syncFramework.disableSyncOnContentChange(account, authority) + } + } + + /** + * Updates automatic synchronization of the given account and all data types according to the account settings. + * + * If there's a [Service] for the given account and data type, automatic sync is enabled (with details from [AccountSettings]). + * Otherwise, automatic synchronization is disabled. + * + * @param account account for which automatic synchronization shall be updated + */ + @WorkerThread + fun updateAutomaticSync(account: Account) { + for (dataType in SyncDataType.entries) + updateAutomaticSync(account, dataType) + } + + /** + * Updates automatic synchronization of the given account and data type according to the account services and settings. + * + * If there's a [Service] for the given account and data type, automatic sync may be enabled if sync interval is set + * in [AccountSettings]. + * Otherwise, automatic synchronization is disabled. + * + * @param account account for which automatic synchronization shall be updated + * @param dataType sync data type for which automatic synchronization shall be updated + */ + @WorkerThread + fun updateAutomaticSync(account: Account, dataType: SyncDataType) { + val serviceType = when (dataType) { + SyncDataType.CONTACTS -> Service.TYPE_CARDDAV + SyncDataType.EVENTS, + SyncDataType.TASKS -> Service.TYPE_CALDAV + } + val hasService = runBlocking { serviceRepository.getByAccountAndType(account.name, serviceType) != null } + + val hasProvider = if (dataType == SyncDataType.TASKS) + tasksAppManager.get().currentProvider() != null + else + true + + if (hasService && hasProvider) + enableAutomaticSync(account, dataType) + else + disableAutomaticSync(account, dataType) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncManager.kt new file mode 100644 index 0000000..1867b19 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncManager.kt @@ -0,0 +1,319 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.accounts.Account +import android.text.format.Formatter +import at.bitfire.dav4jvm.DavCalendar +import at.bitfire.dav4jvm.MultiResponseCallback +import at.bitfire.dav4jvm.Response +import at.bitfire.dav4jvm.exception.DavException +import at.bitfire.dav4jvm.property.caldav.CalendarData +import at.bitfire.dav4jvm.property.caldav.GetCTag +import at.bitfire.dav4jvm.property.caldav.MaxResourceSize +import at.bitfire.dav4jvm.property.caldav.ScheduleTag +import at.bitfire.dav4jvm.property.webdav.GetETag +import at.bitfire.dav4jvm.property.webdav.SupportedReportSet +import at.bitfire.dav4jvm.property.webdav.SyncToken +import at.bitfire.davdroid.Constants +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.di.SyncDispatcher +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.resource.LocalCalendar +import at.bitfire.davdroid.resource.LocalEvent +import at.bitfire.davdroid.resource.LocalResource +import at.bitfire.davdroid.resource.SyncState +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.util.DavUtils.lastSegment +import at.bitfire.ical4android.Event +import at.bitfire.ical4android.EventReader +import at.bitfire.ical4android.EventWriter +import at.bitfire.ical4android.util.DateUtils +import at.bitfire.synctools.exception.InvalidICalendarException +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.runInterruptible +import net.fortuna.ical4j.model.Component +import net.fortuna.ical4j.model.component.VAlarm +import net.fortuna.ical4j.model.property.Action +import okhttp3.HttpUrl +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.Reader +import java.io.StringReader +import java.io.StringWriter +import java.time.Duration +import java.time.ZonedDateTime +import java.util.Optional +import java.util.logging.Level + +/** + * Synchronization manager for CalDAV collections; handles events (VEVENT). + */ +class CalendarSyncManager @AssistedInject constructor( + @Assisted account: Account, + @Assisted httpClient: HttpClient, + @Assisted syncResult: SyncResult, + @Assisted localCalendar: LocalCalendar, + @Assisted collection: Collection, + @Assisted resync: ResyncType?, + accountSettingsFactory: AccountSettings.Factory, + @SyncDispatcher syncDispatcher: CoroutineDispatcher +): SyncManager( + account, + httpClient, + SyncDataType.EVENTS, + syncResult, + localCalendar, + collection, + resync, + syncDispatcher +) { + + @AssistedFactory + interface Factory { + fun calendarSyncManager( + account: Account, + httpClient: HttpClient, + syncResult: SyncResult, + localCalendar: LocalCalendar, + collection: Collection, + resync: ResyncType? + ): CalendarSyncManager + } + + private val accountSettings = accountSettingsFactory.create(account) + + + override fun prepare(): Boolean { + davCollection = DavCalendar(httpClient.okHttpClient, collection.url) + + // if there are dirty exceptions for events, mark their master events as dirty, too + localCollection.processDirtyExceptions() + + // now find dirty events that have no instances and set them to deleted + localCollection.deleteDirtyEventsWithoutInstances() + + return true + } + + override suspend fun queryCapabilities(): SyncState? = + SyncException.wrapWithRemoteResourceSuspending(collection.url) { + var syncState: SyncState? = null + runInterruptible { + davCollection.propfind(0, MaxResourceSize.NAME, SupportedReportSet.NAME, GetCTag.NAME, SyncToken.NAME) { response, relation -> + if (relation == Response.HrefRelation.SELF) { + response[MaxResourceSize::class.java]?.maxSize?.let { maxSize -> + logger.info("Calendar accepts events up to ${Formatter.formatFileSize(context, maxSize)}") + } + + response[SupportedReportSet::class.java]?.let { supported -> + hasCollectionSync = supported.reports.contains(SupportedReportSet.SYNC_COLLECTION) + } + syncState = syncState(response) + } + } + } + + logger.info("Calendar supports Collection Sync: $hasCollectionSync") + syncState + } + + override fun syncAlgorithm() = + if (accountSettings.getTimeRangePastDays() != null || !hasCollectionSync) + SyncAlgorithm.PROPFIND_REPORT + else + SyncAlgorithm.COLLECTION_SYNC + + override suspend fun processLocallyDeleted(): Boolean { + if (localCollection.readOnly) { + var modified = false + for (event in localCollection.findDeleted()) { + logger.warning("Restoring locally deleted event (read-only calendar!)") + SyncException.wrapWithLocalResource(event) { + event.resetDeleted() + } + modified = true + } + + // This is unfortunately ugly: When an event has been inserted to a read-only calendar + // it's not enough to force synchronization (by returning true), + // but we also need to make sure all events are downloaded again. + if (modified) + localCollection.lastSyncState = null + + return modified + } + // mirror deletions to remote collection (DELETE) + return super.processLocallyDeleted() + } + + override suspend fun uploadDirty(): Boolean { + var modified = false + if (localCollection.readOnly) { + for (event in localCollection.findDirty()) { + logger.warning("Resetting locally modified event to ETag=null (read-only calendar!)") + SyncException.wrapWithLocalResource(event) { + event.clearDirty(Optional.empty(), null, null) + } + modified = true + } + + // This is unfortunately ugly: When an event has been inserted to a read-only calendar + // it's not enough to force synchronization (by returning true), + // but we also need to make sure all events are downloaded again. + if (modified) + localCollection.lastSyncState = null + } + + // generate UID/file name for newly created events + val superModified = super.uploadDirty() + + // return true when any operation returned true + return modified or superModified + } + + override fun onSuccessfulUpload(local: LocalEvent, newFileName: String, eTag: String?, scheduleTag: String?) { + super.onSuccessfulUpload(local, newFileName, eTag, scheduleTag) + + // update local SEQUENCE to new value after successful upload + local.updateSequence(local.getCachedEvent().sequence) + } + + override fun generateUpload(resource: LocalEvent): RequestBody = + SyncException.wrapWithLocalResource(resource) { + val event = resource.eventToUpload() + logger.log(Level.FINE, "Preparing upload of event ${resource.fileName}", event) + + // write iCalendar to string and convert to request body + val iCalWriter = StringWriter() + EventWriter(Constants.iCalProdId).write(event, iCalWriter) + iCalWriter.toString().toRequestBody(DavCalendar.MIME_ICALENDAR_UTF8) + } + + override suspend fun listAllRemote(callback: MultiResponseCallback) { + // calculate time range limits + val limitStart = accountSettings.getTimeRangePastDays()?.let { pastDays -> + ZonedDateTime.now().minusDays(pastDays.toLong()).toInstant() + } + + return SyncException.wrapWithRemoteResourceSuspending(collection.url) { + logger.info("Querying events since $limitStart") + runInterruptible { + davCollection.calendarQuery(Component.VEVENT, limitStart, null, callback) + } + } + } + + override suspend fun downloadRemote(bunch: List) { + logger.info("Downloading ${bunch.size} iCalendars: $bunch") + SyncException.wrapWithRemoteResourceSuspending(collection.url) { + runInterruptible { + davCollection.multiget(bunch) { response, _ -> + /* + * Real-world servers may return: + * + * - unrelated resources + * - the collection itself + * - the requested resources, but with a different collection URL (for instance, `/cal/1.ics` instead of `/shared-cal/1.ics`). + * + * So we: + * + * - ignore unsuccessful responses, + * - ignore responses without requested calendar data (should also ignore collections and hopefully unrelated resources), and + * - take the last segment of the href as the file name and assume that it's in the requested collection. + */ + SyncException.wrapWithRemoteResource(response.href) wrapResource@{ + if (!response.isSuccess()) { + logger.warning("Ignoring non-successful multi-get response for ${response.href}") + return@wrapResource + } + + val iCal = response[CalendarData::class.java]?.iCalendar + if (iCal == null) { + logger.warning("Ignoring multi-get response without calendar-data") + return@wrapResource + } + + val eTag = response[GetETag::class.java]?.eTag + ?: throw DavException("Received multi-get response without ETag") + val scheduleTag = response[ScheduleTag::class.java]?.scheduleTag + + processVEvent( + response.href.lastSegment, + eTag, + scheduleTag, + StringReader(iCal) + ) + } + } + } + } + } + + override fun postProcess() {} + + + // helpers + + private fun processVEvent(fileName: String, eTag: String, scheduleTag: String?, reader: Reader) { + val events: List + try { + events = EventReader().readEvents(reader) + } catch (e: InvalidICalendarException) { + logger.log(Level.SEVERE, "Received invalid iCalendar, ignoring", e) + notifyInvalidResource(e, fileName) + return + } + + if (events.size == 1) { + val event = events.first() + + // set default reminder for non-full-day events, if requested + val defaultAlarmMinBefore = accountSettings.getDefaultAlarm() + if (defaultAlarmMinBefore != null && DateUtils.isDateTime(event.dtStart) && event.alarms.isEmpty()) { + val alarm = VAlarm(Duration.ofMinutes(-defaultAlarmMinBefore.toLong())).apply { + // Sets METHOD_ALERT instead of METHOD_DEFAULT in the calendar provider. + // Needed for calendars to actually show a notification. + properties += Action.DISPLAY + } + logger.log(Level.FINE, "${event.uid}: Adding default alarm", alarm) + event.alarms += alarm + } + + // update local event, if it exists + val local = localCollection.findByName(fileName) + SyncException.wrapWithLocalResource(local) { + if (local != null) { + logger.log(Level.INFO, "Updating $fileName in local calendar", event) + local.update( + data = event, + fileName = fileName, + eTag = eTag, + scheduleTag = scheduleTag, + flags = LocalResource.FLAG_REMOTELY_PRESENT + ) + } else { + logger.log(Level.INFO, "Adding $fileName to local calendar", event) + localCollection.add( + event = event, + fileName = fileName, + eTag = eTag, + scheduleTag = scheduleTag, + flags = LocalResource.FLAG_REMOTELY_PRESENT + ) + } + } + } else + logger.info("Received VCALENDAR with not exactly one VEVENT with UID and without RECURRENCE-ID; ignoring $fileName") + } + + override fun notifyInvalidResourceTitle(): String = + context.getString(R.string.sync_invalid_event) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncer.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncer.kt new file mode 100644 index 0000000..cc34683 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncer.kt @@ -0,0 +1,74 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.accounts.Account +import android.content.ContentProviderClient +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.resource.LocalCalendar +import at.bitfire.davdroid.resource.LocalCalendarStore +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.runBlocking + +/** + * Sync logic for calendars + */ +class CalendarSyncer @AssistedInject constructor( + @Assisted account: Account, + @Assisted resync: ResyncType?, + @Assisted syncResult: SyncResult, + calendarStore: LocalCalendarStore, + private val accountSettingsFactory: AccountSettings.Factory, + private val calendarSyncManagerFactory: CalendarSyncManager.Factory +): Syncer(account, resync, syncResult) { + + @AssistedFactory + interface Factory { + fun create(account: Account, resyncType: ResyncType?, syncResult: SyncResult): CalendarSyncer + } + + override val dataStore = calendarStore + + override val serviceType: String + get() = Service.TYPE_CALDAV + + + override fun prepare(provider: ContentProviderClient): Boolean { + // Update colors + val accountSettings = accountSettingsFactory.create(account) + + val calendarProvider = AndroidCalendarProvider(account, provider) + if (accountSettings.getEventColors()) + calendarProvider.provideCss3ColorIndices() + else + calendarProvider.removeColorIndices() + return true + } + + override fun getDbSyncCollections(serviceId: Long): List = + collectionRepository.getSyncCalendars(serviceId) + + override fun syncCollection(provider: ContentProviderClient, localCollection: LocalCalendar, remoteCollection: Collection) { + logger.info("Synchronizing calendar #${localCollection.androidCalendar.id}, DB Collection ID: ${localCollection.dbCollectionId}, URL: ${localCollection.androidCalendar.name}") + + val syncManager = calendarSyncManagerFactory.calendarSyncManager( + account, + httpClient.value, + syncResult, + localCollection, + remoteCollection, + resync + ) + runBlocking { + syncManager.performSync() + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/ContactsSyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/ContactsSyncManager.kt new file mode 100644 index 0000000..68add45 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/ContactsSyncManager.kt @@ -0,0 +1,506 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.accounts.Account +import android.content.ContentProviderClient +import android.text.format.Formatter +import at.bitfire.dav4jvm.DavAddressBook +import at.bitfire.dav4jvm.MultiResponseCallback +import at.bitfire.dav4jvm.Response +import at.bitfire.dav4jvm.exception.DavException +import at.bitfire.dav4jvm.property.caldav.GetCTag +import at.bitfire.dav4jvm.property.carddav.AddressData +import at.bitfire.dav4jvm.property.carddav.MaxResourceSize +import at.bitfire.dav4jvm.property.carddav.SupportedAddressData +import at.bitfire.dav4jvm.property.webdav.GetContentType +import at.bitfire.dav4jvm.property.webdav.GetETag +import at.bitfire.dav4jvm.property.webdav.ResourceType +import at.bitfire.dav4jvm.property.webdav.SupportedReportSet +import at.bitfire.dav4jvm.property.webdav.SyncToken +import at.bitfire.davdroid.Constants +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.di.SyncDispatcher +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.resource.LocalAddress +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.resource.LocalContact +import at.bitfire.davdroid.resource.LocalGroup +import at.bitfire.davdroid.resource.LocalResource +import at.bitfire.davdroid.resource.SyncState +import at.bitfire.davdroid.resource.workaround.ContactDirtyVerifier +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.sync.groups.CategoriesStrategy +import at.bitfire.davdroid.sync.groups.VCard4Strategy +import at.bitfire.davdroid.util.DavUtils +import at.bitfire.davdroid.util.DavUtils.lastSegment +import at.bitfire.davdroid.util.DavUtils.sameTypeAs +import at.bitfire.vcard4android.Contact +import at.bitfire.vcard4android.GroupMethod +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import ezvcard.VCardVersion +import ezvcard.io.CannotParseException +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.runInterruptible +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.MediaType +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.Reader +import java.io.StringReader +import java.util.Optional +import java.util.logging.Level +import kotlin.jvm.optionals.getOrNull + +/** + * Synchronization manager for CardDAV collections; handles contacts and groups. + * + * Group handling differs according to the {@link #groupMethod}. There are two basic methods to + * handle/manage groups: + * + * 1. CATEGORIES: groups memberships are attached to each contact and represented as + * "category". When a group is dirty or has been deleted, all its members have to be set to + * dirty, too (because they have to be uploaded without the respective category). This + * is done in [uploadDirty]. Empty groups can be deleted without further processing, + * which is done in [postProcess] because groups may become empty after downloading + * updated remote contacts. + * + * 2. Groups as separate VCards: individual and group contacts (with a list of member UIDs) are + * distinguished. When a local group is dirty, its members don't need to be set to dirty. + * + * However, when a contact is dirty, it has + * to be checked whether its group memberships have changed. In this case, the respective + * groups have to be set to dirty. For instance, if contact A is in group G and H, and then + * group membership of G is removed, the contact will be set to dirty because of the changed + * [android.provider.ContactsContract.CommonDataKinds.GroupMembership]. DAVx5 will + * then have to check whether the group memberships have actually changed, and if so, + * all affected groups have to be set to dirty. To detect changes in group memberships, + * DAVx5 always mirrors all [android.provider.ContactsContract.CommonDataKinds.GroupMembership] + * data rows in respective [at.bitfire.vcard4android.CachedGroupMembership] rows. + * If the cached group memberships are not the same as the current group member ships, the + * difference set (in our example G, because its in the cached memberships, but not in the + * actual ones) is marked as dirty. This is done in [uploadDirty]. + * + * When downloading remote contacts, groups (+ member information) may be received + * by the actual members. Thus, the member lists have to be cached until all VCards + * are received. This is done by caching the member UIDs of each group in + * [LocalGroup.COLUMN_PENDING_MEMBERS]. In [postProcess], + * these "pending memberships" are assigned to the actual contacts and then cleaned up. + * + * @param syncFrameworkUpload set when this sync is caused by the sync framework and [android.content.ContentResolver.SYNC_EXTRAS_UPLOAD] was set + */ +class ContactsSyncManager @AssistedInject constructor( + @Assisted account: Account, + @Assisted httpClient: HttpClient, + @Assisted syncResult: SyncResult, + @Assisted val provider: ContentProviderClient, + @Assisted localAddressBook: LocalAddressBook, + @Assisted collection: Collection, + @Assisted resync: ResyncType?, + @Assisted val syncFrameworkUpload: Boolean, + val dirtyVerifier: Optional, + accountSettingsFactory: AccountSettings.Factory, + private val httpClientBuilder: HttpClient.Builder, + @SyncDispatcher syncDispatcher: CoroutineDispatcher +): SyncManager( + account, + httpClient, + SyncDataType.CONTACTS, + syncResult, + localAddressBook, + collection, + resync, + syncDispatcher +) { + + @AssistedFactory + interface Factory { + fun contactsSyncManager( + account: Account, + httpClient: HttpClient, + syncResult: SyncResult, + provider: ContentProviderClient, + localAddressBook: LocalAddressBook, + collection: Collection, + resync: ResyncType?, + syncFrameworkUpload: Boolean + ): ContactsSyncManager + } + + companion object { + infix fun Set.disjunct(other: Set) = (this - other) union (other - this) + } + + private val accountSettings = accountSettingsFactory.create(account) + + private var hasVCard4 = false + private var hasJCard = false + private val groupStrategy = when (accountSettings.getGroupMethod()) { + GroupMethod.GROUP_VCARDS -> VCard4Strategy(localAddressBook) + GroupMethod.CATEGORIES -> CategoriesStrategy(localAddressBook) + } + + /** + * Used to download images which are referenced by URL + */ + private lateinit var resourceDownloader: ResourceDownloader + + + override fun prepare(): Boolean { + if (dirtyVerifier.isPresent) { + logger.info("Sync will verify dirty contacts (Android 7.x workaround)") + if (!dirtyVerifier.get().prepareAddressBook(localCollection, isUpload = syncFrameworkUpload)) + return false + } + + davCollection = DavAddressBook(httpClient.okHttpClient, collection.url) + resourceDownloader = ResourceDownloader(davCollection.location) + + logger.info("Contact group strategy: ${groupStrategy::class.java.simpleName}") + return true + } + + override suspend fun queryCapabilities(): SyncState? { + return SyncException.wrapWithRemoteResourceSuspending(collection.url) { + var syncState: SyncState? = null + runInterruptible { + davCollection.propfind(0, MaxResourceSize.NAME, SupportedAddressData.NAME, SupportedReportSet.NAME, GetCTag.NAME, SyncToken.NAME) { response, relation -> + if (relation == Response.HrefRelation.SELF) { + response[MaxResourceSize::class.java]?.maxSize?.let { maxSize -> + logger.info("Address book accepts vCards up to ${Formatter.formatFileSize(context, maxSize)}") + } + + response[SupportedAddressData::class.java]?.let { supported -> + hasVCard4 = supported.hasVCard4() + + // temporarily disable jCard because of https://github.com/nextcloud/server/issues/29693 + // hasJCard = supported.hasJCard() + } + response[SupportedReportSet::class.java]?.let { supported -> + hasCollectionSync = supported.reports.contains(SupportedReportSet.SYNC_COLLECTION) + } + syncState = syncState(response) + } + } + } + + // logger.info("Server supports jCard: $hasJCard") + logger.info("Address book supports vCard4: $hasVCard4") + logger.info("Address book supports Collection Sync: $hasCollectionSync") + + syncState + } + } + + override fun syncAlgorithm() = + if (hasCollectionSync) + SyncAlgorithm.COLLECTION_SYNC + else + SyncAlgorithm.PROPFIND_REPORT + + override suspend fun processLocallyDeleted() = + if (localCollection.readOnly) { + var modified = false + for (group in localCollection.findDeletedGroups()) { + logger.warning("Restoring locally deleted group (read-only address book!)") + SyncException.wrapWithLocalResource(group) { + group.resetDeleted() + } + modified = true + } + + for (contact in localCollection.findDeletedContacts()) { + logger.warning("Restoring locally deleted contact (read-only address book!)") + SyncException.wrapWithLocalResource(contact) { + contact.resetDeleted() + } + modified = true + } + + /* This is unfortunately dirty: When a contact has been inserted to a read-only address book + that supports Collection Sync, it's not enough to force synchronization (by returning true), + but we also need to make sure all contacts are downloaded again. */ + if (modified) + localCollection.lastSyncState = null + + modified + } else + // mirror deletions to remote collection (DELETE) + super.processLocallyDeleted() + + override suspend fun uploadDirty(): Boolean { + var modified = false + + if (localCollection.readOnly) { + for (group in localCollection.findDirtyGroups()) { + logger.warning("Resetting locally modified group to ETag=null (read-only address book!)") + SyncException.wrapWithLocalResource(group) { + group.clearDirty(Optional.empty(), null) + } + modified = true + } + + for (contact in localCollection.findDirtyContacts()) { + logger.warning("Resetting locally modified contact to ETag=null (read-only address book!)") + SyncException.wrapWithLocalResource(contact) { + contact.clearDirty(Optional.empty(), null) + } + modified = true + } + + // see same position in processLocallyDeleted + if (modified) + localCollection.lastSyncState = null + + } else + // we only need to handle changes in groups when the address book is read/write + groupStrategy.beforeUploadDirty() + + // generate UID/file name for newly created contacts + val superModified = super.uploadDirty() + + // return true when any operation returned true + return modified or superModified + } + + override fun generateUpload(resource: LocalAddress): RequestBody = + SyncException.wrapWithLocalResource(resource) { + val contact: Contact = when (resource) { + is LocalContact -> resource.getContact() + is LocalGroup -> resource.getContact() + else -> throw IllegalArgumentException("resource must be LocalContact or LocalGroup") + } + + logger.log(Level.FINE, "Preparing upload of vCard ${resource.fileName}", contact) + + val os = ByteArrayOutputStream() + val mimeType: MediaType + when { + hasJCard -> { + mimeType = DavAddressBook.MIME_JCARD + contact.writeJCard(os, Constants.vCardProdId) + } + hasVCard4 -> { + mimeType = DavAddressBook.MIME_VCARD4 + contact.writeVCard(VCardVersion.V4_0, os, Constants.vCardProdId) + } + else -> { + mimeType = DavAddressBook.MIME_VCARD3_UTF8 + contact.writeVCard(VCardVersion.V3_0, os, Constants.vCardProdId) + } + } + + return@wrapWithLocalResource os.toByteArray().toRequestBody(mimeType) + } + + override suspend fun listAllRemote(callback: MultiResponseCallback) = + SyncException.wrapWithRemoteResourceSuspending(collection.url) { + runInterruptible { + davCollection.propfind(1, ResourceType.NAME, GetETag.NAME, callback = callback) + } + } + + override suspend fun downloadRemote(bunch: List) { + logger.info("Downloading ${bunch.size} vCard(s): $bunch") + SyncException.wrapWithRemoteResourceSuspending(collection.url) { + val contentType: String? + val version: String? + when { + hasJCard -> { + contentType = DavUtils.MEDIA_TYPE_JCARD.toString() + version = VCardVersion.V4_0.version + } + hasVCard4 -> { + contentType = DavUtils.MEDIA_TYPE_VCARD.toString() + version = VCardVersion.V4_0.version + } + else -> { + contentType = DavUtils.MEDIA_TYPE_VCARD.toString() + version = null // 3.0 is the default version; don't request 3.0 explicitly because maybe some vCard3-only servers don't understand it + } + } + runInterruptible { + davCollection.multiget(bunch, contentType, version) { response, _ -> + // See CalendarSyncManager for more information about the multi-get response + SyncException.wrapWithRemoteResource(response.href) wrapResource@{ + if (!response.isSuccess()) { + logger.warning("Ignoring non-successful multi-get response for ${response.href}") + return@wrapResource + } + + val card = response[AddressData::class.java]?.card + if (card == null) { + logger.warning("Ignoring multi-get response without address-data") + return@wrapResource + } + + val eTag = response[GetETag::class.java]?.eTag + ?: throw DavException("Received multi-get response without ETag") + + var isJCard = hasJCard // assume that server has sent what we have requested (we ask for jCard only when the server advertises it) + response[GetContentType::class.java]?.type?.let { type -> + isJCard = type.sameTypeAs(DavUtils.MEDIA_TYPE_JCARD) + } + + processCard( + response.href.lastSegment, + eTag, + StringReader(card), + isJCard, + resourceDownloader + ) + } + } + } + } + } + + override fun postProcess() { + groupStrategy.postProcess() + } + + + // helpers + + private fun processCard(fileName: String, eTag: String, reader: Reader, jCard: Boolean, downloader: Contact.Downloader) { + logger.info("Processing CardDAV resource $fileName") + + val contacts = try { + Contact.fromReader(reader, jCard, downloader) + } catch (e: CannotParseException) { + logger.log(Level.SEVERE, "Received invalid vCard, ignoring", e) + notifyInvalidResource(e, fileName) + return + } + + if (contacts.isEmpty()) { + logger.warning("Received vCard without data, ignoring") + return + } else if (contacts.size > 1) + logger.warning("Received multiple vCards, using first one") + + val newData = contacts.first() + groupStrategy.verifyContactBeforeSaving(newData) + + var updated: LocalAddress? = null + + val existing = localCollection.findByName(fileName) + if (existing == null) { + // create new contact/group + if (newData.group) { + logger.log(Level.INFO, "Creating local group", newData) + val newGroup = LocalGroup(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT) + SyncException.wrapWithLocalResource(newGroup) { + newGroup.add() + updated = newGroup + } + + } else { + logger.log(Level.INFO, "Creating local contact", newData) + val newContact = LocalContact(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT) + SyncException.wrapWithLocalResource(newContact) { + newContact.add() + updated = newContact + } + } + + } else { + // update existing local contact/group + logger.log(Level.INFO, "Updating $fileName in local address book", newData) + + SyncException.wrapWithLocalResource(existing) { + if ((existing is LocalGroup && newData.group) || (existing is LocalContact && !newData.group)) { + // update contact / group + + existing.update( + data = newData, + fileName = fileName, + eTag = eTag, + flags = LocalResource.FLAG_REMOTELY_PRESENT, + scheduleTag = null + ) + updated = existing + + } else { + // group has become an individual contact or vice versa, delete and create with new type + existing.deleteLocal() + + if (newData.group) { + logger.log(Level.INFO, "Creating local group (was contact before)", newData) + val newGroup = LocalGroup(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT) + SyncException.wrapWithLocalResource(newGroup) { + newGroup.add() + updated = newGroup + } + + } else { + logger.log(Level.INFO, "Creating local contact (was group before)", newData) + val newContact = LocalContact(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT) + SyncException.wrapWithLocalResource(newContact) { + newContact.add() + updated = newContact + } + } + } + } + } + + // update hash code of updated contact, if applicable + (updated as? LocalContact)?.let { updatedContact -> + // workaround for Android 7 which sets DIRTY flag when only meta-data is changed + dirtyVerifier.getOrNull()?.updateHashCode(localCollection, updatedContact) + } + } + + + // downloader helper class + + private inner class ResourceDownloader( + val baseUrl: HttpUrl + ): Contact.Downloader { + + override fun download(url: String, accepts: String): ByteArray? { + val httpUrl = url.toHttpUrlOrNull() + if (httpUrl == null) { + logger.log(Level.SEVERE, "Invalid external resource URL", url) + return null + } + + // authenticate only against a certain host, and only upon request + httpClientBuilder + .fromAccount(account, onlyHost = baseUrl.host) + .followRedirects(true) // allow redirects + .build() + .use { httpClient -> + try { + val response = httpClient.okHttpClient.newCall(Request.Builder() + .get() + .url(httpUrl) + .build()).execute() + + if (response.isSuccessful) + return response.body.bytes() + else + logger.warning("Couldn't download external resource") + } catch(e: IOException) { + logger.log(Level.SEVERE, "Couldn't download external resource", e) + } + } + + return null + } + } + + override fun notifyInvalidResourceTitle(): String = + context.getString(R.string.sync_invalid_contact) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncManager.kt new file mode 100644 index 0000000..2c76b8b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncManager.kt @@ -0,0 +1,217 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.accounts.Account +import android.text.format.Formatter +import androidx.annotation.OpenForTesting +import at.bitfire.dav4jvm.DavCalendar +import at.bitfire.dav4jvm.MultiResponseCallback +import at.bitfire.dav4jvm.Response +import at.bitfire.dav4jvm.exception.DavException +import at.bitfire.dav4jvm.property.caldav.CalendarData +import at.bitfire.dav4jvm.property.caldav.GetCTag +import at.bitfire.dav4jvm.property.caldav.MaxResourceSize +import at.bitfire.dav4jvm.property.webdav.GetETag +import at.bitfire.dav4jvm.property.webdav.SyncToken +import at.bitfire.davdroid.Constants +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.di.SyncDispatcher +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.resource.LocalJtxCollection +import at.bitfire.davdroid.resource.LocalJtxICalObject +import at.bitfire.davdroid.resource.LocalResource +import at.bitfire.davdroid.resource.SyncState +import at.bitfire.davdroid.util.DavUtils.lastSegment +import at.bitfire.ical4android.JtxICalObject +import at.bitfire.synctools.exception.InvalidICalendarException +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.runInterruptible +import okhttp3.HttpUrl +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.ByteArrayOutputStream +import java.io.Reader +import java.io.StringReader +import java.util.logging.Level + +class JtxSyncManager @AssistedInject constructor( + @Assisted account: Account, + @Assisted httpClient: HttpClient, + @Assisted syncResult: SyncResult, + @Assisted localCollection: LocalJtxCollection, + @Assisted collection: Collection, + @Assisted resync: ResyncType?, + @SyncDispatcher syncDispatcher: CoroutineDispatcher +): SyncManager( + account, + httpClient, + SyncDataType.TASKS, + syncResult, + localCollection, + collection, + resync, + syncDispatcher +) { + + @AssistedFactory + interface Factory { + fun jtxSyncManager( + account: Account, + httpClient: HttpClient, + syncResult: SyncResult, + localCollection: LocalJtxCollection, + collection: Collection, + resync: ResyncType? + ): JtxSyncManager + } + + + override fun prepare(): Boolean { + davCollection = DavCalendar(httpClient.okHttpClient, collection.url) + + return true + } + + override suspend fun queryCapabilities() = + SyncException.wrapWithRemoteResourceSuspending(collection.url) { + var syncState: SyncState? = null + runInterruptible { + davCollection.propfind(0, GetCTag.NAME, MaxResourceSize.NAME, SyncToken.NAME) { response, relation -> + if (relation == Response.HrefRelation.SELF) { + response[MaxResourceSize::class.java]?.maxSize?.let { maxSize -> + logger.info("Collection accepts resources up to ${Formatter.formatFileSize(context, maxSize)}") + } + + syncState = syncState(response) + } + } + } + syncState + } + + override fun generateUpload(resource: LocalJtxICalObject): RequestBody = + SyncException.wrapWithLocalResource(resource) { + logger.log(Level.FINE, "Preparing upload of icalobject ${resource.fileName}", resource) + val os = ByteArrayOutputStream() + resource.write(os, Constants.iCalProdId) + os.toByteArray().toRequestBody(DavCalendar.MIME_ICALENDAR_UTF8) + } + + override fun syncAlgorithm() = SyncAlgorithm.PROPFIND_REPORT + + override suspend fun listAllRemote(callback: MultiResponseCallback) { + SyncException.wrapWithRemoteResourceSuspending(collection.url) { + if (localCollection.supportsVTODO) { + logger.info("Querying tasks") + runInterruptible { + davCollection.calendarQuery("VTODO", null, null, callback) + } + } + + if (localCollection.supportsVJOURNAL) { + logger.info("Querying journals") + runInterruptible { + davCollection.calendarQuery("VJOURNAL", null, null, callback) + } + } + } + } + + override suspend fun downloadRemote(bunch: List) { + logger.info("Downloading ${bunch.size} iCalendars: $bunch") + // multiple iCalendars, use calendar-multi-get + SyncException.wrapWithRemoteResourceSuspending(collection.url) { + runInterruptible { + davCollection.multiget(bunch) { response, _ -> + // See CalendarSyncManager for more information about the multi-get response + SyncException.wrapWithRemoteResource(response.href) wrapResource@{ + if (!response.isSuccess()) { + logger.warning("Ignoring non-successful multi-get response for ${response.href}") + return@wrapResource + } + + val iCal = response[CalendarData::class.java]?.iCalendar + if (iCal == null) { + logger.warning("Ignoring multi-get response without calendar-data") + return@wrapResource + } + + val eTag = response[GetETag::class.java]?.eTag + ?: throw DavException("Received multi-get response without ETag") + + processICalObject(response.href.lastSegment, eTag, StringReader(iCal)) + } + } + } + } + } + + override fun postProcess() { + localCollection.updateLastSync() + } + + override fun notifyInvalidResourceTitle(): String = + context.getString(R.string.sync_invalid_event) + + + @OpenForTesting + internal fun processICalObject(fileName: String, eTag: String, reader: Reader) { + val icalobjects: MutableList = mutableListOf() + try { + // parse the reader content and return the list of ICalObjects + icalobjects.addAll(JtxICalObject.fromReader(reader, localCollection)) + } catch (e: InvalidICalendarException) { + logger.log(Level.SEVERE, "Received invalid iCalendar, ignoring", e) + notifyInvalidResource(e, fileName) + return + } + + logger.log(Level.INFO, "Found ${icalobjects.size} entries in $fileName", icalobjects) + + icalobjects.forEach { jtxICalObject -> + // if the entry is a recurring entry (and therefore has a recurid) + // we update the existing (generated) entry + val recurid = jtxICalObject.recurid + if(recurid != null) { + val local = localCollection.findRecurInstance(jtxICalObject.uid, recurid) + SyncException.wrapWithLocalResource(local) { + logger.log(Level.INFO, "Updating $fileName with recur instance $recurid in local list", jtxICalObject) + if(local != null) { + local.update(jtxICalObject) + } else { + val newLocal = LocalJtxICalObject(localCollection, fileName, eTag, null, LocalResource.FLAG_REMOTELY_PRESENT) + SyncException.wrapWithLocalResource(newLocal) { + newLocal.applyNewData(jtxICalObject) + newLocal.add() + } + } + } + } else { + // otherwise we insert or update the main entry + val local = localCollection.findByName(fileName) + SyncException.wrapWithLocalResource(local) { + if (local != null) { + logger.log(Level.INFO, "Updating $fileName in local list", jtxICalObject) + local.eTag = eTag + local.update(jtxICalObject) + } else { + logger.log(Level.INFO, "Adding $fileName to local list", jtxICalObject) + + val newLocal = LocalJtxICalObject(localCollection, fileName, eTag, null, LocalResource.FLAG_REMOTELY_PRESENT) + SyncException.wrapWithLocalResource(newLocal) { + newLocal.applyNewData(jtxICalObject) + newLocal.add() + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncer.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncer.kt new file mode 100644 index 0000000..097b8d8 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncer.kt @@ -0,0 +1,85 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.ContentProviderClient +import android.os.Build +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.resource.LocalJtxCollection +import at.bitfire.davdroid.resource.LocalJtxCollectionStore +import at.bitfire.ical4android.TaskProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.runBlocking + +/** + * Sync logic for jtx board + */ +class JtxSyncer @AssistedInject constructor( + @Assisted account: Account, + @Assisted resync: ResyncType?, + @Assisted syncResult: SyncResult, + localJtxCollectionStore: LocalJtxCollectionStore, + private val jtxSyncManagerFactory: JtxSyncManager.Factory, + private val tasksAppManager: dagger.Lazy +): Syncer(account, resync, syncResult) { + + @AssistedFactory + interface Factory { + fun create(account: Account, resyncType: ResyncType?, syncResult: SyncResult): JtxSyncer + } + + override val dataStore = localJtxCollectionStore + + override val serviceType: String + get() = Service.TYPE_CALDAV + + + override fun prepare(provider: ContentProviderClient): Boolean { + // check whether jtx Board is new enough + try { + TaskProvider.checkVersion(context, TaskProvider.ProviderName.JtxBoard) + } catch (e: TaskProvider.ProviderTooOldException) { + tasksAppManager.get().notifyProviderTooOld(e) + syncResult.contentProviderError = true + return false // Don't sync + } + + // make sure account can be seen by task provider + if (Build.VERSION.SDK_INT >= 26) { + /* Warning: If setAccountVisibility is called, Android 12 broadcasts the + AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION Intent. This cancels running syncs + and starts them again! So make sure setAccountVisibility is only called when necessary. */ + val am = AccountManager.get(context) + if (am.getAccountVisibility(account, TaskProvider.ProviderName.JtxBoard.packageName) != AccountManager.VISIBILITY_VISIBLE) + am.setAccountVisibility(account, TaskProvider.ProviderName.JtxBoard.packageName, AccountManager.VISIBILITY_VISIBLE) + } + return true + } + + override fun getDbSyncCollections(serviceId: Long): List = + collectionRepository.getSyncJtxCollections(serviceId) + + override fun syncCollection(provider: ContentProviderClient, localCollection: LocalJtxCollection, remoteCollection: Collection) { + logger.info("Synchronizing jtx collection $localCollection") + + val syncManager = jtxSyncManagerFactory.jtxSyncManager( + account, + httpClient.value, + syncResult, + localCollection, + remoteCollection, + resync + ) + runBlocking { + syncManager.performSync() + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/ResyncType.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/ResyncType.kt new file mode 100644 index 0000000..f8c0623 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/ResyncType.kt @@ -0,0 +1,34 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +/** + * Used to signal that re-synchronization is requested during a sync. + * + * Re-synchronization means that synchronization shouldn't skip listing/downloading + * the entries even when then `sync-token` (or CTag) didn't change since the last sync. + */ +enum class ResyncType { + + /** + * **(Normal) re-synchronization**: all remote entries shall be listed regardless of the + * sync-token (or CTag) of the collection. Modified entries will then be downloaded as usual. + * + * Sample use-case: the past event time range setting has been modified, and we want + * to get the new list of all events (regardless of the sync-token). + */ + RESYNC_LIST, + + /** + * **Full re-synchronization**: all remote entries shall be listed regardless of the + * sync-token (or CTag) of the collection, and all entries will be downloaded again, + * either if they were not changed on the server since the last sync. + * + * Sample use-case: Contact group type setting is changed, and all vCards have to + * be downloaded and parsed again to determine their group memberships. + */ + RESYNC_ENTRIES + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncConditions.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncConditions.kt new file mode 100644 index 0000000..58bc565 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncConditions.kt @@ -0,0 +1,165 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.content.Context +import android.content.Intent +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.net.wifi.WifiManager +import androidx.core.content.getSystemService +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.ui.NotificationRegistry +import at.bitfire.davdroid.ui.account.WifiPermissionsActivity +import at.bitfire.davdroid.util.PermissionUtils +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.qualifiers.ApplicationContext +import java.util.logging.Level +import java.util.logging.Logger + +/** + * Provides methods to check whether a sync shall be run for a given account. + */ +class SyncConditions @AssistedInject constructor( + @Assisted private val accountSettings: AccountSettings, + @ApplicationContext private val context: Context, + private val logger: Logger, + private val notificationRegistry: NotificationRegistry +) { + + @AssistedFactory + interface Factory { + fun create(accountSettings: AccountSettings): SyncConditions + } + + + /** + * Checks whether we are connected to the correct wifi (SSID) defined by user in the + * account settings. + * + * Note: Should be connected to some wifi before calling. + * + * @return *true* if connected to the correct wifi OR no wifi names were specified in + * account settings; *false* otherwise + */ + internal fun correctWifiSsid(): Boolean { + accountSettings.getSyncWifiOnlySSIDs()?.let { onlySSIDs -> + // check required permissions and location status + if (!PermissionUtils.canAccessWifiSsid(context)) { + // not all permissions granted; show notification + val intent = Intent(context, WifiPermissionsActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.putExtra(WifiPermissionsActivity.EXTRA_ACCOUNT, accountSettings.account) + notificationRegistry.notifyPermissions(intent) + + logger.warning("Can't access WiFi SSID, aborting sync") + return false + } + + val wifi = context.getSystemService()!! + @Suppress("DEPRECATION") val info = wifi.connectionInfo + if (info == null || !onlySSIDs.contains(info.ssid.trim('"'))) { + logger.info("Connected to wrong WiFi network (${info.ssid}), aborting sync") + return false + } + logger.fine("Connected to WiFi network ${info.ssid}") + } + return true + } + + /** + * Checks whether we are connected to the Internet. + * + * On API 26+ devices, if a VPN is used, WorkManager might start the SyncWorker without an + * Internet connection (because [NetworkCapabilities.NET_CAPABILITY_VALIDATED] is always set for VPN connections). + * To prevent the start without internet access, we don't check for VPN connections by default + * (by using [NetworkCapabilities.NET_CAPABILITY_NOT_VPN]). + * + * However in special occasions (when syncing over a VPN without validated Internet on the + * underlying connection) we do not want to exclude VPNs. + * + * This method uses [AccountSettings.getIgnoreVpns]: if `true`, it filters VPN connections in the Internet check; + * `false` allows them as valid connection. + * + * @return whether we are connected to the Internet + */ + internal fun internetAvailable(): Boolean { + val connectivityManager = context.getSystemService()!! + @Suppress("DEPRECATION") + return connectivityManager.allNetworks.any { network -> + val capabilities = connectivityManager.getNetworkCapabilities(network) + logger.log( + Level.FINE, "Looking for validated Internet over this connection.", + arrayOf(connectivityManager.getNetworkInfo(network), capabilities) + ) + + if (capabilities != null) { + if (!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { + logger.fine("Missing network capability: INTERNET") + return@any false + } + + if (!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) { + logger.fine("Missing network capability: VALIDATED") + return@any false + } + + val ignoreVpns = accountSettings.getIgnoreVpns() + if (ignoreVpns) + if (!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) { + logger.fine("Missing network capability: NOT_VPN") + return@any false + } + + logger.fine("This connection can be used.") + /* return@any */ true + } else + // no network capabilities available, we can't use this connection + /* return@any */ false + } + } + + /** + * Checks whether we are connected to validated WiFi + */ + internal fun wifiAvailable(): Boolean { + val connectivityManager = context.getSystemService()!! + @Suppress("DEPRECATION") + connectivityManager.allNetworks.forEach { network -> + connectivityManager.getNetworkCapabilities(network)?.let { capabilities -> + if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) && + capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) + return true + } + } + return false + } + + /** + * Checks whether user imposed sync conditions from settings are met: + * - Sync only on WiFi? + * - Sync only on specific WiFi (SSID)? + * + * @return *true* if conditions are met; *false* if not + */ + fun wifiConditionsMet(): Boolean { + // May we sync without WiFi? + if (!accountSettings.getSyncWifiOnly()) + return true // yes, continue + + // WiFi required, is it available? + if (!wifiAvailable()) { + logger.info("Not on connected WiFi, stopping") + return false + } + // If execution reaches this point, we're on a connected WiFi + + // Check whether we are connected to the correct WiFi (in case SSID was provided) + return correctWifiSsid() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncDataType.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncDataType.kt new file mode 100644 index 0000000..9c1ce3d --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncDataType.kt @@ -0,0 +1,78 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.content.Context +import android.provider.CalendarContract +import android.provider.ContactsContract +import at.bitfire.ical4android.TaskProvider +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent + +enum class SyncDataType { + + CONTACTS, + EVENTS, + TASKS; + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface SyncDataTypeEntryPoint { + fun tasksAppManager(): TasksAppManager + } + + /** + * Returns authorities which exist for this sync data type. Used on [TASKS] the method + * may return an empty list if there are no tasks providers (installed tasks apps). + * + * @return list of authorities matching this data type + */ + fun possibleAuthorities(): List = + when (this) { + CONTACTS -> listOf(ContactsContract.AUTHORITY) + EVENTS -> listOf(CalendarContract.AUTHORITY) + TASKS -> TaskProvider.ProviderName.entries.map { it.authority } + } + + /** + * Returns the authority corresponding to this datatype. + * When more than one tasks provider exists (tasks apps installed) the authority for the active + * tasks provider (user selected tasks app) is returned. + * + * @param context android context used to determine the active/selected tasks provider + * @return the authority matching this data type or *null* for [TASKS] if no tasks app is installed + */ + fun currentAuthority(context: Context): String? = + when (this) { + CONTACTS -> ContactsContract.AUTHORITY + EVENTS -> CalendarContract.AUTHORITY + TASKS -> EntryPointAccessors.fromApplication(context) + .tasksAppManager() + .currentProvider() + ?.authority + } + + + companion object { + + fun fromAuthority(authority: String): SyncDataType { + return when (authority) { + ContactsContract.AUTHORITY -> + CONTACTS + CalendarContract.AUTHORITY -> + EVENTS + TaskProvider.ProviderName.JtxBoard.authority, + TaskProvider.ProviderName.TasksOrg.authority, + TaskProvider.ProviderName.OpenTasks.authority -> + TASKS + else -> throw IllegalArgumentException("Unknown authority: $authority") + } + } + + } + +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncException.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncException.kt new file mode 100644 index 0000000..e17e2f0 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncException.kt @@ -0,0 +1,94 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import at.bitfire.davdroid.resource.LocalResource +import kotlinx.coroutines.runBlocking +import okhttp3.HttpUrl + +/** + * Exception that wraps another notification together with potential information about + * a local and/or remote resource that is related to the exception. + */ +class SyncException(cause: Throwable) : Exception(cause) { + + companion object { + + // provide lambda wrappers for setting the local/remote resource + + fun wrapWithLocalResource(localResource: LocalResource<*>?, body: () -> T): T = + runBlocking { + wrapWithLocalResourceSuspending(localResource, body) + } + + suspend fun wrapWithLocalResourceSuspending(localResource: LocalResource<*>?, body: suspend () -> T): T { + try { + return body() + } catch (e: SyncException) { + if (localResource != null) + e.setLocalResourceIfNull(localResource) + throw e + } catch (e: Throwable) { + throw if (localResource != null) + SyncException(e).setLocalResourceIfNull(localResource) + else + e + } + } + + fun wrapWithRemoteResource(remoteResource: HttpUrl?, body: () -> T): T = + runBlocking { + wrapWithRemoteResourceSuspending(remoteResource, body) + } + + suspend fun wrapWithRemoteResourceSuspending(remoteResource: HttpUrl?, body: suspend () -> T): T { + try { + return body() + } catch (e: SyncException) { + if (remoteResource != null) + e.setRemoteResourceIfNull(remoteResource) + throw e + } catch (e: Throwable) { + throw if (remoteResource != null) + SyncException(e).setRemoteResourceIfNull(remoteResource) + else + e + } + } + + fun unwrap(e: Throwable, contextReceiver: (SyncException) -> Unit) = + if (e is SyncException) { + contextReceiver(e) + e.cause!! + } else + e + + } + + + var localResource: LocalResource<*>? = null + private set + var remoteResource: HttpUrl? = null + private set + + fun setLocalResourceIfNull(local: LocalResource<*>): SyncException { + if (localResource == null) + localResource = local + + return this + } + + fun setRemoteResourceIfNull(remote: HttpUrl): SyncException { + if (remoteResource == null) + remoteResource = remote + + return this + } + + override fun toString(): String { + return "SyncException(localResource=$localResource, remoteResource=$remoteResource, cause=$cause)" + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt new file mode 100644 index 0000000..9d8391a --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt @@ -0,0 +1,813 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.accounts.Account +import android.content.Context +import android.os.DeadObjectException +import android.os.RemoteException +import at.bitfire.dav4jvm.DavCollection +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.Error +import at.bitfire.dav4jvm.MultiResponseCallback +import at.bitfire.dav4jvm.QuotedStringUtils +import at.bitfire.dav4jvm.Response +import at.bitfire.dav4jvm.exception.ConflictException +import at.bitfire.dav4jvm.exception.DavException +import at.bitfire.dav4jvm.exception.ForbiddenException +import at.bitfire.dav4jvm.exception.GoneException +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.dav4jvm.exception.NotFoundException +import at.bitfire.dav4jvm.exception.PreconditionFailedException +import at.bitfire.dav4jvm.exception.ServiceUnavailableException +import at.bitfire.dav4jvm.exception.UnauthorizedException +import at.bitfire.dav4jvm.property.caldav.GetCTag +import at.bitfire.dav4jvm.property.caldav.ScheduleTag +import at.bitfire.dav4jvm.property.webdav.GetETag +import at.bitfire.dav4jvm.property.webdav.SyncToken +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.repository.AccountRepository +import at.bitfire.davdroid.repository.DavCollectionRepository +import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.repository.DavSyncStatsRepository +import at.bitfire.davdroid.resource.LocalCollection +import at.bitfire.davdroid.resource.LocalResource +import at.bitfire.davdroid.resource.SyncState +import at.bitfire.davdroid.sync.account.InvalidAccountException +import at.bitfire.synctools.storage.LocalStorageException +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runInterruptible +import kotlinx.coroutines.withContext +import okhttp3.HttpUrl +import okhttp3.RequestBody +import java.io.IOException +import java.net.HttpURLConnection +import java.security.cert.CertificateException +import java.util.LinkedList +import java.util.Optional +import java.util.concurrent.CancellationException +import java.util.concurrent.LinkedBlockingQueue +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject +import javax.net.ssl.SSLHandshakeException + +/** + * Synchronizes a local collection with a remote collection. + * + * @param ResourceType type of local resources + * @param CollectionType type of local collection + * @param RemoteType type of remote collection + * + * @param account account to synchronize + * @param httpClient HTTP client to use for network requests, already authenticated with credentials from [account] + * @param dataType data type to synchronize + * @param syncResult receiver for result of the synchronization (will be updated by [performSync]) + * @param localCollection local collection to synchronize (interface to content provider) + * @param collection collection info in the database + * @param resync whether re-synchronization is requested + */ +abstract class SyncManager, out CollectionType: LocalCollection, RemoteType: DavCollection>( + val account: Account, + val httpClient: HttpClient, + val dataType: SyncDataType, + val syncResult: SyncResult, + val localCollection: CollectionType, + val collection: Collection, + val resync: ResyncType?, + val syncDispatcher: CoroutineDispatcher +) { + + enum class SyncAlgorithm { + PROPFIND_REPORT, + COLLECTION_SYNC + } + + + @Inject + lateinit var accountRepository: AccountRepository + + @Inject + lateinit var collectionRepository: DavCollectionRepository + + @Inject + @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var logger: Logger + + @Inject + lateinit var syncStatsRepository: DavSyncStatsRepository + + @Inject + lateinit var serviceRepository: DavServiceRepository + + @Inject + lateinit var syncNotificationManagerFactory: SyncNotificationManager.Factory + + + protected lateinit var davCollection: RemoteType + + protected var hasCollectionSync = false + + private val syncNotificationManager by lazy { + syncNotificationManagerFactory.create(account) + } + + /** + * Push-Dont-Notify header, added to PUT and DELETE requests if subscription exists. + */ + private val pushDontNotifyHeader by lazy { + collection.pushSubscription?.let { pushSubscription -> + mapOf("Push-Dont-Notify" to QuotedStringUtils.asQuotedString(pushSubscription)) + } ?: emptyMap() + } + + suspend fun performSync() = withContext(syncDispatcher) { + // dismiss previous error notifications + syncNotificationManager.dismissInvalidResource(localCollectionTag = localCollection.tag) + + try { + logger.info("Preparing synchronization") + if (!prepare()) { + logger.info("No reason to synchronize, aborting") + return@withContext + } + syncStatsRepository.logSyncTime(collection.id, dataType) + + logger.info("Querying server capabilities") + var remoteSyncState = queryCapabilities() + + logger.info("Processing local deletes/updates") + val modificationsPresent = + processLocallyDeleted() or uploadDirty() // bitwise OR guarantees that both expressions are evaluated + + if (resync == ResyncType.RESYNC_ENTRIES) { + logger.info("Forcing re-synchronization of all entries") + + // forget sync state of collection (→ initial sync in case of SyncAlgorithm.COLLECTION_SYNC) + localCollection.lastSyncState = null + remoteSyncState = null + + // forget sync state of members (→ download all members again and update them locally) + localCollection.forgetETags() + } + + if (modificationsPresent || syncRequired(remoteSyncState)) + when (syncAlgorithm()) { + SyncAlgorithm.PROPFIND_REPORT -> { + logger.info("Sync algorithm: full listing as one result (PROPFIND/REPORT)") + resetPresentRemotely() + + // get current sync state + if (modificationsPresent) + remoteSyncState = querySyncState() + + // list and process all entries at current sync state (which may be the same as or newer than remoteSyncState) + logger.info("Processing remote entries") + syncRemote { callback -> + listAllRemote(callback) + } + + logger.info("Deleting entries which are not present remotely anymore") + deleteNotPresentRemotely() + + logger.info("Post-processing") + postProcess() + + logger.log(Level.INFO, "Saving sync state", remoteSyncState) + localCollection.lastSyncState = remoteSyncState + } + + SyncAlgorithm.COLLECTION_SYNC -> { + var syncState = localCollection.lastSyncState?.takeIf { it.type == SyncState.Type.SYNC_TOKEN } + + var initialSync = false + if (syncState == null) { + logger.info("Starting initial sync") + initialSync = true + resetPresentRemotely() + } else if (syncState.initialSync == true) { + logger.info("Continuing initial sync") + initialSync = true + } + + var furtherChanges = false + do { + logger.info("Listing changes since $syncState") + syncRemote { callback -> + try { + val result = listRemoteChanges(syncState, callback) + syncState = SyncState.fromSyncToken(result.first, initialSync) + furtherChanges = result.second + } catch (e: HttpException) { + if (e.errors.contains(Error.VALID_SYNC_TOKEN)) { + logger.info("Sync token invalid, performing initial sync") + initialSync = true + resetPresentRemotely() + + val result = listRemoteChanges(null, callback) + syncState = SyncState.fromSyncToken(result.first, initialSync) + furtherChanges = result.second + } else + throw e + } + } + + logger.log(Level.INFO, "Saving sync state", syncState) + localCollection.lastSyncState = syncState + + logger.info("Server has further changes: $furtherChanges") + } while (furtherChanges) + + if (initialSync) { + // initial sync is finished, remove all local resources which have not been listed by server + logger.info("Deleting local resources which are not on server (anymore)") + deleteNotPresentRemotely() + + // remove initial sync flag + syncState!!.initialSync = false + logger.log(Level.INFO, "Initial sync completed, saving sync state", syncState) + localCollection.lastSyncState = syncState + } + + logger.info("Post-processing") + postProcess() + } + } + else + logger.info("Remote collection didn't change, no reason to sync") + + } catch (potentiallyWrappedException: Throwable) { + var local: LocalResource<*>? = null + var remote: HttpUrl? = null + + val e = SyncException.unwrap(potentiallyWrappedException) { + local = it.localResource + remote = it.remoteResource + } + + when (e) { + // DeadObjectException (may occur when syncing takes too long and process is demoted to cached): + // re-throw to base Syncer → will cause soft error and restart the sync process + is DeadObjectException -> + throw e + + // sync was cancelled or account has been removed: re-throw to Syncer + is CancellationException, + is InvalidAccountException -> + throw e + + // specific I/O errors + is SSLHandshakeException -> { + logger.log(Level.WARNING, "SSL handshake failed", e) + + // when a certificate is rejected by cert4android, the cause will be a CertificateException + if (e.cause !is CertificateException) + handleException(e, local, remote) + } + + // specific HTTP errors + is ServiceUnavailableException -> { + logger.log(Level.WARNING, "Got 503 Service unavailable, trying again later", e) + // determine when to retry + syncResult.delayUntil = e.getDelayUntil().epochSecond + syncResult.numServiceUnavailableExceptions++ // Indicate a soft error occurred + } + + // all others + else -> + handleException(e, local, remote) + } + } + } + + + /** + * Prepares synchronization. Sets the lateinit property [davCollection]. + * + * @return whether synchronization shall be performed + */ + protected abstract fun prepare(): Boolean + + /** + * Queries the server for synchronization capabilities like specific report types, + * data formats etc. + * + * Should also query and save the initial sync state (e.g. CTag/sync-token). + * + * @return current sync state + */ + protected abstract suspend fun queryCapabilities(): SyncState? + + /** + * Processes locally deleted entries. This can mean: + * + * - forwarding them to the server (HTTP `DELETE`) + * - resetting their local state so that they will be downloaded again because they're read-only + * + * @return whether local resources have been processed so that a synchronization is always necessary + */ + protected open suspend fun processLocallyDeleted(): Boolean { + var numDeleted = 0 + + // Remove locally deleted entries from server (if they have a name, i.e. if they were uploaded before), + // but only if they don't have changed on the server. Then finally remove them from the local address book. + val localList = localCollection.findDeleted() + for (local in localList) { + SyncException.wrapWithLocalResourceSuspending(local) { + val fileName = local.fileName + if (fileName != null) { + val lastScheduleTag = local.scheduleTag + val lastETag = if (lastScheduleTag == null) local.eTag else null + logger.info("$fileName has been deleted locally -> deleting from server (ETag $lastETag / schedule-tag $lastScheduleTag)") + + val url = collection.url.newBuilder().addPathSegment(fileName).build() + val remote = DavResource(httpClient.okHttpClient, url) + SyncException.wrapWithRemoteResourceSuspending(url) { + try { + runInterruptible { + remote.delete( + ifETag = lastETag, + ifScheduleTag = lastScheduleTag, + headers = pushDontNotifyHeader, + ) {} + } + numDeleted++ + } catch (_: HttpException) { + logger.warning("Couldn't delete $fileName from server; ignoring (may be downloaded again)") + } + } + } else + logger.info("Removing local record #${local.id} which has been deleted locally and was never uploaded") + local.deleteLocal() + } + } + logger.info("Removed $numDeleted record(s) from server") + return numDeleted > 0 + } + + /** + * Processes locally modified resources to the server. This can mean: + * + * - uploading them to the server (HTTP `PUT`) + * - resetting their local state so that they will be downloaded again because they're read-only + * + * @return whether local resources have been processed so that a synchronization is always necessary + */ + protected open suspend fun uploadDirty(): Boolean { + var numUploaded = 0 + + coroutineScope { // structured concurrency + for (local in localCollection.findDirty()) + launch { + SyncException.wrapWithLocalResourceSuspending(local) { + uploadDirty(local) + numUploaded++ + } + } + } + logger.info("Sent $numUploaded record(s) to server") + return numUploaded > 0 + } + + /** + * Uploads a dirty local resource. + * + * @param local resource to upload + * @param forceAsNew whether the ETag (and Schedule-Tag) of [local] are ignored and the resource + * is created as a new resource on the server + */ + protected open suspend fun uploadDirty(local: ResourceType, forceAsNew: Boolean = false) { + val existingFileName = local.fileName + val fileName = if (existingFileName != null) { + // prepare upload (for UID etc), but ignore returned file name suggestion + local.prepareForUpload() + existingFileName + } else { + // prepare upload and use returned file name suggestion as new file name + local.prepareForUpload() + } + + val uploadUrl = collection.url.newBuilder().addPathSegment(fileName).build() + val remote = DavResource(httpClient.okHttpClient, uploadUrl) + + try { + SyncException.wrapWithRemoteResourceSuspending(uploadUrl) { + if (existingFileName == null || forceAsNew) { + // create new resource on server + logger.info("Uploading new resource ${local.id} -> $fileName") + val bodyToUpload = generateUpload(local) + + var newETag: String? = null + var newScheduleTag: String? = null + runInterruptible { + remote.put( + bodyToUpload, + ifNoneMatch = true, // fails if there's already a resource with that name + callback = { response -> + newETag = GetETag.fromResponse(response)?.eTag + newScheduleTag = ScheduleTag.fromResponse(response)?.scheduleTag + }, + headers = pushDontNotifyHeader + ) + } + + logger.fine("Upload successful; new ETag=$newETag / Schedule-Tag=$newScheduleTag") + + // success (no exception thrown) + onSuccessfulUpload(local, fileName, newETag, newScheduleTag) + + } else { + // update resource on server + val ifScheduleTag = local.scheduleTag + val ifETag = if (ifScheduleTag == null) local.eTag else null + + logger.info("Uploading modified resource ${local.id} -> $fileName (if ETag=$ifETag / Schedule-Tag=$ifScheduleTag)") + val bodyToUpload = generateUpload(local) + + var updatedETag: String? = null + var updatedScheduleTag: String? = null + runInterruptible { + remote.put( + bodyToUpload, + ifETag = ifETag, + ifScheduleTag = ifScheduleTag, + callback = { response -> + updatedETag = GetETag.fromResponse(response)?.eTag + updatedScheduleTag = ScheduleTag.fromResponse(response)?.scheduleTag + }, + headers = pushDontNotifyHeader + ) + } + + logger.fine("Upload successful; updated ETag=$updatedETag / Schedule-Tag=$updatedScheduleTag") + + // success (no exception thrown) + onSuccessfulUpload(local, fileName, updatedETag, updatedScheduleTag) + } + } + + } catch (e: SyncException) { + when (val ex = e.cause) { + is ForbiddenException -> { + // HTTP 403 Forbidden + // If and only if the upload failed because of missing permissions, treat it like 412. + if (ex.errors.contains(Error.NEED_PRIVILEGES)) + logger.log(Level.INFO, "Couldn't upload because of missing permissions, ignoring", ex) + else + throw e + } + is NotFoundException, is GoneException -> { + // HTTP 404 Not Found (i.e. either original resource or the whole collection is not there anymore) + if (!forceAsNew) { // first try; if this fails with 404, too, the collection is gone + logger.info("Original version of locally modified resource is not there (anymore), trying as fresh upload") + uploadDirty(local, forceAsNew = true) + return + } else { + // we tried with forceAsNew, collection probably gone + throw e + } + } + is ConflictException -> { + // HTTP 409 Conflict + // We can't interact with the user to resolve the conflict, so we treat 409 like 412. + logger.info("Edit conflict, ignoring") + } + is PreconditionFailedException -> { + // HTTP 412 Precondition failed: Resource has been modified on the server in the meanwhile. + // Ignore this condition so that the resource can be downloaded and reset again. + logger.info("Resource has been modified on the server before upload, ignoring") + } + else -> throw e + } + } + } + + /** + * Called after a successful upload (either of a new or an updated resource) so that the local + * _dirty_ state can be reset. + * + * Note: [CalendarSyncManager] overrides this method to additionally store the updated SEQUENCE. + */ + protected open fun onSuccessfulUpload(local: ResourceType, newFileName: String, eTag: String?, scheduleTag: String?) { + local.clearDirty(Optional.of(newFileName), eTag, scheduleTag) + } + + /** + * Generates the request body (iCalendar or vCard) from a local resource. + * + * @param resource local resource to generate the body from + * + * @return iCalendar or vCard (content + Content-Type) that can be uploaded to the server + */ + protected abstract fun generateUpload(resource: ResourceType): RequestBody + + + /** + * Determines whether a sync is required because there were changes on the server. + * For instance, this method can compare the collection's `CTag`/`sync-token` with + * the last known local value. + * + * When local changes have been uploaded ([processLocallyDeleted] and/or + * [uploadDirty] were true), a sync is always required and this method + * should *not* be evaluated. + * + * Will return _true_ if [resync] is non-null and thus indicates re-synchronization. + * + * @param state remote sync state to compare local sync state with + * + * @return whether data has been changed on the server, i.e. whether running the + * sync algorithm is required + */ + protected open fun syncRequired(state: SyncState?): Boolean { + if (resync != null) + return true + + val localState = localCollection.lastSyncState + logger.info("Local sync state = $localState, remote sync state = $state") + return when (state?.type) { + SyncState.Type.SYNC_TOKEN -> { + val lastKnownToken = localState?.takeIf { it.type == SyncState.Type.SYNC_TOKEN }?.value + lastKnownToken != state.value + } + SyncState.Type.CTAG -> { + val lastKnownCTag = localState?.takeIf { it.type == SyncState.Type.CTAG }?.value + lastKnownCTag != state.value + } + else -> true + } + } + + /** + * Determines which sync algorithm to use. + * @return + * - [SyncAlgorithm.PROPFIND_REPORT]: list all resources (with plain WebDAV + * PROPFIND or specific REPORT requests), then compare and synchronize + * - [SyncAlgorithm.COLLECTION_SYNC]: use incremental collection synchronization (RFC 6578) + */ + protected abstract fun syncAlgorithm(): SyncAlgorithm + + /** + * Marks all local resources which shall be taken into consideration for this + * sync as "synchronizing". Purpose of marking is that resources which have been marked + * and are not present remotely anymore can be deleted. + * + * Used together with [deleteNotPresentRemotely]. + */ + protected open fun resetPresentRemotely() { + val number = localCollection.markNotDirty(0) + logger.info("Number of local non-dirty entries: $number") + } + + /** + * Calls a callback to list remote resources. All resources from the returned + * list are downloaded and processed. + * + * @param listRemote function to list remote resources (for instance, all since a certain sync-token) + */ + protected open suspend fun syncRemote(listRemote: suspend (MultiResponseCallback) -> Unit) = coroutineScope { // structured concurrency + // download queue + val toDownload = LinkedBlockingQueue() + fun download(url: HttpUrl?) { + if (url != null) + toDownload += url + + if (toDownload.size >= MAX_MULTIGET_RESOURCES || url == null) { + while (toDownload.isNotEmpty()) { + val bunch = LinkedList() + toDownload.drainTo(bunch, MAX_MULTIGET_RESOURCES) + launch { + downloadRemote(bunch) + } + } + } + } + + coroutineScope { // structured concurrency + listRemote { response, relation -> + // ignore non-members + if (relation != Response.HrefRelation.MEMBER) + return@listRemote + + // ignore collections + if (response[at.bitfire.dav4jvm.property.webdav.ResourceType::class.java]?.types?.contains(at.bitfire.dav4jvm.property.webdav.ResourceType.COLLECTION) == true) + return@listRemote + + val name = response.hrefName() + + if (response.isSuccess()) { + logger.fine("Found remote resource: $name") + + launch { + val local = localCollection.findByName(name) + SyncException.wrapWithLocalResource(local) { + if (local == null) { + logger.info("$name has been added remotely, queueing download") + download(response.href) + } else { + val localETag = local.eTag + val remoteETag = response[GetETag::class.java]?.eTag + ?: throw DavException("Server didn't provide ETag") + if (localETag == remoteETag) { + logger.info("$name has not been changed on server (ETag still $remoteETag)") + } else { + logger.info("$name has been changed on server (current ETag=$remoteETag, last known ETag=$localETag)") + download(response.href) + } + + // mark as remotely present, so that this resource won't be deleted at the end + local.updateFlags(LocalResource.FLAG_REMOTELY_PRESENT) + } + } + } + + } else if (response.status?.code == HttpURLConnection.HTTP_NOT_FOUND) { + // collection sync: resource has been deleted on remote server + launch { + localCollection.findByName(name)?.let { local -> + SyncException.wrapWithLocalResource(local) { + logger.info("$name has been deleted on server, deleting locally") + local.deleteLocal() + } + } + } + } + } + } + + // download remaining resources + download(null) + } + + protected abstract suspend fun listAllRemote(callback: MultiResponseCallback) + + protected open suspend fun listRemoteChanges(syncState: SyncState?, callback: MultiResponseCallback): Pair { + var furtherResults = false + + val report = runInterruptible { + davCollection.reportChanges( + syncState?.takeIf { syncState.type == SyncState.Type.SYNC_TOKEN }?.value, + false, null, + GetETag.NAME + ) { response, relation -> + when (relation) { + Response.HrefRelation.SELF -> + furtherResults = response.status?.code == 507 + + Response.HrefRelation.MEMBER -> + callback.onResponse(response, relation) + + else -> + logger.fine("Unexpected sync-collection response: $response") + } + } + } + + var syncToken: SyncToken? = null + report.filterIsInstance().firstOrNull()?.let { + syncToken = it + } + if (syncToken == null) + throw DavException("Received sync-collection response without sync-token") + + return Pair(syncToken, furtherResults) + } + + /** + * Downloads and processes resources, given as a list of URLs. Will be called with a list + * of changed/new remote resources. + * + * Implementations should not use GET to fetch single resources, but always multi-get, even + * for single resources for these reasons: + * + * 1. GET can only be used without HTTP compression, because it may change the ETag. + * multi-get sends the ETag in the XML body, so there's no problem with compression. + * 2. Some servers are wrongly configured to suppress the ETag header in the response. + * With multi-get, the ETag is in the XML body, so it won't be affected by that. + * 3. If there are two methods to download resources (GET and multi-get), both methods + * have to be implemented, tested and maintained. Given that multi-get is required + * in any case, it's better to have only one method. + * 4. For users, it's strange behavior when DAVx5 can download multiple remote changes, + * but not a single one (or vice versa). So only one method is more user-friendly. + * 5. March 2020: iCloud now crashes with HTTP 500 upon CardDAV GET requests. + */ + protected abstract suspend fun downloadRemote(bunch: List) + + /** + * Locally deletes entries which are + * 1. not dirty and + * 2. not marked as [LocalResource.FLAG_REMOTELY_PRESENT]. + * + * Used together with [resetPresentRemotely] when a full listing has been received from + * the server to locally delete resources which are not present remotely (anymore). + */ + protected open fun deleteNotPresentRemotely() { + val removed = localCollection.removeNotDirtyMarked(0) + logger.info("Removed $removed local resources which are not present on the server anymore") + } + + /** + * Post-processing of synchronized entries, for instance contact group membership operations. + */ + protected abstract fun postProcess() + + + // sync helpers + + protected fun syncState(dav: Response) = + dav[SyncToken::class.java]?.token?.let { + SyncState(SyncState.Type.SYNC_TOKEN, it) + } ?: + dav[GetCTag::class.java]?.cTag?.let { + SyncState(SyncState.Type.CTAG, it) + } + + private suspend fun querySyncState(): SyncState? { + var state: SyncState? = null + runInterruptible { + davCollection.propfind(0, GetCTag.NAME, SyncToken.NAME) { response, relation -> + if (relation == Response.HrefRelation.SELF) + state = syncState(response) + } + } + return state + } + + /** + * Logs the exception, updates sync result and shows a notification to the user. + */ + private fun handleException(e: Throwable, local: LocalResource<*>?, remote: HttpUrl?) { + var message: String + when (e) { + is IOException -> { + logger.log(Level.WARNING, "I/O error", e) + syncResult.numIoExceptions++ + message = context.getString(R.string.sync_error_io, e.localizedMessage) + } + + is UnauthorizedException -> { + logger.log(Level.SEVERE, "Not authorized anymore", e) + syncResult.numAuthExceptions++ + message = context.getString(R.string.sync_error_authentication_failed) + } + + is HttpException, is DavException -> { + logger.log(Level.SEVERE, "HTTP/DAV exception", e) + syncResult.numHttpExceptions++ + message = context.getString(R.string.sync_error_http_dav, e.localizedMessage) + } + + is LocalStorageException, is RemoteException -> { + logger.log(Level.SEVERE, "Couldn't access local storage", e) + syncResult.localStorageError = true + message = context.getString(R.string.sync_error_local_storage, e.localizedMessage) + } + + else -> { + logger.log(Level.SEVERE, "Unclassified sync error", e) + syncResult.numUnclassifiedErrors++ + message = e.localizedMessage ?: e::class.java.simpleName + } + } + + syncNotificationManager.notifyException( + dataType, + localCollection.tag, + message, + localCollection, + e, + local, + remote + ) + } + + protected fun notifyInvalidResource(e: Throwable, fileName: String) = + syncNotificationManager.notifyInvalidResource( + dataType, + localCollection.tag, + collection, + e, + fileName, + notifyInvalidResourceTitle() + ) + + protected abstract fun notifyInvalidResourceTitle(): String + + + companion object { + + /** Maximum number of resources that are requested with one multiget request. */ + const val MAX_MULTIGET_RESOURCES = 10 + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncNotificationManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncNotificationManager.kt new file mode 100644 index 0000000..8f9a205 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncNotificationManager.kt @@ -0,0 +1,246 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.accounts.Account +import android.app.PendingIntent +import android.app.TaskStackBuilder +import android.content.Context +import android.content.Intent +import android.provider.CalendarContract +import android.provider.ContactsContract +import android.provider.Settings +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.net.toUri +import at.bitfire.dav4jvm.exception.UnauthorizedException +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.resource.LocalCollection +import at.bitfire.davdroid.resource.LocalResource +import at.bitfire.davdroid.ui.DebugInfoActivity +import at.bitfire.davdroid.ui.NotificationRegistry +import at.bitfire.davdroid.ui.account.AccountSettingsActivity +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.qualifiers.ApplicationContext +import okhttp3.HttpUrl +import java.io.IOException +import java.util.logging.Level +import java.util.logging.Logger + +class SyncNotificationManager @AssistedInject constructor( + @Assisted val account: Account, + @ApplicationContext private val context: Context, + private val logger: Logger, + private val notificationRegistry: NotificationRegistry +) { + + @AssistedFactory + interface Factory { + fun create(account: Account): SyncNotificationManager + } + + /** + * Tries to inform the user that the content provider is missing or disabled. + * Use [dismissProviderError] to dismiss the notification. + * + * @param authority The authority of the content provider. + */ + fun notifyProviderError(authority: String) { + val (titleResource, textResource) = when (authority) { + ContactsContract.AUTHORITY -> + R.string.sync_warning_contacts_storage_disabled_title to + R.string.sync_warning_contacts_storage_disabled_description + CalendarContract.AUTHORITY -> + R.string.sync_warning_calendar_storage_disabled_title to + R.string.sync_warning_calendar_storage_disabled_description + else -> { + logger.log(Level.WARNING, "Content provider error for unknown authority: $authority") + return + } + } + + notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_SYNC_ERROR, tag = authority) { + NotificationCompat.Builder(context, notificationRegistry.CHANNEL_SYNC_ERRORS) + .setSmallIcon(R.drawable.ic_sync_problem_notify) + .setContentTitle(context.getString(titleResource)) + .setContentText(context.getString(textResource)) + .setSubText(account.name) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setCategory(NotificationCompat.CATEGORY_ERROR) + .setAutoCancel(true) + .addAction(NotificationCompat.Action( + android.R.drawable.ic_menu_view, + context.getString(R.string.sync_warning_manage_apps), + PendingIntent.getActivity(context, 0, + Intent(Settings.ACTION_APPLICATION_SETTINGS), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + )) + .build() + } + } + + /** + * Dismisses the notification for content provider errors. + * + * @param authority The authority of the content provider used as notification tag. + */ + fun dismissProviderError(authority: String) = + dismissNotification(authority) + + /** + * Tries to inform the user that an exception occurred during synchronization. Includes the affected + * local resource, its collection, the URL, the exception and a user message. + * + * @param syncDataType The type of data which was synced. + * @param notificationTag The tag to use for the notification. + * @param message The message to show to the user. + * @param localCollection The affected local collection. + * @param e The exception that occurred. + * @param local The affected local resource. + * @param remote The remote URL that caused the exception. + */ + fun notifyException( + syncDataType: SyncDataType, + notificationTag: String, + message: String, + localCollection: LocalCollection<*>, + e: Throwable, + local: LocalResource<*>?, + remote: HttpUrl? + ) = notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_SYNC_ERROR, tag = notificationTag) { + val contentIntent: Intent + if (e is UnauthorizedException) { + contentIntent = Intent(context, AccountSettingsActivity::class.java) + contentIntent.putExtra( + AccountSettingsActivity.EXTRA_ACCOUNT, + account + ) + } else { + contentIntent = buildDebugInfoIntent(syncDataType, e, local, remote) + } + + // to make the PendingIntent unique + contentIntent.data = "davdroid:exception/${e.hashCode()}".toUri() + + val channel: String + val priority: Int + if (e is IOException) { + channel = notificationRegistry.CHANNEL_SYNC_IO_ERRORS + priority = NotificationCompat.PRIORITY_MIN + } else { + channel = notificationRegistry.CHANNEL_SYNC_ERRORS + priority = NotificationCompat.PRIORITY_DEFAULT + } + + val builder = NotificationCompat.Builder(context, channel) + builder.setSmallIcon(R.drawable.ic_sync_problem_notify) + .setContentTitle(localCollection.title) + .setContentText(message) + .setStyle(NotificationCompat.BigTextStyle(builder).bigText(message)) + .setSubText(account.name) + .setOnlyAlertOnce(true) + .setContentIntent( + TaskStackBuilder.create(context) + .addNextIntentWithParentStack(contentIntent) + .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + ) + .setPriority(priority) + .setCategory(NotificationCompat.CATEGORY_ERROR) + + builder.build() + } + + /** + * Sends a notification to inform the user that a push notification has been received, the + * sync has been scheduled, but it still has not run. + * Use [dismissInvalidResource] to dismiss the notification. + * + * @param dataType The type of data which was synced. + * @param notificationTag The tag to use for the notification. + * @param collection The affected collection. + * @param fileName The name of the file containing the invalid resource. + * @param title The title of the notification. + */ + fun notifyInvalidResource( + dataType: SyncDataType, + notificationTag: String, + collection: Collection, + e: Throwable, + fileName: String, + title: String + ) { + notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_INVALID_RESOURCE, tag = notificationTag) { + val intent = buildDebugInfoIntent(dataType, e, null, collection.url.resolve(fileName)) + + val builder = NotificationCompat.Builder(context, notificationRegistry.CHANNEL_SYNC_WARNINGS) + builder.setSmallIcon(R.drawable.ic_warning_notify) + .setContentTitle(title) + .setContentText(context.getString(R.string.sync_invalid_resources_ignoring)) + .setSubText(account.name) + .setContentIntent( + TaskStackBuilder.create(context) + .addNextIntent(intent) + .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + ) + .setAutoCancel(true) + .setOnlyAlertOnce(true) + .priority = NotificationCompat.PRIORITY_LOW + builder.build() + } + } + + /** + * Dismisses the (error) notification for a specific collection. + * + * @param localCollectionTag The tag of the local collection which is used as notification tag also. + */ + fun dismissInvalidResource(localCollectionTag: String) = + dismissNotification(localCollectionTag) + + + // helpers + + /** + * Dismisses the sync error notification for a specific tag. + */ + private fun dismissNotification(tag: String) = NotificationManagerCompat.from(context) + .cancel(tag, NotificationRegistry.NOTIFY_SYNC_ERROR) + + /** + * Builds intent to go to debug information with the given exception, resource and remote address. + */ + private fun buildDebugInfoIntent( + dataType: SyncDataType, + e: Throwable, + local: LocalResource<*>?, + remote: HttpUrl? + ): Intent { + val builder = DebugInfoActivity.IntentBuilder(context) + .withAccount(account) + .withSyncDataType(dataType) + .withCause(e) + + if (local != null) + try { + // Add local resource summary, if available + builder.withLocalResource(local.getDebugSummary()) + + // Add URI to view local resource, if available + builder.withLocalResourceUri(local.getViewUri(context)) + } catch (_: Throwable) { + // Ignore all potential exceptions that arise from providing information about the local resource + } + + if (remote != null) + builder.withRemoteResource(remote) + + return builder.build() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncResult.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncResult.kt new file mode 100644 index 0000000..efdd5ee --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncResult.kt @@ -0,0 +1,55 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +/** + * This class represents the results of a sync operation from [Syncer]. + * + * Used by [at.bitfire.davdroid.sync.worker.BaseSyncWorker] to determine whether or not there will be retries etc. + */ +data class SyncResult( + // hard errors by Syncer + var contentProviderError: Boolean = false, + var localStorageError: Boolean = false, + + // hard errors by SyncManager + var numAuthExceptions: Long = 0, + var numHttpExceptions: Long = 0, + var numUnclassifiedErrors: Long = 0, + + // soft errors by SyncMAnager + var numDeadObjectExceptions: Long = 0, + var numIoExceptions: Long = 0, + var numServiceUnavailableExceptions: Long = 0, + + // Other values + var delayUntil: Long = 0 +) { + + /** + * Whether a hard error occurred. + */ + fun hasHardError(): Boolean = + contentProviderError + || localStorageError + || numAuthExceptions > 0 + || numHttpExceptions > 0 + || numUnclassifiedErrors > 0 + + /** + * Whether a soft error occurred. + */ + fun hasSoftError(): Boolean = + numDeadObjectExceptions > 0 + || numIoExceptions > 0 + || numServiceUnavailableExceptions > 0 + + /** + * Whether a hard or a soft error occurred. + */ + fun hasError(): Boolean = + hasHardError() || hasSoftError() + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/Syncer.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/Syncer.kt new file mode 100644 index 0000000..30389b2 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/Syncer.kt @@ -0,0 +1,283 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.Context +import android.os.DeadObjectException +import androidx.annotation.VisibleForTesting +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.ServiceType +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.repository.DavCollectionRepository +import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.resource.LocalCollection +import at.bitfire.davdroid.resource.LocalDataStore +import at.bitfire.davdroid.sync.account.InvalidAccountException +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.runBlocking +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject + +/** + * Base class for sync code. + * + * Contains generic sync code, equal for all sync authorities. + * + * @param account account to synchronize + * @param resync whether re-synchronization is requested (`null` for normal sync) + * @param syncResult synchronization result, to be modified during sync + */ +abstract class Syncer, CollectionType: LocalCollection<*>>( + protected val account: Account, + protected val resync: ResyncType?, + protected val syncResult: SyncResult +) { + + abstract val dataStore: StoreType + + @Inject @ApplicationContext + lateinit var context: Context + + @Inject + lateinit var collectionRepository: DavCollectionRepository + + @Inject + lateinit var httpClientBuilder: HttpClient.Builder + + @Inject + lateinit var logger: Logger + + @Inject + lateinit var serviceRepository: DavServiceRepository + + @Inject + lateinit var syncNotificationManagerFactory: SyncNotificationManager.Factory + + @ServiceType + abstract val serviceType: String + + val syncNotificationManager by lazy { + syncNotificationManagerFactory.create(account) + } + + val httpClient = lazy { + httpClientBuilder.fromAccount(account).build() + } + + /** + * Creates, updates and/or deletes local collections (calendars, address books, etc) according to + * remote collection information. Then syncs the actual entries (events, tasks, contacts, etc) + * of the remaining, now up-to-date, collections. + */ + @VisibleForTesting + internal fun sync(provider: ContentProviderClient) { + // Collection type specific preparations + if (!prepare(provider)) { + logger.log(Level.WARNING, "Failed to prepare sync. Won't run sync.") + return + } + + // Find collections in database and provider which should be synced (are sync-enabled) + val dbCollections = getSyncEnabledCollections() + val localCollections = dataStore.getAll(account, provider) + + // Create/update/delete local collections according to DB + val updatedLocalCollections = updateCollections(provider, localCollections, dbCollections) + + // Sync local collection contents (events, contacts, tasks) + syncCollectionContents(provider, updatedLocalCollections, dbCollections) + } + + /** + * Finds sync enabled collections in database. They contain collection info which might have + * been updated by collection refresh [at.bitfire.davdroid.servicedetection.DavResourceFinder]. + * + * @return The sync enabled database collections as hash map identified by their ID + */ + @VisibleForTesting + internal fun getSyncEnabledCollections(): Map = runBlocking { + val dbCollections = mutableMapOf() + serviceRepository.getByAccountAndType(account.name, serviceType)?.let { service -> + for (dbCollection in getDbSyncCollections(service.id)) + dbCollections[dbCollection.id] = dbCollection + } + + dbCollections + } + + /** + * Updates and deletes local collections. + * + * - Updates local collections with possibly new info from corresponding database collections. + * - Deletes local collections without a corresponding database collection. + * - Creates local collections for database collections without local match. + * + * @param provider Content provider client, used to create local collections + * @param localCollections The current local collections + * @param dbCollections The current database collections, possibly containing new information + * + * @return Updated list of local collections (obsolete collections removed, new collections added) + */ + @VisibleForTesting + internal fun updateCollections( + provider: ContentProviderClient, + localCollections: List, + dbCollections: Map + ): List { + // create mutable copies of input + val updatedLocalCollections = localCollections.toMutableList() + val newDbCollections = dbCollections.toMutableMap() + + for (localCollection in localCollections) { + val dbCollection = dbCollections.getOrDefault(localCollection.dbCollectionId, null) + if (dbCollection == null) { + // Collection not available in db = on server (anymore), delete and remove from the updated list + logger.info("Deleting local collection ${localCollection.title} without matching remote collection") + dataStore.delete(localCollection) + updatedLocalCollections -= localCollection + } else { + // Collection exists locally, update local collection and remove it from "to be created" map + logger.fine("Updating local collection ${localCollection.title} with $dbCollection") + dataStore.update(provider, localCollection, dbCollection) + newDbCollections -= dbCollection.id + } + } + + // Create local collections which are in DB, but don't exist locally yet + if (newDbCollections.isNotEmpty()) { + val toBeCreated = newDbCollections.values.toList() + logger.log(Level.INFO, "Creating new local collections", toBeCreated.toTypedArray()) + val newLocalCollections = createLocalCollections(provider, toBeCreated) + // Add the newly created collections to the updated list + updatedLocalCollections.addAll(newLocalCollections) + } + + return updatedLocalCollections + } + + /** + * Creates new local collections from database collections. + * + * @param provider Content provider client to access local collections + * @param dbCollections Database collections to be created as local collections + * + * @return Newly created local collections + */ + @VisibleForTesting + internal fun createLocalCollections( + provider: ContentProviderClient, + dbCollections: List + ): List = + dbCollections.map { collection -> + dataStore.create(provider, collection) + ?: throw IllegalStateException("Couldn't create local collection for $collection") + } + + /** + * Synchronize the actual collection contents. + * + * @param provider Content provider client to access local collections + * @param localCollections Collections to be synchronized + * @param dbCollections Remote collection information + */ + @VisibleForTesting + internal fun syncCollectionContents( + provider: ContentProviderClient, + localCollections: List, + dbCollections: Map + ) = localCollections.forEach { localCollection -> + dbCollections[localCollection.dbCollectionId]?.let { dbCollection -> + syncCollection(provider, localCollection, dbCollection) + } + } + + /** + * For collection specific sync preparations. + * + * @param provider Content provider for data store + * + * @return *true* to run the sync; *false* to abort + */ + open fun prepare(provider: ContentProviderClient): Boolean = true + + /** + * Get the local database collections which are sync-enabled (should by synchronized). + * + * @param serviceId The CalDAV or CardDAV service (account) to be synchronized + * + * @return Database collections to be synchronized + */ + abstract fun getDbSyncCollections(serviceId: Long): List + + /** + * Synchronizes local with remote collection contents. + * + * @param provider The content provider client to access the local collection to be updated + * @param localCollection The local collection to be synchronized + * @param remoteCollection The database collection representing the remote collection. Contains + * remote address of the collection to be synchronized. + */ + abstract fun syncCollection(provider: ContentProviderClient, localCollection: CollectionType, remoteCollection: Collection) + + /** + * Prepares the sync: + * + * - acquire content provider + * - handle occurring sync errors + */ + operator fun invoke() { + logger.info("${dataStore.authority} sync of $account initiated (resync=$resync)") + + try { + dataStore.acquireContentProvider(throwOnMissingPermissions = true) + } catch (e: SecurityException) { + logger.log(Level.WARNING, "Missing permissions for content provider authority ${dataStore.authority}", e) + /* Don't show a notification here without possibility to permanently dismiss it! + Some users intentionally don't grant all permissions for what is syncable. */ + return + }.use { provider -> + if (provider == null) { + /* Content provider is not available at all. + I.E. system app (like "calendar storage") is missing or disabled */ + logger.warning("Couldn't connect to content provider of authority ${dataStore.authority}") + syncNotificationManager.notifyProviderError(dataStore.authority) + syncResult.contentProviderError = true + return // Don't continue without provider + } + + // Dismiss previous content provider error notification + syncNotificationManager.dismissProviderError(dataStore.authority) + + // run sync + try { + val runSync = /* ose */ true + if (runSync) + sync(provider) + Unit + } catch (e: DeadObjectException) { + /* May happen when the remote process dies or (since Android 14) when IPC (for instance with the calendar provider) + is suddenly forbidden because our sync process was demoted from a "service process" to a "cached process". */ + logger.log(Level.WARNING, "Received DeadObjectException, treating as soft error", e) + syncResult.numDeadObjectExceptions++ + + } catch (e: InvalidAccountException) { + logger.log(Level.WARNING, "Account was removed during synchronization", e) + + } catch (e: Exception) { + logger.log(Level.SEVERE, "Couldn't sync ${dataStore.authority}", e) + syncResult.numUnclassifiedErrors++ // Hard sync error + + } finally { + if (httpClient.isInitialized()) + httpClient.value.close() + logger.info("${dataStore.authority} sync of $account finished") + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/TaskSyncer.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/TaskSyncer.kt new file mode 100644 index 0000000..bd8981a --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/TaskSyncer.kt @@ -0,0 +1,86 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.ContentProviderClient +import android.os.Build +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.resource.LocalTaskList +import at.bitfire.davdroid.resource.LocalTaskListStore +import at.bitfire.ical4android.TaskProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.runBlocking + +/** + * Sync logic for tasks in CalDAV collections ({@code VTODO}). + */ +class TaskSyncer @AssistedInject constructor( + @Assisted account: Account, + @Assisted val providerName: TaskProvider.ProviderName, + @Assisted resync: ResyncType?, + @Assisted syncResult: SyncResult, + localTaskListStoreFactory: LocalTaskListStore.Factory, + private val tasksAppManager: dagger.Lazy, + private val tasksSyncManagerFactory: TasksSyncManager.Factory, +): Syncer(account, resync, syncResult) { + + @AssistedFactory + interface Factory { + fun create(account: Account, providerName: TaskProvider.ProviderName, resyncType: ResyncType?, syncResult: SyncResult): TaskSyncer + } + + override val dataStore = localTaskListStoreFactory.create(providerName) + + override val serviceType: String + get() = Service.TYPE_CALDAV + + + override fun prepare(provider: ContentProviderClient): Boolean { + // Don't sync if task provider is too old + try { + TaskProvider.checkVersion(context, providerName) + } catch (e: TaskProvider.ProviderTooOldException) { + tasksAppManager.get().notifyProviderTooOld(e) + syncResult.contentProviderError = true + return false // Don't sync + } + + // make sure account can be seen by task provider + if (Build.VERSION.SDK_INT >= 26) { + /* Warning: If setAccountVisibility is called, Android 12 broadcasts the + AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION Intent. This cancels running syncs + and starts them again! So make sure setAccountVisibility is only called when necessary. */ + val am = AccountManager.get(context) + if (am.getAccountVisibility(account, providerName.packageName) != AccountManager.VISIBILITY_VISIBLE) + am.setAccountVisibility(account, providerName.packageName, AccountManager.VISIBILITY_VISIBLE) + } + return true + } + + override fun getDbSyncCollections(serviceId: Long): List = + collectionRepository.getSyncTaskLists(serviceId) + + override fun syncCollection(provider: ContentProviderClient, localCollection: LocalTaskList, remoteCollection: Collection) { + logger.info("Synchronizing task list ${localCollection.id} with database collection ID: ${localCollection.dbCollectionId}") + + val syncManager = tasksSyncManagerFactory.tasksSyncManager( + account, + httpClient.value, + syncResult, + localCollection, + remoteCollection, + resync + ) + runBlocking { + syncManager.performSync() + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/TasksAppManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/TasksAppManager.kt new file mode 100644 index 0000000..5017978 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/TasksAppManager.kt @@ -0,0 +1,149 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.drawable.BitmapDrawable +import androidx.annotation.WorkerThread +import androidx.core.app.NotificationCompat +import androidx.core.app.TaskStackBuilder +import androidx.core.net.toUri +import at.bitfire.davdroid.R +import at.bitfire.davdroid.repository.AccountRepository +import at.bitfire.davdroid.resource.LocalDataStore +import at.bitfire.davdroid.resource.LocalJtxCollectionStore +import at.bitfire.davdroid.resource.LocalTaskListStore +import at.bitfire.davdroid.settings.Settings +import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.ui.NotificationRegistry +import at.bitfire.davdroid.util.PermissionUtils +import at.bitfire.ical4android.TaskProvider +import at.bitfire.ical4android.TaskProvider.ProviderName +import dagger.Lazy +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import java.util.logging.Logger +import javax.inject.Inject + +/** + * Responsible for setting/getting the currently used tasks app, and for communicating with it. + */ +class TasksAppManager @Inject constructor( + @ApplicationContext private val context: Context, + private val accountRepository: Lazy, + private val automaticSyncManager: AutomaticSyncManager, + private val logger: Logger, + private val notificationRegistry: Lazy, + private val settingsManager: SettingsManager, + private val localTaskListStoreFactory: LocalTaskListStore.Factory, + private val localJtxCollectionStore: Lazy, +) { + + /** + * Gets the currently selected tasks app, if installed. + * + * @return currently selected tasks app (when installed), or `null` if no tasks app is selected or the selected app is not installed + */ + fun currentProvider(): ProviderName? { + val authority = settingsManager.getString(Settings.SELECTED_TASKS_PROVIDER) ?: return null + return authorityToProviderName(authority) + } + + /** + * Like [currentProvider, but as a [Flow]. + */ + fun currentProviderFlow(): Flow = + settingsManager.getStringFlow(Settings.SELECTED_TASKS_PROVIDER).map { preferred -> + if (preferred != null) + authorityToProviderName(preferred) + else + null + } + + /** + * Converts an authority to a [ProviderName], if the authority is known and the provider is installed. + */ + private fun authorityToProviderName(authority: String): ProviderName? = + ProviderName.entries + .firstOrNull { it.authority == authority } + .takeIf { context.packageManager.resolveContentProvider(authority, 0) != null } + + + /** + * Sets up sync for the selected TaskProvider. + */ + @WorkerThread + fun selectProvider(selectedProvider: ProviderName?) { + logger.info("Selecting tasks app: $selectedProvider") + + val selectedAuthority = selectedProvider?.authority + settingsManager.putString(Settings.SELECTED_TASKS_PROVIDER, selectedAuthority) + + // check permission + if (selectedProvider != null && !PermissionUtils.havePermissions(context, selectedProvider.permissions)) + notificationRegistry.get().notifyPermissions() + + // check all accounts and update task sync + for (account in accountRepository.get().getAll()) + automaticSyncManager.updateAutomaticSync(account, SyncDataType.TASKS) + } + + + /** + * Show a notification that starts an Intent and redirects the user to the tasks app in the app store. + * + * @param e the TaskProvider.ProviderTooOldException to be shown + */ + fun notifyProviderTooOld(e: TaskProvider.ProviderTooOldException) { + val registry = notificationRegistry.get() + registry.notifyIfPossible(NotificationRegistry.NOTIFY_TASKS_PROVIDER_TOO_OLD) { + val message = context.getString(R.string.sync_error_tasks_required_version, e.provider.minVersionName) + + val pm = context.packageManager + val tasksAppInfo = pm.getPackageInfo(e.provider.packageName, 0) + val tasksAppLabel = tasksAppInfo.applicationInfo?.loadLabel(pm) + + val notify = NotificationCompat.Builder(context, registry.CHANNEL_SYNC_ERRORS) + .setSmallIcon(R.drawable.ic_sync_problem_notify) + .setContentTitle(context.getString(R.string.sync_error_tasks_too_old, tasksAppLabel)) + .setContentText(message) + .setSubText("$tasksAppLabel ${e.installedVersionName}") + .setCategory(NotificationCompat.CATEGORY_ERROR) + + try { + val icon = pm.getApplicationIcon(e.provider.packageName) + if (icon is BitmapDrawable) + notify.setLargeIcon(icon.bitmap) + } catch (_: PackageManager.NameNotFoundException) { + // couldn't get provider app icon + } + + val intent = Intent(Intent.ACTION_VIEW, "market://details?id=${e.provider.packageName}".toUri()) + val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + + if (intent.resolveActivity(pm) != null) + notify.setContentIntent( + TaskStackBuilder.create(context) + .addNextIntent(intent) + .getPendingIntent(0, flags) + ) + + notify.build() + } + } + + fun getDataStore(): LocalDataStore<*>? { + val provider = currentProvider() ?: return null + return when (provider) { + ProviderName.TasksOrg, ProviderName.OpenTasks -> localTaskListStoreFactory.create(provider) + ProviderName.JtxBoard -> localJtxCollectionStore.get() + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/TasksSyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/TasksSyncManager.kt new file mode 100644 index 0000000..9dec0fb --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/TasksSyncManager.kt @@ -0,0 +1,195 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.accounts.Account +import android.text.format.Formatter +import at.bitfire.dav4jvm.DavCalendar +import at.bitfire.dav4jvm.MultiResponseCallback +import at.bitfire.dav4jvm.Response +import at.bitfire.dav4jvm.exception.DavException +import at.bitfire.dav4jvm.property.caldav.CalendarData +import at.bitfire.dav4jvm.property.caldav.GetCTag +import at.bitfire.dav4jvm.property.caldav.MaxResourceSize +import at.bitfire.dav4jvm.property.webdav.GetETag +import at.bitfire.dav4jvm.property.webdav.SyncToken +import at.bitfire.davdroid.Constants +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.di.SyncDispatcher +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.resource.LocalResource +import at.bitfire.davdroid.resource.LocalTask +import at.bitfire.davdroid.resource.LocalTaskList +import at.bitfire.davdroid.resource.SyncState +import at.bitfire.davdroid.util.DavUtils.lastSegment +import at.bitfire.ical4android.Task +import at.bitfire.synctools.exception.InvalidICalendarException +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.runInterruptible +import okhttp3.HttpUrl +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.ByteArrayOutputStream +import java.io.Reader +import java.io.StringReader +import java.util.logging.Level + +/** + * Synchronization manager for CalDAV collections; handles tasks (VTODO) + */ +class TasksSyncManager @AssistedInject constructor( + @Assisted account: Account, + @Assisted httpClient: HttpClient, + @Assisted syncResult: SyncResult, + @Assisted localCollection: LocalTaskList, + @Assisted collection: Collection, + @Assisted resync: ResyncType?, + @SyncDispatcher syncDispatcher: CoroutineDispatcher +): SyncManager( + account, + httpClient, + SyncDataType.TASKS, + syncResult, + localCollection, + collection, + resync, + syncDispatcher +) { + + @AssistedFactory + interface Factory { + fun tasksSyncManager( + account: Account, + httpClient: HttpClient, + syncResult: SyncResult, + localCollection: LocalTaskList, + collection: Collection, + resync: ResyncType? + ): TasksSyncManager + } + + + override fun prepare(): Boolean { + davCollection = DavCalendar(httpClient.okHttpClient, collection.url) + + return true + } + + override suspend fun queryCapabilities() = + SyncException.wrapWithRemoteResourceSuspending(collection.url) { + var syncState: SyncState? = null + runInterruptible { + davCollection.propfind(0, MaxResourceSize.NAME, GetCTag.NAME, SyncToken.NAME) { response, relation -> + if (relation == Response.HrefRelation.SELF) { + response[MaxResourceSize::class.java]?.maxSize?.let { maxSize -> + logger.info("Calendar accepts tasks up to ${Formatter.formatFileSize(context, maxSize)}") + } + + syncState = syncState(response) + } + } + } + + syncState + } + + override fun syncAlgorithm() = SyncAlgorithm.PROPFIND_REPORT + + override fun generateUpload(resource: LocalTask): RequestBody = + SyncException.wrapWithLocalResource(resource) { + val task = requireNotNull(resource.task) + logger.log(Level.FINE, "Preparing upload of task ${resource.fileName}", task) + + val os = ByteArrayOutputStream() + task.write(os, Constants.iCalProdId) + + os.toByteArray().toRequestBody(DavCalendar.MIME_ICALENDAR_UTF8) + } + + override suspend fun listAllRemote(callback: MultiResponseCallback) { + SyncException.wrapWithRemoteResourceSuspending(collection.url) { + logger.info("Querying tasks") + runInterruptible { + davCollection.calendarQuery("VTODO", null, null, callback) + } + } + } + + override suspend fun downloadRemote(bunch: List) { + logger.info("Downloading ${bunch.size} iCalendars: $bunch") + // multiple iCalendars, use calendar-multi-get + SyncException.wrapWithRemoteResourceSuspending(collection.url) { + runInterruptible { + davCollection.multiget(bunch) { response, _ -> + // See CalendarSyncManager for more information about the multi-get response + SyncException.wrapWithRemoteResource(response.href) wrapResource@{ + if (!response.isSuccess()) { + logger.warning("Ignoring non-successful multi-get response for ${response.href}") + return@wrapResource + } + + val iCal = response[CalendarData::class.java]?.iCalendar + if (iCal == null) { + logger.warning("Ignoring multi-get response without calendar-data") + return@wrapResource + } + + val eTag = response[GetETag::class.java]?.eTag + ?: throw DavException("Received multi-get response without ETag") + + processVTodo(response.href.lastSegment, eTag, StringReader(iCal)) + } + } + } + } + } + + override fun postProcess() { + val touched = localCollection.touchRelations() + logger.info("Touched $touched relations") + } + + // helpers + + private fun processVTodo(fileName: String, eTag: String, reader: Reader) { + val tasks: List + try { + tasks = Task.tasksFromReader(reader) + } catch (e: InvalidICalendarException) { + logger.log(Level.SEVERE, "Received invalid iCalendar, ignoring", e) + notifyInvalidResource(e, fileName) + return + } + + if (tasks.size == 1) { + val newData = tasks.first() + + // update local task, if it exists + val local = localCollection.findByName(fileName) + SyncException.wrapWithLocalResource(local) { + if (local != null) { + logger.log(Level.INFO, "Updating $fileName in local task list", newData) + local.eTag = eTag + local.update(newData) + } else { + logger.log(Level.INFO, "Adding $fileName to local task list", newData) + val newLocal = LocalTask(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT) + SyncException.wrapWithLocalResource(newLocal) { + newLocal.add() + } + } + } + } else + logger.info("Received VCALENDAR with not exactly one VTODO; ignoring $fileName") + } + + override fun notifyInvalidResourceTitle(): String = + context.getString(R.string.sync_invalid_task) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/account/AccountAuthenticatorService.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/account/AccountAuthenticatorService.kt new file mode 100644 index 0000000..ca7b57e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/account/AccountAuthenticatorService.kt @@ -0,0 +1,53 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ +package at.bitfire.davdroid.sync.account + +import android.accounts.AbstractAccountAuthenticator +import android.accounts.Account +import android.accounts.AccountAuthenticatorResponse +import android.accounts.AccountManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.core.os.bundleOf +import at.bitfire.davdroid.R + + +/** + * Account authenticator for the DAVx5 account type. + */ +class AccountAuthenticatorService: Service() { + + private lateinit var accountAuthenticator: AccountAuthenticator + + override fun onCreate() { + accountAuthenticator = AccountAuthenticator(this) + } + + override fun onBind(intent: Intent?) = + accountAuthenticator.iBinder.takeIf { intent?.action == AccountManager.ACTION_AUTHENTICATOR_INTENT } + + + private class AccountAuthenticator( + val context: Context + ): AbstractAccountAuthenticator(context) { + + override fun addAccount(response: AccountAuthenticatorResponse?, accountType: String?, authTokenType: String?, requiredFeatures: Array?, options: Bundle?) = + bundleOf( + AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE to response, + AccountManager.KEY_ERROR_CODE to AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, + AccountManager.KEY_ERROR_MESSAGE to context.getString(R.string.account_prefs_use_app) + ) + + override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?) = null + override fun getAuthTokenLabel(p0: String?) = null + override fun confirmCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Bundle?) = null + override fun updateCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null + override fun getAuthToken(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null + override fun hasFeatures(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Array?) = null + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorker.kt new file mode 100644 index 0000000..5278237 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorker.kt @@ -0,0 +1,125 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync.account + +import android.accounts.AccountManager +import android.content.Context +import androidx.annotation.VisibleForTesting +import androidx.hilt.work.HiltWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.Worker +import androidx.work.WorkerParameters +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.repository.AccountRepository +import at.bitfire.davdroid.resource.LocalAddressBook +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import java.time.Duration +import java.util.concurrent.Semaphore +import java.util.logging.Level +import java.util.logging.Logger + +@HiltWorker +class AccountsCleanupWorker @AssistedInject constructor( + @Assisted val context: Context, + @Assisted workerParameters: WorkerParameters, + private val accountRepository: AccountRepository, + private val db: AppDatabase, + private val logger: Logger +): Worker(context, workerParameters) { + + @AssistedFactory + @VisibleForTesting + interface Factory { + fun create(appContext: Context, workerParams: WorkerParameters): AccountsCleanupWorker + } + + private val accountManager = AccountManager.get(context) + + override fun doWork(): Result { + lockAccountsCleanup() + try { + cleanUpServices() + cleanUpAddressBooks() + } finally { + unlockAccountsCleanup() + } + return Result.success() + } + + /** + * Deletes services in the database which are not associated to a valid account. + */ + @VisibleForTesting + internal fun cleanUpServices() { + // Later, accounts which are not in the DB should be deleted here + + // Delete orphaned services in DB – only necessary as long as accounts are implemented as system accounts (not in DB) + val accounts = accountRepository.getAll() + logger.log(Level.INFO, "Cleaning up accounts. Currently existing accounts:", accounts) + val serviceDao = db.serviceDao() + if (accounts.isEmpty()) + serviceDao.deleteAll() + else + serviceDao.deleteExceptAccounts(accounts.map { it.name }.toTypedArray()) + } + + /** + * Deletes address book accounts which are not assigned to a valid account. + */ + @VisibleForTesting + internal fun cleanUpAddressBooks() { + val accounts = accountRepository.getAll() + for (addressBookAccount in accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))) { + val accountName = accountManager.getUserData(addressBookAccount, LocalAddressBook.USER_DATA_ACCOUNT_NAME) + val accountType = accountManager.getUserData(addressBookAccount, LocalAddressBook.USER_DATA_ACCOUNT_TYPE) + if (!accounts.any { it.name == accountName && it.type == accountType }) { + // If no valid account exists for this address book, we can delete it + logger.info("Deleting address book account without valid account: $addressBookAccount") + accountManager.removeAccountExplicitly(addressBookAccount) + } + } + } + + + companion object { + + const val NAME = "accounts-cleanup" + + private val mutex = Semaphore(1) + /** + * Prevents account cleanup from being run until `unlockAccountsCleanup` is called. + * Can only be active once at the same time globally (blocking). + */ + fun lockAccountsCleanup() = mutex.acquire() + /** Must be called exactly one time after calling `lockAccountsCleanup`. */ + fun unlockAccountsCleanup() = mutex.release() + + /** + * Enqueues [AccountsCleanupWorker] to be run once as soon as possible. + */ + fun enqueue(context: Context) { + // run once + val rq = OneTimeWorkRequestBuilder() + WorkManager.getInstance(context).enqueue(rq.build()) + } + + /** + * Enqueues [AccountsCleanupWorker] to be run regularly (but not necessarily now). + */ + fun enable(context: Context) { + // run every day + val rq = PeriodicWorkRequestBuilder(Duration.ofDays(1)) + WorkManager.getInstance(context).enqueueUniquePeriodicWork(NAME, ExistingPeriodicWorkPolicy.UPDATE, rq.build()) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/account/AddressBookAuthenticatorService.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/account/AddressBookAuthenticatorService.kt new file mode 100644 index 0000000..245275d --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/account/AddressBookAuthenticatorService.kt @@ -0,0 +1,48 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ +package at.bitfire.davdroid.sync.account + +import android.accounts.AbstractAccountAuthenticator +import android.accounts.Account +import android.accounts.AccountAuthenticatorResponse +import android.accounts.AccountManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.core.os.bundleOf +import at.bitfire.davdroid.R + +class AddressBookAuthenticatorService: Service() { + + private lateinit var accountAuthenticator: AccountAuthenticator + + override fun onCreate() { + accountAuthenticator = AccountAuthenticator(this) + } + + override fun onBind(intent: Intent?) = + accountAuthenticator.iBinder.takeIf { intent?.action == AccountManager.ACTION_AUTHENTICATOR_INTENT } + + + private class AccountAuthenticator( + val context: Context + ): AbstractAccountAuthenticator(context) { + + override fun addAccount(response: AccountAuthenticatorResponse?, accountType: String?, authTokenType: String?, requiredFeatures: Array?, options: Bundle?) = bundleOf( + AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE to response, + AccountManager.KEY_ERROR_CODE to AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, + AccountManager.KEY_ERROR_MESSAGE to context.getString(R.string.account_prefs_use_app) + ) + + override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?) = null + override fun getAuthTokenLabel(p0: String?) = null + override fun confirmCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Bundle?) = null + override fun updateCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null + override fun getAuthToken(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null + override fun hasFeatures(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Array?) = null + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/account/InvalidAccountException.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/account/InvalidAccountException.kt new file mode 100644 index 0000000..c7a4cfc --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/account/InvalidAccountException.kt @@ -0,0 +1,12 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync.account + +import android.accounts.Account + +/** + * Thrown when an account is invalid (usually because it doesn't exist anymore). + */ +class InvalidAccountException(account: Account): Exception("Invalid account: $account") \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/account/SystemAccountUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/account/SystemAccountUtils.kt new file mode 100644 index 0000000..f361cd9 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/account/SystemAccountUtils.kt @@ -0,0 +1,71 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync.account + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import android.os.Bundle +import at.bitfire.davdroid.util.SensitiveString +import java.util.logging.Logger + +object SystemAccountUtils { + + /** + * Creates a system account and makes sure the user data are set correctly. + * + * @param context operating context + * @param account account to create + * @param userData user data to set + * @param password password to set + * + * @return whether the account has been created + * + * @throws IllegalArgumentException when user data contains non-String values + * @throws IllegalStateException if user data can't be set + */ + fun createAccount(context: Context, account: Account, userData: Bundle, password: SensitiveString? = null): Boolean { + // validate user data + for (key in userData.keySet()) { + userData.get(key)?.let { entry -> + if (entry !is String) + throw IllegalArgumentException("userData[$key] is ${entry::class.java} (expected: String)") + } + } + + // create account + val manager = AccountManager.get(context) + if (!manager.addAccountExplicitly(account, password?.asString(), userData)) + return false + + // Android seems to lose the initial user data sometimes, so make sure that the values are set + for (key in userData.keySet()) + manager.setAndVerifyUserData(account, key, userData.getString(key)) + + return true + } + +} + +/** + * [AccountManager.setUserData] has been found to be unreliable at times. This extension function + * checks whether the user data has actually been set and retries up to ten times before failing silently. + * + * It should only be used to store the reference to the database (like the collection ID that this account represents). + * Everything else should be in the DB. + */ +fun AccountManager.setAndVerifyUserData(account: Account, key: String, value: String?) { + for (i in 1..10) { + if (getUserData(account, key) == value) + /* already set / success */ + return + + setUserData(account, key, value) + + // wait a bit because AccountManager access sometimes seems a bit asynchronous + Thread.sleep(100) + } + Logger.getGlobal().warning("AccountManager failed to set $account user data $key := $value") +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncAdapter.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncAdapter.kt new file mode 100644 index 0000000..d99fd41 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncAdapter.kt @@ -0,0 +1,19 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync.adapter + +import android.os.IBinder + +/** + * Interface for an Android sync adapter, as created by [SyncAdapterService]. + * + * Sync adapters are bound services that communicate over IPC, so the only method is + * [getBinder], which returns the sync adapter binder. + */ +interface SyncAdapter { + + fun getBinder(): IBinder + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncAdapterImpl.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncAdapterImpl.kt new file mode 100644 index 0000000..3249f85 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncAdapterImpl.kt @@ -0,0 +1,186 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync.adapter + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.AbstractThreadedSyncAdapter +import android.content.ContentProviderClient +import android.content.ContentResolver +import android.content.Context +import android.content.SyncResult +import android.os.Bundle +import android.os.IBinder +import androidx.work.WorkInfo +import androidx.work.WorkManager +import at.bitfire.davdroid.R +import at.bitfire.davdroid.repository.DavCollectionRepository +import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.sync.SyncConditions +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.sync.account.InvalidAccountException +import at.bitfire.davdroid.sync.worker.BaseSyncWorker +import at.bitfire.davdroid.sync.worker.SyncWorkerManager +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject + +/** + * Entry point for the Sync Adapter Framework. + * + * Handles incoming sync requests from the Sync Adapter Framework. + * + * Although we do not use the sync adapter for syncing anymore, we keep this sole + * adapter to provide exported services, which allow android system components and calendar, + * contacts or task apps to sync via DAVx5. + * + * All Sync Adapter Framework related interaction should happen inside [SyncFrameworkIntegration]. + */ +class SyncAdapterImpl @Inject constructor( + private val accountSettingsFactory: AccountSettings.Factory, + private val collectionRepository: DavCollectionRepository, + private val serviceRepository: DavServiceRepository, + @ApplicationContext context: Context, + private val logger: Logger, + private val syncConditionsFactory: SyncConditions.Factory, + private val syncWorkerManager: SyncWorkerManager +): AbstractThreadedSyncAdapter( + /* context = */ context, + /* autoInitialize = */ true // Sets isSyncable=1 when isSyncable=-1 and SYNC_EXTRAS_INITIALIZE is set. + // Doesn't matter for us because we have android:isAlwaysSyncable="true" for all sync adapters. +), SyncAdapter { + + /** + * Scope used to wait until the synchronization is finished. Will be cancelled when the sync framework + * requests cancellation. + */ + private val waitScope = CoroutineScope(Dispatchers.Default) + + override fun onPerformSync(accountOrAddressBookAccount: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) { + // We have to pass this old SyncFramework extra for an Android 7 workaround + val upload = extras.containsKey(ContentResolver.SYNC_EXTRAS_UPLOAD) + logger.info("Sync request via sync framework for $accountOrAddressBookAccount $authority (upload=$upload)") + + // If we should sync an address book account - find the account storing the settings + val account = if (accountOrAddressBookAccount.type == context.getString(R.string.account_type_address_book)) + AccountManager.get(context) + .getUserData(accountOrAddressBookAccount, LocalAddressBook.USER_DATA_COLLECTION_ID) + ?.toLongOrNull() + ?.let { collectionId -> + collectionRepository.get(collectionId)?.let { collection -> + serviceRepository.getBlocking(collection.serviceId)?.let { service -> + Account(service.accountName, context.getString(R.string.account_type)) + } + } + } + else + accountOrAddressBookAccount + + if (account == null) { + logger.warning("Address book account $accountOrAddressBookAccount doesn't have an associated collection") + return + } + + // Check sync conditions + val accountSettings = try { + accountSettingsFactory.create(account) + } catch (e: InvalidAccountException) { + logger.log(Level.WARNING, "Account doesn't exist anymore", e) + return + } + val syncConditions = syncConditionsFactory.create(accountSettings) + // Should we run the sync at all? + if (!syncConditions.wifiConditionsMet()) { + logger.info("Sync conditions not met. Aborting sync framework initiated sync") + return + } + + logger.fine("Starting OneTimeSyncWorker for $account $authority and waiting for it") + val workerName = syncWorkerManager.enqueueOneTime(account, dataType = SyncDataType.Companion.fromAuthority(authority), fromUpload = upload) + + // Android 14+ does not handle pending sync state correctly. + // As a defensive workaround, we can cancel specifically this still pending sync only + // See: https://github.com/bitfireAT/davx5-ose/issues/1458 +// if (Build.VERSION.SDK_INT >= 34) { +// logger.fine("Android 14+ bug: Canceling forever pending sync adapter framework sync request for " + +// "account=$accountOrAddressBookAccount authority=$authority upload=$upload") +// syncFrameworkIntegration.cancelSync(accountOrAddressBookAccount, authority, extras) +// } + + /* Because we are not allowed to observe worker state on a background thread, we can not + use it to block the sync adapter. Instead we use a Flow to get notified when the sync + has finished. */ + val workManager = WorkManager.getInstance(context) + + try { + val waitJob = waitScope.launch { + // wait for finished worker state + workManager.getWorkInfosForUniqueWorkFlow(workerName).collect { infoList -> + for (info in infoList) + if (info.state.isFinished) { + if (info.state == WorkInfo.State.FAILED) { + if (info.outputData.getBoolean(BaseSyncWorker.OUTPUT_TOO_MANY_RETRIES, false)) + syncResult.tooManyRetries = true + else + syncResult.databaseError = true + } + cancel("$workerName has finished") + } + } + } + + runBlocking { + withTimeout(10 * 60 * 1000) { // block max. 10 minutes + waitJob.join() // wait until worker has finished + } + } + } catch (_: CancellationException) { + // waiting for work was cancelled, either by timeout or because the worker has finished + logger.fine("Not waiting for OneTimeSyncWorker anymore.") + } + + logger.log(Level.INFO, "Returning to sync framework.", syncResult) + } + + override fun onSecurityException(account: Account, extras: Bundle, authority: String, syncResult: SyncResult) { + logger.log(Level.WARNING, "Security exception for $account/$authority") + } + + override fun onSyncCanceled() { + logger.info("Sync adapter requested cancellation – won't cancel sync, but also won't block sync framework anymore") + + // unblock sync framework + waitScope.cancel() + } + + override fun onSyncCanceled(thread: Thread) = onSyncCanceled() + + + // SyncAdapter implementation and Hilt module + + override fun getBinder(): IBinder = syncAdapterBinder + + @Module + @InstallIn(SingletonComponent::class) + abstract class RealSyncAdapterModule { + @Binds + abstract fun provide(impl: SyncAdapterImpl): SyncAdapter + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncAdapterServices.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncAdapterServices.kt new file mode 100644 index 0000000..0d2f3f5 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncAdapterServices.kt @@ -0,0 +1,41 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync.adapter + +import android.app.Service +import android.content.Intent +import dagger.hilt.InstallIn +import dagger.hilt.android.EarlyEntryPoint +import dagger.hilt.android.EarlyEntryPoints +import dagger.hilt.components.SingletonComponent + +abstract class SyncAdapterService: Service() { + + /** + * We don't use @AndroidEntryPoint / @Inject because it's unavoidable that instrumented tests sometimes accidentally / asynchronously + * create a [SyncAdapterService] instance before Hilt is initialized by the HiltTestRunner. + */ + @EarlyEntryPoint + @InstallIn(SingletonComponent::class) + interface SyncAdapterServicesEntryPoint { + fun syncAdapter(): SyncAdapter + } + + // create syncAdapter on demand and cache it + val syncAdapter by lazy { + val entryPoint = EarlyEntryPoints.get(applicationContext, SyncAdapterServicesEntryPoint::class.java) + entryPoint.syncAdapter() + } + + override fun onBind(intent: Intent?) = syncAdapter.getBinder() + +} + +// exported sync adapter services; we need a separate class for each authority +class CalendarsSyncAdapterService: SyncAdapterService() +class ContactsSyncAdapterService: SyncAdapterService() +class JtxSyncAdapterService: SyncAdapterService() +class OpenTasksSyncAdapterService: SyncAdapterService() +class TasksOrgSyncAdapterService: SyncAdapterService() \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncFrameworkIntegration.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncFrameworkIntegration.kt new file mode 100644 index 0000000..5197194 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncFrameworkIntegration.kt @@ -0,0 +1,270 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync.adapter + +import android.accounts.Account +import android.content.ContentResolver +import android.content.Context +import android.content.SyncRequest +import android.os.Build +import android.os.Bundle +import androidx.annotation.WorkerThread +import at.bitfire.davdroid.resource.LocalAddressBookStore +import at.bitfire.davdroid.sync.SyncDataType +import dagger.Lazy +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import java.util.logging.Logger +import javax.inject.Inject + +/** + * Handles all Sync Adapter Framework related interaction. Other classes should never call + * `ContentResolver.setIsSyncable()` or something similar themselves. Everything sync-framework + * related must be handled by this class. + * + * Sync requests from the Sync Adapter Framework are handled by [SyncAdapterService]. + */ +class SyncFrameworkIntegration @Inject constructor( + @ApplicationContext private val context: Context, + private val localAddressBookStore: Lazy, + private val logger: Logger +) { + + /** + * Gets the global auto-sync setting that applies to all the providers and accounts. If this is + * false then the per-provider auto-sync setting is ignored. + */ + fun getMasterSyncAutomatically() = + ContentResolver.getMasterSyncAutomatically() + + /** + * Check if this account/provider is syncable. + */ + fun isSyncable(account: Account, authority: String): Boolean = + ContentResolver.getIsSyncable(account, authority) > 0 + + /** + * Enable this account/provider to be syncable. + */ + fun enableSyncAbility(account: Account, authority: String) { + logger.fine("Enabling sync framework for account=$account, authority=$authority") + if (ContentResolver.getIsSyncable(account, authority) != 1) + ContentResolver.setIsSyncable(account, authority, 1) + } + + /** + * Disable this account/provider to be syncable. + * + * If an authority is not syncable, this implies that there's no sync on content changes, too. + */ + fun disableSyncAbility(account: Account, authority: String) { + logger.fine("Disabling sync framework for account=$account, authority=$authority") + if (ContentResolver.getIsSyncable(account, authority) != 0) + ContentResolver.setIsSyncable(account, authority, 0) + } + + /** + * Check if the provider should be synced when content (contact, calendar event or task) changes. + */ + fun syncsOnContentChange(account: Account, authority: String) = + ContentResolver.getSyncAutomatically(account, authority) + + /** + * Enable syncing on content (contact, calendar event or task) changes. + * + * This implies that the [authority] is syncable, so this method makes the [authority] + * syncable if required. + */ + fun enableSyncOnContentChange(account: Account, authority: String) { + if (!isSyncable(account, authority)) + enableSyncAbility(account, authority) + + if (!ContentResolver.getSyncAutomatically(account, authority)) + setSyncOnContentChange(account, authority, true) + } + + /** + * Disable syncing on content (contact, calendar event or task) changes. + */ + fun disableSyncOnContentChange(account: Account, authority: String) { + if (ContentResolver.getSyncAutomatically(account, authority)) + setSyncOnContentChange(account, authority, false) + } + + /** + * Cancels the sync request in the Sync Framework for Android 14+. + * This is a workaround for the bug that the sync framework does not handle pending syncs correctly + * on Android 14+ (API level 34+). + * + * See: https://github.com/bitfireAT/davx5-ose/issues/1458 + * + * @param account The account for which the sync request should be canceled. + * @param authority The authority for which the sync request should be canceled. + * @param extras The original extras Bundle used to start the sync. + */ + fun cancelSync(account: Account, authority: String, extras: Bundle) { + // Recreate the sync request which was used to start this sync + val syncRequest = SyncRequest.Builder() + .setSyncAdapter(account, authority) + .setExtras(extras) + .syncOnce() + .build() + + // Cancel it + ContentResolver.cancelSync(syncRequest) + } + + /** + * Enables/disables sync adapter automatic sync (content triggered sync) for the given + * account and authority. Does *not* call [ContentResolver.setIsSyncable]. + * + * We use the sync adapter framework only for the trigger, actual syncing is implemented + * with WorkManager. The trigger comes in through SyncAdapterService. + * + * Because there is no callback for when the sync status/interval has been updated, this method + * blocks until the sync-on-content-change has been enabled or disabled, so it should not be + * called from the UI thread. + * + * @param account account to enable/disable content change sync triggers for + * @param enable *true* enables automatic sync; *false* disables it + * @param authority sync authority (like [android.provider.CalendarContract.AUTHORITY]) + * @return whether the content triggered sync was enabled successfully + */ + @WorkerThread + private fun setSyncOnContentChange(account: Account, authority: String, enable: Boolean): Boolean { + logger.fine("Setting content-triggered syncs (sync framework) for account=$account, authority=$authority to enable=$enable") + // Try up to 10 times with 100 ms pause + repeat(10) { + if (setContentTrigger(account, authority, enable)) { + // Remove periodic syncs created by ContentResolver.setSyncAutomatically + ContentResolver.getPeriodicSyncs(account, authority).forEach { periodicSync -> + ContentResolver.removePeriodicSync( + periodicSync.account, + periodicSync.authority, + periodicSync.extras + ) + } + // Set successfully + return true + } + Thread.sleep(100) + } + // Failed to set + return false + } + + /** + * Enable or disable content change sync triggers of the Sync Adapter Framework. + * + * @param account account to enable/disable content change sync triggers for + * @param enable *true* enables automatic sync; *false* disables it + * @param authority sync authority (like [android.provider.CalendarContract.AUTHORITY]) + * @return whether the content triggered sync was enabled successfully + */ + private fun setContentTrigger(account: Account, authority: String, enable: Boolean): Boolean = + if (enable) { + ContentResolver.setSyncAutomatically(account, authority, true) + /* return */ ContentResolver.getSyncAutomatically(account, authority) + } else { + ContentResolver.setSyncAutomatically(account, authority, false) + /* return */ !ContentResolver.getSyncAutomatically(account, authority) + } + + /** + * Observe whether any of the given data types is currently pending for sync. + * + * Note: On Android 14+ finished syncs stay by default pending. This is why we + * explicitly cancel the active sync in [SyncAdapterImpl] for Android 14+. Doing + * so allows us to have a reliable "pending" flag again, which is used in this method. + * + * @param account account to observe sync status for + * @param dataTypes data types to observe sync status for + * + * @return flow emitting true if any of the given data types has a sync pending, false otherwise + */ + @OptIn(ExperimentalCoroutinesApi::class) + fun isSyncPending(account: Account, dataTypes: Iterable): Flow { + // Android 14+ does not handle pending sync state correctly. + // For now we simply always return false + // See also sync cancellation in [SyncAdapterImpl.onPerformSync] + if (Build.VERSION.SDK_INT >= 34) + return flowOf(false) + + // Determine the pending state for each data type of the account as separate flows + val pendingStateFlows: List> = dataTypes.mapNotNull { dataType -> + // Map datatype to authority + dataType.currentAuthority(context)?.let { authority -> + // If checking contacts, we need to check all address book accounts instead of the single main account + val accountsFlow: Flow> = when (dataType) { + SyncDataType.CONTACTS -> localAddressBookStore.get().getAddressBookAccountsFlow(account) + else -> flowOf(listOf(account)) + } + + // Return the pending state flow for accounts with this authority + anyPendingSyncFlow(accountsFlow, authority) + } + } + + // Combine the different per data type pending state flows into one + return combine(pendingStateFlows) { pendingStates -> + pendingStates.any { pending -> pending } + }.distinctUntilChanged() + } + + /** + * Maps the given accounts flow to a simple boolean flow telling us whether any of the accounts + * has a pending sync for given authority. + * + * @param accountsFlow accounts to check sync status for + * @param authority authority to check sync status for + * + * @return returns flow which emits *true* if any of the accounts has a sync pending for + * the given authority and *false* otherwise + */ + @OptIn(ExperimentalCoroutinesApi::class) + private fun anyPendingSyncFlow( + accountsFlow: Flow>, + authority: String + ): Flow = accountsFlow.flatMapLatest { accounts -> + // Observe sync pending state for the given accounts and data types + callbackFlow { + // Observe sync pending state + val listener = ContentResolver.addStatusChangeListener( + ContentResolver.SYNC_OBSERVER_TYPE_PENDING + ) { + trySend(anyPendingSync(accounts, authority)) + } + + // Emit initial value + trySend(anyPendingSync(accounts, authority)) + + // Clean up listener on close + awaitClose { ContentResolver.removeStatusChangeListener(listener) } + } + } + + /** + * Check if any of the given accounts have a sync pending for given authority. + * + * @param accounts accounts to check sync status for + * @param authority authority to check sync status for + * + * @return *true* if any of the given accounts has a sync pending for given authority; *false* otherwise + */ + private fun anyPendingSync(accounts: List, authority: String): Boolean = + accounts.any { account -> + ContentResolver.isSyncPending(account, authority).also { pending -> + logger.finer("Sync pending($account, $authority) = $pending") + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/groups/CategoriesStrategy.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/groups/CategoriesStrategy.kt new file mode 100644 index 0000000..b86997e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/groups/CategoriesStrategy.kt @@ -0,0 +1,46 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync.groups + +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.vcard4android.Contact +import java.util.Optional +import java.util.logging.Logger + +class CategoriesStrategy(val addressBook: LocalAddressBook): ContactGroupStrategy { + + private val logger: Logger + get() = Logger.getGlobal() + + override fun beforeUploadDirty() { + // groups with DELETED=1: set all members to dirty, then remove group + for (group in addressBook.findDeletedGroups()) { + logger.fine("Finally removing group $group") + group.markMembersDirty() + group.delete() + } + + // groups with DIRTY=1: mark all members as dirty, then clean DIRTY flag of group + for (group in addressBook.findDirtyGroups()) { + logger.fine("Marking members of modified group $group as dirty") + group.markMembersDirty() + group.clearDirty(Optional.empty(), null) + } + } + + override fun verifyContactBeforeSaving(contact: Contact) { + if (contact.group || contact.members.isNotEmpty()) { + logger.warning("Received group vCard although group method is CATEGORIES. Saving as regular contact") + contact.group = false + contact.members.clear() + } + } + + override fun postProcess() { + logger.info("Removing empty groups") + addressBook.removeEmptyGroups() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/groups/ContactGroupStrategy.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/groups/ContactGroupStrategy.kt new file mode 100644 index 0000000..4e7dbaa --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/groups/ContactGroupStrategy.kt @@ -0,0 +1,15 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync.groups + +import at.bitfire.vcard4android.Contact + +interface ContactGroupStrategy { + + fun beforeUploadDirty() + fun verifyContactBeforeSaving(contact: Contact) + fun postProcess() + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/groups/VCard4Strategy.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/groups/VCard4Strategy.kt new file mode 100644 index 0000000..9773446 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/groups/VCard4Strategy.kt @@ -0,0 +1,54 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync.groups + +import android.content.ContentUris +import android.provider.ContactsContract +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.resource.LocalGroup +import at.bitfire.davdroid.sync.ContactsSyncManager.Companion.disjunct +import at.bitfire.synctools.storage.BatchOperation +import at.bitfire.synctools.storage.ContactsBatchOperation +import at.bitfire.vcard4android.Contact +import java.io.FileNotFoundException +import java.util.logging.Logger + +class VCard4Strategy(val addressBook: LocalAddressBook): ContactGroupStrategy { + + private val logger: Logger + get() = Logger.getGlobal() + + override fun beforeUploadDirty() { + /* Mark groups with changed members as dirty: + 1. Iterate over all dirty contacts. + 2. Check whether group memberships have changed by comparing group memberships and cached group memberships. + 3. Mark groups which have been added to/removed from the contact as dirty so that they will be uploaded. + 4. Successful upload will reset dirty flag and update cached group memberships. + */ + val batch = ContactsBatchOperation(addressBook.provider!!) + for (contact in addressBook.findDirtyContacts()) + try { + logger.fine("Looking for changed group memberships of contact ${contact.fileName}") + val cachedGroups = contact.getCachedGroupMemberships() + val currentGroups = contact.getGroupMemberships() + for (groupID in cachedGroups disjunct currentGroups) { + logger.fine("Marking group as dirty: $groupID") + batch += BatchOperation.CpoBuilder + .newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(ContactsContract.Groups.CONTENT_URI, groupID))) + .withValue(ContactsContract.Groups.DIRTY, 1) + } + } catch(_: FileNotFoundException) { + } + batch.commit() + } + + override fun verifyContactBeforeSaving(contact: Contact) { + } + + override fun postProcess() { + LocalGroup.applyPendingMemberships(addressBook) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/BaseSyncWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/BaseSyncWorker.kt new file mode 100644 index 0000000..ae8cf49 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/BaseSyncWorker.kt @@ -0,0 +1,284 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync.worker + +import android.accounts.Account +import android.content.Context +import android.os.Build +import androidx.annotation.IntDef +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import at.bitfire.davdroid.R +import at.bitfire.davdroid.push.PushNotificationManager +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.sync.AddressBookSyncer +import at.bitfire.davdroid.sync.CalendarSyncer +import at.bitfire.davdroid.sync.JtxSyncer +import at.bitfire.davdroid.sync.ResyncType +import at.bitfire.davdroid.sync.SyncConditions +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.sync.SyncResult +import at.bitfire.davdroid.sync.TaskSyncer +import at.bitfire.davdroid.sync.TasksAppManager +import at.bitfire.davdroid.sync.account.InvalidAccountException +import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.NO_RESYNC +import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.RESYNC_ENTRIES +import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.RESYNC_LIST +import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.commonTag +import at.bitfire.davdroid.ui.NotificationRegistry +import at.bitfire.ical4android.TaskProvider +import dagger.Lazy +import kotlinx.coroutines.delay +import java.util.Collections +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject + +abstract class BaseSyncWorker( + context: Context, + private val workerParams: WorkerParameters +) : CoroutineWorker(context, workerParams) { + + @Inject + lateinit var accountSettingsFactory: AccountSettings.Factory + + @Inject + lateinit var addressBookSyncer: AddressBookSyncer.Factory + + @Inject + lateinit var calendarSyncer: CalendarSyncer.Factory + + @Inject + lateinit var jtxSyncer: JtxSyncer.Factory + + @Inject + lateinit var logger: Logger + + @Inject + lateinit var notificationRegistry: NotificationRegistry + + @Inject + lateinit var pushNotificationManager: PushNotificationManager + + @Inject + lateinit var syncConditionsFactory: SyncConditions.Factory + + @Inject + lateinit var tasksAppManager: Lazy + + @Inject + lateinit var taskSyncer: TaskSyncer.Factory + + + override suspend fun doWork(): Result { + // ensure we got the required arguments + val account = Account( + inputData.getString(INPUT_ACCOUNT_NAME) ?: throw IllegalArgumentException("INPUT_ACCOUNT_NAME required"), + inputData.getString(INPUT_ACCOUNT_TYPE) ?: throw IllegalArgumentException("INPUT_ACCOUNT_TYPE required") + ) + val dataType = SyncDataType.valueOf(inputData.getString(INPUT_DATA_TYPE) ?: throw IllegalArgumentException("INPUT_SYNC_DATA_TYPE required")) + + val syncTag = commonTag(account, dataType) + logger.info("${javaClass.simpleName} called for $syncTag") + + if (!runningSyncs.add(syncTag)) { + logger.info("There's already another worker running for $syncTag, skipping") + return Result.success() + } + + // Dismiss any pending push notification + pushNotificationManager.dismiss(account, dataType) + + try { + val accountSettings = try { + accountSettingsFactory.create(account) + } catch (_: InvalidAccountException) { + val workId = workerParams.id + logger.warning("No valid account settings for account $account, cancelling worker $workId") + + val workManager = WorkManager.getInstance(applicationContext) + workManager.cancelWorkById(workId) + + return Result.failure() + } + + if (inputData.getBoolean(INPUT_MANUAL, false)) + logger.info("Manual sync, skipping network checks") + else { + val syncConditions = syncConditionsFactory.create(accountSettings) + + // check internet connection + if (!syncConditions.internetAvailable()) { + logger.info("WorkManager started SyncWorker without Internet connection. Aborting.") + return Result.success() + } + + // check WiFi restriction + if (!syncConditions.wifiConditionsMet()) { + logger.info("WiFi conditions not met. Won't run periodic sync.") + return Result.success() + } + } + + return doSyncWork(account, dataType) + } finally { + logger.info("${javaClass.simpleName} finished for $syncTag") + runningSyncs -= syncTag + + if (Build.VERSION.SDK_INT >= 31 && stopReason != WorkInfo.STOP_REASON_NOT_STOPPED) + logger.warning("Worker was stopped with reason: $stopReason") + } + } + + suspend fun doSyncWork(account: Account, dataType: SyncDataType): Result { + logger.info("Running ${javaClass.name}: account=$account, dataType=$dataType") + + // pass supplied parameters to the selected syncer + val resyncType: ResyncType? = when (inputData.getInt(INPUT_RESYNC, NO_RESYNC)) { + RESYNC_ENTRIES -> ResyncType.RESYNC_ENTRIES + RESYNC_LIST -> ResyncType.RESYNC_LIST + else -> null + } + + // Comes in through SyncAdapterService and is used only by ContactsSyncManager for an Android 7 workaround. + val syncFrameworkUpload = inputData.getBoolean(INPUT_UPLOAD, false) + + val syncResult = SyncResult() + + // What are we going to sync? Select syncer based on authority + val syncer = when (dataType) { + SyncDataType.CONTACTS -> + addressBookSyncer.create(account, resyncType, syncFrameworkUpload, syncResult) + SyncDataType.EVENTS -> + calendarSyncer.create(account, resyncType, syncResult) + SyncDataType.TASKS -> { + val currentProvider = tasksAppManager.get().currentProvider() + when (currentProvider) { + TaskProvider.ProviderName.JtxBoard -> + jtxSyncer.create(account, resyncType, syncResult) + TaskProvider.ProviderName.OpenTasks, + TaskProvider.ProviderName.TasksOrg -> + taskSyncer.create(account, currentProvider, resyncType, syncResult) + else -> { + logger.warning("No valid tasks provider found, aborting sync") + return Result.failure() + } + } + } + } + + // Start syncing + syncer() + + // convert SyncResult from Syncers to worker Data + val output = Data.Builder() + .putString("syncresult", syncResult.toString()) + + // Check for errors + if (syncResult.hasError()) { + val softErrorNotificationTag = "${account.type}-${account.name}-$dataType" + + // On soft errors the sync is retried a few times before considered failed + if (syncResult.hasSoftError()) { + logger.log(Level.WARNING, "Soft error while syncing", syncResult) + if (runAttemptCount < MAX_RUN_ATTEMPTS) { + val blockDuration = syncResult.delayUntil - System.currentTimeMillis() / 1000 + logger.warning("Waiting for $blockDuration seconds, before retrying ...") + + // We block the SyncWorker here so that it won't be started by the sync framework immediately again. + // This should be replaced by proper work scheduling as soon as we don't depend on the sync framework anymore. + if (blockDuration > 0) + delay(blockDuration * 1000) + + logger.warning("Retrying on soft error (attempt $runAttemptCount of $MAX_RUN_ATTEMPTS)") + return Result.retry() + } + + logger.warning("Max retries on soft errors reached ($runAttemptCount of $MAX_RUN_ATTEMPTS). Treating as failed") + notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_SYNC_ERROR, tag = softErrorNotificationTag) { + NotificationCompat.Builder(applicationContext, notificationRegistry.CHANNEL_SYNC_IO_ERRORS) + .setSmallIcon(R.drawable.ic_sync_problem_notify) + .setContentTitle(account.name) + .setContentText(applicationContext.getString(R.string.sync_error_retry_limit_reached)) + .setSubText(account.name) + .setOnlyAlertOnce(true) + .setPriority(NotificationCompat.PRIORITY_MIN) + .setCategory(NotificationCompat.CATEGORY_ERROR) + .build() + } + + output.putBoolean(OUTPUT_TOO_MANY_RETRIES, true) + return Result.failure(output.build()) + } + + // If no soft error found, dismiss sync error notification + val notificationManager = NotificationManagerCompat.from(applicationContext) + notificationManager.cancel( + softErrorNotificationTag, + NotificationRegistry.NOTIFY_SYNC_ERROR + ) + + // On a hard error - fail with an error message + // Note: SyncManager should have notified the user + if (syncResult.hasHardError()) { + logger.log(Level.WARNING, "Hard error while syncing", syncResult) + return Result.failure(output.build()) + } + } + + logger.log(Level.INFO, "Sync worker succeeded", syncResult) + return Result.success(output.build()) + } + + + companion object { + + // common worker input parameters + internal const val INPUT_ACCOUNT_NAME = "accountName" + internal const val INPUT_ACCOUNT_TYPE = "accountType" + internal const val INPUT_DATA_TYPE = "dataType" + + /** set to `true` for user-initiated sync that skips network checks */ + internal const val INPUT_MANUAL = "manual" + + /** set to `true` for syncs that are caused because the sync framework notified us about local changes */ + internal const val INPUT_UPLOAD = "upload" + + /** Whether re-synchronization is requested. One of [NO_RESYNC] (default), [RESYNC_LIST] or [RESYNC_ENTRIES]. */ + internal const val INPUT_RESYNC = "resync" + @IntDef(NO_RESYNC, RESYNC_LIST, RESYNC_ENTRIES) + annotation class InputResync + internal const val NO_RESYNC = 0 + /** Re-synchronization is requested. See [ResyncType.RESYNC_LIST] for details. */ + internal const val RESYNC_LIST = 1 + /** Full re-synchronization is requested. See [ResyncType.RESYNC_ENTRIES] for details. */ + internal const val RESYNC_ENTRIES = 2 + + const val OUTPUT_TOO_MANY_RETRIES = "tooManyRetries" + + /** + * How often this work will be retried to run after soft (network) errors. + */ + internal const val MAX_RUN_ATTEMPTS = 5 + + /** + * Set of currently running syncs, identified by their [commonTag]. + */ + private val runningSyncs = Collections.synchronizedSet(HashSet()) + + /** + * This tag shall be added to every worker that is enqueued by a subclass. + */ + fun commonTag(account: Account, dataType: SyncDataType): String = + "sync-$dataType ${account.type}/${account.name}" + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/OneTimeSyncWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/OneTimeSyncWorker.kt new file mode 100644 index 0000000..e0f0713 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/OneTimeSyncWorker.kt @@ -0,0 +1,68 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync.worker + +import android.accounts.Account +import android.content.Context +import androidx.core.app.NotificationCompat +import androidx.hilt.work.HiltWorker +import androidx.work.ForegroundInfo +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import at.bitfire.davdroid.R +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.ui.NotificationRegistry +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + +/** + * One-time sync worker. + * + * Expedited: yes + * + * Long-running: no + */ +@HiltWorker +class OneTimeSyncWorker @AssistedInject constructor( + @Assisted appContext: Context, + @Assisted workerParams: WorkerParameters +) : BaseSyncWorker(appContext, workerParams) { + + /** + * Used by WorkManager to show a foreground service notification for expedited jobs on Android <12. + */ + override suspend fun getForegroundInfo(): ForegroundInfo { + val notification = NotificationCompat.Builder(applicationContext, notificationRegistry.CHANNEL_STATUS) + .setSmallIcon(R.drawable.ic_foreground_notify) + .setContentTitle(applicationContext.getString(R.string.foreground_service_notify_title)) + .setContentText(applicationContext.getString(R.string.foreground_service_notify_text)) + .setStyle(NotificationCompat.BigTextStyle()) + .setCategory(NotificationCompat.CATEGORY_STATUS) + .setOngoing(true) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED) + .build() + return ForegroundInfo(NotificationRegistry.NOTIFY_SYNC_EXPEDITED, notification) + } + + + companion object { + + /** + * Unique work name of this worker. Can also be used as tag. + * + * Mainly used to query [WorkManager] for work state (by unique work name or tag). + * + * @param account the account this worker is running for + * @param dataType data type to be synchronized + * + * @return Name of this worker composed as "onetime-sync $authority ${account.type}/${account.name}" + */ + fun workerName(account: Account, dataType: SyncDataType): String = + "onetime-sync $dataType ${account.type}/${account.name}" + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorker.kt new file mode 100644 index 0000000..4f6d183 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorker.kt @@ -0,0 +1,63 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync.worker + +import android.accounts.Account +import android.content.Context +import androidx.annotation.VisibleForTesting +import androidx.hilt.work.HiltWorker +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import at.bitfire.davdroid.sync.SyncDataType +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** + * Handles scheduled sync requests. + * + * The different periodic sync workers each carry a unique work name composed of the account and + * authority which they are responsible for. For each account there will be multiple dedicated periodic + * sync workers for each authority. See [PeriodicSyncWorker.workerName] for more information. + * + * Deferrable: yes (periodic) + * + * Expedited: no (→ no [getForegroundInfo]) + * + * Long-running: no + * + * **Important:** If this class is renamed (or its package is changed), already enqueued workers won't + * run anymore because WorkManager references the work by the full class name. + */ +@HiltWorker +class PeriodicSyncWorker @AssistedInject constructor( + @Assisted appContext: Context, + @Assisted workerParams: WorkerParameters +) : BaseSyncWorker(appContext, workerParams) { + + @AssistedFactory + @VisibleForTesting + interface Factory { + fun create(appContext: Context, workerParams: WorkerParameters): PeriodicSyncWorker + } + + companion object { + + /** + * Unique work name of this worker. Can also be used as tag. + * + * Mainly used to query [WorkManager] for work state (by unique work name or tag). + * + * @param account the account this worker is running for + * @param dataType data type to be synchronized + * + * @return Name of this worker composed as "periodic-sync $authority ${account.type}/${account.name}" + */ + fun workerName(account: Account, dataType: SyncDataType): String = + "periodic-sync $dataType ${account.type}/${account.name}" + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManager.kt new file mode 100644 index 0000000..0bec97f --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManager.kt @@ -0,0 +1,309 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync.worker + +import android.accounts.Account +import android.content.Context +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.Data +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequest +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.Operation +import androidx.work.OutOfQuotaPolicy +import androidx.work.PeriodicWorkRequest +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkQuery +import androidx.work.WorkRequest +import at.bitfire.davdroid.push.PushNotificationManager +import at.bitfire.davdroid.sync.ResyncType +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.sync.TasksAppManager +import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_ACCOUNT_NAME +import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_ACCOUNT_TYPE +import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_DATA_TYPE +import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_MANUAL +import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_RESYNC +import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_UPLOAD +import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.RESYNC_ENTRIES +import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.RESYNC_LIST +import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.commonTag +import dagger.Lazy +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import java.util.concurrent.TimeUnit +import java.util.logging.Logger +import javax.inject.Inject + +/** + * For building and managing synchronization workers (both one-time and periodic). + * + * One-time sync workers can be enqueued. Periodic sync workers can be enabled and disabled. + */ +class SyncWorkerManager @Inject constructor( + @ApplicationContext val context: Context, + val logger: Logger, + val pushNotificationManager: Lazy, + val tasksAppManager: Lazy +) { + + // one-time sync workers + + /** + * Builds a one-time sync worker for a specific account and authority. + * + * Arguments: see [enqueueOneTime] + * + * @return one-time sync work request for the given arguments + */ + fun buildOneTime( + account: Account, + dataType: SyncDataType, + manual: Boolean = false, + resync: ResyncType? = null, + fromUpload: Boolean = false + ): OneTimeWorkRequest { + // worker arguments + val argumentsBuilder = Data.Builder() + .putString(INPUT_DATA_TYPE, dataType.toString()) + .putString(INPUT_ACCOUNT_NAME, account.name) + .putString(INPUT_ACCOUNT_TYPE, account.type) + + if (manual) + argumentsBuilder.putBoolean(INPUT_MANUAL, true) + + when (resync) { + ResyncType.RESYNC_ENTRIES -> argumentsBuilder.putInt(INPUT_RESYNC, RESYNC_ENTRIES) + ResyncType.RESYNC_LIST -> argumentsBuilder.putInt(INPUT_RESYNC, RESYNC_LIST) + else -> { /* no explicit re-synchronization */ } + } + + argumentsBuilder.putBoolean(INPUT_UPLOAD, fromUpload) + + // build work request + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) // require a network connection + .build() + return OneTimeWorkRequestBuilder() + .addTag(OneTimeSyncWorker.workerName(account, dataType)) + .addTag(commonTag(account, dataType)) + .setInputData(argumentsBuilder.build()) + .setBackoffCriteria( + BackoffPolicy.EXPONENTIAL, + WorkRequest.DEFAULT_BACKOFF_DELAY_MILLIS, // 30 sec + TimeUnit.MILLISECONDS + ) + .setConstraints(constraints) + + /* OneTimeSyncWorker is started by user or sync framework when there are local changes. + In both cases, synchronization should be done as soon as possible, so we set expedited. */ + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + + // build work request + .build() + } + + /** + * Requests immediate synchronization of an account with a specific authority. + * + * If there is no currently running one-time sync, the sync is enqueued normally. + * + * If there is a currently running one-time sync, another sync is appended to make sure + * a complete sync is run. This method makes however sure that there's only _one_ + * further sync in the queue. + * + * @param account account to sync + * @param dataType type of data to synchronize + * @param manual user-initiated sync (ignores network checks) + * @param resync whether to request (full) re-synchronization (`null` for normal sync) + * @param fromUpload whether this sync is initiated by a local change + * @param fromPush whether this sync is initiated by a push notification + * + * @return existing or newly created worker name + */ + fun enqueueOneTime( + account: Account, + dataType: SyncDataType, + manual: Boolean = false, + resync: ResyncType? = null, + fromUpload: Boolean = false, + fromPush: Boolean = false + ): String { + logger.info("Enqueueing unique worker for account=$account, dataType=$dataType, manual=$manual, resync=$resync, fromUpload=$fromUpload, fromPush=$fromPush") + + // enqueue and start syncing + val name = OneTimeSyncWorker.workerName(account, dataType) + val request = buildOneTime( + account = account, + dataType = dataType, + manual = manual, + resync = resync, + fromUpload = fromUpload + ) + + if (fromPush) + pushNotificationManager.get().notify(account, dataType) + + /* We want to append only one work request, regardless of how many sync requests came in. + So we have to append the work one time, and as soon as there is already a pending + appended work, stop adding more work. */ + + val workManager = WorkManager.getInstance(context) + synchronized(SyncWorkerManager::class.java) { + val currentWork = workManager.getWorkInfosForUniqueWork(name).get() + val alreadyAppended = currentWork.any { + it.state in setOf(WorkInfo.State.BLOCKED, WorkInfo.State.ENQUEUED) + } + if (!alreadyAppended) { + val op = workManager.enqueueUniqueWork(name, ExistingWorkPolicy.APPEND_OR_REPLACE, request) + // for synchronization: wait until work is actually enqueued + op.result + } else + logger.fine("Another one-time sync already waiting, not adding more of $name") + } + + return name + } + + /** + * Requests immediate synchronization of an account with all applicable + * authorities (contacts, calendars, …). + * + * Arguments: see [enqueueOneTime] + */ + fun enqueueOneTimeAllAuthorities( + account: Account, + manual: Boolean = false, + resync: ResyncType? = null, + fromUpload: Boolean = false, + fromPush: Boolean = false + ) { + for (dataType in SyncDataType.entries) + enqueueOneTime( + account = account, + dataType = dataType, + manual = manual, + resync = resync, + fromUpload = fromUpload, + fromPush = fromPush + ) + } + + + // periodic sync workers + + /** + * Builds a periodic sync worker for a specific account and authority. + * + * Arguments: see [enablePeriodic] + * + * @return periodic sync work request for the given arguments + */ + fun buildPeriodic(account: Account, dataType: SyncDataType, interval: Long, syncWifiOnly: Boolean): PeriodicWorkRequest { + val arguments = Data.Builder() + .putString(INPUT_DATA_TYPE, dataType.toString()) + .putString(INPUT_ACCOUNT_NAME, account.name) + .putString(INPUT_ACCOUNT_TYPE, account.type) + .build() + val constraints = Constraints.Builder() + .setRequiredNetworkType( + if (syncWifiOnly) + NetworkType.UNMETERED + else + NetworkType.CONNECTED + ).build() + return PeriodicWorkRequestBuilder(interval, TimeUnit.SECONDS) + .addTag(PeriodicSyncWorker.workerName(account, dataType)) + .addTag(commonTag(account, dataType)) + .setInputData(arguments) + .setConstraints(constraints) + .build() + } + + /** + * Activate periodic synchronization of an account with a specific authority. + * + * @param account account to sync + * @param dataType type of data to synchronize + * @param interval interval between recurring syncs in seconds + * @return operation object to check when and whether activation was successful + */ + fun enablePeriodic(account: Account, dataType: SyncDataType, interval: Long, syncWifiOnly: Boolean): Operation { + logger.fine("Updating periodic worker for account=$account, dataType=$dataType, interval=$interval, syncWifiOnly=$syncWifiOnly") + val workRequest = buildPeriodic(account, dataType, interval, syncWifiOnly) + return WorkManager.getInstance(context).enqueueUniquePeriodicWork( + PeriodicSyncWorker.workerName(account, dataType), + // if a periodic sync exists already, we want to update it with the new interval + // and/or new required network type (applies on next iteration of periodic worker) + ExistingPeriodicWorkPolicy.UPDATE, + workRequest + ) + } + + /** + * Disables periodic synchronization of an account for a specific authority. + * + * @param account account to sync + * @param dataType type of data to synchronize + * @return operation object to check process state of work cancellation + */ + fun disablePeriodic(account: Account, dataType: SyncDataType): Operation { + logger.fine("Disabling periodic worker for account=$account, dataType=$dataType") + return WorkManager.getInstance(context) + .cancelUniqueWork(PeriodicSyncWorker.workerName(account, dataType)) + } + + + // common / helpers + + /** + * Stops running sync workers and removes pending sync workers from queue, for all authorities. + */ + fun cancelAllWork(account: Account) { + val workManager = WorkManager.getInstance(context) + for (dataType in SyncDataType.entries) { + workManager.cancelUniqueWork(OneTimeSyncWorker.workerName(account, dataType)) + workManager.cancelUniqueWork(PeriodicSyncWorker.workerName(account, dataType)) + } + } + + /** + * Observes whether >0 sync workers (both [PeriodicSyncWorker] and [OneTimeSyncWorker]) + * exist, belonging to given account and authorities, and which are/is in the given worker state. + * + * @param workStates list of states of workers to match + * @param account the account which the workers belong to + * @param dataTypes data types of sync work + * @param whichTag function to generate tag that should be observed for given account and authority + * + * @return flow that emits `true` if at least one worker with matching query was found; `false` otherwise + */ + fun hasAnyFlow( + workStates: List, + account: Account? = null, + dataTypes: Iterable? = null, + whichTag: (account: Account, dataType: SyncDataType) -> String = { account, dataType -> + commonTag(account, dataType) + } + ): Flow { + val workQuery = WorkQuery.Builder.fromStates(workStates) + if (account != null && dataTypes != null) + workQuery.addTags( + dataTypes.map { dataType -> whichTag(account, dataType) } + ) + return WorkManager.getInstance(context) + .getWorkInfosFlow(workQuery.build()) + .map { workInfoList -> + workInfoList.isNotEmpty() + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AboutActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AboutActivity.kt new file mode 100644 index 0000000..e944cac --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AboutActivity.kt @@ -0,0 +1,360 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.content.Context +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Home +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import at.bitfire.davdroid.BuildConfig +import at.bitfire.davdroid.R +import at.bitfire.davdroid.di.IoDispatcher +import at.bitfire.davdroid.ui.ExternalUris.withStatParams +import at.bitfire.davdroid.ui.composable.PixelBoxes +import com.mikepenz.aboutlibraries.Libs +import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults +import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer +import com.mikepenz.aboutlibraries.util.withContext +import dagger.BindsOptionalOf +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.json.JSONObject +import java.text.Collator +import java.util.LinkedList +import java.util.Locale +import java.util.Optional +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject +import kotlin.jvm.optionals.getOrNull + +@AndroidEntryPoint +class AboutActivity: AppCompatActivity() { + + val model by viewModels() + + @Inject + lateinit var licenseInfoProvider: Optional + + + @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + AppTheme { + val uriHandler = LocalUriHandler.current + + Scaffold( + topBar = { + TopAppBar( + navigationIcon = { + IconButton(onClick = { onSupportNavigateUp() }) { + Icon( + Icons.AutoMirrored.Default.ArrowBack, + contentDescription = stringResource(R.string.navigate_up) + ) + } + }, + title = { + Text(stringResource(R.string.navigation_drawer_about)) + }, + actions = { + IconButton(onClick = { + uriHandler.openUri(ExternalUris.Homepage.baseUrl + .buildUpon() + .withStatParams(javaClass.simpleName) + .build().toString()) + }) { + Icon( + Icons.Default.Home, + contentDescription = stringResource(R.string.navigation_drawer_website) + ) + } + } + ) + } + ) { paddingValues -> + Column(Modifier.padding(paddingValues)) { + val scope = rememberCoroutineScope() + val state = rememberPagerState(pageCount = { 3 }) + + TabRow(state.currentPage) { + Tab(state.currentPage == 0, onClick = { + scope.launch { state.scrollToPage(0) } + }) { + Text( + stringResource(R.string.app_name), + modifier = Modifier.padding(8.dp) + ) + } + Tab(state.currentPage == 1, onClick = { + scope.launch { state.scrollToPage(1) } + }) { + Text( + stringResource(R.string.about_translations), + modifier = Modifier.padding(8.dp) + ) + } + Tab(state.currentPage == 2, onClick = { + scope.launch { state.scrollToPage(2) } + }) { + Text( + stringResource(R.string.about_libraries), + modifier = Modifier.padding(8.dp) + ) + } + } + + HorizontalPager( + state, + modifier = Modifier + .fillMaxWidth() + .weight(1f), + verticalAlignment = Alignment.Top + ) { index -> + when (index) { + 0 -> AboutApp(licenseInfoProvider = licenseInfoProvider.getOrNull()) + 1 -> { + val translations = model.translations.collectAsStateWithLifecycle(emptyList()) + TranslatorsGallery(translations.value) + } + + 2 -> LibrariesContainer( + modifier = Modifier.fillMaxSize(), + padding = LibraryDefaults.libraryPadding( + contentPadding = PaddingValues(8.dp) + ), + dimensions = LibraryDefaults.libraryDimensions( + itemSpacing = 8.dp + ), + libraries = Libs.Builder() + .withContext(LocalContext.current) + .build() + ) + } + } + } + } + } + } + } + + + @HiltViewModel + class Model @Inject constructor( + @ApplicationContext val context: Context, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val logger: Logger + ): ViewModel() { + + data class Translation( + val language: String, + val translators: Set + ) + + val translations: Flow> = flow { + val translations = loadTranslations() + emit(translations) + } + + private suspend fun loadTranslations(): List = withContext(ioDispatcher) { + try { + context.resources.assets.open("translators.json").use { stream -> + val jsonTranslations = JSONObject(stream.readBytes().decodeToString()) + val result = LinkedList() + for (langCode in jsonTranslations.keys()) { + val jsonTranslators = jsonTranslations.getJSONArray(langCode) + val translators = Array(jsonTranslators.length()) { idx -> + jsonTranslators.getString(idx) + } + + val langTag = langCode.replace('_', '-') + val language = Locale.forLanguageTag(langTag).displayName + result += Translation(language, translators.toSet()) + } + + // sort translations by localized language name + val collator = Collator.getInstance() + result.sortWith { o1, o2 -> + collator.compare(o1.language, o2.language) + } + + result + } + } catch (e: Exception) { + logger.log(Level.WARNING, "Couldn't load translators", e) + emptyList() + } + } + + } + + + interface AppLicenseInfoProvider { + @Composable + fun LicenseInfo() + } + + @Module + @InstallIn(ActivityComponent::class) + interface AppLicenseInfoProviderModule { + @BindsOptionalOf + fun appLicenseInfoProvider(): AppLicenseInfoProvider + } + +} + + +@Composable +fun AboutApp(licenseInfoProvider: AboutActivity.AppLicenseInfoProvider? = null) { + Column( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth() + .verticalScroll(rememberScrollState())) { + Image( + UiUtils.adaptiveIconPainterResource(R.mipmap.ic_launcher), + contentDescription = stringResource(R.string.app_name), + modifier = Modifier + .size(128.dp) + .align(Alignment.CenterHorizontally) + ) + Text( + stringResource(R.string.app_name), + style = MaterialTheme.typography.headlineMedium, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) + + Text( + stringResource(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE), + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Text( + stringResource(R.string.about_copyright), + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + + Text( + stringResource(R.string.about_license_info_no_warranty), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + ) + + PixelBoxes( + arrayOf(Color(0xFFFCF434), Color.White, Color(0xFF9C59D1), Color.Black), + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(16.dp) + ) + + licenseInfoProvider?.LicenseInfo() + } +} + +@Composable +@Preview +fun AboutApp_Preview() { + AboutApp(licenseInfoProvider = object : AboutActivity.AppLicenseInfoProvider { + @Composable + override fun LicenseInfo() { + Text("Some flavored License Info") + } + }) +} + + +@Composable +fun TranslatorsGallery( + translations: List +) { + val collator = Collator.getInstance() + LazyColumn(Modifier.padding(8.dp)) { + items(translations) { translation -> + Text( + translation.language, + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(vertical = 4.dp) + ) + Text( + translation.translators + .sortedWith { a, b -> collator.compare(a, b) } + .joinToString(" · "), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(bottom = 16.dp) + ) + } + } +} + +@Composable +@Preview +fun TranslatorsGallery_Sample() { + TranslatorsGallery(listOf( + AboutActivity.Model.Translation("Some Language", setOf("User 1", "User 2")), + AboutActivity.Model.Translation("Another Language", setOf("User 3", "User 4")) + )) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt new file mode 100644 index 0000000..ad6752e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt @@ -0,0 +1,58 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import at.bitfire.davdroid.ui.account.AccountActivity +import at.bitfire.davdroid.ui.intro.IntroActivity +import at.bitfire.davdroid.ui.setup.LoginActivity +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + + +@AndroidEntryPoint +class AccountsActivity: AppCompatActivity() { + + @Inject + lateinit var accountsDrawerHandler: AccountsDrawerHandler + + private val introActivityLauncher = registerForActivityResult(IntroActivity.Contract) { cancelled -> + if (cancelled) + finish() + } + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // handle "Sync all" intent from launcher shortcut + val syncAccounts = intent.action == Intent.ACTION_SYNC + + setContent { + AccountsScreen( + initialSyncAccounts = syncAccounts, + onShowAppIntro = { + introActivityLauncher.launch(null) + }, + accountsDrawerHandler = accountsDrawerHandler, + onAddAccount = { + startActivity(Intent(this, LoginActivity::class.java)) + }, + onShowAccount = { account -> + val intent = Intent(this, AccountActivity::class.java) + intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account) + startActivity(intent) + }, + onManagePermissions = { + startActivity(Intent(this, PermissionsActivity::class.java)) + } + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsDrawerHandler.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsDrawerHandler.kt new file mode 100644 index 0000000..d9e757e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsDrawerHandler.kt @@ -0,0 +1,304 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Feedback +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Storage +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +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.core.net.toUri +import at.bitfire.davdroid.BuildConfig +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.webdav.WebdavMountsActivity +import kotlinx.coroutines.launch +import java.net.URI + +val LocalCloseDrawerHandler = compositionLocalOf { + AccountsDrawerHandler.CloseDrawerHandler() +} + +abstract class AccountsDrawerHandler { + + open class CloseDrawerHandler { + open fun closeDrawer() {} + } + + + @Composable + abstract fun MenuEntries( + snackbarHostState: SnackbarHostState + ) + + + @Composable + fun AccountsDrawer( + snackbarHostState: SnackbarHostState, + onCloseDrawer: () -> Unit + ) { + Column(modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { + BrandingHeader() + + val closeDrawerHandler = object : CloseDrawerHandler() { + override fun closeDrawer() { + onCloseDrawer() + } + } + CompositionLocalProvider(LocalCloseDrawerHandler provides closeDrawerHandler) { + MenuEntries(snackbarHostState) + } + } + } + + + // menu section composables + + @Composable + open fun ImportantEntries( + snackbarHostState: SnackbarHostState + ) { + val context = LocalContext.current + val isBeta = + LocalInspectionMode.current || + BuildConfig.VERSION_NAME.contains("-alpha") || + BuildConfig.VERSION_NAME.contains("-beta") || + BuildConfig.VERSION_NAME.contains("-rc") + val scope = rememberCoroutineScope() + + MenuEntry( + icon = Icons.Default.Info, + title = stringResource(R.string.navigation_drawer_about), + onClick = { + context.startActivity(Intent(context, AboutActivity::class.java)) + } + ) + + if (isBeta) + MenuEntry( + icon = Icons.Default.Feedback, + title = stringResource(R.string.navigation_drawer_beta_feedback), + onClick = { + onBetaFeedback( + context, + onShowSnackbar = { text: String, actionLabel: String, action: () -> Unit -> + scope.launch { + if (snackbarHostState.showSnackbar(text, actionLabel) == SnackbarResult.ActionPerformed) + action() + } + } + ) + } + ) + + MenuEntry( + icon = Icons.Default.Settings, + title = stringResource(R.string.navigation_drawer_settings), + onClick = { + context.startActivity(Intent(context, AppSettingsActivity::class.java)) + } + ) + } + + @Composable + fun Tools() { + val context = LocalContext.current + + MenuHeading(R.string.navigation_drawer_tools) + MenuEntry( + icon = Icons.Default.Storage, + title = stringResource(R.string.webdav_mounts_title), + onClick = { + context.startActivity(Intent(context, WebdavMountsActivity::class.java)) + } + ) + } + + + // overridable actions + + open fun onBetaFeedback( + context: Context, + onShowSnackbar: (message: String, actionLabel: String, action: () -> Unit) -> Unit + ) { + val mailto = URI( + "mailto", "play@bitfire.at?subject=${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} feedback (${BuildConfig.VERSION_CODE})", null + ) + val intent = Intent(Intent.ACTION_SENDTO, mailto.toString().toUri()) + try { + context.startActivity(intent) + } catch (_: ActivityNotFoundException) { + } + } + +} + + +// generic building blocks + +@Composable +fun MenuHeading(text: String) { + HorizontalDivider(Modifier.padding(vertical = 8.dp)) + + Text( + text, + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.padding(8.dp) + ) +} + +@Composable +fun MenuHeading(@StringRes text: Int) = MenuHeading(stringResource(text)) + +@Composable +@Preview +fun MenuHeading_Preview() { + MenuHeading("Tools") +} + +@Composable +fun MenuEntry( + icon: Painter, + title: String, + onClick: () -> Unit +) { + val closeHandler = LocalCloseDrawerHandler.current + NavigationDrawerItem( + icon = { Icon(icon, contentDescription = title) }, + label = { Text(title, style = MaterialTheme.typography.labelLarge) }, + selected = false, + shape = RectangleShape, + onClick = { + onClick() + closeHandler.closeDrawer() + } + ) +} + +@Composable +fun MenuEntry( + icon: ImageVector, + title: String, + onClick: () -> Unit +) { + MenuEntry( + icon = rememberVectorPainter(icon), + title = title, + onClick = onClick + ) +} + +@Composable +@Preview +fun MenuEntry_Preview() { + MenuEntry( + icon = Icons.Default.Info, + title = "About", + onClick = {} + ) +} + + +// specific blocks + +@Composable +fun BrandingHeader() { + Column( + Modifier + .statusBarsPadding() + .background(Color.DarkGray) + .fillMaxWidth() + .padding(16.dp) + ) { + Spacer(Modifier.height(16.dp)) + Box( + Modifier.background( + color = M3ColorScheme.primaryLight, + shape = RoundedCornerShape(16.dp) + ) + ) { + Icon( + painterResource(R.drawable.ic_launcher_foreground), + stringResource(R.string.app_name), + tint = Color.White, + modifier = Modifier + .scale(1.2f) + .size(64.dp) + ) + } + Spacer(Modifier.height(8.dp)) + + Text( + stringResource(R.string.app_name), + color = Color.White, + style = MaterialTheme.typography.bodyLarge + ) + Text( + stringResource(R.string.navigation_drawer_subtitle), + color = Color.White.copy(alpha = 0.7f), + style = MaterialTheme.typography.bodyMedium + ) + } + + Spacer(Modifier.height(8.dp)) +} + +@Composable +@Preview +fun BrandingHeader_Preview_Light() { + AppTheme(darkTheme = false) { + BrandingHeader() + } +} + +@Composable +@Preview +fun BrandingHeader_Preview_Dark() { + AppTheme(darkTheme = true) { + BrandingHeader() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsModel.kt new file mode 100644 index 0000000..8292b7e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsModel.kt @@ -0,0 +1,299 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.accounts.Account +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager.NameNotFoundException +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.os.PowerManager +import android.provider.CalendarContract +import android.provider.ContactsContract +import androidx.core.content.getSystemService +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.lifecycle.ViewModel +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkQuery +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.repository.AccountRepository +import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker +import at.bitfire.davdroid.settings.Settings +import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.sync.adapter.SyncFrameworkIntegration +import at.bitfire.davdroid.sync.worker.BaseSyncWorker +import at.bitfire.davdroid.sync.worker.OneTimeSyncWorker +import at.bitfire.davdroid.sync.worker.SyncWorkerManager +import at.bitfire.davdroid.ui.account.AccountProgress +import at.bitfire.davdroid.ui.intro.IntroPage +import at.bitfire.davdroid.ui.intro.IntroPageFactory +import at.bitfire.davdroid.util.broadcastReceiverFlow +import at.bitfire.davdroid.util.packageChangedFlow +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import java.text.Collator +import java.util.logging.Logger + +@HiltViewModel(assistedFactory = AccountsModel.Factory::class) +class AccountsModel @AssistedInject constructor( + @Assisted private val syncAccountsOnInit: Boolean, + private val accountRepository: AccountRepository, + @ApplicationContext private val context: Context, + private val db: AppDatabase, + introPageFactory: IntroPageFactory, + private val logger: Logger, + private val settings: SettingsManager, + private val syncWorkerManager: SyncWorkerManager, + private val syncFrameWork: SyncFrameworkIntegration +): ViewModel() { + + @AssistedFactory + interface Factory { + fun create(syncAccountsOnInit: Boolean): AccountsModel + } + + // Accounts UI state + + enum class FABStyle { + WithText, + Standard, + None + } + + data class AccountInfo( + val name: Account, + val progress: AccountProgress + ) + + private val accounts = accountRepository.getAllFlow() + + private val maxAccounts = settings.getIntFlow(Settings.MAX_ACCOUNTS) + val showAddAccount: Flow = combine(accounts, maxAccounts) { accounts, maxAccounts -> + if (maxAccounts != null && accounts.size >= maxAccounts) + FABStyle.None + else if (accounts.isEmpty()) + FABStyle.WithText + else + FABStyle.Standard + } + val showSyncAll: Flow = accounts.map { it.isNotEmpty() } + + private val workManager = WorkManager.getInstance(context) + private val runningWorkers = workManager.getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.ENQUEUED, WorkInfo.State.RUNNING)) + + @OptIn(ExperimentalCoroutinesApi::class) + private val accountsSyncPending: Flow> = + accounts.flatMapLatest { accounts -> + if (accounts.isEmpty()) + flowOf(emptyList()) + else { + // To create the Flow> that emits the accounts with pending sync, + val pendingSyncAccountsFlows: List> = + // for each existing account with unknown sync pending state ... + accounts.map { account -> + // ... create a Flow which emits the sync pending state + syncFrameWork.isSyncPending(account, SyncDataType.entries) + .map { hasPendingSync -> + // ... and map this boolean answer back to its Account if it is pending, or null if not. + if (hasPendingSync) account else null + } + } + // Combine all account flows Flow in the list into a single flow, emitting a list of + // accounts with pending sync. The null values which we filter out are the non-pending accounts. + // Now, whenever any account's pending state changes, the combined flow emits the updated list. + combine(pendingSyncAccountsFlows) { combinedAccounts -> + // combinedAccounts is an Array of the most recently emitted values of the + // pendingSyncCheckFlows, with one entry for every pendingSyncCheckFlow that is either + // the account name (sync pending) or null (no sync pending). + combinedAccounts.filterNotNull() + } + } + } + + + val accountInfos: Flow> = combine( + accounts, + runningWorkers, + accountsSyncPending + ) { accounts, workInfos, accountsSyncPending -> + val collator = Collator.getInstance() + + accounts + .sortedWith { a, b -> collator.compare(a.name, b.name) } + .map { account -> + val services = db.serviceDao().getIdsByAccountAsync(account.name) + val progress = when { + workInfos.any { info -> + info.state == WorkInfo.State.RUNNING && ( + services.any { serviceId -> + info.tags.contains(RefreshCollectionsWorker.workerName(serviceId)) + } || SyncDataType.entries.any { dataType -> + info.tags.contains(BaseSyncWorker.commonTag(account, dataType)) + } + ) + } -> AccountProgress.Active + + workInfos.any { info -> + info.state == WorkInfo.State.ENQUEUED && SyncDataType.entries.any { dataType -> + info.tags.contains(OneTimeSyncWorker.workerName(account, dataType)) + } + } -> AccountProgress.Pending + + account in accountsSyncPending + -> AccountProgress.Pending + + else -> AccountProgress.Idle + } + + AccountInfo(account, progress) + } + } + + + // other UI state + + val showAppIntro: Flow = flow { + val anyShowAlwaysPage = introPageFactory.introPages.any { introPage -> + val policy = introPage.getShowPolicy() + logger.fine("Intro page ${introPage::class.java.name} policy = $policy") + + policy == IntroPage.ShowPolicy.SHOW_ALWAYS + } + + emit(anyShowAlwaysPage) + }.flowOn(Dispatchers.Default) + + + // warnings + + private val connectivityManager = context.getSystemService()!! + private val powerManager: PowerManager = context.getSystemService()!! + + /** whether a usable network connection is available (sync framework won't run synchronization otherwise) */ + val networkAvailable = callbackFlow { + val networkCallback = object: ConnectivityManager.NetworkCallback() { + val availableNetworks = hashSetOf() + + override fun onAvailable(network: Network) { + availableNetworks += network + update() + } + + override fun onLost(network: Network) { + availableNetworks -= network + update() + } + + private fun update() { + trySend(availableNetworks.isNotEmpty()) + } + } + + val networkRequest = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + .build() + connectivityManager.registerNetworkCallback(networkRequest, networkCallback) + + awaitClose { + connectivityManager.unregisterNetworkCallback(networkCallback) + } + } + + /** whether battery saver is active */ + val batterySaverActive = + broadcastReceiverFlow( + context = context, + filter = IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED), + immediate = true + ).map { powerManager.isPowerSaveMode } + + /** whether data saver is restricting background synchronization ([ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED]) */ + val dataSaverEnabled = + broadcastReceiverFlow( + context = context, + filter = IntentFilter(ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED), + immediate = true + ).map { connectivityManager.restrictBackgroundStatus == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED } + + /** whether storage is low (prevents sync framework from running synchronization) */ + @Suppress("DEPRECATION") + val storageLow = + broadcastReceiverFlow( + context = context, + filter = IntentFilter().apply { + addAction(Intent.ACTION_DEVICE_STORAGE_LOW) + addAction(Intent.ACTION_DEVICE_STORAGE_OK) + }, + immediate = false // "storage low" intent is sticky + ).map { intent -> + when (intent.action) { + Intent.ACTION_DEVICE_STORAGE_LOW -> true + else -> false + } + } + + /** whether the calendar storage is missing or disabled */ + val calendarStorageDisabled = packageChangedFlow(context).map { + !contentProviderAvailable(CalendarContract.AUTHORITY) + } + + /** whether the calendar storage is missing or disabled */ + val contactsStorageDisabled = packageChangedFlow(context).map { + !contentProviderAvailable(ContactsContract.AUTHORITY) + } + + + init { + if (syncAccountsOnInit) + syncAllAccounts() + } + + + // actions + + fun syncAllAccounts() { + // report shortcut action to system + ShortcutManagerCompat.reportShortcutUsed(context, UiUtils.SHORTCUT_SYNC_ALL) + + // Enqueue sync worker for all accounts and authorities. Will sync once internet is available + for (account in accountRepository.getAll()) + syncWorkerManager.enqueueOneTimeAllAuthorities(account, manual = true) + } + + + // helpers + + fun contentProviderAvailable(authority: String): Boolean = + try { + // resolveContentProvider returns null if the provider app is disabled or missing; + // so we can't distinguish between "disabled" and "not found" + context.packageManager.resolveContentProvider(authority, 0) != null + } catch (_: NameNotFoundException) { + logger.fine("$authority provider app not found") + false + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsScreen.kt new file mode 100644 index 0000000..6e58ab9 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsScreen.kt @@ -0,0 +1,595 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.Manifest +import android.accounts.Account +import android.content.Intent +import android.os.Build +import android.provider.Settings +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.BatterySaver +import androidx.compose.material.icons.filled.DataSaverOn +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.NotificationsOff +import androidx.compose.material.icons.filled.SignalCellularOff +import androidx.compose.material.icons.filled.Storage +import androidx.compose.material.icons.filled.Sync +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconToggleButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.rememberDrawerState +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.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import at.bitfire.davdroid.BuildConfig +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.account.AccountProgress +import at.bitfire.davdroid.ui.composable.ActionCard +import at.bitfire.davdroid.ui.composable.ProgressBar +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Composable +fun AccountsScreen( + initialSyncAccounts: Boolean, + onShowAppIntro: () -> Unit, + accountsDrawerHandler: AccountsDrawerHandler, + onAddAccount: () -> Unit, + onShowAccount: (Account) -> Unit, + onManagePermissions: () -> Unit, + model: AccountsModel = hiltViewModel( + creationCallback = { factory: AccountsModel.Factory -> + factory.create(initialSyncAccounts) + } + ) +) { + val accounts by model.accountInfos.collectAsStateWithLifecycle(emptyList()) + val showSyncAll by model.showSyncAll.collectAsStateWithLifecycle(true) + val showAddAccount by model.showAddAccount.collectAsStateWithLifecycle(AccountsModel.FABStyle.Standard) + + // Remember shown state, so the intro does not restart on rotation or theme-change + var shown by rememberSaveable { mutableStateOf(false) } + val showAppIntro by model.showAppIntro.collectAsState(false) + LaunchedEffect(showAppIntro) { + if (showAppIntro && !shown) { + shown = true + onShowAppIntro() + } + } + + AccountsScreen( + accountsDrawerHandler = accountsDrawerHandler, + accounts = accounts, + showSyncAll = showSyncAll, + onSyncAll = { model.syncAllAccounts() }, + showAddAccount = showAddAccount, + onAddAccount = onAddAccount, + onShowAccount = onShowAccount, + onManagePermissions = onManagePermissions, + internetUnavailable = !model.networkAvailable.collectAsStateWithLifecycle(false).value, + batterySaverActive = model.batterySaverActive.collectAsStateWithLifecycle(false).value, + dataSaverActive = model.dataSaverEnabled.collectAsStateWithLifecycle(false).value, + storageLow = model.storageLow.collectAsStateWithLifecycle(false).value, + calendarStorageDisabled = model.calendarStorageDisabled.collectAsStateWithLifecycle(false).value, + contactsStorageDisabled = model.contactsStorageDisabled.collectAsStateWithLifecycle(false).value + ) +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) +@Composable +fun AccountsScreen( + accountsDrawerHandler: AccountsDrawerHandler, + accounts: List, + showSyncAll: Boolean = true, + onSyncAll: () -> Unit = {}, + showAddAccount: AccountsModel.FABStyle = AccountsModel.FABStyle.Standard, + onAddAccount: () -> Unit = {}, + onShowAccount: (Account) -> Unit = {}, + onManagePermissions: () -> Unit = {}, + internetUnavailable: Boolean = false, + batterySaverActive: Boolean = false, + dataSaverActive: Boolean = false, + storageLow: Boolean = false, + calendarStorageDisabled: Boolean = false, + contactsStorageDisabled: Boolean = false +) { + val scope = rememberCoroutineScope() + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + + var isRefreshing by remember { mutableStateOf(false) } + LaunchedEffect(isRefreshing) { + if (isRefreshing) { + delay(300) + isRefreshing = false + } + } + + val snackbarHostState = remember { SnackbarHostState() } + AppTheme { + ModalNavigationDrawer( + drawerState = drawerState, + drawerContent = { + ModalDrawerSheet(drawerState) { + accountsDrawerHandler.AccountsDrawer( + snackbarHostState = snackbarHostState, + onCloseDrawer = { + scope.launch { + drawerState.close() + } + } + ) + } + } + ) { + Scaffold( + topBar = { + TopAppBar( + navigationIcon = { + IconToggleButton(false, onCheckedChange = { openDrawer -> + scope.launch { + if (openDrawer) + drawerState.open() + else + drawerState.close() + } + }) { + Icon( + Icons.Filled.Menu, + stringResource(androidx.compose.ui.R.string.navigation_menu) + ) + } + }, + title = { + Text(stringResource(R.string.app_name)) + } + ) + }, + floatingActionButton = { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + if (showAddAccount == AccountsModel.FABStyle.WithText) + ExtendedFloatingActionButton( + text = { Text(stringResource(R.string.login_add_account)) }, + icon = { Icon(Icons.Filled.Add, stringResource(R.string.login_add_account)) }, + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + onClick = onAddAccount + ) + else if (showAddAccount == AccountsModel.FABStyle.Standard) + FloatingActionButton( + onClick = onAddAccount, + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary + ) { + Icon(Icons.Filled.Add, stringResource(R.string.login_add_account)) + } + + if (showSyncAll) + FloatingActionButton( + onClick = onSyncAll, + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.padding(top = 24.dp) + ) { + Icon( + Icons.Default.Sync, + contentDescription = stringResource(R.string.accounts_sync_all) + ) + } + } + }, + snackbarHost = { SnackbarHost(snackbarHostState) } + ) { padding -> + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = { isRefreshing = true; onSyncAll() }, + modifier = Modifier.padding(padding) + ) { + Box( + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + // background image + Image( + painterResource(R.drawable.accounts_background), + contentDescription = null, + modifier = Modifier + .matchParentSize() + .align(Alignment.Center) + ) + + Column { + val notificationsPermissionState = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !LocalInspectionMode.current) + rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) + else + null + + // Warnings show as action cards + val context = LocalContext.current + SyncWarnings( + notificationsWarning = notificationsPermissionState?.status?.isGranted == false, + onManagePermissions = onManagePermissions, + internetWarning = internetUnavailable, + onManageConnections = { + val intent = Intent(Settings.ACTION_WIRELESS_SETTINGS) + if (intent.resolveActivity(context.packageManager) != null) + context.startActivity(intent) + }, + batterySaverActive = batterySaverActive, + onManageBatterySaver = { + val intent = Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS) + if (intent.resolveActivity(context.packageManager) != null) + context.startActivity(intent) + }, + dataSaverActive = dataSaverActive, + onManageDataSaver = { + val intent = Intent( + /* action = */ Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS, + /* uri = */ "package:${BuildConfig.APPLICATION_ID}".toUri() + ) + if (intent.resolveActivity(context.packageManager) != null) + context.startActivity(intent) + }, + lowStorageWarning = storageLow, + onManageStorage = { + val intent = Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS) + if (intent.resolveActivity(context.packageManager) != null) + context.startActivity(intent) + }, + calendarStorageDisabled = calendarStorageDisabled, + contactsStorageDisabled = contactsStorageDisabled, + onManageApps = { + val intent = Intent(Settings.ACTION_APPLICATION_SETTINGS) + if (intent.resolveActivity(context.packageManager) != null) + context.startActivity(intent) + }, + ) + + // account list + AccountList( + accounts = accounts, + onClickAccount = { account -> + onShowAccount(account) + }, + modifier = Modifier + .fillMaxSize() + .padding(8.dp) + ) + } + } + } + } + } + } +} + +@Composable +@Preview +fun AccountsScreen_Preview_Empty() { + AccountsScreen( + accountsDrawerHandler = object: AccountsDrawerHandler() { + @Composable + override fun MenuEntries(snackbarHostState: SnackbarHostState) { + Text("Menu entries") + } + }, + accounts = emptyList(), + showAddAccount = AccountsModel.FABStyle.WithText, + showSyncAll = false + ) +} + +@Composable +@Preview +fun AccountsScreen_Preview_OneAccount() { + AccountsScreen( + accountsDrawerHandler = object: AccountsDrawerHandler() { + @Composable + override fun MenuEntries(snackbarHostState: SnackbarHostState) { + Text("Menu entries") + } + }, + accounts = listOf( + AccountsModel.AccountInfo( + Account("Account Name", "test"), + AccountProgress.Idle + ) + ) + ) +} + +@Composable +fun AccountList( + accounts: List, + modifier: Modifier = Modifier, + onClickAccount: (Account) -> Unit = {} +) { + Column(modifier) { + if (accounts.isEmpty()) + Column( + modifier = Modifier + .fillMaxSize() + .padding(8.dp) + ) { + Text( + text = stringResource(R.string.account_list_welcome), + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, bottom = 32.dp) + ) + Text( + text = stringResource(R.string.account_list_empty), + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } + else + for ((account, progress) in accounts) + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ), + elevation = CardDefaults.cardElevation(1.dp), + modifier = Modifier + .clickable { onClickAccount(account) } + .fillMaxWidth() + .padding(bottom = 8.dp) + ) { + Column { + val progressAlpha = progress.rememberAlpha() + when (progress) { + AccountProgress.Active -> + ProgressBar( + modifier = Modifier + .alpha(progressAlpha) + .fillMaxWidth() + ) + AccountProgress.Pending, + AccountProgress.Idle -> + ProgressBar( + progress = { 1f }, + modifier = Modifier + .alpha(progressAlpha) + .fillMaxWidth() + ) + } + + Column(Modifier.padding(vertical = 12.dp)) { + Icon( + imageVector = Icons.Default.AccountCircle, + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .size(48.dp) + ) + + Text( + text = account.name, + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(top = 4.dp) + .fillMaxWidth() + ) + } + } + } + } +} + +@Composable +@Preview +fun AccountList_Preview_Idle() { + AppTheme { + AccountList( + listOf( + AccountsModel.AccountInfo( + Account("Account Name", "test"), + AccountProgress.Idle + ) + ) + ) + } +} + +@Composable +@Preview +fun AccountList_Preview_SyncPending() { + AppTheme { + AccountList(listOf( + AccountsModel.AccountInfo( + Account("Account Name", "test"), + AccountProgress.Pending + ) + )) + } +} + +@Composable +@Preview +fun AccountList_Preview_Syncing() { + AppTheme { + AccountList(listOf( + AccountsModel.AccountInfo( + Account("Account Name", "test"), + AccountProgress.Active + ) + )) + } +} + + +@Composable +fun SyncWarnings( + notificationsWarning: Boolean = true, + onManagePermissions: () -> Unit = {}, + internetWarning: Boolean = true, + onManageConnections: () -> Unit = {}, + batterySaverActive: Boolean = true, + onManageBatterySaver: () -> Unit = {}, + dataSaverActive: Boolean = true, + onManageDataSaver: () -> Unit = {}, + lowStorageWarning: Boolean = true, + onManageStorage: () -> Unit = {}, + calendarStorageDisabled: Boolean = false, + contactsStorageDisabled: Boolean = false, + onManageApps: () -> Unit = {} +) { + Column(Modifier.padding(horizontal = 8.dp)) { + if (notificationsWarning) + ActionCard( + icon = Icons.Default.NotificationsOff, + actionText = stringResource(R.string.account_manage_permissions), + onAction = onManagePermissions, + modifier = Modifier.padding(vertical = 4.dp) + ) { + Text(stringResource(R.string.sync_warning_no_notification_permission)) + } + + if (internetWarning) + ActionCard( + icon = Icons.Default.SignalCellularOff, + actionText = stringResource(R.string.sync_warning_manage_connections), + onAction = onManageConnections, + modifier = Modifier.padding(vertical = 4.dp) + ) { + Text(stringResource(R.string.sync_warning_no_internet)) + } + + if (batterySaverActive) + ActionCard( + icon = Icons.Default.BatterySaver, + actionText = stringResource(R.string.sync_warning_manage_battery_saver), + onAction = onManageBatterySaver, + modifier = Modifier.padding(vertical = 4.dp) + ) { + Text(stringResource(R.string.sync_warning_battery_saver_enabled)) + } + + if (dataSaverActive) + ActionCard( + icon = Icons.Default.DataSaverOn, + actionText = stringResource(R.string.sync_warning_manage_datasaver), + onAction = onManageDataSaver, + modifier = Modifier.padding(vertical = 4.dp) + ) { + Text(stringResource(R.string.sync_warning_datasaver_enabled)) + } + + if (lowStorageWarning) + ActionCard( + icon = Icons.Default.Storage, + actionText = stringResource(R.string.sync_warning_manage_storage), + onAction = onManageStorage, + modifier = Modifier.padding(vertical = 4.dp) + ) { + Text(stringResource(R.string.sync_warning_low_storage)) + } + + if (calendarStorageDisabled) + ActionCard( + icon = ImageVector.vectorResource(R.drawable.ic_database_off), + actionText = stringResource(R.string.sync_warning_manage_apps), + onAction = onManageApps, + modifier = Modifier.padding(vertical = 4.dp) + ) { + Column { + Text( + text = stringResource(R.string.sync_warning_calendar_storage_disabled_title), + modifier = Modifier.padding(vertical = 4.dp) + ) + Text(stringResource(R.string.sync_warning_calendar_storage_disabled_description)) + } + } + + if (contactsStorageDisabled) + ActionCard( + icon = ImageVector.vectorResource(R.drawable.ic_database_off), + actionText = stringResource(R.string.sync_warning_manage_apps), + onAction = onManageApps, + modifier = Modifier.padding(vertical = 4.dp) + ) { + Column { + Text( + text = stringResource(R.string.sync_warning_contacts_storage_disabled_title), + modifier = Modifier.padding(vertical = 4.dp) + ) + Text(stringResource(R.string.sync_warning_contacts_storage_disabled_description)) + } + } + } +} + +@Composable +@Preview +fun SyncWarnings_Preview() { + AppTheme { + SyncWarnings( + notificationsWarning = true, + internetWarning = true, + batterySaverActive = true, + dataSaverActive = true, + lowStorageWarning = true, + calendarStorageDisabled = true, + contactsStorageDisabled = true + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt new file mode 100644 index 0000000..2a07fc1 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt @@ -0,0 +1,60 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.core.net.toUri +import at.bitfire.davdroid.BuildConfig +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class AppSettingsActivity: AppCompatActivity() { + + @SuppressLint("BatteryLife") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + AppSettingsScreen( + onNavDebugInfo = { + startActivity(Intent(this, DebugInfoActivity::class.java)) + }, + onExemptFromBatterySaving = { + startActivity( + Intent( + Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + ("package:" + BuildConfig.APPLICATION_ID).toUri() + ) + ) + }, + onBatterySavingSettings = { + startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)) + }, + onNavTasksScreen = { + startActivity(Intent(this, TasksActivity::class.java)) + }, + onShowNotificationSettings = { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + startActivity( + Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, BuildConfig.APPLICATION_ID) + } + ) + }, + onNavPermissionsScreen = { + startActivity(Intent(this, PermissionsActivity::class.java)) + }, + onNavUp = ::onSupportNavigateUp + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsModel.kt new file mode 100644 index 0000000..579379b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsModel.kt @@ -0,0 +1,185 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.content.Context +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.os.PowerManager +import androidx.core.content.getSystemService +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.bitfire.cert4android.CustomCertStore +import at.bitfire.davdroid.BuildConfig +import at.bitfire.davdroid.di.IoDispatcher +import at.bitfire.davdroid.push.PushRegistrationManager +import at.bitfire.davdroid.repository.PreferenceRepository +import at.bitfire.davdroid.settings.Settings +import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.sync.TasksAppManager +import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPageModel +import at.bitfire.davdroid.ui.intro.OpenSourcePage +import at.bitfire.davdroid.util.PermissionUtils +import at.bitfire.davdroid.util.broadcastReceiverFlow +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class AppSettingsModel @Inject constructor( + @ApplicationContext private val context: Context, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val preferences: PreferenceRepository, + private val pushRegistrationManager: PushRegistrationManager, + private val settings: SettingsManager, + tasksAppManager: TasksAppManager +) : ViewModel() { + + + // debugging + + private val powerManager = context.getSystemService()!! + val batterySavingExempted = + broadcastReceiverFlow(context, IntentFilter(PermissionUtils.ACTION_POWER_SAVE_WHITELIST_CHANGED), immediate = true) + .map { powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID) } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) + + fun verboseLogging() = preferences.logToFileFlow() + fun updateVerboseLogging(verbose: Boolean) { + preferences.logToFile(verbose) + } + + + // connection + + fun proxyType() = settings.getIntFlow(Settings.PROXY_TYPE) + fun updateProxyType(type: Int) { + settings.putInt(Settings.PROXY_TYPE, type) + } + + fun proxyHostName() = settings.getStringFlow(Settings.PROXY_HOST) + fun updateProxyHostName(host: String) { + settings.putString(Settings.PROXY_HOST, host) + } + + fun proxyPort() = settings.getIntFlow(Settings.PROXY_PORT) + fun updateProxyPort(port: Int) { + settings.putInt(Settings.PROXY_PORT, port) + } + + + // security + + fun distrustSystemCertificates() = settings.getBooleanFlow(Settings.DISTRUST_SYSTEM_CERTIFICATES) + fun updateDistrustSystemCertificates(distrust: Boolean) { + settings.putBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES, distrust) + } + + fun resetCertificates() { + CustomCertStore.getInstance(context).clearUserDecisions() + } + + + // user interface + + fun theme() = settings.getIntFlow(Settings.PREFERRED_THEME) + fun updateTheme(theme: Int) { + settings.putInt(Settings.PREFERRED_THEME, theme) + UiUtils.updateTheme(context) + } + + fun resetHints() { + settings.remove(BatteryOptimizationsPageModel.HINT_BATTERY_OPTIMIZATIONS) + settings.remove(BatteryOptimizationsPageModel.HINT_AUTOSTART_PERMISSION) + settings.remove(TasksModel.HINT_OPENTASKS_NOT_INSTALLED) + settings.remove(OpenSourcePage.Model.SETTING_NEXT_DONATION_POPUP) + } + + + // tasks + + private val pm: PackageManager = context.packageManager + private val appInfoFlow = tasksAppManager.currentProviderFlow().map { tasksProvider -> + tasksProvider?.packageName?.let { pkgName -> + pm.getApplicationInfo(pkgName, 0) + } + } + val tasksAppName = appInfoFlow.map { it?.loadLabel(pm)?.toString() } + val tasksAppIcon = appInfoFlow.map { it?.loadIcon(pm) } + + + // push + + private val _pushDistributor = MutableStateFlow(null) + val pushDistributor = _pushDistributor.asStateFlow() + + private val _pushDistributors = MutableStateFlow?>(null) + val pushDistributors = _pushDistributors.asStateFlow() + + /** + * Loads the push distributors configuration: + * + * - Loads the currently selected distributor into [pushDistributor]. + * - Loads all the available distributors into [pushDistributors]. + * - If there's only one push distributor available, and none is selected, it's selected automatically. + * - Makes sure the app is registered with UnifiedPush if there's already a distributor selected. + */ + private fun loadPushDistributors() { + val currentPushDistributor = pushRegistrationManager.getCurrentDistributor() + _pushDistributor.value = currentPushDistributor + + val pushDistributors = pushRegistrationManager.getDistributors() + .map { pushDistributor -> + try { + val applicationInfo = pm.getApplicationInfo(pushDistributor, 0) + val label = pm.getApplicationLabel(applicationInfo).toString() + val icon = pm.getApplicationIcon(applicationInfo) + PushDistributorInfo(pushDistributor, label, icon) + } catch (_: PackageManager.NameNotFoundException) { + // The app is not available for some reason, do not include the app data. + PushDistributorInfo(pushDistributor) + } + } + _pushDistributors.value = pushDistributors + } + + /** + * Updates the current push distributor selection. + * + * Saves the preference in UnifiedPush, (un)registers the app, and writes the selection to [pushDistributor]. + * + * @param pushDistributor The package name of the push distributor, _null_ to disable push. + */ + fun updatePushDistributor(pushDistributor: String?) { + viewModelScope.launch(ioDispatcher) { + pushRegistrationManager.setPushDistributor(pushDistributor) + + _pushDistributor.value = pushDistributor + } + } + + + init { + viewModelScope.launch(ioDispatcher) { + loadPushDistributors() + } + } + + + data class PushDistributorInfo( + val packageName: String, + val appName: String? = null, + val appIcon: Drawable? = null + ) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsScreen.kt new file mode 100644 index 0000000..373eb16 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsScreen.kt @@ -0,0 +1,759 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.annotation.SuppressLint +import android.graphics.drawable.Drawable +import android.os.Build +import androidx.appcompat.content.res.AppCompatResources +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.Help +import androidx.compose.material.icons.filled.Adb +import androidx.compose.material.icons.filled.BugReport +import androidx.compose.material.icons.filled.InvertColors +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.RadioButtonChecked +import androidx.compose.material.icons.filled.RadioButtonUnchecked +import androidx.compose.material.icons.filled.SyncProblem +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import at.bitfire.davdroid.R +import at.bitfire.davdroid.settings.Settings +import at.bitfire.davdroid.ui.AppSettingsModel.PushDistributorInfo +import at.bitfire.davdroid.ui.composable.EditTextInputDialog +import at.bitfire.davdroid.ui.composable.MultipleChoiceInputDialog +import at.bitfire.davdroid.ui.composable.Setting +import at.bitfire.davdroid.ui.composable.SettingsHeader +import at.bitfire.davdroid.ui.composable.SwitchSetting +import kotlinx.coroutines.launch + +@Composable +fun AppSettingsScreen( + onNavDebugInfo: () -> Unit, + onExemptFromBatterySaving: () -> Unit, + onBatterySavingSettings: () -> Unit, + onNavPermissionsScreen: () -> Unit, + onShowNotificationSettings: () -> Unit, + onNavTasksScreen: () -> Unit, + onNavUp: () -> Unit, + model: AppSettingsModel = viewModel() +) { + AppTheme { + AppSettingsScreen( + onNavDebugInfo = onNavDebugInfo, + verboseLogging = model.verboseLogging().collectAsStateWithLifecycle(false).value, + onUpdateVerboseLogging = model::updateVerboseLogging, + batterySavingExempted = model.batterySavingExempted.collectAsStateWithLifecycle().value, + onExemptFromBatterySaving = onExemptFromBatterySaving, + onBatterySavingSettings = onBatterySavingSettings, + onNavUp = onNavUp, + + // Connection + proxyType = model.proxyType().collectAsStateWithLifecycle(null).value ?: Settings.PROXY_TYPE_NONE, + onProxyTypeUpdated = model::updateProxyType, + proxyHostName = model.proxyHostName().collectAsStateWithLifecycle(null).value, + onProxyHostNameUpdated = model::updateProxyHostName, + proxyPort = model.proxyPort().collectAsStateWithLifecycle(null).value, + onProxyPortUpdated = model::updateProxyPort, + + // Security + distrustSystemCerts = model.distrustSystemCertificates().collectAsStateWithLifecycle(null).value ?: false, + onDistrustSystemCertsUpdated = model::updateDistrustSystemCertificates, + onResetCertificates = model::resetCertificates, + onNavPermissionsScreen = onNavPermissionsScreen, + + // User interface + onShowNotificationSettings = onShowNotificationSettings, + theme = model.theme().collectAsStateWithLifecycle(null).value ?: Settings.PREFERRED_THEME_DEFAULT, + onThemeSelected = model::updateTheme, + onResetHints = model::resetHints, + + // Integration (Tasks and Push) + tasksAppName = model.tasksAppName.collectAsStateWithLifecycle(null).value ?: stringResource(R.string.app_settings_tasks_provider_none), + tasksAppIcon = model.tasksAppIcon.collectAsStateWithLifecycle(null).value, + pushDistributors = model.pushDistributors.collectAsState().value, + pushDistributor = model.pushDistributor.collectAsState().value, + onPushDistributorChange = model::updatePushDistributor, + onNavTasksScreen = onNavTasksScreen + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@SuppressLint("BatteryLife") +@Composable +fun AppSettingsScreen( + onNavDebugInfo: () -> Unit, + verboseLogging: Boolean, + onUpdateVerboseLogging: (Boolean) -> Unit, + batterySavingExempted: Boolean, + onExemptFromBatterySaving: () -> Unit, + onBatterySavingSettings: () -> Unit, + + // AppSettings connection + proxyType: Int, + onProxyTypeUpdated: (Int) -> Unit, + proxyHostName: String?, + onProxyHostNameUpdated: (String) -> Unit, + proxyPort: Int?, + onProxyPortUpdated: (Int) -> Unit, + + // AppSettings security + distrustSystemCerts: Boolean, + onDistrustSystemCertsUpdated: (Boolean) -> Unit, + onResetCertificates: () -> Unit, + onNavPermissionsScreen: () -> Unit, + + // AppSettings UserInterface + theme: Int, + onThemeSelected: (Int) -> Unit, + onResetHints: () -> Unit, + + // AppSettings Integration + tasksAppName: String, + tasksAppIcon: Drawable?, + pushDistributors: List?, + pushDistributor: String?, + onPushDistributorChange: (String?) -> Unit, + onNavTasksScreen: () -> Unit, + + onShowNotificationSettings: () -> Unit, + onNavUp: () -> Unit +) { + val coroutineScope = rememberCoroutineScope() + val uriHandler = LocalUriHandler.current + + val snackbarHostState = remember { SnackbarHostState() } + Scaffold( + topBar = { + TopAppBar( + navigationIcon = { + IconButton(onClick = onNavUp) { + Icon(Icons.AutoMirrored.Default.ArrowBack, stringResource(R.string.navigate_up)) + } + }, + title = { Text(stringResource(R.string.app_settings)) }, + actions = { + IconButton(onClick = { + val settingsUri = ExternalUris.Manual.baseUrl.buildUpon() + .appendPath(ExternalUris.Manual.PATH_SETTINGS) + .fragment(ExternalUris.Manual.FRAGMENT_APP_SETTINGS) + .build() + uriHandler.openUri(settingsUri.toString()) + }) { + Icon(Icons.AutoMirrored.Filled.Help, stringResource(R.string.help)) + } + } + ) + }, + snackbarHost = { SnackbarHost(snackbarHostState) } + ) { padding -> + Column( + Modifier + .padding(padding) + .verticalScroll(rememberScrollState()) + ) { + Column(Modifier.padding(8.dp)) { + AppSettings_Debugging( + onNavDebugInfo = onNavDebugInfo, + verboseLogging = verboseLogging, + onUpdateVerboseLogging = onUpdateVerboseLogging, + batterySavingExempted = batterySavingExempted, + onExemptFromBatterySaving = onExemptFromBatterySaving, + onBatterySavingSettings = onBatterySavingSettings + ) + + AppSettings_Connection( + proxyType = proxyType, + onProxyTypeUpdated = onProxyTypeUpdated, + proxyHostName = proxyHostName, + onProxyHostNameUpdated = onProxyHostNameUpdated, + proxyPort = proxyPort, + onProxyPortUpdated = onProxyPortUpdated, + ) + + val resetCertificatesSuccessMessage = stringResource(R.string.app_settings_reset_certificates_success) + AppSettings_Security( + distrustSystemCerts = distrustSystemCerts, + onDistrustSystemCertsUpdated = onDistrustSystemCertsUpdated, + onResetCertificates = { + onResetCertificates() + coroutineScope.launch { + snackbarHostState.showSnackbar(resetCertificatesSuccessMessage) + } + }, + onNavPermissionsScreen = onNavPermissionsScreen + ) + + val resetHintsSuccessMessage = stringResource(R.string.app_settings_reset_hints_success) + AppSettings_UserInterface( + theme = theme, + onThemeSelected = onThemeSelected, + onResetHints = { + onResetHints() + coroutineScope.launch { + snackbarHostState.showSnackbar(resetHintsSuccessMessage) + } + }, + onShowNotificationSettings = onShowNotificationSettings + ) + + AppSettings_Integration( + tasksAppName = tasksAppName, + tasksAppIcon = tasksAppIcon, + pushDistributors = pushDistributors, + pushDistributor = pushDistributor, + onPushDistributorChange = onPushDistributorChange, + onNavTasksScreen = onNavTasksScreen + ) + } + } + } +} + +@Composable +@Preview +fun AppSettingsScreen_Preview() { + AppTheme { + AppSettingsScreen( + onNavDebugInfo = {}, + verboseLogging = true, + batterySavingExempted = true, + proxyType = 0, + proxyHostName = "true", + proxyPort = 0, + distrustSystemCerts = true, + theme = 0, + onUpdateVerboseLogging = {}, + onProxyHostNameUpdated = {}, + onExemptFromBatterySaving = {}, + onBatterySavingSettings = {}, + onShowNotificationSettings = {}, + onNavUp = {}, + onProxyTypeUpdated = {}, + onProxyPortUpdated = {}, + onDistrustSystemCertsUpdated = {}, + onResetCertificates = {}, + onNavPermissionsScreen = {}, + onThemeSelected = {}, + onResetHints = {}, + tasksAppName = "No tasks app", + tasksAppIcon = null, + pushDistributors = null, + pushDistributor = null, + onPushDistributorChange = {}, + onNavTasksScreen = {} + ) + } +} + +@Composable +fun AppSettings_Debugging( + onNavDebugInfo: () -> Unit, + verboseLogging: Boolean, + onUpdateVerboseLogging: (Boolean) -> Unit, + batterySavingExempted: Boolean, + onExemptFromBatterySaving: () -> Unit, + onBatterySavingSettings: () -> Unit +) { + SettingsHeader { + Text(stringResource(R.string.app_settings_debug)) + } + + Setting( + icon = Icons.Default.BugReport, + name = stringResource(R.string.app_settings_show_debug_info), + summary = stringResource(R.string.app_settings_show_debug_info_details) + ) { + onNavDebugInfo() + } + + SwitchSetting( + icon = Icons.Default.Adb, + checked = verboseLogging, + name = stringResource(R.string.app_settings_logging), + summaryOn = stringResource(R.string.app_settings_logging_on), + summaryOff = stringResource(R.string.app_settings_logging_off) + ) { + onUpdateVerboseLogging(it) + } + + SwitchSetting( + checked = batterySavingExempted, + icon = Icons.Default.SyncProblem.takeUnless { batterySavingExempted }, + name = stringResource(R.string.app_settings_battery_optimization), + summaryOn = stringResource(R.string.app_settings_battery_optimization_exempted), + summaryOff = stringResource(R.string.app_settings_battery_optimization_optimized) + ) { + if (batterySavingExempted) + onBatterySavingSettings() + else + onExemptFromBatterySaving() + } +} + +@Composable +fun AppSettings_Connection( + proxyType: Int, + onProxyTypeUpdated: (Int) -> Unit = {}, + proxyHostName: String? = null, + onProxyHostNameUpdated: (String) -> Unit = {}, + proxyPort: Int? = null, + onProxyPortUpdated: (Int) -> Unit = {} +) { + SettingsHeader(divider = true) { + Text(stringResource(R.string.app_settings_connection)) + } + + val proxyTypeNames = stringArrayResource(R.array.app_settings_proxy_types) + val proxyTypeValues = stringArrayResource(R.array.app_settings_proxy_type_values).map { it.toInt() } + var showProxyTypeInputDialog by remember { mutableStateOf(false) } + Setting( + name = stringResource(R.string.app_settings_proxy), + summary = proxyTypeNames[proxyTypeValues.indexOf(proxyType)] + ) { + showProxyTypeInputDialog = true + } + if (showProxyTypeInputDialog) + MultipleChoiceInputDialog( + title = stringResource(R.string.app_settings_proxy), + namesAndValues = proxyTypeNames.zip(proxyTypeValues.map { it.toString() }), + initialValue = proxyType.toString(), + onValueSelected = { newValue -> + onProxyTypeUpdated(newValue.toInt()) + }, + onDismiss = { showProxyTypeInputDialog = false } + ) + + if (proxyType !in listOf(Settings.PROXY_TYPE_SYSTEM, Settings.PROXY_TYPE_NONE)) { + var showProxyHostNameInputDialog by remember { mutableStateOf(false) } + Setting( + name = stringResource(R.string.app_settings_proxy_host), + summary = proxyHostName + ) { + showProxyHostNameInputDialog = true + } + if (showProxyHostNameInputDialog) + EditTextInputDialog( + title = stringResource(R.string.app_settings_proxy_host), + initialValue = proxyHostName, + keyboardType = KeyboardType.Uri, + onValueEntered = onProxyHostNameUpdated, + onDismiss = { showProxyHostNameInputDialog = false } + ) + + var showProxyPortInputDialog by remember { mutableStateOf(false) } + Setting( + name = stringResource(R.string.app_settings_proxy_port), + summary = proxyPort?.toString() + ) { + showProxyPortInputDialog = true + } + if (showProxyPortInputDialog) + EditTextInputDialog( + title = stringResource(R.string.app_settings_proxy_port), + initialValue = proxyPort?.toString(), + keyboardType = KeyboardType.Number, + onValueEntered = { + try { + val newPort = it.toInt() + if (newPort in 1..65535) + onProxyPortUpdated(newPort) + } catch (_: NumberFormatException) { + // user entered invalid port number + } + }, + onDismiss = { showProxyPortInputDialog = false } + ) + } +} + +@Composable +fun AppSettings_Security( + distrustSystemCerts: Boolean, + onDistrustSystemCertsUpdated: (Boolean) -> Unit, + onResetCertificates: () -> Unit, + onNavPermissionsScreen: () -> Unit +) { + SettingsHeader(divider = true) { + Text(stringResource(R.string.app_settings_security)) + } + + var showingDistrustWarning by remember { mutableStateOf(false) } + if (showingDistrustWarning) { + DistrustSystemCertificatesAlertDialog( + onDistrustSystemCertsRequested = { onDistrustSystemCertsUpdated(true) }, + onDismissRequested = { showingDistrustWarning = false } + ) + } + + SwitchSetting( + checked = distrustSystemCerts, + name = stringResource(R.string.app_settings_distrust_system_certs), + summaryOn = stringResource(R.string.app_settings_distrust_system_certs_on), + summaryOff = stringResource(R.string.app_settings_distrust_system_certs_off) + ) { checked -> + if (checked) { + // Show warning before enabling. + showingDistrustWarning = true + } else { + onDistrustSystemCertsUpdated(false) + } + } + + Setting( + name = stringResource(R.string.app_settings_reset_certificates), + summary = stringResource(R.string.app_settings_reset_certificates_summary), + onClick = onResetCertificates + ) + + Setting( + name = stringResource(R.string.app_settings_security_app_permissions), + summary = stringResource(R.string.app_settings_security_app_permissions_summary), + onClick = onNavPermissionsScreen + ) +} + +@Composable +fun DistrustSystemCertificatesAlertDialog( + onDistrustSystemCertsRequested: () -> Unit, + onDismissRequested: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismissRequested, + icon = { Icon(Icons.Default.Warning, stringResource(R.string.app_settings_distrust_system_certs)) }, + title = { Text(stringResource(R.string.app_settings_distrust_system_certs)) }, + text = { Text(stringResource(R.string.app_settings_distrust_system_certs_dialog_message)) }, + confirmButton = { + TextButton( + onClick = { + onDistrustSystemCertsRequested() + onDismissRequested() + } + ) { Text(stringResource(R.string.dialog_enable)) } + }, + dismissButton = { + TextButton( + onClick = onDismissRequested + ) { Text(stringResource(R.string.dialog_deny)) } + }, + ) +} + +@Preview +@Composable +fun DistrustSystemCertificatesAlertDialog_Preview() { + AppTheme { + DistrustSystemCertificatesAlertDialog({}, {}) + } +} + +@Composable +fun AppSettings_UserInterface( + theme: Int, + onThemeSelected: (Int) -> Unit = {}, + onResetHints: () -> Unit = {}, + onShowNotificationSettings: () -> Unit = {} +) { + SettingsHeader(divider = true) { + Text(stringResource(R.string.app_settings_user_interface)) + } + + if (Build.VERSION.SDK_INT >= 26) + Setting( + icon = Icons.Default.Notifications, + name = stringResource(R.string.app_settings_notification_settings), + summary = stringResource(R.string.app_settings_notification_settings_summary), + onClick = onShowNotificationSettings + ) + + val themeNames = stringArrayResource(R.array.app_settings_theme_names) + val themeValues = stringArrayResource(R.array.app_settings_theme_values).map { it.toInt() } + var showThemeDialog by remember { mutableStateOf(false) } + val themeValueIdx = themeValues.indexOf(theme).takeIf { it != -1 } + Setting( + icon = Icons.Default.InvertColors, + name = stringResource(R.string.app_settings_theme_title), + summary = themeValueIdx?.let { themeNames[it] } + ) { + showThemeDialog = true + } + if (showThemeDialog) + MultipleChoiceInputDialog( + title = stringResource(R.string.app_settings_theme_title), + namesAndValues = themeNames.zip(themeValues.map { it.toString() }), + initialValue = theme.toString(), + onValueSelected = { + onThemeSelected(it.toInt()) + }, + onDismiss = { showThemeDialog = false } + ) + + Setting( + name = stringResource(R.string.app_settings_reset_hints), + summary = stringResource(R.string.app_settings_reset_hints_summary), + onClick = onResetHints + ) +} + +@Composable +private fun PushDistributorSelectionDialog( + pushDistributor: String?, + onPushDistributorChange: (String?) -> Unit, + pushDistributors: List?, + onDismissRequested: () -> Unit +) { + var selectedDistributor by remember { mutableStateOf(pushDistributor) } + val context = LocalContext.current + + AlertDialog( + onDismissRequest = onDismissRequested, + confirmButton = { + TextButton( + onClick = { + onPushDistributorChange(selectedDistributor) + onDismissRequested() + } + ) { Text(stringResource(android.R.string.ok)) } + }, + dismissButton = { + TextButton( + onClick = onDismissRequested + ) { Text(stringResource(android.R.string.cancel)) } + }, + title = { + Text(stringResource(R.string.app_settings_unifiedpush_choose_distributor)) + }, + text = { + LazyColumn(modifier = Modifier.fillMaxWidth()) { + if (pushDistributors.isNullOrEmpty()) item { + Text(stringResource(R.string.app_settings_unifiedpush_no_distributor)) + } else item { + ListItem( + leadingContent = { + Icon( + imageVector = if (selectedDistributor == null) { + Icons.Default.RadioButtonChecked + } else { + Icons.Default.RadioButtonUnchecked + }, + contentDescription = null + ) + }, + headlineContent = { + Text(stringResource(R.string.app_settings_unifiedpush_disable)) + }, + modifier = Modifier.clickable { + selectedDistributor = null + }, + colors = ListItemDefaults.colors( + containerColor = Color.Transparent + ) + ) + } + + items(pushDistributors.orEmpty()) { (distributor, name, icon) -> + val isSelf = distributor == context.packageName + val headline = if (isSelf) stringResource(R.string.app_settings_unifiedpush_distributor_fcm) else name ?: distributor + ListItem( + leadingContent = { + Icon( + imageVector = if (selectedDistributor == distributor) { + Icons.Default.RadioButtonChecked + } else { + Icons.Default.RadioButtonUnchecked + }, + contentDescription = null + ) + }, + trailingContent = { + if (isSelf) + Image( + painter = painterResource(R.drawable.product_logomark_cloud_messaging_full_color), + contentDescription = headline, + modifier = Modifier.size(32.dp) + ) + else + icon?.let { + Image( + bitmap = icon.toBitmap().asImageBitmap(), + contentDescription = headline, + modifier = Modifier.size(32.dp) + ) + } + }, + headlineContent = { + Text(headline) + }, + modifier = Modifier.clickable { + selectedDistributor = distributor + }, + colors = ListItemDefaults.colors( + containerColor = Color.Transparent + ) + ) + } + + item { + Text( + text = buildAnnotatedString { + pushStyle( + SpanStyle(color = MaterialTheme.colorScheme.primary, textDecoration = TextDecoration.Underline) + ) + pushLink( + LinkAnnotation.Url( + ExternalUris.Manual.baseUrl.buildUpon() + .appendPath(ExternalUris.Manual.PATH_WEBDAV_PUSH) + .build().toString() + ) + ) + append(stringResource(R.string.app_settings_unifiedpush_encrypted)) + }, + modifier = Modifier.padding(top = 8.dp) + ) + } + } + } + ) +} + +@Composable +@Preview("No distributors installed", "PushDistributorSelectionDialog") +fun PushDistributorSelectionDialog_Preview_NoDistributors() { + PushDistributorSelectionDialog(null, {}, null) { } +} + +@Composable +@Preview("Push disabled", "PushDistributorSelectionDialog") +fun PushDistributorSelectionDialog_Preview_PushDisabled() { + val ctx = LocalContext.current + PushDistributorSelectionDialog( + null, + {}, + listOf( + PushDistributorInfo( + "com.example.distributor1", + "Distributor 1", + AppCompatResources.getDrawable(ctx, R.drawable.ic_launcher_foreground) + ) + ) + ) { } +} + +@Composable +@Preview("Distributor Selected", "PushDistributorSelectionDialog") +fun PushDistributorSelectionDialog_Preview_DistributorSelected() { + val ctx = LocalContext.current + PushDistributorSelectionDialog( + "com.example.distributor1", + {}, + listOf( + PushDistributorInfo( + "com.example.distributor1", + "Distributor 1", + AppCompatResources.getDrawable(ctx, R.drawable.ic_launcher_foreground) + ), + PushDistributorInfo("com.example.distributor2") + ) + ) { } +} + +@Composable +fun AppSettings_Integration( + tasksAppName: String, + tasksAppIcon: Drawable? = null, + pushDistributors: List?, + pushDistributor: String?, + onPushDistributorChange: (String?) -> Unit, + onNavTasksScreen: () -> Unit = {} +) { + SettingsHeader(divider = true) { + Text(stringResource(R.string.app_settings_integration)) + } + Setting( + name = { + Text(stringResource(R.string.app_settings_tasks_provider)) + }, + icon = { + tasksAppIcon?.let { + Image(tasksAppIcon.toBitmap().asImageBitmap(), tasksAppName) + } + }, + summary = tasksAppName, + onClick = onNavTasksScreen + ) + + var showingDistributorDialog by remember { mutableStateOf(false) } + if (showingDistributorDialog) { + PushDistributorSelectionDialog( + pushDistributor = pushDistributor, + onPushDistributorChange = onPushDistributorChange, + pushDistributors = pushDistributors + ) { showingDistributorDialog = false } + } + + val context = LocalContext.current + val pushAppName = if (pushDistributor == context.packageName) { + stringResource(R.string.app_settings_unifiedpush_distributor_fcm) + } else { + pushDistributors?.find { it.packageName == pushDistributor }?.appName + } + Setting( + name = stringResource(R.string.app_settings_unifiedpush), + summary = if (pushDistributor != null) + stringResource(R.string.app_settings_unifiedpush_ready, pushAppName ?: pushDistributor) + else + stringResource(R.string.app_settings_unifiedpush_no_endpoint), + onClick = { showingDistributorDialog = true } + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppTheme.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppTheme.kt new file mode 100644 index 0000000..dfce151 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppTheme.kt @@ -0,0 +1,70 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import androidx.activity.SystemBarStyle +import androidx.activity.compose.LocalActivity +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.LocalView +import androidx.lifecycle.compose.LifecycleResumeEffect +import at.bitfire.davdroid.ui.composable.SafeAndroidUriHandler + +@Composable +fun AppTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + windowInsets: WindowInsets = WindowInsets.safeDrawing, + content: @Composable () -> Unit +) { + val activity = LocalActivity.current + SideEffect { + // If applicable, call Activity.enableEdgeToEdge to enable edge-to-edge layout on Android <15, too. + // When we have moved everything into one Activity with Compose navigation, we can call it there instead. + (activity as? AppCompatActivity)?.enableEdgeToEdge( + navigationBarStyle = SystemBarStyle.auto( + lightScrim = M3ColorScheme.lightScheme.scrim.toArgb(), + darkScrim = M3ColorScheme.darkScheme.scrim.toArgb() + ) { darkTheme } + ) + } + + // Apply SafeAndroidUriHandler to the composition + val uriHandler = SafeAndroidUriHandler(LocalContext.current) + CompositionLocalProvider(LocalUriHandler provides uriHandler) { + MaterialTheme( + colorScheme = if (!darkTheme) + M3ColorScheme.lightScheme + else + M3ColorScheme.darkScheme, + ) { + Box(Modifier.windowInsetsPadding(windowInsets).clipToBounds()) { + content() + } + } + } + + // Track if the app is in the foreground + val view = LocalView.current + LifecycleResumeEffect(view) { + ForegroundTracker.onResume() + onPauseOrDispose { + ForegroundTracker.onPaused() + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/CollectionSelectedUseCase.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/CollectionSelectedUseCase.kt new file mode 100644 index 0000000..631ba4f --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/CollectionSelectedUseCase.kt @@ -0,0 +1,87 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.accounts.Account +import at.bitfire.davdroid.di.ApplicationScope +import at.bitfire.davdroid.di.DefaultDispatcher +import at.bitfire.davdroid.push.PushRegistrationManager +import at.bitfire.davdroid.repository.AccountRepository +import at.bitfire.davdroid.repository.DavCollectionRepository +import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.sync.worker.SyncWorkerManager +import at.bitfire.davdroid.ui.CollectionSelectedUseCase.Companion.DELAY_MS +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +/** + * Performs actions when a collection was (un)selected for synchronization. + * + * @see handleWithDelay + */ +class CollectionSelectedUseCase @Inject constructor( + private val accountRepository: AccountRepository, + @ApplicationScope private val applicationScope: CoroutineScope, + private val collectionRepository: DavCollectionRepository, + @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher, + private val pushRegistrationManager: PushRegistrationManager, + private val serviceRepository: DavServiceRepository, + private val syncWorkerManager: SyncWorkerManager +) { + + /** + * After a delay of [DELAY_MS] ms: + * + * 1. Enqueues a one-time sync for account of the collection. + * 2. Updates push subscriptions for the service of the collection. + * + * Resets delay when called again before delay finishes. + * + * @param collectionId ID of the collection that was (un)selected for synchronization + */ + suspend fun handleWithDelay(collectionId: Long) { + val collection = collectionRepository.getAsync(collectionId) ?: return + val service = serviceRepository.get(collection.serviceId) ?: return + val account = accountRepository.fromName(service.accountName) + + // Atomically cancel, launch and remember delay coroutine of given account + delayJobs.compute(account) { _, previousJob -> + // Stop previous delay, if exists + previousJob?.cancel() + + applicationScope.launch(defaultDispatcher) { + // wait + delay(DELAY_MS) + + // enqueue sync + syncWorkerManager.enqueueOneTimeAllAuthorities(account) + + // update push subscriptions + pushRegistrationManager.update(service.id) + + // remove complete job + delayJobs -= account + } + } + } + + + companion object { + + /** + * Length of delay in milliseconds + */ + const val DELAY_MS = 5000L // 5 seconds + + private val delayJobs: ConcurrentHashMap = ConcurrentHashMap() + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt new file mode 100644 index 0000000..5e913eb --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt @@ -0,0 +1,272 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.accounts.Account +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.provider.CalendarContract +import android.provider.ContactsContract +import android.widget.Toast +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ShareCompat +import androidx.core.content.FileProvider +import androidx.core.content.IntentCompat +import at.bitfire.davdroid.BuildConfig +import at.bitfire.davdroid.R +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.sync.TasksAppManager +import com.google.common.base.Ascii +import dagger.Lazy +import dagger.hilt.android.AndroidEntryPoint +import okhttp3.HttpUrl +import java.io.File +import java.time.Instant +import javax.inject.Inject + +/** + * Debug info activity. Provides verbose information for debugging and support. Should enable users + * to debug problems themselves, but also to send it to the support. + * + * Important use cases to test: + * + * - debug info from App settings / Debug info (should provide debug info) + * - login with some broken login URL (should provide debug info + logs; check logs, too) + * - enable App settings / Verbose logs, then open debug info activity (should provide debug info + logs; check logs, too) + */ +@AndroidEntryPoint +class DebugInfoActivity: AppCompatActivity() { + + @Inject + lateinit var tasksAppManager: Lazy + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val extras = intent.extras + val viewResourceIntent = IntentCompat.getParcelableExtra( + intent, + EXTRA_LOCAL_RESOURCE_URI, + Uri::class.java + )?.let { uri -> + buildViewLocalResourceIntent(uri) + } + + val remoteResource = extras?.getString(EXTRA_REMOTE_RESOURCE) + setContent { + DebugInfoScreen( + account = IntentCompat.getParcelableExtra(intent, EXTRA_ACCOUNT, Account::class.java), + syncDataType = extras?.getString(EXTRA_SYNC_DATA_TYPE), + cause = IntentCompat.getSerializableExtra(intent, EXTRA_CAUSE, Throwable::class.java), + canViewResource = viewResourceIntent != null, + localResource = extras?.getString(EXTRA_LOCAL_RESOURCE_SUMMARY), + remoteResource = remoteResource, + logs = extras?.getString(EXTRA_LOGS), + timestamp = extras?.getLong(EXTRA_TIMESTAMP), + onShareZipFile = ::shareZipFile, + onViewFile = ::viewFile, + onCopyRemoteUrl = { + val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + val clipData = ClipData.newPlainText("Remote resource", remoteResource) + clipboard.setPrimaryClip(clipData) + }, + onViewLocalResource = { viewResource(viewResourceIntent) }, + onNavUp = ::onSupportNavigateUp + ) + } + } + + private fun shareZipFile(file: File) { + shareFile( + file, + "${getString(R.string.app_name)} ${BuildConfig.VERSION_NAME} debug info", + getString(R.string.debug_info_attached), + "*/*", // application/zip won't show all apps that can manage binary files, like ShareViaHttp + ) + } + + /** + * Starts an activity passing sharing intent along + */ + private fun shareFile( + file: File, + subject: String? = null, + text: String? = null, + type: String = "text/plain" + ) { + val uri = FileProvider.getUriForFile( + this, + getString(R.string.authority_debug_provider), + file + ) + ShareCompat.IntentBuilder(this) + .setSubject(subject) + .setText(text) + .setType(type) + .setStream(uri) + .startChooser() + } + + /** + * Starts an activity passing file viewer intent along + */ + private fun viewFile( + file: File, + title: String? = null + ) { + val uri = FileProvider.getUriForFile( + this, + getString(R.string.authority_debug_provider), + file + ) + val intent = Intent(Intent.ACTION_VIEW).apply { + setDataAndType(uri, "text/plain") + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + startActivity(Intent.createChooser(intent, title)) + } + + /** + * Starts activity to view the affected/problematic resource + */ + private fun viewResource(intent: Intent?) = try { + startActivity(intent) + } catch (_: Exception) { + Toast.makeText( + this, + getString(R.string.debug_info_can_not_view_resource), + Toast.LENGTH_LONG + ).show() + } + + /** + * Builds intent to view the problematic local event, task or contact at given Uri. + * + * Note that only OpenTasks is supported as tasks provider. TasksOrg and jtxBoard + * do not support viewing tasks via intent-filter (yet). See also [at.bitfire.davdroid.sync.SyncNotificationManager.getLocalResourceUri] + */ + private fun buildViewLocalResourceIntent(uri: Uri): Intent? { + val activeTasksAuthority = tasksAppManager.get().currentProvider()?.authority + return when (uri.authority) { + ContactsContract.AUTHORITY -> + Intent(Intent.ACTION_VIEW).apply { + setDataAndType(uri, ContactsContract.Contacts.CONTENT_ITEM_TYPE) + } + + CalendarContract.AUTHORITY, activeTasksAuthority -> + Intent(Intent.ACTION_VIEW, uri) + + else -> null + } + } + + /** + * Builder for [DebugInfoActivity] intents + */ + class IntentBuilder(context: Context) { + + companion object { + const val MAX_ELEMENT_SIZE = 800 * 1024 // 800 kB + } + + val intent = Intent(context, DebugInfoActivity::class.java) + .putExtra(EXTRA_TIMESTAMP, Instant.now().epochSecond) + + fun newTask(): IntentBuilder { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + return this + } + + fun withAccount(account: Account?): IntentBuilder { + if (account != null) + intent.putExtra(EXTRA_ACCOUNT, account) + return this + } + + fun withSyncDataType(dataType: SyncDataType?): IntentBuilder { + if (dataType != null) + intent.putExtra(EXTRA_SYNC_DATA_TYPE, dataType.name) + return this + } + + fun withCause(throwable: Throwable?): IntentBuilder { + if (throwable != null) + intent.putExtra(EXTRA_CAUSE, throwable) + return this + } + + fun withLocalResource(dump: String?): IntentBuilder { + if (dump != null) + intent.putExtra( + EXTRA_LOCAL_RESOURCE_SUMMARY, + Ascii.truncate(dump, MAX_ELEMENT_SIZE, "...") + ) + return this + } + + fun withLocalResourceUri(uri: Uri?): IntentBuilder { + if (uri == null) + return this + intent.putExtra(EXTRA_LOCAL_RESOURCE_URI, uri) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + return this + } + + fun withLogs(logs: String?): IntentBuilder { + if (logs != null) + intent.putExtra( + EXTRA_LOGS, + Ascii.truncate(logs, MAX_ELEMENT_SIZE, "...") + ) + return this + } + + fun withRemoteResource(remote: HttpUrl?): IntentBuilder { + if (remote != null) + intent.putExtra(EXTRA_REMOTE_RESOURCE, remote.toString()) + return this + } + + + fun build() = intent + + fun share() = intent.apply { + action = Intent.ACTION_SEND + } + + } + + companion object { + /** [android.accounts.Account] (as [android.os.Parcelable]) related to problem */ + private const val EXTRA_ACCOUNT = "account" + + /** sync data type related to problem */ + private const val EXTRA_SYNC_DATA_TYPE = "syncDataType" + + /** serialized [Throwable] that causes the problem */ + private const val EXTRA_CAUSE = "cause" + + /** Summary (dump of [at.bitfire.davdroid.resource.LocalResource] properties) of local resource related to the problem (plain-text [String]) */ + internal const val EXTRA_LOCAL_RESOURCE_SUMMARY = "localResourceSummary" + + /** [Uri] of local resource related to the problem (as [android.os.Parcelable]) */ + internal const val EXTRA_LOCAL_RESOURCE_URI = "localResourceId" + + /** logs related to the problem (plain-text [String]) */ + private const val EXTRA_LOGS = "logs" + + /** URL of remote resource related to the problem (plain-text [String]) */ + private const val EXTRA_REMOTE_RESOURCE = "remoteResource" + + /** A timestamp of the moment at which the error took place. */ + private const val EXTRA_TIMESTAMP = "timestamp" + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoGenerator.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoGenerator.kt new file mode 100644 index 0000000..be8a45f --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoGenerator.kt @@ -0,0 +1,561 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.accounts.Account +import android.accounts.AccountManager +import android.app.usage.UsageStatsManager +import android.content.ContentUris +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.net.ConnectivityManager +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.os.LocaleList +import android.os.PowerManager +import android.os.StatFs +import android.provider.CalendarContract +import android.provider.ContactsContract +import android.text.format.DateUtils +import android.text.format.Formatter +import androidx.annotation.WorkerThread +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService +import androidx.core.content.pm.PackageInfoCompat +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkQuery +import at.bitfire.dav4jvm.exception.DavException +import at.bitfire.davdroid.BuildConfig +import at.bitfire.davdroid.R +import at.bitfire.davdroid.TextTable +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.repository.AccountRepository +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.sync.account.InvalidAccountException +import at.bitfire.davdroid.sync.adapter.SyncFrameworkIntegration +import at.bitfire.davdroid.sync.worker.BaseSyncWorker +import at.bitfire.ical4android.TaskProvider +import at.techbee.jtx.JtxContract +import dagger.hilt.android.qualifiers.ApplicationContext +import org.dmfs.tasks.contract.TaskContract +import java.io.PrintWriter +import java.io.Writer +import java.time.Instant +import java.time.ZoneId +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter +import java.util.TimeZone +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject +import kotlin.use +import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter as asCalendarSyncAdapter +import at.bitfire.vcard4android.Utils.asSyncAdapter as asContactsSyncAdapter +import at.techbee.jtx.JtxContract.asSyncAdapter as asJtxSyncAdapter + +@WorkerThread +class DebugInfoGenerator @Inject constructor( + private val accountRepository: AccountRepository, + private val accountSettingsFactory: AccountSettings.Factory, + @ApplicationContext private val context: Context, + private val db: AppDatabase, + private val logger: Logger, + private val settings: SettingsManager, + private val syncFramework: SyncFrameworkIntegration +) { + + operator fun invoke( + syncAccount: Account?, + syncDataType: String?, + cause: Throwable?, + localResource: String?, + remoteResource: String?, + timestamp: Long?, + writer: PrintWriter + ) { + writer.println("--- BEGIN DEBUG INFO ---") + writer.println() + + // begin with a timestamp to know when the error occurred + if (timestamp != null) { + val instant = Instant.ofEpochSecond(timestamp) + writer.println("NOTIFICATION TIME") + val iso = DateTimeFormatter.ISO_OFFSET_DATE_TIME + writer.println("Local time: ${instant.atZone(ZoneId.systemDefault()).format(iso)}") + writer.println("UTC: ${instant.atZone(ZoneOffset.UTC).format(iso)}") + writer.println() + } + + // continue with most specific information + if (syncAccount != null || syncDataType != null) { + writer.append("SYNCHRONIZATION INFO\n") + if (syncAccount != null) + writer.append("Account: $syncAccount\n") + if (syncDataType != null) + writer.append("SyncDataType: $syncDataType\n") + writer.append("\n") + } + + if (cause != null) { + writer.println("EXCEPTION") + cause.printStackTrace(writer) + writer.println() + } + + // exception details + if (cause is DavException) { + cause.requestExcerpt?.let { request -> + writer.append("HTTP REQUEST\n") + writer.append(request) + writer.append("\n\n") + } + cause.responseExcerpt?.let { response -> + writer.append("HTTP RESPONSE\n") + writer.append(response) + writer.append("\n\n") + } + } + + if (localResource != null) + writer.append("LOCAL RESOURCE\n$localResource\n\n") + + if (remoteResource != null) + writer.append("REMOTE RESOURCE\n$remoteResource\n\n") + + // software info + try { + writer.append("SOFTWARE INFORMATION\n") + val table = TextTable("Package", "Version", "Code", "Installer", "Notes") + val pm = context.packageManager + + val packageNames = mutableSetOf( // we always want info about these packages: + BuildConfig.APPLICATION_ID, // DAVx5 + TaskProvider.ProviderName.JtxBoard.packageName, // jtx Board + TaskProvider.ProviderName.OpenTasks.packageName, // OpenTasks + TaskProvider.ProviderName.TasksOrg.packageName // tasks.org + ) + // ... and info about contact and calendar provider + for (authority in arrayOf(ContactsContract.AUTHORITY, CalendarContract.AUTHORITY)) + pm.resolveContentProvider(authority, 0)?.let { packageNames += it.packageName } + // ... and info about contact, calendar, task-editing apps + val dataUris = arrayOf( + ContactsContract.Contacts.CONTENT_URI, + CalendarContract.Events.CONTENT_URI, + TaskContract.Tasks.getContentUri(TaskProvider.ProviderName.OpenTasks.authority), + TaskContract.Tasks.getContentUri(TaskProvider.ProviderName.TasksOrg.authority) + ) + for (uri in dataUris) { + val viewIntent = Intent(Intent.ACTION_VIEW, ContentUris.withAppendedId(uri, /* some random ID */ 1)) + for (info in pm.queryIntentActivities(viewIntent, 0)) + packageNames += info.activityInfo.packageName + } + + for (packageName in packageNames) + try { + val info = pm.getPackageInfo(packageName, 0) + val appInfo = info.applicationInfo + val notes = mutableListOf() + if (appInfo?.enabled == false) + notes += "disabled" + if (appInfo?.flags?.and(ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) + notes += "on external storage" + table.addLine( + info.packageName, info.versionName, PackageInfoCompat.getLongVersionCode(info), + pm.getInstallerPackageName(info.packageName) ?: '—', notes.joinToString(", ") + ) + } catch (_: PackageManager.NameNotFoundException) { + } + writer.append(table.toString()) + } catch (e: Exception) { + logger.log(Level.SEVERE, "Couldn't get software information", e) + } + + // system info + val locales: Any = LocaleList.getAdjustedDefault() + writer.append( + "\n\nSYSTEM INFORMATION\n\n" + + "Android version: ${Build.VERSION.RELEASE} (${Build.DISPLAY})\n" + + "Device: ${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE})\n\n" + + "Locale(s): $locales\n" + + "Time zone: ${TimeZone.getDefault().id}\n" + ) + val filesPath = Environment.getDataDirectory() + val statFs = StatFs(filesPath.path) + writer.append("Internal memory ($filesPath): ") + .append(Formatter.formatFileSize(context, statFs.availableBytes)) + .append(" free of ") + .append(Formatter.formatFileSize(context, statFs.totalBytes)) + .append("\n\n") + + // power saving + if (Build.VERSION.SDK_INT >= 28) + context.getSystemService()?.let { statsManager -> + val bucket = statsManager.appStandbyBucket + writer + .append("App standby bucket: ") + .append( + when { + bucket <= 5 -> "exempted (very good)" + bucket <= UsageStatsManager.STANDBY_BUCKET_ACTIVE -> "active (good)" + bucket <= UsageStatsManager.STANDBY_BUCKET_WORKING_SET -> "working set (bad: job restrictions apply)" + bucket <= UsageStatsManager.STANDBY_BUCKET_FREQUENT -> "frequent (bad: job restrictions apply)" + bucket <= UsageStatsManager.STANDBY_BUCKET_RARE -> "rare (very bad: job and network restrictions apply)" + bucket <= UsageStatsManager.STANDBY_BUCKET_RESTRICTED -> "restricted (very bad: job and network restrictions apply)" + else -> "$bucket" + } + ) + writer.append('\n') + } + context.getSystemService()?.let { powerManager -> + writer.append("App exempted from power saving: ") + .append(if (powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)) "yes (good)" else "no (bad)") + .append('\n') + .append("System in power-save mode: ") + .append(if (powerManager.isPowerSaveMode) "yes (restrictions apply!)" else "no") + .append('\n') + } + // system-wide sync + writer.append("System-wide synchronization: ") + .append(if (syncFramework.getMasterSyncAutomatically()) "automatically" else "manually") + .append("\n\n") + + // connectivity + context.getSystemService()?.let { connectivityManager -> + writer.append("\n\nCONNECTIVITY\n\n") + val activeNetwork = connectivityManager.activeNetwork + connectivityManager.allNetworks.sortedByDescending { it == activeNetwork }.forEach { network -> + val properties = connectivityManager.getLinkProperties(network) + connectivityManager.getNetworkCapabilities(network)?.let { capabilities -> + writer.append(if (network == activeNetwork) " ☒ " else " ☐ ") + .append(properties?.interfaceName ?: "?") + .append("\n - ") + .append(capabilities.toString().replace('&', ' ')) + .append('\n') + } + if (properties != null) { + writer.append(" - DNS: ") + .append(properties.dnsServers.joinToString(", ") { it.hostAddress }) + if (Build.VERSION.SDK_INT >= 28 && properties.isPrivateDnsActive) + writer.append(" (private mode)") + writer.append('\n') + } + } + writer.append('\n') + + connectivityManager.defaultProxy?.let { proxy -> + writer.append("System default proxy: ${proxy.host}:${proxy.port}\n") + } + writer.append("Data saver: ").append( + when (connectivityManager.restrictBackgroundStatus) { + ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED -> "enabled" + ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED -> "whitelisted" + ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED -> "disabled" + else -> connectivityManager.restrictBackgroundStatus.toString() + } + ).append('\n') + writer.append('\n') + } + + writer.append("\n\nCONFIGURATION\n") + // notifications + val nm = NotificationManagerCompat.from(context) + writer.append("\nNotifications") + if (!nm.areNotificationsEnabled()) + writer.append(" (blocked!)") + writer.append(":\n") + if (Build.VERSION.SDK_INT >= 26) { + val channelsWithoutGroup = nm.notificationChannels.toMutableSet() + for (group in nm.notificationChannelGroups) { + writer.append(" - ${group.id}") + if (Build.VERSION.SDK_INT >= 28) + writer.append(" isBlocked=${group.isBlocked}") + writer.append('\n') + for (channel in group.channels) { + writer.append(" * ${channel.id}: importance=${channel.importance}\n") + channelsWithoutGroup -= channel + } + } + for (channel in channelsWithoutGroup) + writer.append(" - ${channel.id}: importance=${channel.importance}\n") + } + writer.append('\n') + // permissions + writer.append("Permissions:\n") + val ownPkgInfo = context.packageManager.getPackageInfo(BuildConfig.APPLICATION_ID, PackageManager.GET_PERMISSIONS) + for (permission in ownPkgInfo.requestedPermissions.orEmpty()) { + val shortPermission = permission.removePrefix("android.permission.") + writer.append(" - $shortPermission: ") + .append( + if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) + "granted" + else + "denied" + ) + .append('\n') + } + writer.append('\n') + + // accounts + writer.append("\nACCOUNTS") + val accountManager = AccountManager.get(context) + val accounts = accountRepository.getAll() + for (account in accounts) + dumpAccount(account, writer) + + val addressBookAccounts = accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)).toMutableList() + if (addressBookAccounts.isNotEmpty()) { + writer.append("ADDRESS BOOK ACCOUNTS\n\n") + for (account in addressBookAccounts) + dumpAddressBookAccount(account, accountManager, writer) + } + + // non-sync workers + writer.append("OTHER WORKERS\n") + dumpOtherWorkers(accounts, writer) + + // database dump + writer.append("\n\n\nDATABASE DUMP\n\n") + db.dump(writer, arrayOf("webdav_document")) + + // app settings + writer.append("\nAPP SETTINGS\n\n") + settings.dump(writer) + + writer.append("--- END DEBUG INFO ---\n") + } + + /** + * Appends relevant android account information the given writer. + */ + private fun dumpAccount(account: Account, writer: Writer) { + writer.append("\n\n - Account: ${account.name}\n") + val accountSettings = accountSettingsFactory.create(account) + + writer.append(dumpAndroidAccount(account, AccountDumpInfo.caldavAccount(account))) + try { + val credentials = accountSettings.credentials() + val authStr = mutableListOf() + if (credentials.username != null) + authStr += "user name" + if (credentials.password != null) + authStr += "password" + if (credentials.certificateAlias != null) + authStr += "client certificate" + credentials.authState?.let { authState -> + authStr += "OAuth [${authState.authorizationServiceConfiguration?.authorizationEndpoint}]" + } + if (authStr.isNotEmpty()) + writer .append(" Authentication: ") + .append(authStr.joinToString(", ")) + .append("\n") + + writer.append(" WiFi only: ${accountSettings.getSyncWifiOnly()}") + accountSettings.getSyncWifiOnlySSIDs()?.let { ssids -> + writer.append(", SSIDs: ${ssids.joinToString(", ")}") + } + writer.append( + "\n Contact group method: ${accountSettings.getGroupMethod()}\n" + + " Time range (past days): ${accountSettings.getTimeRangePastDays()}\n" + + " Default alarm (min before): ${accountSettings.getDefaultAlarm()}\n" + + " Manage calendar colors: ${accountSettings.getManageCalendarColors()}\n" + + " Use event colors: ${accountSettings.getEventColors()}\n" + ) + + writer.append("\nSync workers:\n") + dumpSyncWorkers(account, writer) + writer.append("\n") + } catch (e: InvalidAccountException) { + writer.append("$e\n") + } + writer.append('\n') + } + + /** + * Appends relevant address book type android account information to the given writer. + */ + private fun dumpAddressBookAccount(account: Account, accountManager: AccountManager, writer: Writer) { + writer.append(" * Address book: ${account.name}\n") + val table = dumpAndroidAccount(account, AccountDumpInfo.addressBookAccount(account)) + writer.append(TextTable.indent(table, 4)) + .append("Collection ID: ${accountManager.getUserData(account, LocalAddressBook.USER_DATA_COLLECTION_ID)}\n") + .append(" Read-only: ${accountManager.getUserData(account, LocalAddressBook.USER_DATA_READ_ONLY) ?: 0}\n\n") + } + + /** + * Retrieves specified information from an android account. + */ + private fun dumpAndroidAccount(account: Account, infos: Iterable): String { + val table = TextTable("Authority", "isSyncable", "syncsOnContentChange", "Entries") + for (info in infos) { + var nrEntries = "—" + if (info.countUri != null) + try { + context.contentResolver.acquireContentProviderClient(info.authority)?.use { client -> + client.query(info.countUri, null, null, null, null)?.use { cursor -> + nrEntries = "${cursor.count} ${info.countStr}" + } + } + } catch (e: Exception) { + nrEntries = e.toString() + } + table.addLine( + info.authority, + syncFramework.isSyncable(account, info.authority), + syncFramework.syncsOnContentChange(account, info.authority), + nrEntries + ) + } + return table.toString() + } + + /** + * Generates a table to display worker statuses. + * + * By default, the table provides the following columns: + * Tags, State, Next run, Retries, Generation, Periodicity + * + * If more tables are desired, they can be added using [extraColumns]. + * + * The key of the map is the position of the column relative to the default ones, so for example, + * if a column is going to be added between "State" and "Next run", the index should be `2`. + * + * The value the map is a pair whose first element is the column name, and the second one is + * the generator of the value. + * The generator will be called on every worker info found after running the query, and should + * return a String that will be placed in the cell. + * + * @param query The query to use for fetching the workers. + * @param extraColumns Defaults to an empty map, pass extra columns to be added to the table. + * @param filter Allows filtering the results given by the [query]. Will exclude all [WorkInfo] + * whose filter result is `false`. Defaults to all `true` (do not filter). + */ + private fun workersInfoTable( + query: WorkQuery, + extraColumns: Map String>> = emptyMap(), + filter: (WorkInfo) -> Boolean = { true } + ): String { + val columnNames = mutableListOf("Tags", "State", "Next run", "Retries", "Generation", "Periodicity") + for ((index, column) in extraColumns) { + val (columnName) = column + columnNames.add(index, columnName) + } + + val table = TextTable(columnNames) + val wm = WorkManager.getInstance(context) + val workInfos = wm.getWorkInfos(query).get().filter(filter) + for (workInfo in workInfos) { + val line = mutableListOf( + workInfo.tags.map { it.replace("\\bat\\.bitfire\\.davdroid\\.".toRegex(), ".") }, + "${workInfo.state} (${workInfo.stopReason})", + workInfo.nextScheduleTimeMillis.let { nextRun -> + when (nextRun) { + Long.MAX_VALUE -> "—" + else -> DateUtils.getRelativeTimeSpanString(nextRun) + } + }, + workInfo.runAttemptCount, + workInfo.generation, + workInfo.periodicityInfo?.let { periodicity -> + "every ${periodicity.repeatIntervalMillis/60000} min" + } ?: "not periodic" + ) + + for ((index, column) in extraColumns) { + val (_, transformer) = column + val value = transformer(workInfo) + line.add(index, value) + } + + table.addLine(line) + } + return table.toString() + } + + /** + * Gets sync workers info. + * + * Note: WorkManager does not return worker names when queried, so we create them and ask + * whether they exist one by one. + */ + private fun dumpSyncWorkers(account: Account, writer: Writer) { + writer.append(workersInfoTable( + WorkQuery.Builder.fromTags( + SyncDataType.entries.map { BaseSyncWorker.commonTag(account, it) } + ).build(), + mapOf( + 1 to ("Data Type" to { workInfo: WorkInfo -> + // See: BaseSyncWorker.commonTag + // "sync-$dataType ${account.type}/${account.name}" + workInfo.tags + // Search for the first tag that starts with sync- + .first { it.startsWith("sync-") } + // Get everything before the space (get rid of the account) + .substringBefore(' ') + // Remove the "sync-" prefix + .removePrefix("sync-") + }) + ) + )) + } + + /** + * Gets account-independent workers info. This is done by querying all the workers, and + * filtering the ones that depend on an account (the opposite of [dumpSyncWorkers]). + * + * Note: WorkManager does not return worker names when queried, so we create them and ask + * whether they exist one by one. + * + * @param accounts The list of accounts in the system. This is used for filtering account-dependent + * workers. + */ + private fun dumpOtherWorkers(accounts: Array, writer: Writer) { + val syncWorkersTags = accounts.flatMap { account -> + SyncDataType.entries.map { BaseSyncWorker.commonTag(account, it) } + } + + writer.append(workersInfoTable( + // Fetch all workers + WorkQuery.Builder.fromStates(WorkInfo.State.entries).build(), + filter = { it.tags.all { tag -> !syncWorkersTags.contains(tag) } } + )) + } + + + data class AccountDumpInfo( + val account: Account, + val authority: String, + val countUri: Uri?, + val countStr: String?, + ) { + + companion object { + + internal fun caldavAccount(account: Account) = listOf( + AccountDumpInfo(account, CalendarContract.AUTHORITY, CalendarContract.Events.CONTENT_URI.asCalendarSyncAdapter(account), "event(s)"), + AccountDumpInfo(account, TaskProvider.ProviderName.JtxBoard.authority, JtxContract.JtxICalObject.CONTENT_URI.asJtxSyncAdapter(account), "jtx Board ICalObject(s)"), + AccountDumpInfo(account, TaskProvider.ProviderName.OpenTasks.authority, TaskContract.Tasks.getContentUri( + TaskProvider.ProviderName.OpenTasks.authority).asCalendarSyncAdapter(account), "OpenTasks task(s)"), + AccountDumpInfo(account, TaskProvider.ProviderName.TasksOrg.authority, TaskContract.Tasks.getContentUri( + TaskProvider.ProviderName.TasksOrg.authority).asCalendarSyncAdapter(account), "tasks.org task(s)"), + AccountDumpInfo(account, ContactsContract.AUTHORITY, ContactsContract.RawContacts.CONTENT_URI.asContactsSyncAdapter(account), "wrongly assigned raw contact(s)") + ) + + internal fun addressBookAccount(account: Account) = listOf( + AccountDumpInfo(account, ContactsContract.AUTHORITY, ContactsContract.RawContacts.CONTENT_URI.asContactsSyncAdapter(account), "raw contact(s)") + ) + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt new file mode 100644 index 0000000..a1f7e3f --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt @@ -0,0 +1,196 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.accounts.Account +import android.content.Context +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.bitfire.davdroid.log.LogFileHandler +import at.bitfire.davdroid.ui.DebugInfoModel.Companion.FILE_DEBUG_INFO +import at.bitfire.davdroid.ui.DebugInfoModel.Companion.FILE_LOGS +import com.google.common.io.ByteStreams +import com.google.common.io.Files +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.File +import java.io.IOException +import java.util.logging.Level +import java.util.logging.Logger +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +@HiltViewModel(assistedFactory = DebugInfoModel.Factory::class) +class DebugInfoModel @AssistedInject constructor( + @Assisted private val details: DebugInfoDetails, + @ApplicationContext val context: Context, + private val debugInfoGenerator: DebugInfoGenerator, + private val logger: Logger +) : ViewModel() { + + data class DebugInfoDetails( + val account: Account?, + val syncDataType: String?, + val cause: Throwable?, + val localResource: String?, + val remoteResource: String?, + val logs: String?, + val timestamp: Long? + ) + + @AssistedFactory + interface Factory { + fun createWithDetails(details: DebugInfoDetails): DebugInfoModel + } + + data class UiState( + val cause: Throwable? = null, + val localResource: String? = null, + val remoteResource: String? = null, + val logFile: File? = null, + val debugInfo: File? = null, + val zipFile: File? = null, + val zipInProgress: Boolean = false, + val error: String? = null + ) + + var uiState by mutableStateOf(UiState()) + private set + + fun resetError() { + uiState = uiState.copy(error = null) + } + + fun resetZipFile() { + uiState = uiState.copy(zipFile = null) + } + + init { + // create debug info directory + val debugDir = LogFileHandler.debugDir(context) ?: throw IOException("Couldn't create debug info directory") + + viewModelScope.launch(Dispatchers.Default) { + // create log file from EXTRA_LOGS or log file + if (details.logs != null) { + val file = File(debugDir, FILE_LOGS) + if (!file.exists() || file.canWrite()) { + file.printWriter().use { writer -> + writer.write(details.logs) + } + uiState = uiState.copy(logFile = file) + } else + logger.warning("Can't write logs to $file") + } else LogFileHandler.getDebugLogFile(context)?.let { debugLogFile -> + if (debugLogFile.isFile && debugLogFile.canRead()) + uiState = uiState.copy(logFile = debugLogFile) + } + + uiState = uiState.copy( + cause = details.cause, + localResource = details.localResource, + remoteResource = details.remoteResource + ) + generateDebugInfo( + syncAccount = details.account, + syncDataType = details.syncDataType, + cause = details.cause, + localResource = details.localResource, + remoteResource = details.remoteResource, + timestamp = details.timestamp + ) + } + } + + /** + * Creates debug info and saves it to [FILE_DEBUG_INFO] in [LogFileHandler.debugDir] + * + * Note: Part of this method and all of it's helpers (listed below) should probably be extracted in the future + */ + private fun generateDebugInfo( + syncAccount: Account?, + syncDataType: String?, + cause: Throwable?, + localResource: String?, + remoteResource: String?, + timestamp: Long? + ) { + val debugInfoFile = File(LogFileHandler.debugDir(context), FILE_DEBUG_INFO) + debugInfoFile.printWriter().use { writer -> + debugInfoGenerator( + syncAccount = syncAccount, + syncDataType = syncDataType, + cause = cause, + localResource = localResource, + remoteResource = remoteResource, + timestamp = timestamp, + writer = writer + ) + } + uiState = uiState.copy(debugInfo = debugInfoFile) + } + + /** + * Creates the ZIP file containing both [FILE_DEBUG_INFO] and [FILE_LOGS]. + * + * Note: Part of this method should probably be extracted to a more suitable location + */ + fun generateZip() { + try { + uiState = uiState.copy(zipInProgress = true) + + val file = File(LogFileHandler.debugDir(context), "davx5-debug.zip") + logger.fine("Writing debug info to ${file.absolutePath}") + ZipOutputStream(file.outputStream().buffered()).use { zip -> + zip.setLevel(9) + uiState.debugInfo?.let { debugInfo -> + zip.putNextEntry(ZipEntry("debug-info.txt")) + Files.copy(debugInfo, zip) + zip.closeEntry() + } + + val logs = uiState.logFile + if (logs != null) { + // verbose logs available + zip.putNextEntry(ZipEntry(logs.name)) + Files.copy(logs, zip) + zip.closeEntry() + } else { + // logcat (short logs) + try { + Runtime.getRuntime().exec("logcat -d").also { logcat -> + zip.putNextEntry(ZipEntry("logcat.txt")) + ByteStreams.copy(logcat.inputStream, zip) + } + } catch (e: Exception) { + logger.log(Level.SEVERE, "Couldn't attach logcat", e) + } + } + } + + // success, show ZIP file + uiState = uiState.copy(zipFile = file) + } catch (e: Exception) { + logger.log(Level.SEVERE, "Couldn't generate debug info ZIP", e) + uiState = uiState.copy(error = e.localizedMessage) + } finally { + uiState = uiState.copy(zipInProgress = false) + } + } + + + companion object { + private const val FILE_DEBUG_INFO = "debug-info.txt" + private const val FILE_LOGS = "logs.txt" + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoScreen.kt new file mode 100644 index 0000000..ad6c2cb --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoScreen.kt @@ -0,0 +1,361 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.accounts.Account +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.rounded.Adb +import androidx.compose.material.icons.rounded.BugReport +import androidx.compose.material.icons.rounded.Info +import androidx.compose.material.icons.rounded.PrivacyTip +import androidx.compose.material.icons.rounded.Share +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.BiasAlignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import at.bitfire.dav4jvm.exception.DavException +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.composable.CardWithImage +import at.bitfire.davdroid.ui.composable.ProgressBar +import java.io.File +import java.io.IOError +import java.io.IOException + +@Composable +fun DebugInfoScreen( + account: Account?, + syncDataType: String?, + cause: Throwable?, + localResource: String?, + canViewResource: Boolean, + remoteResource: String?, + logs: String?, + timestamp: Long?, + onShareZipFile: (File) -> Unit, + onViewFile: (File) -> Unit, + onCopyRemoteUrl: () -> Unit, + onViewLocalResource: () -> Unit, + onNavUp: () -> Unit +) { + val model: DebugInfoModel = hiltViewModel( + creationCallback = { factory: DebugInfoModel.Factory -> + factory.createWithDetails(DebugInfoModel.DebugInfoDetails( + account = account, + syncDataType = syncDataType, + cause = cause, + localResource = localResource, + remoteResource = remoteResource, + logs = logs, + timestamp = timestamp + )) + } + ) + + val uiState = model.uiState + val debugInfo = uiState.debugInfo + val zipInProgress = uiState.zipInProgress + val zipFile = uiState.zipFile + val logFile = uiState.logFile + val error = uiState.error + + // Share zip file card, once successfully generated + LaunchedEffect(zipFile) { + zipFile?.let { file -> + onShareZipFile(file) + model.resetZipFile() + } + } + + DebugInfoScreen( + error = error, + onResetError = model::resetError, + showDebugInfo = debugInfo != null, + zipProgress = zipInProgress, + showModelCause = cause != null, + modelCauseTitle = when (cause) { + is HttpException -> stringResource(if (cause.isServerError) R.string.debug_info_server_error else R.string.debug_info_http_error) + is DavException -> stringResource(R.string.debug_info_webdav_error) + is IOException, is IOError -> stringResource(R.string.debug_info_io_error) + else -> cause?.let { it::class.java.simpleName } + } ?: "", + modelCauseSubtitle = cause?.localizedMessage, + modelCauseMessage = stringResource( + if (cause is HttpException) + when { + cause.statusCode == 403 -> R.string.debug_info_http_403_description + cause.statusCode == 404 -> R.string.debug_info_http_404_description + cause.statusCode == 405 -> R.string.debug_info_http_405_description + cause.isServerError -> R.string.debug_info_http_5xx_description + else -> R.string.debug_info_unexpected_error + } + else + R.string.debug_info_unexpected_error + ), + localResource = localResource, + canViewResource = canViewResource, + remoteResource = remoteResource, + hasLogFile = logFile != null, + onShareZip = { model.generateZip() }, + onViewLogsFile = { logFile?.let { onViewFile(it) } }, + onViewDebugFile = { debugInfo?.let { onViewFile(it) } }, + onCopyRemoteUrl = onCopyRemoteUrl, + onViewLocalResource = onViewLocalResource, + onNavUp = onNavUp + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DebugInfoScreen( + error: String?, + onResetError: () -> Unit = {}, + showDebugInfo: Boolean, + zipProgress: Boolean, + showModelCause: Boolean, + modelCauseTitle: String, + modelCauseSubtitle: String?, + modelCauseMessage: String?, + localResource: String?, + canViewResource: Boolean, + remoteResource: String?, + hasLogFile: Boolean, + onShareZip: () -> Unit = {}, + onViewLogsFile: () -> Unit = {}, + onViewDebugFile: () -> Unit = {}, + onCopyRemoteUrl: () -> Unit = {}, + onViewLocalResource: () -> Unit = {}, + onNavUp: () -> Unit = {} +) { + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(error) { + error?.let { + snackbarHostState.showSnackbar( + message = it, + duration = SnackbarDuration.Long + ) + onResetError() + } + } + + val uriHandler = LocalUriHandler.current + AppTheme { + Scaffold( + floatingActionButton = { + if (showDebugInfo && !zipProgress) { + FloatingActionButton( + onClick = onShareZip, + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) { + Icon(Icons.Rounded.Share, stringResource(R.string.share)) + } + } + }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.debug_info_title)) }, + navigationIcon = { + IconButton(onClick = onNavUp) { + Icon( + Icons.AutoMirrored.Default.ArrowBack, + stringResource(R.string.navigate_up) + ) + } + } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + ) { + if (!showDebugInfo || zipProgress) + ProgressBar() + + CardWithImage( + title = stringResource(R.string.debug_info_privacy_warning_title), + message = stringResource(R.string.debug_info_privacy_warning_description), + icon = Icons.Rounded.PrivacyTip, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp) + ) + + if (showModelCause) { + CardWithImage( + title = modelCauseTitle, + subtitle = modelCauseSubtitle, + message = modelCauseMessage, + icon = Icons.Rounded.Info, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp) + ) + } + + if (showDebugInfo) + CardWithImage( + image = painterResource(R.drawable.undraw_server_down), + imageAlignment = BiasAlignment(0f, .7f), + title = stringResource(R.string.debug_info_title), + subtitle = stringResource(R.string.debug_info_subtitle), + icon = Icons.Rounded.BugReport, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp) + ) { + OutlinedButton( + onClick = onViewDebugFile, + modifier = Modifier.padding(bottom = 4.dp) + ) { + Text( + stringResource(R.string.debug_info_view_details) + ) + } + } + + if (localResource != null || remoteResource != null) + CardWithImage( + title = stringResource(R.string.debug_info_involved_caption), + subtitle = stringResource(R.string.debug_info_involved_subtitle), + icon = Icons.Rounded.Adb, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp) + ) { + remoteResource?.let { remoteUrl -> + Text( + text = stringResource(R.string.debug_info_involved_remote), + style = MaterialTheme.typography.bodyLarge + ) + SelectionContainer { + Text( + text = remoteUrl, + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = FontFamily.Monospace + ), + modifier = Modifier.padding(bottom = 8.dp) + ) + } + OutlinedButton( + onClick = onCopyRemoteUrl, + modifier = Modifier.padding(end = 8.dp) + ) { + Text(stringResource(R.string.debug_info_copy_remote_url)) + } + } + localResource?.let { + Text( + text = stringResource(R.string.debug_info_involved_local), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 12.dp) + ) + Text( + text = it, + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = FontFamily.Monospace + ), + modifier = Modifier.padding(bottom = 8.dp) + ) + } + if (canViewResource) + OutlinedButton( + onClick = { onViewLocalResource() }, + modifier = Modifier.padding(bottom = 4.dp) + ) { + Text( + stringResource(R.string.debug_info_view_local_resource) + ) + } + } + + if (hasLogFile) { + CardWithImage( + title = stringResource(R.string.debug_info_logs_caption), + subtitle = stringResource(R.string.debug_info_logs_subtitle), + icon = Icons.Rounded.BugReport, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp) + ) { + OutlinedButton( + onClick = onViewLogsFile, + modifier = Modifier.padding(bottom = 4.dp) + ) { + Text( + stringResource(R.string.debug_info_logs_view) + ) + } + } + } + + if (showDebugInfo) { + CardWithImage( + title = stringResource(R.string.debug_info_archive_caption), + subtitle = stringResource(R.string.debug_info_archive_subtitle), + message = stringResource(R.string.debug_info_archive_text), + icon = Icons.Rounded.Share, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp) + ) { + OutlinedButton( + onClick = onShareZip, + enabled = !zipProgress, + modifier = Modifier.padding(bottom = 4.dp) + ) { + Text( + stringResource(R.string.debug_info_archive_share) + ) + } + } + } + + // space for the FAB + Spacer(modifier = Modifier.height(64.dp)) + } + } + } +} + +@Composable +@Preview +fun DebugInfoScreen_Preview() { + DebugInfoScreen( + error = "Some error", + showDebugInfo = true, + zipProgress = false, + showModelCause = true, + modelCauseTitle = "ModelCauseTitle", + modelCauseSubtitle = "ModelCauseSubtitle", + modelCauseMessage = "ModelCauseMessage", + localResource = "local-resource-string", + canViewResource = true, + remoteResource = "remote-resource-string", + hasLogFile = true + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/ExternalUris.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/ExternalUris.kt new file mode 100644 index 0000000..9669bca --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/ExternalUris.kt @@ -0,0 +1,95 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.net.Uri +import androidx.core.net.toUri +import at.bitfire.davdroid.BuildConfig + +/** + * Links to to external pages (Web site, manual, social media etc.) + */ +object ExternalUris { + + /** + * URLs of the DAVx5 homepage + */ + @Suppress("unused") // build variants + object Homepage { + + val baseUrl + get() = "https://www.davx5.com".toUri() + + const val PATH_FAQ = "faq" + const val PATH_FAQ_SYNC_NOT_RUN = "synchronization-is-not-run-as-expected" + const val PATH_FAQ_LOCATION_PERMISSION = "wifi-ssid-restriction-location-permission" + const val PATH_OPEN_SOURCE = "donate" + const val PATH_PRIVACY = "privacy" + const val PATH_TESTED_SERVICES = "tested-with" + + const val PATH_ORGANIZATIONS = "organizations" + const val PATH_ORGANIZATIONS_MANAGED = "managed-davx5" + const val PATH_ORGANIZATIONS_TRY_IT = "try-it-for-free" + } + + + /** + * URLs of the DAVx5 Manual + */ + object Manual { + + val baseUrl + get() = "https://manual.davx5.com".toUri() + + const val PATH_ACCOUNTS_COLLECTIONS = "accounts_collections.html" + const val FRAGMENT_SERVICE_DISCOVERY = "how-does-service-discovery-work" + + const val PATH_INTRODUCTION = "introduction.html" + const val FRAGMENT_AUTHENTICATION_METHODS = "authentication-methods" + + const val PATH_SETTINGS = "settings.html" + const val FRAGMENT_APP_SETTINGS = "app-wide-settings" + const val FRAGMENT_ACCOUNT_SETTINGS = "account-settings" + + const val PATH_WEBDAV_PUSH = "webdav_push.html" + const val PATH_WEBDAV_MOUNTS = "webdav_mounts.html" + + } + + + /** + * URLs of DAVx5 social sites + */ + object Social { + + val discussionsUrl + get() = "https://github.com/bitfireAT/davx5-ose/discussions".toUri() + + const val fediverseHandle = "@davx5app@fosstodon.org" + val fediverseUrl + get() = "https://fosstodon.org/@davx5app".toUri() + + } + + + // helpers + + /** + * Appends query parameters for anonymized usage statistics (app ID, version). + * Can be used by the called Website to get an idea of which versions etc. are currently used. + * + * @param context optional info about from where the URL was opened (like a specific Activity) + */ + fun Uri.Builder.withStatParams(context: String? = null): Uri.Builder { + appendQueryParameter("pk_campaign", BuildConfig.APPLICATION_ID) + appendQueryParameter("app-version", BuildConfig.VERSION_NAME) + + if (context != null) + appendQueryParameter("pk_kwd", context) + + return this + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/ForegroundTracker.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/ForegroundTracker.kt new file mode 100644 index 0000000..ad2c770 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/ForegroundTracker.kt @@ -0,0 +1,40 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * Used to track whether the app is in foreground (visible to user) or not. + */ +object ForegroundTracker { + + /** + * Whether the app is in the foreground. + * Used by cert4android to known when it's possible to show the certificate trust decision dialog. + */ + private val _inForeground = MutableStateFlow(false) + + /** + * Whether the app is in foreground or not. + */ + val inForeground = _inForeground.asStateFlow() + + /** + * Called when the app is resumed (at [androidx.lifecycle.Lifecycle.Event.ON_RESUME]) + */ + fun onResume() { + _inForeground.value = true + } + + /** + * Called when the app is paused (at [androidx.lifecycle.Lifecycle.Event.ON_PAUSE]) + */ + fun onPaused() { + _inForeground.value = false + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/NotificationRegistry.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/NotificationRegistry.kt new file mode 100644 index 0000000..97d55b1 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/NotificationRegistry.kt @@ -0,0 +1,182 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.Manifest.permission.POST_NOTIFICATIONS +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationChannelGroup +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.TaskStackBuilder +import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService +import at.bitfire.davdroid.R +import dagger.hilt.android.qualifiers.ApplicationContext +import java.util.logging.Logger +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Manages notifications and channels. + * + * As soon as the singleton is created, it will create the necessary notification channels on Android 8+. + * + * Don't use the notification IDs for posting notifications directly – always get the instance of this + * class and use [notifyIfPossible]. + */ +@Singleton +class NotificationRegistry @Inject constructor( + @ApplicationContext val context: Context, + private val logger: Logger +) { + + companion object { + + // notification IDs + const val NOTIFY_VERBOSE_LOGGING = 1 + const val NOTIFY_REFRESH_COLLECTIONS = 2 + const val NOTIFY_DATABASE_CORRUPTED = 4 + const val NOTIFY_SYNC_ERROR = 10 + const val NOTIFY_INVALID_RESOURCE = 11 + const val NOTIFY_SYNC_EXPEDITED = 14 + const val NOTIFY_TASKS_PROVIDER_TOO_OLD = 20 + const val NOTIFY_PERMISSIONS = 21 + + @Suppress("unused") // for build variants + const val NOTIFY_LICENSE = 100 + + } + + + // notification channel names, accessible only when instance (and thus the channels) has been created + + /** + * For notifications that don't fit into another channel. + */ + val CHANNEL_GENERAL = "general" + + /** + * For debugging notifications. High priority because a debugging session + * has been activated by the user and they should know all the time. + * + * Currently only used for the "verbose logging active" notification. + */ + val CHANNEL_DEBUG = "debug" + + /** + * Used to show progress, like that a service detection or WebDAV file access is running. + */ + val CHANNEL_STATUS = "status" + + /** + * For sync-related notifications. Use the appropriate sub-channels for different types of sync problems. + */ + val CHANNEL_SYNC = "sync" + + /** + * For sync errors that are not IO errors. Shown as normal priority. + */ + val CHANNEL_SYNC_ERRORS = "syncProblems" + + /** + * For sync warnings. Shown as low priority. + */ + val CHANNEL_SYNC_WARNINGS = "syncWarnings" + + /** + * For sync IO errors. Shown as minimal priority because they might go away automatically, for instance + * when the connection is working again. + */ + val CHANNEL_SYNC_IO_ERRORS = "syncIoErrors" + + + init { + createChannels() + } + + + /** + * Creates notification channels for Android 8+. + */ + private fun createChannels() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val nm = context.getSystemService()!! + + nm.createNotificationChannelGroup(NotificationChannelGroup(CHANNEL_SYNC, context.getString(R.string.notification_channel_sync))) + + nm.createNotificationChannels(listOf( + NotificationChannel(CHANNEL_DEBUG, context.getString(R.string.notification_channel_debugging), NotificationManager.IMPORTANCE_HIGH), + NotificationChannel(CHANNEL_GENERAL, context.getString(R.string.notification_channel_general), NotificationManager.IMPORTANCE_DEFAULT), + NotificationChannel(CHANNEL_STATUS, context.getString(R.string.notification_channel_status), NotificationManager.IMPORTANCE_LOW), + + NotificationChannel(CHANNEL_SYNC_ERRORS, context.getString(R.string.notification_channel_sync_errors), NotificationManager.IMPORTANCE_DEFAULT).apply { + description = context.getString(R.string.notification_channel_sync_errors_desc) + group = CHANNEL_SYNC + }, + NotificationChannel(CHANNEL_SYNC_WARNINGS, context.getString(R.string.notification_channel_sync_warnings), NotificationManager.IMPORTANCE_LOW).apply { + description = context.getString(R.string.notification_channel_sync_warnings_desc) + group = CHANNEL_SYNC + }, + NotificationChannel(CHANNEL_SYNC_IO_ERRORS, context.getString(R.string.notification_channel_sync_io_errors), NotificationManager.IMPORTANCE_MIN).apply { + description = context.getString(R.string.notification_channel_sync_io_errors_desc) + group = CHANNEL_SYNC + } + )) + } + } + + /** + * Shows a notification, if possible. + * + * If the notification is not possible because the user didn't give notification permissions, it will be ignored. + * + * The notification should usually be created using [androidx.core.app.NotificationCompat.Builder]. + * + * @param id Notification ID + * @param tag Notification tag + * @param builder Callback that creates the notification; will only be called if we have the notification permission. + */ + fun notifyIfPossible(id: Int, tag: String? = null, builder: () -> Notification) { + if (ContextCompat.checkSelfPermission(context, POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { + // we have the permission, show notification + val nm = NotificationManagerCompat.from(context) + nm.notify(tag, id, builder()) + } else + logger.warning("Notifications disabled, not showing notification $id") + } + + // specific common notifications + + /** + * Shows a notification about missing permissions. + * + * @param intent will be set as content Intent; if null, an Intent to launch PermissionsActivity will be used + */ + fun notifyPermissions(intent: Intent? = null) { + notifyIfPossible(NOTIFY_PERMISSIONS) { + val contentIntent = intent ?: Intent(context, PermissionsActivity::class.java) + NotificationCompat.Builder(context, CHANNEL_SYNC_ERRORS) + .setSmallIcon(R.drawable.ic_sync_problem_notify) + .setContentTitle(context.getString(R.string.sync_error_permissions)) + .setContentText(context.getString(R.string.sync_error_permissions_text)) + .setContentIntent( + TaskStackBuilder.create(context) + .addNextIntentWithParentStack(contentIntent) + .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + ) + .setCategory(NotificationCompat.CATEGORY_ERROR) + .setAutoCancel(true) + .build() + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/OseAccountsDrawerHandler.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/OseAccountsDrawerHandler.kt new file mode 100644 index 0000000..3a2ad77 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/OseAccountsDrawerHandler.kt @@ -0,0 +1,148 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.HelpCenter +import androidx.compose.material.icons.filled.CloudOff +import androidx.compose.material.icons.filled.CorporateFare +import androidx.compose.material.icons.filled.Forum +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.VolunteerActivism +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.ExternalUris.Homepage +import at.bitfire.davdroid.ui.ExternalUris.Social +import at.bitfire.davdroid.ui.ExternalUris.withStatParams +import javax.inject.Inject + +open class OseAccountsDrawerHandler @Inject constructor(): AccountsDrawerHandler() { + + @Composable + override fun MenuEntries( + snackbarHostState: SnackbarHostState + ) { + val uriHandler = LocalUriHandler.current + + // Most important entries + ImportantEntries(snackbarHostState) + + // News + MenuHeading(R.string.navigation_drawer_news_updates) + MenuEntry( + icon = painterResource(R.drawable.mastodon), + title = Social.fediverseHandle, + onClick = { + uriHandler.openUri(Social.fediverseUrl.toString()) + } + ) + + // Tools + Tools() + + // Support the project + MenuHeading(R.string.navigation_drawer_support_project) + Contribute(onContribute = { + uriHandler.openUri( + Homepage.baseUrl.buildUpon() + .appendPath(Homepage.PATH_OPEN_SOURCE) + .withStatParams(javaClass.simpleName) + .build().toString() + ) + }) + MenuEntry( + icon = Icons.Default.Forum, + title = stringResource(R.string.navigation_drawer_community), + onClick = { + uriHandler.openUri(Social.discussionsUrl.toString()) + } + ) + + + // External links + MenuHeading(R.string.navigation_drawer_external_links) + MenuEntry( + icon = Icons.Default.Home, + title = stringResource(R.string.navigation_drawer_website), + onClick = { + uriHandler.openUri( + Homepage.baseUrl + .buildUpon() + .withStatParams(javaClass.simpleName) + .build().toString()) + } + ) + MenuEntry( + icon = Icons.Default.Info, + title = stringResource(R.string.navigation_drawer_manual), + onClick = { + uriHandler.openUri(ExternalUris.Manual.baseUrl.toString()) + } + ) + MenuEntry( + icon = Icons.AutoMirrored.Default.HelpCenter, + title = stringResource(R.string.navigation_drawer_faq), + onClick = { + uriHandler.openUri( + Homepage.baseUrl.buildUpon() + .appendPath(Homepage.PATH_FAQ) + .withStatParams(javaClass.simpleName) + .build().toString() + ) + } + ) + MenuEntry( + icon = Icons.Default.CorporateFare, + title = stringResource(R.string.navigation_drawer_managed), + onClick = { + uriHandler.openUri( + Homepage.baseUrl.buildUpon() + .appendPath(Homepage.PATH_ORGANIZATIONS) + .appendPath(Homepage.PATH_ORGANIZATIONS_MANAGED) + .withStatParams(javaClass.simpleName) + .build().toString() + ) + } + ) + MenuEntry( + icon = Icons.Default.CloudOff, + title = stringResource(R.string.navigation_drawer_privacy_policy), + onClick = { + uriHandler.openUri( + Homepage.baseUrl.buildUpon() + .appendPath(Homepage.PATH_PRIVACY) + .withStatParams(javaClass.simpleName) + .build().toString() + ) + } + ) + } + + @Composable + @Preview + fun MenuEntries_Standard_Preview() { + Column { + MenuEntries(SnackbarHostState()) + } + } + + + @Composable + open fun Contribute(onContribute: () -> Unit) { + MenuEntry( + icon = Icons.Default.VolunteerActivism, + title = stringResource(R.string.navigation_drawer_contribute), + onClick = onContribute + ) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsActivity.kt new file mode 100644 index 0000000..6f3d5ab --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsActivity.kt @@ -0,0 +1,25 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class PermissionsActivity: AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + PermissionsScreen( + onNavigateUp = ::onSupportNavigateUp + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsModel.kt new file mode 100644 index 0000000..a593af5 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsModel.kt @@ -0,0 +1,57 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.content.Context +import android.os.Build +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.bitfire.davdroid.util.packageChangedFlow +import at.bitfire.ical4android.TaskProvider +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class PermissionsModel @Inject constructor( + @ApplicationContext val context: Context, +): ViewModel() { + + var needKeepPermissions by mutableStateOf(null) + private set + var openTasksAvailable by mutableStateOf(false) + private set + var tasksOrgAvailable by mutableStateOf(false) + private set + var jtxAvailable by mutableStateOf(false) + private set + + init { + viewModelScope.launch { + // check permissions when a package (e.g. tasks app) is (un)installed + packageChangedFlow(context).collect { + checkPermissions() + } + } + } + + fun checkPermissions() { + val pm = context.packageManager + + // auto-reset permissions + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + needKeepPermissions = pm.isAutoRevokeWhitelisted + } + + openTasksAvailable = pm.resolveContentProvider(TaskProvider.ProviderName.OpenTasks.authority, 0) != null + tasksOrgAvailable = pm.resolveContentProvider(TaskProvider.ProviderName.TasksOrg.authority, 0) != null + jtxAvailable = pm.resolveContentProvider(TaskProvider.ProviderName.JtxBoard.authority, 0) != null + } + +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsScreen.kt new file mode 100644 index 0000000..7d1592b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsScreen.kt @@ -0,0 +1,257 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.Manifest +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.widget.Toast +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +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.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.viewmodel.compose.viewModel +import at.bitfire.davdroid.BuildConfig +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.composable.CardWithImage +import at.bitfire.davdroid.ui.composable.PermissionSwitchRow +import at.bitfire.davdroid.util.PermissionUtils +import at.bitfire.ical4android.TaskProvider +import java.util.logging.Level +import java.util.logging.Logger + +/** + * Used when "Manage permissions" is selected in the settings. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PermissionsScreen( + onNavigateUp: () -> Unit +) { + AppTheme { + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.app_settings_security_app_permissions)) }, + navigationIcon = { + IconButton( + onClick = onNavigateUp + ) { + Icon(Icons.AutoMirrored.Default.ArrowBack, stringResource(R.string.navigate_up)) + } + } + ) + } + ) { paddingValues -> + PermissionsScreen(modifier = Modifier.padding(paddingValues)) + } + } +} + +/** + * Used by [PermissionsScreen] and directly embedded in [at.bitfire.davdroid.ui.intro.PermissionsIntroPage]. + */ +@Composable +fun PermissionsScreen( + modifier: Modifier = Modifier, + model: PermissionsModel = viewModel() +) { + // check permissions when the lifecycle owner (for instance Activity) is resumed + val lifecycle = LocalLifecycleOwner.current.lifecycle + DisposableEffect(lifecycle) { + val observer = object: DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + model.checkPermissions() + } + } + + lifecycle.addObserver(observer) + onDispose { + lifecycle.removeObserver(observer) + } + } + + val context = LocalContext.current + PermissionsScreen( + keepPermissions = model.needKeepPermissions, + onKeepPermissionsRequested = { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val intent = Intent( + Intent.ACTION_AUTO_REVOKE_PERMISSIONS, + Uri.fromParts("package", BuildConfig.APPLICATION_ID, null) + ) + try { + context.startActivity(intent) + Toast.makeText(context, R.string.permissions_autoreset_instruction, Toast.LENGTH_LONG).show() + } catch (e: Exception) { + Logger.getGlobal().log(Level.WARNING, "Couldn't start Keep Permissions activity", e) + } + } + }, + openTasksAvailable = model.openTasksAvailable, + tasksOrgAvailable = model.tasksOrgAvailable, + jtxAvailable = model.jtxAvailable, + modifier = modifier + ) +} + + +@Composable +fun PermissionsScreen( + keepPermissions: Boolean?, + onKeepPermissionsRequested: () -> Unit, + openTasksAvailable: Boolean?, + tasksOrgAvailable: Boolean?, + jtxAvailable: Boolean?, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + CardWithImage( + title = stringResource(R.string.permissions_title), + message = stringResource( + R.string.permissions_text, + stringResource(R.string.app_name) + ), + image = painterResource(R.drawable.intro_permissions), + modifier = Modifier.padding(8.dp) + ) { + if (keepPermissions != null) { + PermissionSwitchRow( + text = stringResource(R.string.permissions_autoreset_title), + summaryWhenGranted = stringResource(R.string.permissions_autoreset_status_on), + summaryWhenNotGranted = stringResource(R.string.permissions_autoreset_status_off), + allPermissionsGranted = keepPermissions, + onLaunchRequest = onKeepPermissionsRequested, + modifier = Modifier.padding(vertical = 4.dp) + ) + } + + val allPermissions = mutableListOf() + allPermissions.addAll(PermissionUtils.CONTACT_PERMISSIONS) + allPermissions.addAll(PermissionUtils.CALENDAR_PERMISSIONS) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + allPermissions += Manifest.permission.POST_NOTIFICATIONS + if (openTasksAvailable == true) + allPermissions.addAll(TaskProvider.PERMISSIONS_OPENTASKS) + if (tasksOrgAvailable == true) + allPermissions.addAll(TaskProvider.PERMISSIONS_TASKS_ORG) + if (jtxAvailable == true) + allPermissions.addAll(TaskProvider.PERMISSIONS_JTX) + PermissionSwitchRow( + text = stringResource(R.string.permissions_all_title), + permissions = allPermissions, + summaryWhenGranted = stringResource(R.string.permissions_all_status_on), + summaryWhenNotGranted = stringResource(R.string.permissions_all_status_off), + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(vertical = 4.dp) + ) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + PermissionSwitchRow( + text = stringResource(R.string.permissions_notification_title), + summaryWhenGranted = stringResource(R.string.permissions_notification_status_on), + summaryWhenNotGranted = stringResource(R.string.permissions_notification_status_off), + permissions = listOf(Manifest.permission.POST_NOTIFICATIONS), + modifier = Modifier.padding(vertical = 4.dp) + ) + + PermissionSwitchRow( + text = stringResource(R.string.permissions_calendar_title), + summaryWhenGranted = stringResource(R.string.permissions_calendar_status_on), + summaryWhenNotGranted = stringResource(R.string.permissions_calendar_status_off), + permissions = PermissionUtils.CALENDAR_PERMISSIONS.toList(), + modifier = Modifier.padding(vertical = 4.dp) + ) + PermissionSwitchRow( + text = stringResource(R.string.permissions_contacts_title), + summaryWhenGranted = stringResource(R.string.permissions_contacts_status_on), + summaryWhenNotGranted = stringResource(R.string.permissions_contacts_status_off), + permissions = PermissionUtils.CONTACT_PERMISSIONS.toList(), + modifier = Modifier.padding(vertical = 4.dp) + ) + + if (jtxAvailable == true) + PermissionSwitchRow( + text = stringResource(R.string.permissions_jtx_title), + summaryWhenGranted = stringResource(R.string.permissions_tasks_status_on), + summaryWhenNotGranted = stringResource(R.string.permissions_tasks_status_off), + permissions = TaskProvider.PERMISSIONS_JTX.toList(), + modifier = Modifier.padding(vertical = 4.dp) + ) + if (openTasksAvailable == true) + PermissionSwitchRow( + text = stringResource(R.string.permissions_opentasks_title), + summaryWhenGranted = stringResource(R.string.permissions_tasks_status_on), + summaryWhenNotGranted = stringResource(R.string.permissions_tasks_status_off), + permissions = TaskProvider.PERMISSIONS_OPENTASKS.toList(), + modifier = Modifier.padding(vertical = 4.dp) + ) + if (tasksOrgAvailable == true) + PermissionSwitchRow( + text = stringResource(R.string.permissions_tasksorg_title), + summaryWhenGranted = stringResource(R.string.permissions_tasks_status_on), + summaryWhenNotGranted = stringResource(R.string.permissions_tasks_status_off), + permissions = TaskProvider.PERMISSIONS_TASKS_ORG.toList(), + modifier = Modifier.padding(vertical = 4.dp) + ) + + Text( + text = stringResource(R.string.permissions_app_settings_hint), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 24.dp) + ) + + val context = LocalContext.current + OutlinedButton( + modifier = Modifier.padding(vertical = 8.dp), + onClick = { PermissionUtils.showAppSettings(context) } + ) { + Text(stringResource(R.string.permissions_app_settings)) + } + } + } +} + +@Composable +@Preview +fun PermissionsCard_Preview() { + AppTheme { + PermissionsScreen( + keepPermissions = true, + onKeepPermissionsRequested = {}, + openTasksAvailable = true, + tasksOrgAvailable = true, + jtxAvailable = true + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksActivity.kt new file mode 100644 index 0000000..62685a1 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksActivity.kt @@ -0,0 +1,24 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class TasksActivity: AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + TasksScreen( + onNavUp = ::onSupportNavigateUp + ) + } + } +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksModel.kt new file mode 100644 index 0000000..b67d08e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksModel.kt @@ -0,0 +1,84 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.content.Context +import android.content.pm.PackageManager +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.bitfire.davdroid.di.DefaultDispatcher +import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.sync.TasksAppManager +import at.bitfire.davdroid.util.packageChangedFlow +import at.bitfire.ical4android.TaskProvider +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class TasksModel @Inject constructor( + @ApplicationContext val context: Context, + @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher, + private val settings: SettingsManager, + private val tasksAppManager: TasksAppManager +) : ViewModel() { + + companion object { + + /** + * Whether this fragment (which asks for OpenTasks installation) shall be shown. + * If this setting is true or null/not set, the notice shall be shown. Only if this + * setting is false, the notice shall not be shown. + */ + const val HINT_OPENTASKS_NOT_INSTALLED = "hint_OpenTasksNotInstalled" + + } + + val showAgain = settings.getBooleanFlow(HINT_OPENTASKS_NOT_INSTALLED, true) + fun setShowAgain(showAgain: Boolean) { + if (showAgain) + settings.remove(HINT_OPENTASKS_NOT_INSTALLED) + else + settings.putBoolean(HINT_OPENTASKS_NOT_INSTALLED, false) + } + + val currentProvider = tasksAppManager.currentProviderFlow() + val jtxSelected = currentProvider.map { it == TaskProvider.ProviderName.JtxBoard } + val tasksOrgSelected = currentProvider.map { it == TaskProvider.ProviderName.TasksOrg } + val openTasksSelected = currentProvider.map { it == TaskProvider.ProviderName.OpenTasks } + + var jtxInstalled by mutableStateOf(false) + var tasksOrgInstalled by mutableStateOf(false) + var openTasksInstalled by mutableStateOf(false) + + init { + viewModelScope.launch { + packageChangedFlow(context).collect { + jtxInstalled = isInstalled(TaskProvider.ProviderName.JtxBoard.packageName) + tasksOrgInstalled = isInstalled(TaskProvider.ProviderName.TasksOrg.packageName) + openTasksInstalled = isInstalled(TaskProvider.ProviderName.OpenTasks.packageName) + } + } + } + + private fun isInstalled(packageName: String): Boolean = + try { + context.packageManager.getPackageInfo(packageName, 0) + true + } catch (_: PackageManager.NameNotFoundException) { + false + } + + fun selectProvider(provider: TaskProvider.ProviderName) = viewModelScope.launch(defaultDispatcher) { + tasksAppManager.selectProvider(provider) + } + +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksScreen.kt new file mode 100644 index 0000000..e854f7b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksScreen.kt @@ -0,0 +1,257 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.BiasAlignment +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 +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import androidx.core.text.HtmlCompat +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import at.bitfire.davdroid.BuildConfig +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString +import at.bitfire.davdroid.ui.composable.CardWithImage +import at.bitfire.davdroid.ui.composable.RadioWithSwitch +import at.bitfire.ical4android.TaskProvider +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TasksScreen(onNavUp: () -> Unit) { + AppTheme { + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.intro_tasks_title)) }, + navigationIcon = { + IconButton( + onClick = onNavUp + ) { + Icon(Icons.AutoMirrored.Default.ArrowBack, stringResource(R.string.navigate_up)) + } + } + ) + } + ) { paddingValues -> + Box(Modifier.padding(paddingValues)) { + TasksCard() + } + } + } +} + +@Composable +fun TasksCard( + model: TasksModel = viewModel() +) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + + val currentProvider by model.currentProvider.collectAsStateWithLifecycle(null) + + val jtxInstalled = model.jtxInstalled + val jtxSelected by model.jtxSelected.collectAsStateWithLifecycle(false) + + val tasksOrgInstalled = model.tasksOrgInstalled + val tasksOrgSelected by model.tasksOrgSelected.collectAsStateWithLifecycle(false) + + val openTasksInstalled = model.openTasksInstalled + val openTasksSelected by model.openTasksSelected.collectAsStateWithLifecycle(false) + + val showAgain by model.showAgain.collectAsStateWithLifecycle(true) + + TasksCard( + jtxSelected = jtxSelected, + jtxInstalled = jtxInstalled, + tasksOrgSelected = tasksOrgSelected, + tasksOrgInstalled = tasksOrgInstalled, + openTasksSelected = openTasksSelected, + openTasksInstalled = openTasksInstalled, + showAgain = showAgain, + onSetShowAgain = model::setShowAgain, + onProviderSelected = { provider -> + if (currentProvider != provider) + model.selectProvider(provider) + }, + installApp = { packageName -> + val uri = ("market://details?id=$packageName&referrer=" + + Uri.encode("utm_source=" + BuildConfig.APPLICATION_ID)).toUri() + val intent = Intent(Intent.ACTION_VIEW, uri) + if (intent.resolveActivity(context.packageManager) != null) + context.startActivity(intent) + else + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = context.getString(R.string.intro_tasks_no_app_store), + duration = SnackbarDuration.Long + ) + } + } + ) +} + +@Composable +fun TasksCard( + jtxSelected: Boolean, + jtxInstalled: Boolean, + tasksOrgSelected: Boolean, + tasksOrgInstalled: Boolean, + openTasksSelected: Boolean, + openTasksInstalled: Boolean, + onProviderSelected: (TaskProvider.ProviderName) -> Unit = {}, + installApp: (String) -> Unit = {}, + showAgain: Boolean, + onSetShowAgain: (Boolean) -> Unit = {} +) { + Column( + modifier = Modifier + .fillMaxHeight() + .verticalScroll(rememberScrollState()) + ) { + CardWithImage( + image = painterResource(R.drawable.intro_tasks), + imageAlignment = BiasAlignment(0f, .1f), + title = stringResource(R.string.intro_tasks_title), + message = stringResource(R.string.intro_tasks_text1), + modifier = Modifier.padding(8.dp) + ) { + RadioWithSwitch( + title = stringResource(R.string.intro_tasks_jtx), + summary = { + Text(stringResource(R.string.intro_tasks_jtx_info)) + }, + isSelected = jtxSelected, + isToggled = jtxInstalled, + enabled = jtxInstalled, + onSelected = { onProviderSelected(TaskProvider.ProviderName.JtxBoard) }, + onToggled = { toggled -> + if (toggled) installApp(TaskProvider.ProviderName.JtxBoard.packageName) + }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + ) + + RadioWithSwitch( + title = stringResource(R.string.intro_tasks_tasks_org), + summary = { + val summary = HtmlCompat.fromHtml( + stringResource(R.string.intro_tasks_tasks_org_info), + HtmlCompat.FROM_HTML_MODE_COMPACT + ).toAnnotatedString() + Text(summary) + }, + isSelected = tasksOrgSelected, + isToggled = tasksOrgInstalled, + enabled = tasksOrgInstalled, + onSelected = { onProviderSelected(TaskProvider.ProviderName.TasksOrg) }, + onToggled = { toggled -> + if (toggled) installApp(TaskProvider.ProviderName.TasksOrg.packageName) + }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp) + ) + + RadioWithSwitch( + title = stringResource(R.string.intro_tasks_opentasks), + summary = { + Text(stringResource(R.string.intro_tasks_opentasks_info)) + }, + isSelected = openTasksSelected, + isToggled = openTasksInstalled, + enabled = openTasksInstalled, + onSelected = { onProviderSelected(TaskProvider.ProviderName.OpenTasks) }, + onToggled = { toggled -> + if (toggled) installApp(TaskProvider.ProviderName.OpenTasks.packageName) + }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp) + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp) + ) { + Checkbox( + checked = !showAgain, + onCheckedChange = { onSetShowAgain(!it) } + ) + Text( + text = stringResource(R.string.intro_tasks_dont_show), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .fillMaxWidth() + .clickable { onSetShowAgain(!showAgain) } + ) + } + } + + Text( + text = stringResource( + R.string.intro_leave_unchecked, + stringResource(R.string.app_settings_reset_hints) + ), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp) + ) + } +} + +@Preview(showBackground = true, showSystemUi = true) +@Composable +fun TasksCard_Preview() { + AppTheme { + TasksCard( + jtxSelected = true, + jtxInstalled = true, + tasksOrgSelected = false, + tasksOrgInstalled = false, + openTasksSelected = false, + openTasksInstalled = false, + showAgain = true + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/UiUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/UiUtils.kt new file mode 100644 index 0000000..4b1038a --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/UiUtils.kt @@ -0,0 +1,141 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.content.Context +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager +import android.graphics.Typeface +import android.graphics.drawable.AdaptiveIconDrawable +import android.graphics.drawable.Icon +import android.os.Build +import android.text.Spanned +import android.text.style.StyleSpan +import android.text.style.URLSpan +import androidx.annotation.DrawableRes +import androidx.appcompat.app.AppCompatDelegate +import androidx.browser.customtabs.CustomTabsClient +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.core.content.getSystemService +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.drawable.toBitmap +import androidx.core.text.getSpans +import at.bitfire.davdroid.R +import at.bitfire.davdroid.settings.Settings +import at.bitfire.davdroid.settings.SettingsManager +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent +import java.util.logging.Level +import java.util.logging.Logger + +object UiUtils { + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface UiUtilsEntryPoint { + fun logger(): Logger + fun settingsManager(): SettingsManager + } + + const val SHORTCUT_SYNC_ALL = "syncAllAccounts" + + + @Composable + fun adaptiveIconPainterResource(@DrawableRes id: Int): Painter { + val context = LocalContext.current + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val adaptiveIcon = ResourcesCompat.getDrawable(context.resources, id, null) as? AdaptiveIconDrawable + if (adaptiveIcon != null) + BitmapPainter(adaptiveIcon.toBitmap().asImageBitmap()) + else + painterResource(id) + } else + painterResource(id) + } + + fun haveCustomTabs(context: Context) = CustomTabsClient.getPackageName(context, null, false) != null + + fun updateTheme(context: Context) { + val settings = EntryPointAccessors.fromApplication(context, UiUtilsEntryPoint::class.java).settingsManager() + val mode = settings.getIntOrNull(Settings.PREFERRED_THEME) ?: Settings.PREFERRED_THEME_DEFAULT + AppCompatDelegate.setDefaultNightMode(mode) + } + + fun updateShortcuts(context: Context) { + if (Build.VERSION.SDK_INT >= 25) + context.getSystemService()?.let { shortcutManager -> + try { + shortcutManager.dynamicShortcuts = listOf( + ShortcutInfo.Builder(context, SHORTCUT_SYNC_ALL) + .setIcon(Icon.createWithResource(context, R.drawable.ic_sync_shortcut)) + .setShortLabel(context.getString(R.string.accounts_sync_all)) + .setIntent(Intent(Intent.ACTION_SYNC, null, context, AccountsActivity::class.java)) + .build() + ) + } catch(e: Exception) { + val logger = EntryPointAccessors.fromApplication(context, UiUtilsEntryPoint::class.java).logger() + logger.log(Level.WARNING, "Couldn't update dynamic shortcut(s)", e) + } + } + } + + @Composable + fun Spanned.toAnnotatedString() = buildAnnotatedString { + val spanned = this@toAnnotatedString + append(spanned.toString()) + + for (span in getSpans(0, spanned.length)) { + val start = getSpanStart(span) + val end = getSpanEnd(span) + when (span) { + is StyleSpan -> + when (span.style) { + Typeface.BOLD -> addStyle( + SpanStyle(fontWeight = FontWeight.Bold), + start = start, end = end + ) + Typeface.ITALIC -> addStyle( + SpanStyle(fontStyle = FontStyle.Italic), + start = start, end = end + ) + } + is URLSpan -> { + addLink( + LinkAnnotation.Url(span.url), + start = start, end = end + ) + addStyle( + SpanStyle( + textDecoration = TextDecoration.Underline, + color = MaterialTheme.colorScheme.primary + ), + start = start, end = end + ) + } + else -> { + val context = LocalContext.current + val logger = EntryPointAccessors.fromApplication(context, UiUtilsEntryPoint::class.java).logger() + logger.warning("Ignoring unknown span type ${span.javaClass.name}") + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt new file mode 100644 index 0000000..2ba4659 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt @@ -0,0 +1,81 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import AccountScreen +import android.accounts.Account +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.IntentCompat +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.AccountsActivity +import dagger.hilt.android.AndroidEntryPoint +import java.util.logging.Logger +import javax.inject.Inject + +@AndroidEntryPoint +class AccountActivity : AppCompatActivity() { + + @Inject + lateinit var logger: Logger + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val account = + IntentCompat.getParcelableExtra(intent, EXTRA_ACCOUNT, Account::class.java) ?: + intent.getStringExtra(EXTRA_ACCOUNT)?.let { Account(it, getString(R.string.account_type)) } + + // If account is not passed, log warning and redirect to accounts overview + if (account == null) { + logger.warning("AccountActivity requires EXTRA_ACCOUNT") + + // Redirect to accounts overview activity + val intent = Intent(this, AccountsActivity::class.java).apply { + // Create a new root activity, do not allow going back. + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + } + startActivity(intent) + finish() + return + } + + setContent { + AccountScreen( + account = account, + onAccountSettings = { + val intent = Intent(this, AccountSettingsActivity::class.java) + intent.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account) + startActivity(intent, null) + }, + onCreateAddressBook = { + val intent = Intent(this, CreateAddressBookActivity::class.java) + intent.putExtra(CreateAddressBookActivity.EXTRA_ACCOUNT, account) + startActivity(intent) + }, + onCreateCalendar = { + val intent = Intent(this, CreateCalendarActivity::class.java) + intent.putExtra(CreateCalendarActivity.EXTRA_ACCOUNT, account) + startActivity(intent) + }, + onCollectionDetails = { collection -> + val intent = Intent(this, CollectionActivity::class.java) + intent.putExtra(CollectionActivity.EXTRA_ACCOUNT, account) + intent.putExtra(CollectionActivity.EXTRA_COLLECTION_ID, collection.id) + startActivity(intent, null) + }, + onNavUp = ::onSupportNavigateUp, + onFinish = ::finish + ) + } + } + + companion object { + const val EXTRA_ACCOUNT = "account" + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountProgress.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountProgress.kt new file mode 100644 index 0000000..504383e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountProgress.kt @@ -0,0 +1,32 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue + +/** Tri-state enum to represent active / pending / idle status */ +enum class AccountProgress { + Active, // syncing or refreshing + Pending, // sync pending + Idle; // idle + + @Composable + fun rememberAlpha(): Float { + val progressAlpha by animateFloatAsState( + when (this@AccountProgress) { + Active -> 1f + Pending -> 0.5f + Idle -> 0f + }, + label = "progressAlpha", + animationSpec = tween(500) + ) + return progressAlpha + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountProgressUseCase.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountProgressUseCase.kt new file mode 100644 index 0000000..240ce36 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountProgressUseCase.kt @@ -0,0 +1,86 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import android.accounts.Account +import android.content.Context +import androidx.work.WorkInfo +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.sync.adapter.SyncFrameworkIntegration +import at.bitfire.davdroid.sync.worker.OneTimeSyncWorker +import at.bitfire.davdroid.sync.worker.SyncWorkerManager +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import javax.inject.Inject + +class AccountProgressUseCase @Inject constructor( + @ApplicationContext val context: Context, + private val syncFramework: SyncFrameworkIntegration, + private val syncWorkerManager: SyncWorkerManager +) { + + /** + * Returns the current sync state of the account. + */ + operator fun invoke( + account: Account, + serviceFlow: Flow, + dataTypes: Iterable + ): Flow { + val serviceRefreshing = isServiceRefreshing(serviceFlow) + val syncEnqueued = isSyncEnqueued(account, dataTypes) + val syncPending = syncFramework.isSyncPending(account, dataTypes) + val syncRunning = isSyncRunning(account, dataTypes) + + return combine( + serviceRefreshing, + syncEnqueued, + syncPending, + syncRunning + ) { refreshing, enqueued, pending, syncing -> + when { + refreshing || syncing -> AccountProgress.Active + enqueued || pending -> AccountProgress.Pending + else -> AccountProgress.Idle + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun isServiceRefreshing(serviceFlow: Flow): Flow = + serviceFlow.flatMapLatest { service -> + if (service == null) + flowOf(false) + else + RefreshCollectionsWorker.existsFlow(context, RefreshCollectionsWorker.workerName(service.id)) + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun isSyncEnqueued(account: Account, dataTypes: Iterable): Flow = + syncWorkerManager.hasAnyFlow( + workStates = listOf(WorkInfo.State.ENQUEUED), + account = account, + dataTypes = dataTypes, + whichTag = { _, authority -> + // we are only interested in enqueued OneTimeSyncWorkers because there's always an enqueued PeriodicSyncWorker + OneTimeSyncWorker.workerName(account, authority) + } + ) + + @OptIn(ExperimentalCoroutinesApi::class) + fun isSyncRunning(account: Account, dataTypes: Iterable): Flow = + syncWorkerManager.hasAnyFlow( + workStates = listOf(WorkInfo.State.RUNNING), + account = account, + dataTypes = dataTypes + ) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreen.kt new file mode 100644 index 0000000..d3f795d --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreen.kt @@ -0,0 +1,774 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +import android.Manifest +import android.accounts.Account +import android.content.Intent +import android.widget.Toast +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.CalendarToday +import androidx.compose.material.icons.filled.CreateNewFolder +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.DriveFileRenameOutline +import androidx.compose.material.icons.filled.Group +import androidx.compose.material.icons.filled.Link +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Sync +import androidx.compose.material.icons.filled.SyncProblem +import androidx.compose.material.icons.outlined.RuleFolder +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalMinimumInteractiveComponentSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.PrimaryTabRow +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.ui.AppTheme +import at.bitfire.davdroid.ui.PermissionsActivity +import at.bitfire.davdroid.ui.account.AccountProgress +import at.bitfire.davdroid.ui.account.AccountScreenModel +import at.bitfire.davdroid.ui.account.CollectionsList +import at.bitfire.davdroid.ui.account.RenameAccountDialog +import at.bitfire.davdroid.ui.composable.ActionCard +import at.bitfire.davdroid.ui.composable.ProgressBar +import at.bitfire.ical4android.TaskProvider +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberMultiplePermissionsState +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Composable +fun AccountScreen( + account: Account, + onAccountSettings: () -> Unit, + onCreateAddressBook: () -> Unit, + onCreateCalendar: () -> Unit, + onCollectionDetails: (Collection) -> Unit, + onNavUp: () -> Unit, + onFinish: () -> Unit +) { + val model: AccountScreenModel = hiltViewModel( + creationCallback = { factory: AccountScreenModel.Factory -> + factory.create(account) + } + ) + + val cardDavService by model.cardDavSvc.collectAsStateWithLifecycle() + val addressBooks = model.addressBooks.collectAsLazyPagingItems() + + val calDavService by model.calDavSvc.collectAsStateWithLifecycle() + val calendars = model.calendars.collectAsLazyPagingItems() + val currentTasksApp by model.tasksProvider.collectAsStateWithLifecycle(null) + val subscriptions = model.subscriptions.collectAsLazyPagingItems() + + val context = LocalContext.current + AccountScreen( + accountName = account.name, + error = model.error, + onResetError = model::resetError, + invalidAccount = model.invalidAccount.collectAsStateWithLifecycle(false).value, + showOnlyPersonal = model.showOnlyPersonal.collectAsStateWithLifecycle().value, + showOnlyPersonalLocked = model.showOnlyPersonalLocked.collectAsStateWithLifecycle().value, + onSetShowOnlyPersonal = model::setShowOnlyPersonal, + hasCardDav = cardDavService != null, + canCreateAddressBook = model.canCreateAddressBook.collectAsStateWithLifecycle(false).value, + cardDavProgress = model.cardDavProgress.collectAsStateWithLifecycle(AccountProgress.Idle).value, + addressBooks = addressBooks, + hasCalDav = calDavService != null, + canCreateCalendar = model.canCreateCalendar.collectAsStateWithLifecycle(false).value, + calDavProgress = model.calDavProgress.collectAsStateWithLifecycle(AccountProgress.Idle).value, + calendars = calendars, + currentTasksProvider = currentTasksApp, + hasWebcal = subscriptions.itemCount != 0, + subscriptions = subscriptions, + onUpdateCollectionSync = model::setCollectionSync, + onSubscribe = { collection -> + // subscribe + var uri = collection.source.toString().toUri() + when { + uri.scheme.equals("http", true) -> uri = uri.buildUpon().scheme("webcal").build() + uri.scheme.equals("https", true) -> uri = uri.buildUpon().scheme("webcals").build() + } + + val intent = Intent(Intent.ACTION_VIEW, uri) + collection.displayName?.let { intent.putExtra("title", it) } + collection.color?.let { intent.putExtra("color", it) } + + if (context.packageManager.resolveActivity(intent, 0) != null) + context.startActivity(intent) + else + model.noWebcalApp() + }, + onCollectionDetails = onCollectionDetails, + showNoWebcalApp = model.showNoWebcalApp, + resetShowNoWebcalApp = model::resetShowNoWebcalApp, + onRefreshCollections = model::refreshCollections, + onSync = model::sync, + onAccountSettings = onAccountSettings, + onCreateAddressBook = onCreateAddressBook, + onCreateCalendar = onCreateCalendar, + onRenameAccount = model::renameAccount, + onDeleteAccount = model::deleteAccount, + onNavUp = onNavUp, + onFinish = onFinish + ) +} + +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class) +@Composable +fun AccountScreen( + accountName: String, + error: String? = null, + onResetError: () -> Unit = {}, + invalidAccount: Boolean = false, + showOnlyPersonal: Boolean = false, + showOnlyPersonalLocked: Boolean = false, + onSetShowOnlyPersonal: (showOnlyPersonal: Boolean) -> Unit = {}, + hasCardDav: Boolean, + canCreateAddressBook: Boolean, + cardDavProgress: AccountProgress, + addressBooks: LazyPagingItems?, + hasCalDav: Boolean, + canCreateCalendar: Boolean, + calDavProgress: AccountProgress, + calendars: LazyPagingItems?, + currentTasksProvider: TaskProvider.ProviderName?, + hasWebcal: Boolean, + subscriptions: LazyPagingItems?, + onUpdateCollectionSync: (collectionId: Long, sync: Boolean) -> Unit = { _, _ -> }, + onSubscribe: (Collection) -> Unit = {}, + onCollectionDetails: (Collection) -> Unit = {}, + showNoWebcalApp: Boolean = false, + resetShowNoWebcalApp: () -> Unit = {}, + onRefreshCollections: () -> Unit = {}, + onSync: () -> Unit = {}, + onAccountSettings: () -> Unit = {}, + onCreateAddressBook: () -> Unit = {}, + onCreateCalendar: () -> Unit = {}, + onRenameAccount: (newName: String) -> Unit = {}, + onDeleteAccount: () -> Unit = {}, + onNavUp: () -> Unit = {}, + onFinish: () -> Unit = {} +) { + AppTheme { + val context = LocalContext.current + val scope = rememberCoroutineScope() + + LaunchedEffect(invalidAccount) { + if (invalidAccount) { + Toast.makeText(context, R.string.account_invalid_account, Toast.LENGTH_LONG).show() + onFinish() + } + } + + val snackbarHostState = remember { SnackbarHostState() } + LaunchedEffect(error) { + if (error != null) + scope.launch { + snackbarHostState.showSnackbar(error) + onResetError() + } + } + + var isRefreshing by remember { mutableStateOf(false) } + LaunchedEffect(isRefreshing) { + if (isRefreshing) { + delay(300) + isRefreshing = false + } + } + + // tabs calculation + var nextIdx = -1 + + @Suppress("KotlinConstantConditions") + val idxCalDav: Int? = if (hasCalDav) ++nextIdx else null + val idxCardDav: Int? = if (hasCardDav) ++nextIdx else null + val idxWebcal: Int? = if (hasWebcal) ++nextIdx else null + val nrPages = + (if (idxCalDav != null) 1 else 0) + + (if (idxCardDav != null) 1 else 0) + + (if (idxWebcal != null) 1 else 0) + val pagerState = rememberPagerState(pageCount = { nrPages }) + + val calDavScrollState = rememberLazyListState() + val cardDavScrollState = rememberLazyListState() + val webcalScrollState = rememberLazyListState() + + Scaffold( + topBar = { + TopAppBar( + navigationIcon = { + IconButton(onClick = onNavUp) { + Icon(Icons.AutoMirrored.Default.ArrowBack, stringResource(R.string.navigate_up)) + } + }, + title = { + Text( + text = accountName, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + actions = { + AccountScreen_Actions( + accountName = accountName, + canCreateAddressBook = canCreateAddressBook, + onCreateAddressBook = onCreateAddressBook, + canCreateCalendar = canCreateCalendar, + onCreateCalendar = onCreateCalendar, + showOnlyPersonal = showOnlyPersonal, + showOnlyPersonalLocked = showOnlyPersonalLocked, + onSetShowOnlyPersonal = onSetShowOnlyPersonal, + currentPage = pagerState.currentPage, + idxCardDav = idxCardDav, + idxCalDav = idxCalDav, + onRenameAccount = onRenameAccount, + onDeleteAccount = onDeleteAccount, + onAccountSettings = onAccountSettings + ) + } + ) + }, + floatingActionButton = { + Column(horizontalAlignment = Alignment.End) { + ExtendedFloatingActionButton( + text = { + Text(stringResource(R.string.account_refresh_collections)) + }, + icon = { + Icon(Icons.Outlined.RuleFolder, stringResource(R.string.account_refresh_collections)) + }, + onClick = onRefreshCollections, + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary, + modifier = Modifier.padding(bottom = 16.dp) + ) + + if (pagerState.currentPage == idxCardDav || pagerState.currentPage == idxCalDav) + ExtendedFloatingActionButton( + text = { + Text(stringResource(R.string.account_synchronize_now)) + }, + icon = { + Icon(Icons.Default.Sync, stringResource(R.string.account_synchronize_now)) + }, + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + onClick = onSync + ) + } + }, + snackbarHost = { + SnackbarHost(snackbarHostState) + } + ) { padding -> + Column( + modifier = Modifier.padding(padding) + ) { + if (nrPages > 0) { + SharedTransitionLayout { + val idxCurrentPage = pagerState.currentPage + + // The icon shall be shown when the scroll state is at the top (= we can't scroll backward) + val currentPageScrollState = when (idxCurrentPage) { + idxCalDav -> calDavScrollState + idxCardDav -> cardDavScrollState + idxWebcal -> webcalScrollState + else -> null + } + AnimatedContent( + targetState = currentPageScrollState?.canScrollBackward != true + ) { showIcon -> + PrimaryTabRow(selectedTabIndex = idxCurrentPage) { + if (idxCalDav != null) + AccountScreen_Tab( + selected = idxCurrentPage == idxCalDav, + showIcon = showIcon, + icon = Icons.Default.CalendarToday, + text = stringResource(R.string.account_caldav), + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout, + ) { + scope.launch { + pagerState.scrollToPage(idxCalDav) + } + } + + if (idxCardDav != null) + AccountScreen_Tab( + selected = idxCurrentPage == idxCardDav, + showIcon = showIcon, + icon = Icons.Default.Group, + text = stringResource(R.string.account_carddav), + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout, + ) { + scope.launch { + pagerState.scrollToPage(idxCardDav) + } + } + + if (idxWebcal != null) + AccountScreen_Tab( + selected = idxCurrentPage == idxWebcal, + showIcon = showIcon, + icon = Icons.Default.Link, + text = stringResource(R.string.account_webcal), + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout, + ) { + scope.launch { + pagerState.scrollToPage(idxWebcal) + } + } + } + } + } + + HorizontalPager( + pagerState, + verticalAlignment = Alignment.Top, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { index -> + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = { isRefreshing = true; onSync() } + ) { + when (index) { + idxCardDav -> + AccountScreen_ServiceTab( + requiredPermissions = listOf(Manifest.permission.WRITE_CONTACTS), + progress = cardDavProgress, + collections = addressBooks, + onUpdateCollectionSync = onUpdateCollectionSync, + onCollectionDetails = onCollectionDetails, + state = cardDavScrollState + ) + + idxCalDav -> { + val permissions = mutableListOf(Manifest.permission.WRITE_CALENDAR) + if (currentTasksProvider != null) + permissions += currentTasksProvider.permissions + AccountScreen_ServiceTab( + requiredPermissions = permissions, + progress = calDavProgress, + collections = calendars, + onUpdateCollectionSync = onUpdateCollectionSync, + onCollectionDetails = onCollectionDetails, + state = calDavScrollState + ) + } + + idxWebcal -> { + LaunchedEffect(showNoWebcalApp) { + if (showNoWebcalApp) { + if (snackbarHostState.showSnackbar( + message = context.getString(R.string.account_no_webcal_handler_found), + actionLabel = context.getString(R.string.account_install_icsx5), + duration = SnackbarDuration.Long + ) == SnackbarResult.ActionPerformed + ) { + val installIntent = Intent( + Intent.ACTION_VIEW, + "market://details?id=at.bitfire.icsdroid".toUri() + ) + if (context.packageManager.resolveActivity(installIntent, 0) != null) + context.startActivity(installIntent) + } + resetShowNoWebcalApp() + } + } + + Column { + Text( + stringResource(R.string.account_webcal_external_app), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(top = 8.dp, start = 8.dp, end = 8.dp) + ) + + AccountScreen_ServiceTab( + requiredPermissions = listOf(Manifest.permission.WRITE_CALENDAR), + progress = calDavProgress, + collections = subscriptions, + onSubscribe = onSubscribe, + state = webcalScrollState + ) + } + } + } + } + } + } + } + } + } +} + +@Composable +@OptIn(ExperimentalSharedTransitionApi::class) +fun AccountScreen_Tab( + selected: Boolean, + showIcon: Boolean, + icon: ImageVector, + text: String, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope, + onClick: () -> Unit, +) { + with(sharedTransitionScope) { + if (showIcon) { + Tab( + selected = selected, + onClick = onClick, + icon = { Icon(imageVector = icon, contentDescription = text) }, + text = { + Text( + text, + modifier = Modifier + .sharedBounds( + rememberSharedContentState(key = text), + animatedVisibilityScope = animatedVisibilityScope, + ) + .padding(8.dp) + ) + } + ) + } else { + Tab( + selected = selected, + onClick = onClick, + content = { + Text( + text, + modifier = Modifier + .sharedBounds( + rememberSharedContentState(key = text), + animatedVisibilityScope = animatedVisibilityScope, + ) + .padding(8.dp) + ) + } + ) + } + } +} + +@Composable +fun AccountScreen_Actions( + accountName: String, + canCreateAddressBook: Boolean, + onCreateAddressBook: () -> Unit, + canCreateCalendar: Boolean, + onCreateCalendar: () -> Unit, + showOnlyPersonal: Boolean, + showOnlyPersonalLocked: Boolean, + onSetShowOnlyPersonal: (showOnlyPersonal: Boolean) -> Unit, + currentPage: Int, + idxCardDav: Int?, + idxCalDav: Int?, + onRenameAccount: (newName: String) -> Unit, + onDeleteAccount: () -> Unit, + onAccountSettings: () -> Unit +) { + var showDeleteAccountDialog by remember { mutableStateOf(false) } + var showRenameAccountDialog by remember { mutableStateOf(false) } + + var overflowOpen by remember { mutableStateOf(false) } + IconButton(onClick = onAccountSettings) { + Icon(Icons.Default.Settings, stringResource(R.string.account_settings)) + } + IconButton(onClick = { overflowOpen = !overflowOpen }) { + Icon(Icons.Default.MoreVert, stringResource(R.string.options_menu)) + } + DropdownMenu( + expanded = overflowOpen, + onDismissRequest = { overflowOpen = false } + ) { + // TAB-SPECIFIC ACTIONS + + // create collection + if (currentPage == idxCardDav && canCreateAddressBook) { + // create address book + DropdownMenuItem( + leadingIcon = { + Icon( + Icons.Default.CreateNewFolder, + contentDescription = stringResource(R.string.create_addressbook), + modifier = Modifier.padding(end = 8.dp) + ) + }, + text = { + Text(stringResource(R.string.create_addressbook)) + }, + onClick = { + onCreateAddressBook() + overflowOpen = false + } + ) + } else if (currentPage == idxCalDav && canCreateCalendar) { + // create calendar + DropdownMenuItem( + leadingIcon = { + Icon( + Icons.Default.CreateNewFolder, + contentDescription = stringResource(R.string.create_calendar), + modifier = Modifier.padding(end = 8.dp) + ) + }, + text = { + Text(stringResource(R.string.create_calendar)) + }, + onClick = { + onCreateCalendar() + overflowOpen = false + } + ) + } + + // GENERAL ACTIONS + + // show only personal + DropdownMenuItem( + leadingIcon = { + CompositionLocalProvider( + LocalMinimumInteractiveComponentSize provides Dp.Unspecified + ) { + Checkbox( + checked = showOnlyPersonal, + enabled = !showOnlyPersonalLocked, + onCheckedChange = { + onSetShowOnlyPersonal(it) + overflowOpen = false + }, + modifier = Modifier.padding(end = 8.dp) + ) + } + }, + text = { + Text(stringResource(R.string.account_only_personal)) + }, + onClick = { + onSetShowOnlyPersonal(!showOnlyPersonal) + overflowOpen = false + }, + enabled = !showOnlyPersonalLocked + ) + + // rename account + DropdownMenuItem( + leadingIcon = { + Icon( + Icons.Default.DriveFileRenameOutline, + contentDescription = stringResource(R.string.account_rename), + modifier = Modifier.padding(end = 8.dp) + ) + }, + text = { + Text(stringResource(R.string.account_rename)) + }, + onClick = { + showRenameAccountDialog = true + overflowOpen = false + } + ) + + // delete account + DropdownMenuItem( + leadingIcon = { + Icon( + Icons.Default.Delete, + contentDescription = stringResource(R.string.account_delete), + modifier = Modifier.padding(end = 8.dp) + ) + }, + text = { + Text(stringResource(R.string.account_delete)) + }, + onClick = { + showDeleteAccountDialog = true + overflowOpen = false + } + ) + } + + // modal dialogs + if (showRenameAccountDialog) + RenameAccountDialog( + oldName = accountName, + onRenameAccount = { newName -> + onRenameAccount(newName) + showRenameAccountDialog = false + }, + onDismiss = { showRenameAccountDialog = false } + ) + if (showDeleteAccountDialog) + DeleteAccountDialog( + onConfirm = onDeleteAccount, + onDismiss = { showDeleteAccountDialog = false } + ) +} + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun AccountScreen_ServiceTab( + requiredPermissions: List, + progress: AccountProgress, + collections: LazyPagingItems?, + onUpdateCollectionSync: (collectionId: Long, sync: Boolean) -> Unit = { _, _ -> }, + onSubscribe: (Collection) -> Unit = {}, + onCollectionDetails: ((Collection) -> Unit)? = null, + state: LazyListState = rememberLazyListState() +) { + val context = LocalContext.current + + Column { + // progress indicator + val progressAlpha = progress.rememberAlpha() + when (progress) { + AccountProgress.Active -> ProgressBar( + modifier = Modifier + .alpha(progressAlpha) + .fillMaxWidth() + ) + AccountProgress.Pending, + AccountProgress.Idle -> ProgressBar( + progress = { 1f }, + modifier = Modifier + .alpha(progressAlpha) + .fillMaxWidth() + ) + } + + // permissions warning + if (!LocalInspectionMode.current) { + val permissionsState = rememberMultiplePermissionsState(requiredPermissions) + if (!permissionsState.allPermissionsGranted) + ActionCard( + icon = Icons.Default.SyncProblem, + actionText = stringResource(R.string.account_manage_permissions), + onAction = { + val intent = Intent(context, PermissionsActivity::class.java) + context.startActivity(intent) + }, + modifier = Modifier.padding(8.dp) + ) { + Text(stringResource(R.string.account_missing_permissions)) + } + + // collection list + if (collections != null) + CollectionsList( + collections, + onChangeSync = onUpdateCollectionSync, + onSubscribe = onSubscribe, + onCollectionDetails = onCollectionDetails, + modifier = Modifier.weight(1f), + state = state + ) + } + } +} + +@Preview +@Composable +fun AccountScreen_Preview() { + AccountScreen( + accountName = "test@example.com", + showOnlyPersonal = false, + showOnlyPersonalLocked = true, + hasCardDav = true, + canCreateAddressBook = false, + cardDavProgress = AccountProgress.Active, + addressBooks = null, + hasCalDav = true, + canCreateCalendar = true, + calDavProgress = AccountProgress.Pending, + calendars = null, + currentTasksProvider = TaskProvider.ProviderName.JtxBoard, + hasWebcal = true, + subscriptions = null + ) +} + +@Composable +@Preview +fun DeleteAccountDialog( + onConfirm: () -> Unit = {}, + onDismiss: () -> Unit = {} +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.account_delete_confirmation_title)) }, + text = { Text(stringResource(R.string.account_delete_confirmation_text)) }, + confirmButton = { + Button(onClick = onConfirm) { + Text(stringResource(android.R.string.ok)) + } + }, + dismissButton = { + OutlinedButton(onClick = onDismiss) { + Text(stringResource(android.R.string.cancel)) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreenModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreenModel.kt new file mode 100644 index 0000000..9e1607a --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreenModel.kt @@ -0,0 +1,209 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import android.accounts.Account +import android.content.Context +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.repository.AccountRepository +import at.bitfire.davdroid.repository.DavCollectionRepository +import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.sync.TasksAppManager +import at.bitfire.davdroid.sync.account.InvalidAccountException +import at.bitfire.davdroid.sync.worker.SyncWorkerManager +import dagger.Lazy +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.logging.Level +import java.util.logging.Logger + +@HiltViewModel(assistedFactory = AccountScreenModel.Factory::class) +class AccountScreenModel @AssistedInject constructor( + @Assisted val account: Account, + private val accountRepository: AccountRepository, + accountProgressUseCase: AccountProgressUseCase, + private val accountSettingsFactory: AccountSettings.Factory, + private val collectionRepository: DavCollectionRepository, + @ApplicationContext val context: Context, + private val collectionSelectedUseCase: Lazy, + getBindableHomesetsFromService: GetBindableHomeSetsFromServiceUseCase, + getServiceCollectionPager: GetServiceCollectionPagerUseCase, + private val logger: Logger, + serviceRepository: DavServiceRepository, + private val syncWorkerManager: SyncWorkerManager, + tasksAppManager: TasksAppManager +): ViewModel() { + + @AssistedFactory + interface Factory { + fun create(account: Account): AccountScreenModel + } + + /** + * Only acquire account settings on a worker thread! + */ + private val accountSettings: AccountSettings? by lazy { + try { + accountSettingsFactory.create(account) + } catch (_: InvalidAccountException) { + null + } + } + + /** whether the account is invalid and the screen shall be closed */ + val invalidAccount = accountRepository.getAllFlow().map { accounts -> + !accounts.contains(account) + } + + /** + * Whether to show only personal collections. + */ + private val _showOnlyPersonal = MutableStateFlow(false) + val showOnlyPersonal = _showOnlyPersonal.asStateFlow() + private suspend fun reloadShowOnlyPersonal() = withContext(Dispatchers.Default) { + accountSettings?.let { + _showOnlyPersonal.value = it.getShowOnlyPersonal() + } + } + fun setShowOnlyPersonal(showOnlyPersonal: Boolean) { + viewModelScope.launch { + accountSettings?.setShowOnlyPersonal(showOnlyPersonal) + reloadShowOnlyPersonal() + } + } + + /** + * Whether the user setting to show only personal collections is locked. + */ + private var _showOnlyPersonalLocked = MutableStateFlow(false) + val showOnlyPersonalLocked = _showOnlyPersonalLocked.asStateFlow() + private suspend fun reloadShowOnlyPersonalLocked() = withContext(Dispatchers.Default) { + accountSettings?.let { + _showOnlyPersonalLocked.value = it.getShowOnlyPersonalLocked() + } + } + + init { + viewModelScope.launch { + reloadShowOnlyPersonal() + reloadShowOnlyPersonalLocked() + } + } + + val cardDavSvc = serviceRepository + .getCardDavServiceFlow(account.name) + .stateIn(viewModelScope, initialValue = null, started = SharingStarted.Eagerly) + private val bindableAddressBookHomesets = getBindableHomesetsFromService(cardDavSvc) + val canCreateAddressBook = bindableAddressBookHomesets.map { homeSets -> + homeSets.isNotEmpty() + } + val cardDavProgress: Flow = accountProgressUseCase( + account = account, + serviceFlow = cardDavSvc, + dataTypes = listOf(SyncDataType.CONTACTS) + ) + val addressBooks = getServiceCollectionPager(cardDavSvc, Collection.TYPE_ADDRESSBOOK, showOnlyPersonal) + + val calDavSvc = serviceRepository + .getCalDavServiceFlow(account.name) + .stateIn(viewModelScope, initialValue = null, started = SharingStarted.Eagerly) + private val bindableCalendarHomesets = getBindableHomesetsFromService(calDavSvc) + val canCreateCalendar = bindableCalendarHomesets.map { homeSets -> + homeSets.isNotEmpty() + } + val tasksProvider = tasksAppManager.currentProviderFlow() + val calDavProgress = accountProgressUseCase( + account = account, + serviceFlow = calDavSvc, + dataTypes = listOf(SyncDataType.EVENTS, SyncDataType.TASKS) + ) + val calendars = getServiceCollectionPager(calDavSvc, Collection.TYPE_CALENDAR, showOnlyPersonal) + val subscriptions = getServiceCollectionPager(calDavSvc, Collection.TYPE_WEBCAL, showOnlyPersonal) + + + var error by mutableStateOf(null) + private set + + fun resetError() { error = null } + + + var showNoWebcalApp by mutableStateOf(false) + private set + + fun noWebcalApp() { showNoWebcalApp = true } + fun resetShowNoWebcalApp() { showNoWebcalApp = false } + + + // actions + + /** Deletes the account from the system (won't touch collections on the server). */ + fun deleteAccount() { + viewModelScope.launch { + accountRepository.delete(account.name) + } + } + + fun refreshCollections() { + cardDavSvc.value?.let { svc -> + RefreshCollectionsWorker.enqueue(context, svc.id) + } + calDavSvc.value?.let { svc -> + RefreshCollectionsWorker.enqueue(context, svc.id) + } + } + + /** + * Renames the [account] to given name. + * + * @param newName new account name + */ + fun renameAccount(newName: String) { + viewModelScope.launch { + try { + accountRepository.rename(account.name, newName) + + // synchronize again + val newAccount = Account(newName, context.getString(R.string.account_type)) + syncWorkerManager.enqueueOneTimeAllAuthorities(newAccount, manual = true) + } catch (e: Exception) { + logger.log(Level.SEVERE, "Couldn't rename account", e) + error = e.localizedMessage + } + } + } + + fun setCollectionSync(id: Long, sync: Boolean) { + viewModelScope.launch { + collectionRepository.setSync(id, sync) + collectionSelectedUseCase.get().handleWithDelay(id) + } + } + + fun sync() { + syncWorkerManager.enqueueOneTimeAllAuthorities(account, manual = true) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt new file mode 100644 index 0000000..7e09a35 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt @@ -0,0 +1,51 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import android.accounts.Account +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.TaskStackBuilder +import androidx.core.content.IntentCompat +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class AccountSettingsActivity: AppCompatActivity() { + + companion object { + const val EXTRA_ACCOUNT = "account" + } + + private val account by lazy { + IntentCompat.getParcelableExtra(intent, EXTRA_ACCOUNT, Account::class.java) ?: throw IllegalArgumentException("EXTRA_ACCOUNT must be set") + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + title = account.name + + setContent { + AccountSettingsScreen( + account = account, + onNavWifiPermissionsScreen = { + val intent = Intent(this, WifiPermissionsActivity::class.java) + intent.putExtra(WifiPermissionsActivity.EXTRA_ACCOUNT, account) + startActivity(intent) + }, + onNavUp = ::onSupportNavigateUp, + ) + } + } + + override fun supportShouldUpRecreateTask(targetIntent: Intent) = true + + override fun onPrepareSupportNavigateUpTaskStack(builder: TaskStackBuilder) { + builder.editIntentAt(builder.intentCount - 1)?.putExtra(AccountActivity.EXTRA_ACCOUNT, account) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsModel.kt new file mode 100644 index 0000000..7d2344e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsModel.kt @@ -0,0 +1,290 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import android.accounts.Account +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.di.DefaultDispatcher +import at.bitfire.davdroid.network.OAuthIntegration +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.settings.Credentials +import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.sync.ResyncType +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.sync.TasksAppManager +import at.bitfire.davdroid.sync.worker.SyncWorkerManager +import at.bitfire.vcard4android.GroupMethod +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.openid.appauth.AuthorizationRequest +import net.openid.appauth.AuthorizationResponse +import net.openid.appauth.AuthorizationService +import java.util.logging.Level +import java.util.logging.Logger + +@HiltViewModel(assistedFactory = AccountSettingsModel.Factory::class) +class AccountSettingsModel @AssistedInject constructor( + @Assisted val account: Account, + private val accountSettingsFactory: AccountSettings.Factory, + private val authService: AuthorizationService, + @ApplicationContext val context: Context, + db: AppDatabase, + @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher, + private val logger: Logger, + private val settings: SettingsManager, + private val syncWorkerManager: SyncWorkerManager, + private val tasksAppManager: TasksAppManager +): ViewModel(), SettingsManager.OnChangeListener { + + @AssistedFactory + interface Factory { + fun create(account: Account): AccountSettingsModel + } + + // settings + data class UiState( + val status: String? = null, + + val hasContactsSync: Boolean = false, + val syncIntervalContacts: Long? = null, + val hasCalendarsSync: Boolean = false, + val syncIntervalCalendars: Long? = null, + val hasTasksSync: Boolean = false, + val syncIntervalTasks: Long? = null, + + val syncWifiOnly: Boolean = false, + val syncWifiOnlySSIDs: List? = null, + val ignoreVpns: Boolean = false, + + val credentials: Credentials = Credentials(), + val allowCredentialsChange: Boolean = true, + + val timeRangePastDays: Int? = null, + val defaultAlarmMinBefore: Int? = null, + val manageCalendarColors: Boolean = false, + val eventColors: Boolean = false, + + val contactGroupMethod: GroupMethod = GroupMethod.GROUP_VCARDS + ) + + private val _uiState = MutableStateFlow(UiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val serviceDao = db.serviceDao() + private val tasksProvider + get() = tasksAppManager.currentProvider() + + /** + * Only acquire account settings on a worker thread! + */ + private val accountSettings by lazy { accountSettingsFactory.create(account) } + + + init { + settings.addOnChangeListener(this) + viewModelScope.launch { + reload() + } + } + + override fun onCleared() { + authService.dispose() + settings.removeOnChangeListener(this) + } + + override fun onSettingsChanged() { + viewModelScope.launch { + reload() + } + } + + private suspend fun reload() = withContext(defaultDispatcher) { + val hasContactsSync = serviceDao.getByAccountAndType(account.name, Service.TYPE_CARDDAV) != null + val hasCalendarSync = serviceDao.getByAccountAndType(account.name, Service.TYPE_CALDAV) != null + val hasTasksSync = hasCalendarSync && tasksProvider != null + + _uiState.value = UiState( + hasContactsSync = hasContactsSync, + syncIntervalContacts = accountSettings.getSyncInterval(SyncDataType.CONTACTS), + hasCalendarsSync = hasCalendarSync, + syncIntervalCalendars = accountSettings.getSyncInterval(SyncDataType.EVENTS), + hasTasksSync = hasTasksSync, + syncIntervalTasks = accountSettings.getSyncInterval(SyncDataType.TASKS), + + syncWifiOnly = accountSettings.getSyncWifiOnly(), + syncWifiOnlySSIDs = accountSettings.getSyncWifiOnlySSIDs(), + ignoreVpns = accountSettings.getIgnoreVpns(), + + credentials = accountSettings.credentials(), + allowCredentialsChange = accountSettings.changingCredentialsAllowed(), + + timeRangePastDays = accountSettings.getTimeRangePastDays(), + defaultAlarmMinBefore = accountSettings.getDefaultAlarm(), + manageCalendarColors = accountSettings.getManageCalendarColors(), + eventColors = accountSettings.getEventColors(), + + contactGroupMethod = accountSettings.getGroupMethod(), + ) + } + + + fun updateContactsSyncInterval(syncInterval: Long) { + CoroutineScope(defaultDispatcher).launch { + accountSettings.setSyncInterval(SyncDataType.CONTACTS, syncInterval.takeUnless { it == -1L }) + reload() + } + } + + fun updateCalendarSyncInterval(syncInterval: Long) { + CoroutineScope(defaultDispatcher).launch { + accountSettings.setSyncInterval(SyncDataType.EVENTS, syncInterval.takeUnless { it == -1L }) + reload() + } + } + + fun updateTasksSyncInterval(syncInterval: Long) { + CoroutineScope(defaultDispatcher).launch { + accountSettings.setSyncInterval(SyncDataType.TASKS, syncInterval.takeUnless { it == -1L }) + reload() + } + } + + fun updateSyncWifiOnly(wifiOnly: Boolean) = CoroutineScope(defaultDispatcher).launch { + accountSettings.setSyncWiFiOnly(wifiOnly) + reload() + } + + fun updateSyncWifiOnlySSIDs(ssids: List?) = CoroutineScope(defaultDispatcher).launch { + accountSettings.setSyncWifiOnlySSIDs(ssids) + reload() + } + + fun updateIgnoreVpns(ignoreVpns: Boolean) = CoroutineScope(defaultDispatcher).launch { + accountSettings.setIgnoreVpns(ignoreVpns) + reload() + } + + + fun authorizationContract() = OAuthIntegration.AuthorizationContract(authService) + + fun newAuthorizationRequest(): AuthorizationRequest? = + accountSettings.credentials().authState?.lastAuthorizationResponse?.request + + fun authenticate(authResponse: AuthorizationResponse) { + CoroutineScope(defaultDispatcher).launch { + try { + // save new credentials + val authState = OAuthIntegration.authenticate(authService, authResponse) + accountSettings.updateAuthState(authState) + + _uiState.update { + it.copy(status = context.getString(R.string.settings_reauthorize_oauth_success)) + } + } catch (e: Exception) { + logger.log(Level.WARNING, "Authentication failed", e) + _uiState.update { + it.copy(status = e.localizedMessage) + } + } + } + } + + fun authCodeFailed() { + _uiState.update { + it.copy(status = context.getString(R.string.login_oauth_couldnt_obtain_auth_code)) + } + } + + fun updateCredentials(credentials: Credentials) = CoroutineScope(defaultDispatcher).launch { + accountSettings.credentials(credentials) + reload() + } + + + fun updateTimeRangePastDays(days: Int?) = CoroutineScope(defaultDispatcher).launch { + accountSettings.setTimeRangePastDays(days) + reload() + + /* If the new setting is a certain number of days, no full resync is required, + because every sync will cause a REPORT calendar-query with the given number of days. + However, if the new setting is "all events", collection sync may/should be used, so + the last sync-token has to be reset, which is done by setting fullResync=true. + */ + resyncCalendars( + resync = if (days == null) ResyncType.RESYNC_ENTRIES else ResyncType.RESYNC_LIST, + tasks = false + ) + } + + fun updateDefaultAlarm(minBefore: Int?) = CoroutineScope(defaultDispatcher).launch { + accountSettings.setDefaultAlarm(minBefore) + reload() + + resyncCalendars(resync = ResyncType.RESYNC_ENTRIES, tasks = false) + } + + fun updateManageCalendarColors(manage: Boolean) = CoroutineScope(defaultDispatcher).launch { + accountSettings.setManageCalendarColors(manage) + reload() + + resyncCalendars(resync = ResyncType.RESYNC_LIST, tasks = true) + } + + fun updateEventColors(manageColors: Boolean) = CoroutineScope(defaultDispatcher).launch { + accountSettings.setEventColors(manageColors) + reload() + + resyncCalendars(resync = ResyncType.RESYNC_ENTRIES, tasks = false) + } + + + fun updateContactGroupMethod(groupMethod: GroupMethod) = CoroutineScope(defaultDispatcher).launch { + accountSettings.setGroupMethod(groupMethod) + reload() + + resync(SyncDataType.CONTACTS, ResyncType.RESYNC_ENTRIES) + } + + /** + * Initiates calendar re-synchronization. + * + * @param resync whether only the list of entries (resync) or also all entries + * themselves (full resync) shall be downloaded again + * @param tasks whether tasks shall be synchronized, too (false: only events, true: events and tasks) + */ + private fun resyncCalendars(resync: ResyncType, tasks: Boolean) { + resync(SyncDataType.EVENTS, resync) + if (tasks) + resync(SyncDataType.TASKS, resync) + } + + /** + * Initiates re-synchronization for given authority. + * + * @param dataType type of data to synchronize + * @param resync whether only the list of entries (resync) or also all entries + * themselves (full resync) shall be downloaded again + */ + private fun resync(dataType: SyncDataType, resync: ResyncType) { + syncWorkerManager.enqueueOneTime(account, dataType = dataType, resync = resync) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsScreen.kt new file mode 100644 index 0000000..d25be3c --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsScreen.kt @@ -0,0 +1,804 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import android.accounts.Account +import android.app.Activity +import android.security.KeyChain +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.Help +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.Contacts +import androidx.compose.material.icons.filled.Event +import androidx.compose.material.icons.filled.History +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Password +import androidx.compose.material.icons.filled.SyncProblem +import androidx.compose.material.icons.filled.Wifi +import androidx.compose.material.icons.outlined.Task +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +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.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringArrayResource +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 at.bitfire.davdroid.R +import at.bitfire.davdroid.settings.AccountSettings.Companion.SYNC_INTERVAL_MANUALLY +import at.bitfire.davdroid.settings.Credentials +import at.bitfire.davdroid.ui.AppTheme +import at.bitfire.davdroid.ui.ExternalUris +import at.bitfire.davdroid.ui.composable.ActionCard +import at.bitfire.davdroid.ui.composable.EditTextInputDialog +import at.bitfire.davdroid.ui.composable.MultipleChoiceInputDialog +import at.bitfire.davdroid.ui.composable.Setting +import at.bitfire.davdroid.ui.composable.SettingsHeader +import at.bitfire.davdroid.ui.composable.SwitchSetting +import at.bitfire.davdroid.util.PermissionUtils +import at.bitfire.davdroid.util.SensitiveString.Companion.toSensitiveString +import at.bitfire.vcard4android.GroupMethod +import kotlinx.coroutines.launch + +@Composable +fun AccountSettingsScreen( + onNavUp: () -> Unit, + account: Account, + onNavWifiPermissionsScreen: () -> Unit +) { + val model = hiltViewModel { factory: AccountSettingsModel.Factory -> + factory.create(account) + } + val uiState by model.uiState.collectAsState() + val canAccessWifiSsid by PermissionUtils.rememberCanAccessWifiSsid() + + // contract to open the browser for re-authentication + val authRequestContract = rememberLauncherForActivityResult(model.authorizationContract()) { authResponse -> + if (authResponse != null) + model.authenticate(authResponse) + else + model.authCodeFailed() + } + + AppTheme { + AccountSettingsScreen( + accountName = account.name, + onNavUp = onNavUp, + status = uiState.status, + + // Sync settings + canAccessWifiSsid = canAccessWifiSsid, + onSyncWifiOnlyPermissionsAction = onNavWifiPermissionsScreen, + hasContactsSync = uiState.hasContactsSync, + contactsSyncInterval = uiState.syncIntervalContacts, + onUpdateContactsSyncInterval = model::updateContactsSyncInterval, + hasCalendarsSync = uiState.hasCalendarsSync, + calendarSyncInterval = uiState.syncIntervalCalendars, + onUpdateCalendarSyncInterval = model::updateCalendarSyncInterval, + hasTasksSync = uiState.hasTasksSync, + tasksSyncInterval = uiState.syncIntervalTasks, + onUpdateTasksSyncInterval = model::updateTasksSyncInterval, + syncOnlyOnWifi = uiState.syncWifiOnly, + onUpdateSyncOnlyOnWifi = model::updateSyncWifiOnly, + onlyOnSsids = uiState.syncWifiOnlySSIDs, + onUpdateOnlyOnSsids = model::updateSyncWifiOnlySSIDs, + ignoreVpns = uiState.ignoreVpns, + onUpdateIgnoreVpns = model::updateIgnoreVpns, + + // Authentication Settings + credentials = uiState.credentials, + onUpdateCredentials = model::updateCredentials, + onAuthenticateOAuth = { + val request = model.newAuthorizationRequest() + if (request != null) + authRequestContract.launch(request) + }, + isCredentialsUpdateAllowed = uiState.allowCredentialsChange, + + // CalDav Settings + timeRangePastDays = uiState.timeRangePastDays, + onUpdateTimeRangePastDays = model::updateTimeRangePastDays, + defaultAlarmMinBefore = uiState.defaultAlarmMinBefore, + onUpdateDefaultAlarmMinBefore = model::updateDefaultAlarm, + manageCalendarColors = uiState.manageCalendarColors, + onUpdateManageCalendarColors = model::updateManageCalendarColors, + eventColors = uiState.eventColors, + onUpdateEventColors = model::updateEventColors, + + // CardDav Settings + contactGroupMethod = uiState.contactGroupMethod, + onUpdateContactGroupMethod = model::updateContactGroupMethod, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AccountSettingsScreen( + onNavUp: () -> Unit, + accountName: String, + status: String? = null, + + // Sync settings + canAccessWifiSsid: Boolean, + onSyncWifiOnlyPermissionsAction: () -> Unit, + hasContactsSync: Boolean, + contactsSyncInterval: Long?, + onUpdateContactsSyncInterval: ((Long) -> Unit) = {}, + hasCalendarsSync: Boolean, + calendarSyncInterval: Long?, + onUpdateCalendarSyncInterval: ((Long) -> Unit) = {}, + hasTasksSync: Boolean, + tasksSyncInterval: Long?, + onUpdateTasksSyncInterval: ((Long) -> Unit) = {}, + syncOnlyOnWifi: Boolean, + onUpdateSyncOnlyOnWifi: (Boolean) -> Unit = {}, + onlyOnSsids: List?, + onUpdateOnlyOnSsids: (List) -> Unit = {}, + ignoreVpns: Boolean, + onUpdateIgnoreVpns: (Boolean) -> Unit = {}, + + // Authentication Settings + credentials: Credentials?, + onUpdateCredentials: (Credentials) -> Unit = {}, + onAuthenticateOAuth: () -> Unit = {}, + isCredentialsUpdateAllowed: Boolean, + + // CalDav Settings + timeRangePastDays: Int?, + onUpdateTimeRangePastDays: (Int?) -> Unit = {}, + defaultAlarmMinBefore: Int?, + onUpdateDefaultAlarmMinBefore: (Int?) -> Unit = {}, + manageCalendarColors: Boolean, + onUpdateManageCalendarColors: (Boolean) -> Unit = {}, + eventColors: Boolean, + onUpdateEventColors: (Boolean) -> Unit = {}, + + // CardDav Settings + contactGroupMethod: GroupMethod, + onUpdateContactGroupMethod: (GroupMethod) -> Unit = {}, +) { + val uriHandler = LocalUriHandler.current + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(status) { + if (status != null) + snackbarHostState.showSnackbar(status) + } + + Scaffold( + topBar = { + TopAppBar( + navigationIcon = { + IconButton(onClick = onNavUp) { + Icon( + Icons.AutoMirrored.Default.ArrowBack, + contentDescription = stringResource(R.string.navigate_up) + ) + } + }, + title = { Text(accountName) }, + actions = { + IconButton(onClick = { + val settingsUri = ExternalUris.Manual.baseUrl.buildUpon() + .appendPath(ExternalUris.Manual.PATH_SETTINGS) + .fragment(ExternalUris.Manual.FRAGMENT_ACCOUNT_SETTINGS) + .build() + uriHandler.openUri(settingsUri.toString()) + }) { + Icon(Icons.AutoMirrored.Filled.Help, stringResource(R.string.help)) + } + } + ) + }, + snackbarHost = { SnackbarHost(snackbarHostState) } + ) { padding -> + Box( + Modifier + .padding(padding) + .verticalScroll(rememberScrollState()) + ) { + AccountSettings_FromModel( + snackbarHostState = snackbarHostState, + + // Sync settings + canAccessWifiSsid = canAccessWifiSsid, + onSyncWifiOnlyPermissionsAction = onSyncWifiOnlyPermissionsAction, + hasContactsSync = hasContactsSync, + contactsSyncInterval = contactsSyncInterval, + onUpdateContactsSyncInterval = onUpdateContactsSyncInterval, + hasCalendarsSync = hasCalendarsSync, + calendarSyncInterval = calendarSyncInterval, + onUpdateCalendarSyncInterval = onUpdateCalendarSyncInterval, + hasTasksSync = hasTasksSync, + taskSyncInterval = tasksSyncInterval, + onUpdateTaskSyncInterval = onUpdateTasksSyncInterval, + syncOnlyOnWifi = syncOnlyOnWifi, + onUpdateSyncOnlyOnWifi = onUpdateSyncOnlyOnWifi, + onlyOnSsids = onlyOnSsids, + onUpdateOnlyOnSsids = onUpdateOnlyOnSsids, + ignoreVpns = ignoreVpns, + onUpdateIgnoreVpns = onUpdateIgnoreVpns, + + // Authentication Settings + credentials = credentials, + onUpdateCredentials = onUpdateCredentials, + onAuthenticateOAuth = onAuthenticateOAuth, + isCredentialsUpdateAllowed = isCredentialsUpdateAllowed, + + // CalDav Settings + timeRangePastDays = timeRangePastDays, + onUpdateTimeRangePastDays = onUpdateTimeRangePastDays, + defaultAlarmMinBefore = defaultAlarmMinBefore, + onUpdateDefaultAlarmMinBefore = onUpdateDefaultAlarmMinBefore, + manageCalendarColors = manageCalendarColors, + onUpdateManageCalendarColors = onUpdateManageCalendarColors, + eventColors = eventColors, + onUpdateEventColors = onUpdateEventColors, + + // CardDav Settings + contactGroupMethod = contactGroupMethod, + onUpdateContactGroupMethod = onUpdateContactGroupMethod + ) + } + } +} + +@Composable +fun AccountSettings_FromModel( + snackbarHostState: SnackbarHostState, + + // Sync settings + canAccessWifiSsid: Boolean, + onSyncWifiOnlyPermissionsAction: () -> Unit, + hasContactsSync: Boolean, + contactsSyncInterval: Long?, + onUpdateContactsSyncInterval: ((Long) -> Unit) = {}, + hasCalendarsSync: Boolean, + calendarSyncInterval: Long?, + onUpdateCalendarSyncInterval: ((Long) -> Unit) = {}, + hasTasksSync: Boolean, + taskSyncInterval: Long?, + onUpdateTaskSyncInterval: ((Long) -> Unit) = {}, + syncOnlyOnWifi: Boolean, + onUpdateSyncOnlyOnWifi: (Boolean) -> Unit = {}, + onlyOnSsids: List?, + onUpdateOnlyOnSsids: (List) -> Unit = {}, + ignoreVpns: Boolean, + onUpdateIgnoreVpns: (Boolean) -> Unit = {}, + + // Authentication Settings + credentials: Credentials?, + onUpdateCredentials: (Credentials) -> Unit = {}, + onAuthenticateOAuth: () -> Unit = {}, + isCredentialsUpdateAllowed: Boolean, + + // CalDav Settings + timeRangePastDays: Int?, + onUpdateTimeRangePastDays: (Int?) -> Unit = {}, + defaultAlarmMinBefore: Int?, + onUpdateDefaultAlarmMinBefore: (Int?) -> Unit = {}, + manageCalendarColors: Boolean, + onUpdateManageCalendarColors: (Boolean) -> Unit = {}, + eventColors: Boolean, + onUpdateEventColors: (Boolean) -> Unit = {}, + + // CardDav Settings + contactGroupMethod: GroupMethod, + onUpdateContactGroupMethod: (GroupMethod) -> Unit = {}, +) { + Column(Modifier.padding(8.dp)) { + SyncSettings( + canAccessWifiSsid = canAccessWifiSsid, + onSyncWifiOnlyPermissionsAction = onSyncWifiOnlyPermissionsAction, + hasContactsSync = hasContactsSync, + contactsSyncInterval = contactsSyncInterval, + onUpdateContactsSyncInterval = onUpdateContactsSyncInterval, + hasCalendarsSync = hasCalendarsSync, + calendarSyncInterval = calendarSyncInterval, + onUpdateCalendarSyncInterval = onUpdateCalendarSyncInterval, + hasTasksSync = hasTasksSync, + taskSyncInterval = taskSyncInterval, + onUpdateTaskSyncInterval = onUpdateTaskSyncInterval, + syncOnlyOnWifi = syncOnlyOnWifi, + onUpdateSyncOnlyOnWifi = onUpdateSyncOnlyOnWifi, + onlyOnSsids = onlyOnSsids, + onUpdateOnlyOnSsids = onUpdateOnlyOnSsids, + ignoreVpns = ignoreVpns, + onUpdateIgnoreVpns = onUpdateIgnoreVpns + ) + + credentials?.let { + AuthenticationSettings( + snackbarHostState = snackbarHostState, + credentials = credentials, + isEnabled = isCredentialsUpdateAllowed, + onUpdateCredentials = onUpdateCredentials, + onAuthenticateOAuth = onAuthenticateOAuth + ) + } + + CalDavSettings( + timeRangePastDays = timeRangePastDays, + onUpdateTimeRangePastDays = onUpdateTimeRangePastDays, + defaultAlarmMinBefore = defaultAlarmMinBefore, + onUpdateDefaultAlarmMinBefore = onUpdateDefaultAlarmMinBefore, + manageCalendarColors = manageCalendarColors, + onUpdateManageCalendarColors = onUpdateManageCalendarColors, + eventColors = eventColors, + onUpdateEventColors = onUpdateEventColors, + ) + + CardDavSettings( + contactGroupMethod = contactGroupMethod, + onUpdateContactGroupMethod = onUpdateContactGroupMethod + ) + } +} + +@Composable +fun SyncSettings( + canAccessWifiSsid: Boolean, + onSyncWifiOnlyPermissionsAction: () -> Unit, + hasContactsSync: Boolean, + contactsSyncInterval: Long?, + onUpdateContactsSyncInterval: ((Long) -> Unit) = {}, + hasCalendarsSync: Boolean, + calendarSyncInterval: Long?, + onUpdateCalendarSyncInterval: ((Long) -> Unit) = {}, + hasTasksSync: Boolean, + taskSyncInterval: Long?, + onUpdateTaskSyncInterval: ((Long) -> Unit) = {}, + syncOnlyOnWifi: Boolean, + onUpdateSyncOnlyOnWifi: (Boolean) -> Unit = {}, + onlyOnSsids: List?, + onUpdateOnlyOnSsids: (List) -> Unit = {}, + ignoreVpns: Boolean, + onUpdateIgnoreVpns: (Boolean) -> Unit = {} +) { + Column { + SettingsHeader(false) { + Text(stringResource(R.string.settings_sync)) + } + + if (hasContactsSync) + SyncIntervalSetting( + icon = Icons.Default.Contacts, + name = R.string.settings_sync_interval_contacts, + syncInterval = contactsSyncInterval, + onUpdateSyncInterval = onUpdateContactsSyncInterval + ) + if (hasCalendarsSync) + SyncIntervalSetting( + icon = Icons.Default.Event, + name = R.string.settings_sync_interval_calendars, + syncInterval = calendarSyncInterval, + onUpdateSyncInterval = onUpdateCalendarSyncInterval + ) + if (hasTasksSync) + SyncIntervalSetting( + icon = Icons.Outlined.Task, + name = R.string.settings_sync_interval_tasks, + syncInterval = taskSyncInterval, + onUpdateSyncInterval = onUpdateTaskSyncInterval + ) + + SwitchSetting( + icon = Icons.Default.Wifi, + name = stringResource(R.string.settings_sync_wifi_only), + summaryOn = stringResource(R.string.settings_sync_wifi_only_on), + summaryOff = stringResource(R.string.settings_sync_wifi_only_off), + checked = syncOnlyOnWifi, + onCheckedChange = onUpdateSyncOnlyOnWifi + ) + + var showWifiOnlySsidsDialog by remember { mutableStateOf(false) } + Setting( + icon = null, + name = stringResource(R.string.settings_sync_wifi_only_ssids), + enabled = syncOnlyOnWifi, + summary = + if (onlyOnSsids != null) + stringResource(R.string.settings_sync_wifi_only_ssids_on, onlyOnSsids.joinToString(", ")) + else + stringResource(R.string.settings_sync_wifi_only_ssids_off), + onClick = { + showWifiOnlySsidsDialog = true + } + ) + if (showWifiOnlySsidsDialog) + EditTextInputDialog( + title = stringResource(R.string.settings_sync_wifi_only_ssids_message), + initialValue = onlyOnSsids?.joinToString(", ") ?: "", + onValueEntered = { newValue -> + val newSsids = newValue.split(',') + .map { it.trim() } + .distinct() + onUpdateOnlyOnSsids(newSsids) + showWifiOnlySsidsDialog = false + }, + onDismiss = { showWifiOnlySsidsDialog = false } + ) + + if (LocalInspectionMode.current || onlyOnSsids != null) + ActionCard( + icon = if (!canAccessWifiSsid) Icons.Default.SyncProblem else Icons.Default.Info, + actionText = stringResource(R.string.settings_sync_wifi_only_ssids_permissions_action), + onAction = onSyncWifiOnlyPermissionsAction + ) { + Column { + if (!canAccessWifiSsid) + Text(stringResource(R.string.settings_sync_wifi_only_ssids_permissions_required)) + Text( + stringResource( + R.string.wifi_permissions_background_location_disclaimer, stringResource( + R.string.app_name) + ), + style = MaterialTheme.typography.bodyMedium + ) + } + } + + SwitchSetting( + icon = null, + name = stringResource(R.string.settings_ignore_vpns), + summaryOn = stringResource(R.string.settings_ignore_vpns_on), + summaryOff = stringResource(R.string.settings_ignore_vpns_off), + checked = ignoreVpns, + onCheckedChange = onUpdateIgnoreVpns + ) + } +} + +@Composable +fun SyncIntervalSetting( + icon: ImageVector, + @StringRes name: Int, + syncInterval: Long?, + onUpdateSyncInterval: (Long) -> Unit +) { + var showSyncIntervalDialog by remember { mutableStateOf(false) } + Setting( + icon = icon, + name = stringResource(name), + summary = + if (syncInterval == null) + stringResource(R.string.settings_sync_summary_manually) + else + stringResource(R.string.settings_sync_summary_periodically, syncInterval / 60), + onClick = { + showSyncIntervalDialog = true + } + ) + if (showSyncIntervalDialog) { + val syncIntervalNames = stringArrayResource(R.array.settings_sync_interval_names) + val syncIntervalSeconds = stringArrayResource(R.array.settings_sync_interval_seconds) + MultipleChoiceInputDialog( + title = stringResource(name), + namesAndValues = syncIntervalNames.zip(syncIntervalSeconds), + initialValue = (syncInterval ?: SYNC_INTERVAL_MANUALLY).toString(), + onValueSelected = { newValue -> + try { + val seconds = newValue.toLong() + onUpdateSyncInterval(seconds) + } catch (_: NumberFormatException) { + } + showSyncIntervalDialog = false + }, + onDismiss = { + showSyncIntervalDialog = false + } + ) + } +} + +@Composable +fun AuthenticationSettings( + credentials: Credentials, + snackbarHostState: SnackbarHostState = SnackbarHostState(), + isEnabled: Boolean = true, + onUpdateCredentials: (Credentials) -> Unit = {}, + onAuthenticateOAuth: () -> Unit = {} +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + + if (credentials.authState != null || credentials.username != null || credentials.password != null || credentials.certificateAlias != null) + Column { + SettingsHeader(false) { + Text(stringResource(R.string.settings_authentication)) + } + + // username/password + if (credentials.username != null || credentials.password != null) { + var showUsernameDialog by remember { mutableStateOf(false) } + Setting( + icon = Icons.Default.AccountCircle, + name = stringResource(R.string.settings_username), + summary = credentials.username, + enabled = isEnabled, + onClick = { + showUsernameDialog = true + } + ) + if (showUsernameDialog) + EditTextInputDialog( + title = stringResource(R.string.settings_username), + initialValue = credentials.username, + onValueEntered = { newValue -> + onUpdateCredentials(credentials.copy(username = newValue)) + }, + onDismiss = { showUsernameDialog = false } + ) + + var showPasswordDialog by remember { mutableStateOf(false) } + Setting( + icon = Icons.Default.Password, + name = stringResource(R.string.settings_password), + summary = stringResource(R.string.settings_password_summary), + enabled = isEnabled, + onClick = { + showPasswordDialog = true + } + ) + if (showPasswordDialog) + EditTextInputDialog( + title = stringResource(R.string.settings_password), + inputLabel = stringResource(R.string.settings_new_password), + initialValue = null, // Do not show the existing password + passwordField = true, + onValueEntered = { newValue -> + onUpdateCredentials(credentials.copy(password = newValue.toSensitiveString())) + }, + onDismiss = { showPasswordDialog = false } + ) + } + + // OAuth + if (credentials.authState != null) { + Setting( + icon = Icons.Default.Password, + name = stringResource(R.string.settings_reauthorize_oauth), + summary = stringResource(R.string.settings_reauthorize_oauth_summary), + enabled = isEnabled, + onClick = onAuthenticateOAuth + ) + } + + // client certificate + Setting( + icon = null, + name = stringResource(R.string.settings_certificate_alias), + summary = credentials.certificateAlias ?: stringResource(R.string.settings_certificate_alias_empty), + enabled = isEnabled, + onClick = { + val activity = context as Activity + KeyChain.choosePrivateKeyAlias(activity, { newAlias -> + if (newAlias != null) + onUpdateCredentials(credentials.copy(certificateAlias = newAlias)) + else + scope.launch { + if (snackbarHostState.showSnackbar( + context.getString(R.string.settings_certificate_alias_empty), + actionLabel = context.getString(R.string.settings_certificate_install) + ) == SnackbarResult.ActionPerformed) { + val intent = KeyChain.createInstallIntent() + if (intent.resolveActivity(context.packageManager) != null) + context.startActivity(intent) + } + } + }, null, null, null, -1, credentials.certificateAlias) + } + ) + } +} + +@Composable +fun CalDavSettings( + timeRangePastDays: Int?, + onUpdateTimeRangePastDays: (Int?) -> Unit = {}, + defaultAlarmMinBefore: Int?, + onUpdateDefaultAlarmMinBefore: (Int?) -> Unit = {}, + manageCalendarColors: Boolean, + onUpdateManageCalendarColors: (Boolean) -> Unit = {}, + eventColors: Boolean, + onUpdateEventColors: (Boolean) -> Unit = {} +) { + Column { + SettingsHeader { + Text(stringResource(R.string.settings_caldav)) + } + + var showTimeRangePastDialog by remember { mutableStateOf(false) } + Setting( + icon = Icons.Default.History, + name = stringResource(R.string.settings_sync_time_range_past), + summary = + if (timeRangePastDays != null) + pluralStringResource(R.plurals.settings_sync_time_range_past_days, timeRangePastDays, timeRangePastDays) + else + stringResource(R.string.settings_sync_time_range_past_none), + onClick = { + showTimeRangePastDialog = true + } + ) + if (showTimeRangePastDialog) + EditTextInputDialog( + title = stringResource(R.string.settings_sync_time_range_past_message), + initialValue = timeRangePastDays?.toString() ?: "", + onValueEntered = { newValue -> + val days = try { + newValue.toInt() + } catch (_: NumberFormatException) { + null + } + onUpdateTimeRangePastDays(days) + showTimeRangePastDialog = false + }, + onDismiss = { showTimeRangePastDialog = false } + ) + + var showDefaultAlarmDialog by remember { mutableStateOf(false) } + Setting( + icon = null, + name = stringResource(R.string.settings_default_alarm), + summary = + if (defaultAlarmMinBefore != null) + pluralStringResource(R.plurals.settings_default_alarm_on, defaultAlarmMinBefore, defaultAlarmMinBefore) + else + stringResource(R.string.settings_default_alarm_off), + onClick = { + showDefaultAlarmDialog = true + } + ) + if (showDefaultAlarmDialog) + EditTextInputDialog( + title = stringResource(R.string.settings_default_alarm_message), + initialValue = defaultAlarmMinBefore?.toString() ?: "", + onValueEntered = { newValue -> + val minBefore = try { + newValue.toInt() + } catch (_: NumberFormatException) { + null + } + onUpdateDefaultAlarmMinBefore(minBefore) + showDefaultAlarmDialog = false + }, + onDismiss = { showDefaultAlarmDialog = false } + ) + + SwitchSetting( + icon = null, + name = stringResource(R.string.settings_manage_calendar_colors), + summaryOn = stringResource(R.string.settings_manage_calendar_colors_on), + summaryOff = stringResource(R.string.settings_manage_calendar_colors_off), + checked = manageCalendarColors, + onCheckedChange = onUpdateManageCalendarColors + ) + + SwitchSetting( + icon = null, + name = stringResource(R.string.settings_event_colors), + summaryOn = stringResource(R.string.settings_event_colors_on), + summaryOff = stringResource(R.string.settings_event_colors_off), + checked = eventColors, + onCheckedChange = onUpdateEventColors + ) + } +} + +@Composable +fun CardDavSettings( + contactGroupMethod: GroupMethod, + onUpdateContactGroupMethod: (GroupMethod) -> Unit = {} +) { + Column { + SettingsHeader { + Text(stringResource(R.string.settings_carddav)) + } + + val groupMethodNames = stringArrayResource(R.array.settings_contact_group_method_entries) + val groupMethodValues = stringArrayResource(R.array.settings_contact_group_method_values) + var showGroupMethodDialog by remember { mutableStateOf(false) } + Setting( + icon = Icons.Default.Contacts, + name = stringResource(R.string.settings_contact_group_method), + summary = groupMethodNames[groupMethodValues.indexOf(contactGroupMethod.name)], + onClick = { + showGroupMethodDialog = true + } + ) + if (showGroupMethodDialog) + MultipleChoiceInputDialog( + title = stringResource(R.string.settings_contact_group_method), + namesAndValues = groupMethodNames.zip(groupMethodValues), + initialValue = contactGroupMethod.name, + onValueSelected = { newValue -> + onUpdateContactGroupMethod(GroupMethod.valueOf(newValue)) + showGroupMethodDialog = false + }, + onDismiss = { showGroupMethodDialog = false } + ) + } +} + +@Composable +@Preview +fun AccountSettingsScreen_Preview() { + AppTheme { + AccountSettingsScreen( + accountName = "Account Name Here", + onNavUp = {}, + status = "Some Status", + + // Sync settings + canAccessWifiSsid = true, + onSyncWifiOnlyPermissionsAction = {}, + hasContactsSync = true, + contactsSyncInterval = 80000L, + onUpdateContactsSyncInterval = {}, + hasCalendarsSync = true, + calendarSyncInterval = 50000L, + onUpdateCalendarSyncInterval = {}, + hasTasksSync = true, + tasksSyncInterval = 900000L, + onUpdateTasksSyncInterval = {}, + syncOnlyOnWifi = true, + onUpdateSyncOnlyOnWifi = {}, + onlyOnSsids = listOf("HeyWifi", "Another"), + onUpdateOnlyOnSsids = {}, + ignoreVpns = true, + onUpdateIgnoreVpns = {}, + + // Authentication Settings + credentials = Credentials(username = "test", password = "test".toSensitiveString()), + onUpdateCredentials = {}, + isCredentialsUpdateAllowed = true, + + // CalDav Settings + timeRangePastDays = 365, + onUpdateTimeRangePastDays = {}, + defaultAlarmMinBefore = 585, + onUpdateDefaultAlarmMinBefore = {}, + manageCalendarColors = false, + onUpdateManageCalendarColors = {}, + eventColors = false, + onUpdateEventColors = {}, + + // CardDav Settings + contactGroupMethod = GroupMethod.GROUP_VCARDS, + onUpdateContactGroupMethod = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionActivity.kt new file mode 100644 index 0000000..1449854 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionActivity.kt @@ -0,0 +1,45 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import android.accounts.Account +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.TaskStackBuilder +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class CollectionActivity: AppCompatActivity() { + + companion object { + const val EXTRA_ACCOUNT = "account" + const val EXTRA_COLLECTION_ID = "collection_id" + } + + val account by lazy { intent.getParcelableExtra(EXTRA_ACCOUNT)!! } + val collectionId by lazy { intent.getLongExtra(EXTRA_COLLECTION_ID, -1) } + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + CollectionScreen( + collectionId = collectionId, + onFinish = ::finish, + onNavUp = ::onSupportNavigateUp + ) + } + } + + override fun supportShouldUpRecreateTask(targetIntent: Intent) = true + + override fun onPrepareSupportNavigateUpTaskStack(builder: TaskStackBuilder) { + builder.editIntentAt(builder.intentCount - 1)?.putExtra(AccountActivity.EXTRA_ACCOUNT, account) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionScreen.kt new file mode 100644 index 0000000..cbea975 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionScreen.kt @@ -0,0 +1,418 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.AccountBox +import androidx.compose.material.icons.filled.CloudSync +import androidx.compose.material.icons.filled.DeleteForever +import androidx.compose.material.icons.filled.DoNotDisturbOn +import androidx.compose.material.icons.filled.Sync +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MediumTopAppBar +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import at.bitfire.davdroid.R +import at.bitfire.davdroid.repository.DavSyncStatsRepository +import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.ui.AppTheme +import at.bitfire.davdroid.ui.composable.ExceptionInfoDialog +import at.bitfire.davdroid.ui.composable.ProgressBar +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle + +@Composable +fun CollectionScreen( + collectionId: Long, + onFinish: () -> Unit, + onNavUp: () -> Unit +) { + val model: CollectionScreenModel = hiltViewModel( + creationCallback = { factory: CollectionScreenModel.Factory -> + factory.create(collectionId) + } + ) + + val collectionOrNull by model.collection.collectAsStateWithLifecycle(null) + if (model.invalid) { + onFinish() + return + } + + val collection = collectionOrNull ?: return + CollectionScreen( + inProgress = model.inProgress, + error = model.error, + onResetError = model::resetError, + color = collection.color, + sync = collection.sync, + onSetSync = model::setSync, + readOnly = model.readOnly.collectAsStateWithLifecycle(CollectionScreenModel.ReadOnlyState.READ_WRITE).value, + onSetForceReadOnly = model::setForceReadOnly, + title = collection.title(), + displayName = collection.displayName, + description = collection.description, + owner = model.owner.collectAsStateWithLifecycle(null).value, + lastSynced = model.lastSynced.collectAsStateWithLifecycle(emptyList()).value, + supportsWebPush = collection.supportsWebPush, + pushSubscriptionCreated = collection.pushSubscriptionCreated, + pushSubscriptionExpires = collection.pushSubscriptionExpires, + url = collection.url.toString(), + onDelete = model::delete, + onNavUp = onNavUp + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CollectionScreen( + inProgress: Boolean, + error: Exception? = null, + onResetError: () -> Unit = {}, + color: Int?, + sync: Boolean, + onSetSync: (Boolean) -> Unit = {}, + readOnly: CollectionScreenModel.ReadOnlyState, + onSetForceReadOnly: (Boolean) -> Unit = {}, + title: String, + displayName: String? = null, + description: String? = null, + owner: String? = null, + lastSynced: List = emptyList(), + supportsWebPush: Boolean = false, + pushSubscriptionCreated: Long? = null, + pushSubscriptionExpires: Long? = null, + url: String, + onDelete: () -> Unit = {}, + onNavUp: () -> Unit = {} +) { + AppTheme { + if (error != null) + ExceptionInfoDialog( + exception = error, + onDismiss = onResetError + ) + + Scaffold( + topBar = { + MediumTopAppBar( + navigationIcon = { + IconButton(onClick = onNavUp) { + Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = stringResource(R.string.navigate_up)) + } + }, + title = { + Text( + text = title, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + actions = { + var showDeleteDialog by remember { mutableStateOf(false) } + IconButton( + onClick = { showDeleteDialog = true }, + enabled = !inProgress + ) { + Icon(Icons.Default.DeleteForever, contentDescription = stringResource(R.string.collection_delete)) + } + + if (showDeleteDialog) + DeleteCollectionDialog( + displayName = title, + onDismiss = { showDeleteDialog = false }, + onConfirm = { + onDelete() + showDeleteDialog = false + } + ) + } + ) + } + ) { padding -> + Column( + Modifier + .padding(padding) + .verticalScroll(rememberScrollState()) + ) { + if (inProgress) + ProgressBar( + Modifier + .fillMaxWidth() + .padding(bottom = 8.dp)) + + if (color != null) { + Box( + Modifier + .background(Color(color)) + .fillMaxWidth() + .height(16.dp) + ) + Spacer(Modifier.height(8.dp)) + } + + Column(Modifier.padding(8.dp)) { + CollectionScreen_Entry( + icon = Icons.Default.Sync, + title = stringResource(R.string.collection_synchronization), + text = + if (sync) + stringResource(R.string.collection_synchronization_on) + else + stringResource(R.string.collection_synchronization_off), + control = { + Switch( + checked = sync, + onCheckedChange = onSetSync + ) + } + ) + + CollectionScreen_Entry( + icon = Icons.Default.DoNotDisturbOn, + title = stringResource(R.string.collection_read_only), + text = when (readOnly) { + CollectionScreenModel.ReadOnlyState.READ_ONLY_BY_SERVER -> + stringResource(R.string.collection_read_only_by_server) + CollectionScreenModel.ReadOnlyState.READ_ONLY_BY_SETTING -> + stringResource(R.string.collection_read_only_by_setting) + CollectionScreenModel.ReadOnlyState.READ_ONLY_BY_USER -> + stringResource(R.string.collection_read_only_forced) + else -> stringResource(R.string.collection_read_write) + }, + control = { + Switch( + checked = readOnly.isReadOnly(), + enabled = readOnly.canUserChange(), + onCheckedChange = onSetForceReadOnly + ) + } + ) + + if (displayName != null) + CollectionScreen_Entry( + title = stringResource(R.string.collection_title), + text = title + ) + + if (description != null) + CollectionScreen_Entry( + title = stringResource(R.string.collection_description), + text = description + ) + + if (owner != null) + CollectionScreen_Entry( + icon = Icons.Default.AccountBox, + title = stringResource(R.string.collection_owner), + text = owner + ) + + if (supportsWebPush) { + val text = + if (pushSubscriptionCreated != null && pushSubscriptionExpires != null) { + val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).withZone(ZoneId.systemDefault()) + stringResource( + R.string.collection_push_subscribed_at, + formatter.format(Instant.ofEpochSecond(pushSubscriptionCreated)), + formatter.format(Instant.ofEpochSecond(pushSubscriptionExpires)) + ) + } else + stringResource(R.string.collection_push_web_push) + CollectionScreen_Entry( + icon = Icons.Default.CloudSync, + title = stringResource(R.string.collection_push_support), + text = text + ) + } + + Column(Modifier.padding(start = 44.dp)) { + if (sync && lastSynced.isNotEmpty()) { + val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) + + for (lastSync in lastSynced) { + val dataType = when (lastSync.dataType) { + SyncDataType.EVENTS.name -> stringResource(R.string.collection_datatype_events) + SyncDataType.TASKS.name -> stringResource(R.string.collection_datatype_tasks) + SyncDataType.CONTACTS.name -> stringResource(R.string.collection_datatype_contacts) + else -> lastSync.dataType + } + Text( + text = stringResource(R.string.collection_last_sync, dataType), + style = MaterialTheme.typography.titleMedium + ) + + val time = ZonedDateTime.ofInstant(Instant.ofEpochMilli(lastSync.lastSynced), ZoneId.systemDefault()) + Text( + text = formatter.format(time), + style = MaterialTheme.typography.bodyLarge + ) + + Spacer(Modifier.height(16.dp)) + } + } + + Text( + text = stringResource(R.string.collection_url), + style = MaterialTheme.typography.titleMedium + ) + SelectionContainer { + Text( + text = url, + style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), + modifier = Modifier + ) + } + } + } + } + } + } +} + +@Composable +fun CollectionScreen_Entry( + icon: ImageVector? = null, + title: String? = null, + text: String? = null, + control: @Composable (() -> Unit)? = null +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = 16.dp) + ) { + if (icon != null) + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier + .padding(end = 12.dp) + .size(32.dp) + ) + else + Spacer(Modifier.width(44.dp)) + + Column(Modifier.weight(1f)) { + if (title != null) + Text( + text = title, + style = MaterialTheme.typography.titleMedium + ) + + if (text != null) + Text( + text = text, + style = MaterialTheme.typography.bodyLarge + ) + } + + if (control != null) + control() + } +} + +@Composable +@Preview +fun CollectionScreen_Preview() { + CollectionScreen( + inProgress = true, + color = 0xff14c0c4.toInt(), + sync = true, + readOnly = CollectionScreenModel.ReadOnlyState.READ_ONLY_BY_USER, + url = "https://example.com/calendar", + title = "Some Calendar, with some additional text to make it wrap around and stuff.", + displayName = "Some Calendar, with some additional text to make it wrap around and stuff.", + description = "This is some description of the calendar. It can be long and wrap around.", + owner = "Some One", + lastSynced = listOf( + DavSyncStatsRepository.LastSynced( + dataType = "Some Sync Data Type", + lastSynced = 1234567890 + ) + ), + supportsWebPush = true, + pushSubscriptionCreated = 1731846565, + pushSubscriptionExpires = 1731847565 + ) +} + + +@Composable +fun DeleteCollectionDialog( + displayName: String, + onDismiss: () -> Unit = {}, + onConfirm: () -> Unit = {} +) { + AlertDialog( + icon = { + Icon(Icons.Default.DeleteForever, contentDescription = null) + }, + title = { + Text(stringResource(R.string.collection_delete)) + }, + text = { + Text(stringResource(R.string.collection_delete_warning, displayName)) + }, + confirmButton = { + Button(onClick = onConfirm) { + Text(stringResource(R.string.dialog_delete)) + } + }, + dismissButton = { + OutlinedButton(onClick = onDismiss) { + Text(stringResource(android.R.string.cancel)) + } + }, + onDismissRequest = onDismiss + ) +} + +@Composable +@Preview +fun DeleteCollectionDialog_Preview() { + DeleteCollectionDialog( + displayName = "Some Calendar" + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionScreenModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionScreenModel.kt new file mode 100644 index 0000000..9208678 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionScreenModel.kt @@ -0,0 +1,144 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.repository.AccountRepository +import at.bitfire.davdroid.repository.DavCollectionRepository +import at.bitfire.davdroid.repository.DavSyncStatsRepository +import at.bitfire.davdroid.settings.Settings +import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.util.DavUtils.lastSegment +import dagger.Lazy +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +@HiltViewModel(assistedFactory = CollectionScreenModel.Factory::class) +class CollectionScreenModel @AssistedInject constructor( + private val accountRepository: AccountRepository, + @Assisted val collectionId: Long, + db: AppDatabase, + private val collectionRepository: DavCollectionRepository, + private val collectionSelectedUseCase: Lazy, + settings: SettingsManager, + syncStatsRepository: DavSyncStatsRepository +): ViewModel() { + + @AssistedFactory + interface Factory { + fun create(collectionId: Long): CollectionScreenModel + } + + /** Whether an operation (like deleting the collection) is currently in progress */ + var inProgress by mutableStateOf(false) + private set + + var invalid by mutableStateOf(false) + var error by mutableStateOf(null) + private set + + val collection = collectionRepository.getFlow(collectionId) + .map { + if (it == null) + invalid = true + it + } + .stateIn(viewModelScope, SharingStarted.Eagerly, null) + + + enum class ReadOnlyState { + READ_ONLY_BY_SETTING, + READ_ONLY_BY_SERVER, + READ_ONLY_BY_USER, + READ_WRITE; + + fun canUserChange() = this == READ_WRITE || this == READ_ONLY_BY_USER + fun isReadOnly() = this != READ_WRITE + } + + /** whether address-books are read-only by policy (if yes, it overrides everything else) */ + private val forceReadOnlyAddressBooks = settings.getBooleanFlow(Settings.FORCE_READ_ONLY_ADDRESSBOOKS, false) + + val readOnly: Flow = combine(collection, forceReadOnlyAddressBooks) { collection, forceReadOnlyAddressBook -> + when { + collection?.type == Collection.TYPE_ADDRESSBOOK && forceReadOnlyAddressBook -> + ReadOnlyState.READ_ONLY_BY_SETTING + collection?.privWriteContent == false -> + ReadOnlyState.READ_ONLY_BY_SERVER + collection?.forceReadOnly == true -> + ReadOnlyState.READ_ONLY_BY_USER + else -> + ReadOnlyState.READ_WRITE + } + } + + + private val principalDao = db.principalDao() + val owner: Flow = collection.map { collection -> + collection?.ownerId?.let { ownerId -> + val principal = principalDao.getAsync(ownerId) + principal.displayName ?: principal.url.lastSegment + } + } + + val lastSynced = syncStatsRepository.getLastSyncedFlow(collectionId) + + + /** Scope for operations that must not be cancelled. */ + private val noCancellationScope = CoroutineScope(SupervisorJob()) + + /** + * Deletes the collection from the database and the server. + */ + fun delete() { + val collection = collection.value ?: return + + inProgress = true + noCancellationScope.launch { + try { + collectionRepository.deleteRemote(collection) + } catch (e: Exception) { + error = e + } finally { + inProgress = false + } + } + } + + fun resetError() { + error = null + } + + fun setForceReadOnly(forceReadOnly: Boolean) { + viewModelScope.launch { + collectionRepository.setForceReadOnly(collectionId, forceReadOnly) + collectionSelectedUseCase.get().handleWithDelay(collectionId) + } + } + + fun setSync(sync: Boolean) { + viewModelScope.launch { + collectionRepository.setSync(collectionId, sync) + collectionSelectedUseCase.get().handleWithDelay(collectionId) + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionSelectedUseCase.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionSelectedUseCase.kt new file mode 100644 index 0000000..5ee5427 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionSelectedUseCase.kt @@ -0,0 +1,89 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import android.accounts.Account +import at.bitfire.davdroid.di.DefaultDispatcher +import at.bitfire.davdroid.push.PushRegistrationManager +import at.bitfire.davdroid.repository.AccountRepository +import at.bitfire.davdroid.repository.DavCollectionRepository +import at.bitfire.davdroid.repository.DavServiceRepository +import at.bitfire.davdroid.sync.worker.SyncWorkerManager +import at.bitfire.davdroid.ui.account.CollectionSelectedUseCase.Companion.DELAY_MS +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Performs actions when a collection was (un)selected for synchronization. + * + * @see handleWithDelay + */ +@Singleton +class CollectionSelectedUseCase @Inject constructor( + private val accountRepository: AccountRepository, + private val collectionRepository: DavCollectionRepository, + @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher, + private val pushRegistrationManager: PushRegistrationManager, + private val serviceRepository: DavServiceRepository, + private val syncWorkerManager: SyncWorkerManager +) { + + private val delayJobs: ConcurrentHashMap = ConcurrentHashMap() + private val scope = CoroutineScope(SupervisorJob()) + + /** + * After a delay of [DELAY_MS] ms: + * + * 1. Enqueues a one-time sync for account of the collection. + * 2. Updates push subscriptions for the service of the collection. + * + * Resets delay when called again before delay finishes. + * + * @param collectionId ID of the collection that was (un)selected for synchronization + */ + suspend fun handleWithDelay(collectionId: Long) { + val collection = collectionRepository.getAsync(collectionId) ?: return + val service = serviceRepository.get(collection.serviceId) ?: return + val account = accountRepository.fromName(service.accountName) + + // Atomically cancel, launch and remember delay coroutine of given account + delayJobs.compute(account) { _, previousJob -> + // Stop previous delay, if exists + previousJob?.cancel() + + scope.launch(defaultDispatcher) { + // wait + delay(DELAY_MS) + + // enqueue sync + syncWorkerManager.enqueueOneTimeAllAuthorities(account) + + // update push subscriptions + pushRegistrationManager.update(service.id) + + // remove complete job + delayJobs -= account + } + } + } + + + companion object { + + /** + * Length of delay in milliseconds + */ + const val DELAY_MS = 5000L // 5 seconds + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionsList.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionsList.kt new file mode 100644 index 0000000..4feb939 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionsList.kt @@ -0,0 +1,275 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.EventNote +import androidx.compose.material.icons.filled.Contacts +import androidx.compose.material.icons.filled.RemoveCircle +import androidx.compose.material.icons.filled.Task +import androidx.compose.material.icons.filled.Today +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.itemKey +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.ui.AppTheme +import okhttp3.HttpUrl.Companion.toHttpUrl + +@Composable +fun CollectionsList( + collections: LazyPagingItems, + onChangeSync: (collectionId: Long, sync: Boolean) -> Unit, + modifier: Modifier = Modifier, + onSubscribe: (collection: Collection) -> Unit = {}, + onCollectionDetails: ((collection: Collection) -> Unit)? = null, + state: LazyListState = rememberLazyListState() +) { + LazyColumn( + contentPadding = PaddingValues(8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top), + modifier = modifier, + state = state + ) { + items( + count = collections.itemCount, + key = collections.itemKey { it.id } + ) { index -> + collections[index]?.let { item -> + if (item.type == Collection.TYPE_WEBCAL) + CollectionsList_Item_Webcal( + item, + onSubscribe = { onSubscribe(item) } + ) + else + CollectionsList_Item_Standard( + item, + onChangeSync = { onChangeSync(item.id, it) }, + onCollectionDetails = onCollectionDetails + ) + } + } + + // make sure we can scroll down far enough so that the last item is not covered by a FAB + item { + Spacer(Modifier.height(140.dp)) + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun CollectionList_Item( + color: Color? = null, + title: String, + description: String? = null, + addressBook: Boolean = false, + calendar: Boolean = false, + todoList: Boolean = false, + journal: Boolean = false, + readOnly: Boolean = false, + onShowDetails: (() -> Unit)? = null, + syncControl: @Composable () -> Unit +) { + var modifier = Modifier.fillMaxWidth() + if (onShowDetails != null) + modifier = modifier.clickable(onClick = onShowDetails) + + ElevatedCard( + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer + ), + modifier = modifier + ) { + Row(Modifier.height(IntrinsicSize.Max)) { + Box( + Modifier + .background(color ?: Color.Transparent) + .width(8.dp) + .fillMaxHeight()) + + Column(Modifier.padding(8.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Column(Modifier.weight(1f)) { + Text(title, style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold)) + + if (description != null) + Text(description, style = MaterialTheme.typography.bodyMedium) + } + + syncControl() + } + FlowRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (addressBook) + CollectionList_Item_Chip(Icons.Default.Contacts, stringResource(R.string.account_contacts)) + + if (calendar) + CollectionList_Item_Chip(Icons.Default.Today, stringResource(R.string.account_calendar)) + if (todoList) + CollectionList_Item_Chip(Icons.Default.Task, stringResource(R.string.account_task_list)) + if (journal) + CollectionList_Item_Chip(Icons.AutoMirrored.Default.EventNote, stringResource(R.string.account_journal)) + + if (readOnly) + CollectionList_Item_Chip(Icons.Default.RemoveCircle, stringResource(R.string.account_read_only)) + } + } + } + } +} + +@Composable +fun CollectionsList_Item_Standard( + collection: Collection, + onChangeSync: (sync: Boolean) -> Unit = {}, + onCollectionDetails: ((collection: Collection) -> Unit)? = null +) { + CollectionList_Item( + color = collection.color?.let { Color(it) }, + title = collection.title(), + description = collection.description, + addressBook = collection.type == Collection.TYPE_ADDRESSBOOK, + calendar = collection.supportsVEVENT == true, + todoList = collection.supportsVTODO == true, + journal = collection.supportsVJOURNAL == true, + readOnly = collection.readOnly(), + onShowDetails = { + if (onCollectionDetails != null) + onCollectionDetails(collection) + } + ) { + val context = LocalContext.current + Switch( + checked = collection.sync, + onCheckedChange = onChangeSync, + modifier = Modifier + .padding(start = 4.dp, top = 4.dp, bottom = 4.dp) + .semantics { + contentDescription = context.getString(R.string.account_synchronize_this_collection) + } + ) + } +} + +@Composable +@Preview(locale = "de") +fun CollectionsList_Item_Standard_Preview() { + AppTheme { + CollectionsList_Item_Standard( + Collection( + type = Collection.TYPE_CALENDAR, + url = "https://example.com/caldav/sample".toHttpUrl(), + displayName = "Sample Calendar", + description = "This Sample Calendar even has some lengthy description.", + color = 0xffff0000.toInt(), + sync = true, + forceReadOnly = true, + supportsVEVENT = true, + supportsVTODO = true, + supportsVJOURNAL = true + ) + ) + } +} + +@Composable +fun CollectionsList_Item_Webcal( + collection: Collection, + onSubscribe: () -> Unit = {} +) { + CollectionList_Item( + color = collection.color?.let { Color(it) }, + title = collection.title(), + description = collection.description, + calendar = true, + readOnly = true + ) { + OutlinedButton( + onClick = onSubscribe, + modifier = Modifier.padding(start = 4.dp) + ) { + Text("Subscribe") + } + } +} + +@Composable +@Preview +fun CollectionList_Item_Webcal_Preview() { + AppTheme { + CollectionsList_Item_Webcal( + Collection( + type = Collection.TYPE_WEBCAL, + url = "https://example.com/caldav/sample".toHttpUrl(), + displayName = "Sample Subscription", + description = "This Sample Subscription even has some lengthy description.", + color = 0xffff0000.toInt() + ) + ) + } +} + +@Composable +fun CollectionList_Item_Chip(icon: ImageVector, text: String) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .background(MaterialTheme.colorScheme.surfaceContainerHighest, shape = RoundedCornerShape(8.dp)) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Icon( + icon, + contentDescription = text, + modifier = Modifier.size(20.dp) + ) + Text( + text, + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(start = 4.dp) + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateAddressBookActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateAddressBookActivity.kt new file mode 100644 index 0000000..6a704d0 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateAddressBookActivity.kt @@ -0,0 +1,45 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import android.accounts.Account +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.TaskStackBuilder +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class CreateAddressBookActivity: AppCompatActivity() { + + companion object { + const val EXTRA_ACCOUNT = "account" + } + + val account by lazy { + intent.getParcelableExtra(EXTRA_ACCOUNT) ?: throw IllegalArgumentException("EXTRA_ACCOUNT must be set") + } + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + CreateAddressBookScreen( + account = account, + onNavUp = ::onSupportNavigateUp, + onFinish = ::finish + ) + } + } + + override fun supportShouldUpRecreateTask(targetIntent: Intent) = true + + override fun onPrepareSupportNavigateUpTaskStack(builder: TaskStackBuilder) { + builder.editIntentAt(builder.intentCount - 1)?.putExtra(AccountActivity.EXTRA_ACCOUNT, account) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateAddressBookModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateAddressBookModel.kt new file mode 100644 index 0000000..a6bc6cd --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateAddressBookModel.kt @@ -0,0 +1,98 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import android.accounts.Account +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import at.bitfire.davdroid.db.HomeSet +import at.bitfire.davdroid.repository.DavCollectionRepository +import at.bitfire.davdroid.repository.DavHomeSetRepository +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch + +@HiltViewModel(assistedFactory = CreateAddressBookModel.Factory::class) +class CreateAddressBookModel @AssistedInject constructor( + @Assisted val account: Account, + private val collectionRepository: DavCollectionRepository, + homeSetRepository: DavHomeSetRepository +): ViewModel() { + + @AssistedFactory + interface Factory { + fun create(account: Account): CreateAddressBookModel + } + + val addressBookHomeSets = homeSetRepository.getAddressBookHomeSetsFlow(account) + + + // UI state + + data class UiState( + val error: Exception? = null, + val success: Boolean = false, + + val displayName: String = "", + val description: String = "", + val selectedHomeSet: HomeSet? = null, + val isCreating: Boolean = false + ) { + val canCreate = !isCreating && displayName.isNotBlank() && selectedHomeSet != null + } + + var uiState by mutableStateOf(UiState()) + private set + + fun resetError() { + uiState = uiState.copy(error = null) + } + + fun setDisplayName(displayName: String) { + uiState = uiState.copy(displayName = displayName) + } + + fun setDescription(description: String) { + uiState = uiState.copy(description = description) + } + + fun setHomeSet(homeSet: HomeSet) { + uiState = uiState.copy(selectedHomeSet = homeSet) + } + + + // actions + + /* Creating collections shouldn't be cancelled when the view is destroyed, otherwise we might + end up with collections on the server that are not represented in the database/UI. */ + private val createCollectionScope = CoroutineScope(SupervisorJob()) + + fun createAddressBook() { + val homeSet = uiState.selectedHomeSet ?: return + uiState = uiState.copy(isCreating = true) + + createCollectionScope.launch { + uiState = try { + collectionRepository.createAddressBook( + account = account, + homeSet = homeSet, + displayName = uiState.displayName, + description = uiState.description + ) + + uiState.copy(isCreating = false, success = true) + } catch (e: Exception) { + uiState.copy(isCreating = false, error = e) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateAddressBookScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateAddressBookScreen.kt new file mode 100644 index 0000000..44ff97e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateAddressBookScreen.kt @@ -0,0 +1,203 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import android.accounts.Account +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.HomeSet +import at.bitfire.davdroid.ui.AppTheme +import at.bitfire.davdroid.ui.composable.ExceptionInfoDialog +import at.bitfire.davdroid.ui.composable.ProgressBar +import okhttp3.HttpUrl.Companion.toHttpUrl + +@Composable +fun CreateAddressBookScreen( + account: Account, + onNavUp: () -> Unit = {}, + onFinish: () -> Unit = {} +) { + val model: CreateAddressBookModel = hiltViewModel( + creationCallback = { factory: CreateAddressBookModel.Factory -> + factory.create(account) + } + ) + val uiState = model.uiState + + if (uiState.success) + onFinish() + + CreateAddressBookScreen( + error = uiState.error, + onResetError = model::resetError, + displayName = uiState.displayName, + onSetDisplayName = model::setDisplayName, + description = uiState.description, + onSetDescription = model::setDescription, + homeSets = model.addressBookHomeSets.collectAsStateWithLifecycle(emptyList()).value, + selectedHomeSet = uiState.selectedHomeSet, + onSelectHomeSet = model::setHomeSet, + canCreate = uiState.canCreate, + isCreating = uiState.isCreating, + onCreate = model::createAddressBook, + onNavUp = onNavUp + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CreateAddressBookScreen( + error: Exception? = null, + onResetError: () -> Unit = {}, + displayName: String = "", + onSetDisplayName: (String) -> Unit = {}, + description: String = "", + onSetDescription: (String) -> Unit = {}, + homeSets: List, + selectedHomeSet: HomeSet? = null, + onSelectHomeSet: (HomeSet) -> Unit = {}, + canCreate: Boolean = false, + isCreating: Boolean = false, + onCreate: () -> Unit = {}, + onNavUp: () -> Unit = {} +) { + AppTheme { + if (error != null) + ExceptionInfoDialog( + exception = error, + onDismiss = onResetError + ) + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.create_addressbook)) }, + navigationIcon = { + IconButton(onClick = onNavUp) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.navigate_up)) + } + } + ) + } + ) { padding -> + Column( + Modifier + .padding(padding) + .verticalScroll(rememberScrollState()) + ) { + if (isCreating) + ProgressBar( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + + Column( + Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + val focusRequester = remember { FocusRequester() } + OutlinedTextField( + value = displayName, + onValueChange = onSetDisplayName, + label = { Text(stringResource(R.string.create_collection_display_name)) }, + singleLine = true, + enabled = !isCreating, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next + ), + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester) + ) + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + OutlinedTextField( + value = description, + onValueChange = onSetDescription, + label = { Text(stringResource(R.string.create_collection_description_optional)) }, + singleLine = true, + enabled = !isCreating, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + onCreate() + } + ), + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + ) + + HomeSetSelection( + homeSet = selectedHomeSet, + homeSets = homeSets, + onSelectHomeSet = onSelectHomeSet, + enabled = !isCreating, + modifier = Modifier.padding(vertical = 8.dp) + ) + + Text( + stringResource(R.string.create_addressbook_maybe_not_supported), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(vertical = 8.dp) + ) + + Button( + onClick = onCreate, + enabled = canCreate + ) { + Text(stringResource(R.string.create_addressbook)) + } + } + } + } + } +} + +@Composable +@Preview +fun CreateAddressBookScreen_Preview() { + CreateAddressBookScreen( + displayName = "Address Book", + homeSets = listOf( + HomeSet(0, 0, true, "https://example.com/some/homeset".toHttpUrl()) + ) + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateCalendarActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateCalendarActivity.kt new file mode 100644 index 0000000..7a8be75 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateCalendarActivity.kt @@ -0,0 +1,48 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import android.accounts.Account +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.TaskStackBuilder +import at.bitfire.davdroid.ui.AppTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class CreateCalendarActivity: AppCompatActivity() { + + companion object { + const val EXTRA_ACCOUNT = "account" + } + + val account by lazy { + intent.getParcelableExtra(EXTRA_ACCOUNT) ?: throw IllegalArgumentException("EXTRA_ACCOUNT must be set") + } + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + AppTheme { + CreateCalendarScreen( + account = account, + onNavUp = ::onSupportNavigateUp, + onFinish = ::finish + ) + } + } + } + + override fun supportShouldUpRecreateTask(targetIntent: Intent) = true + + override fun onPrepareSupportNavigateUpTaskStack(builder: TaskStackBuilder) { + builder.editIntentAt(builder.intentCount - 1)?.putExtra(AccountActivity.EXTRA_ACCOUNT, account) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateCalendarModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateCalendarModel.kt new file mode 100644 index 0000000..ff0d72b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateCalendarModel.kt @@ -0,0 +1,158 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import android.accounts.Account +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import at.bitfire.davdroid.db.HomeSet +import at.bitfire.davdroid.repository.DavCollectionRepository +import at.bitfire.davdroid.repository.DavHomeSetRepository +import at.bitfire.synctools.icalendar.Css3Color +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +import java.text.Collator +import java.time.ZoneId +import java.time.format.TextStyle +import java.util.Locale + +@HiltViewModel(assistedFactory = CreateCalendarModel.Factory::class) +class CreateCalendarModel @AssistedInject constructor( + @Assisted val account: Account, + private val collectionRepository: DavCollectionRepository, + homeSetRepository: DavHomeSetRepository +): ViewModel() { + + @AssistedFactory + interface Factory { + fun create(account: Account): CreateCalendarModel + } + + val calendarHomeSets = homeSetRepository.getCalendarHomeSetsFlow(account) + + data class TimeZoneInfo( + val id: String, + val displayName: String, + ) + + /** List of available time zones as pairs. */ + val timeZones: Flow> = flow { + val timeZones = mutableListOf() + val locale = Locale.getDefault() + for (id in ZoneId.getAvailableZoneIds()) + timeZones += TimeZoneInfo( + id, + ZoneId.of(id).getDisplayName(TextStyle.FULL, locale), + ) + + val collator = Collator.getInstance() + val result = timeZones.sortedBy { collator.getCollationKey(it.displayName) } + + emit(result) + }.flowOn(Dispatchers.Default) + + + // UI state + + data class UiState( + val error: Exception? = null, + val success: Boolean = false, + + val color: Int = Css3Color.entries.random().argb, + val displayName: String = "", + val description: String = "", + val timeZoneId: String? = null, + val supportVEVENT: Boolean = true, + val supportVTODO: Boolean = true, + val supportVJOURNAL: Boolean = true, + val homeSet: HomeSet? = null, + val isCreating: Boolean = false + ) { + val canCreate = !isCreating && displayName.isNotBlank() && homeSet != null + } + + var uiState by mutableStateOf(UiState()) + private set + + fun resetError() { + uiState = uiState.copy(error = null) + } + + fun setColor(color: Int) { + uiState = uiState.copy(color = color) + } + + fun setDisplayName(displayName: String) { + uiState = uiState.copy(displayName = displayName) + } + + fun setDescription(description: String) { + uiState = uiState.copy(description = description) + } + + fun setTimeZoneId(timeZoneId: String?) { + uiState = uiState.copy(timeZoneId = timeZoneId) + } + + fun setSupportVEVENT(supportVEVENT: Boolean) { + uiState = uiState.copy(supportVEVENT = supportVEVENT) + } + + fun setSupportVTODO(supportVTODO: Boolean) { + uiState = uiState.copy(supportVTODO = supportVTODO) + } + + fun setSupportVJOURNAL(supportVJOURNAL: Boolean) { + uiState = uiState.copy(supportVJOURNAL = supportVJOURNAL) + } + + fun setHomeSet(homeSet: HomeSet) { + uiState = uiState.copy(homeSet = homeSet) + } + + + // actions + + /* Creating collections shouldn't be cancelled when the view is destroyed, otherwise we might + end up with collections on the server that are not represented in the database/UI. */ + private val createCollectionScope = CoroutineScope(SupervisorJob()) + + fun createCalendar() { + val homeSet = uiState.homeSet ?: return + uiState = uiState.copy(isCreating = true) + + createCollectionScope.launch { + uiState = try { + collectionRepository.createCalendar( + account = account, + homeSet = homeSet, + color = uiState.color, + displayName = uiState.displayName, + description = uiState.description, + timeZoneId = uiState.timeZoneId, + supportVEVENT = uiState.supportVEVENT, + supportVTODO = uiState.supportVTODO, + supportVJOURNAL = uiState.supportVJOURNAL + ) + + uiState.copy(isCreating = false, success = true) + } catch (e: Exception) { + uiState.copy(isCreating = false, error = e) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateCalendarScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateCalendarScreen.kt new file mode 100644 index 0000000..e1b3af7 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateCalendarScreen.kt @@ -0,0 +1,381 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import android.accounts.Account +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +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.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.HomeSet +import at.bitfire.davdroid.ui.AppTheme +import at.bitfire.davdroid.ui.composable.ExceptionInfoDialog +import at.bitfire.davdroid.ui.composable.ProgressBar +import at.bitfire.davdroid.ui.widget.CalendarColorPickerDialog +import at.bitfire.synctools.icalendar.Css3Color +import okhttp3.HttpUrl.Companion.toHttpUrl + +@Composable +fun CreateCalendarScreen( + account: Account, + onFinish: () -> Unit, + onNavUp: () -> Unit +) { + val model: CreateCalendarModel = hiltViewModel( + creationCallback = { factory: CreateCalendarModel.Factory -> + factory.create(account) + } + ) + val uiState = model.uiState + + if (uiState.success) + onFinish() + + CreateCalendarScreen( + isCreating = uiState.isCreating, + error = uiState.error, + onResetError = model::resetError, + color = uiState.color, + onSetColor = model::setColor, + displayName = uiState.displayName, + onSetDisplayName = model::setDisplayName, + description = uiState.description, + onSetDescription = model::setDescription, + timeZones = model.timeZones.collectAsStateWithLifecycle(emptyList()).value, + timeZone = uiState.timeZoneId, + onSelectTimeZone = model::setTimeZoneId, + supportVEVENT = uiState.supportVEVENT, + onSetSupportVEVENT = model::setSupportVEVENT, + supportVTODO = uiState.supportVTODO, + onSetSupportVTODO = model::setSupportVTODO, + supportVJOURNAL = uiState.supportVJOURNAL, + onSetSupportVJOURNAL = model::setSupportVJOURNAL, + homeSets = model.calendarHomeSets.collectAsStateWithLifecycle(emptyList()).value, + selectedHomeSet = uiState.homeSet, + onSelectHomeSet = model::setHomeSet, + canCreate = uiState.canCreate, + onCreate = model::createCalendar, + onNavUp = onNavUp + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CreateCalendarScreen( + error: Exception? = null, + onResetError: () -> Unit = {}, + color: Int = Css3Color.green.argb, + onSetColor: (Int) -> Unit = {}, + displayName: String = "", + onSetDisplayName: (String) -> Unit = {}, + description: String = "", + onSetDescription: (String) -> Unit = {}, + timeZones: List, + timeZone: String? = null, + onSelectTimeZone: (String?) -> Unit = {}, + supportVEVENT: Boolean = true, + onSetSupportVEVENT: (Boolean) -> Unit = {}, + supportVTODO: Boolean = true, + onSetSupportVTODO: (Boolean) -> Unit = {}, + supportVJOURNAL: Boolean = true, + onSetSupportVJOURNAL: (Boolean) -> Unit = {}, + homeSets: List, + selectedHomeSet: HomeSet? = null, + onSelectHomeSet: (HomeSet) -> Unit = {}, + canCreate: Boolean = false, + isCreating: Boolean = false, + onCreate: () -> Unit = {}, + onNavUp: () -> Unit = {} +) { + val context = LocalContext.current + + AppTheme { + if (error != null) + ExceptionInfoDialog( + exception = error, + onDismiss = onResetError + ) + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.create_calendar)) }, + navigationIcon = { + IconButton(onClick = onNavUp) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.navigate_up)) + } + } + ) + } + ) { padding -> + Column( + Modifier + .padding(padding) + .verticalScroll(rememberScrollState()) + ) { + if (isCreating) + ProgressBar( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + + Column( + Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.height(IntrinsicSize.Max) + ) { + val focusRequester = remember { FocusRequester() } + OutlinedTextField( + value = displayName, + onValueChange = onSetDisplayName, + label = { Text(stringResource(R.string.create_collection_display_name)) }, + singleLine = true, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next + ), + modifier = Modifier + .weight(1f) + .focusRequester(focusRequester) + .padding(end = 8.dp) + ) + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + var showColorPicker by remember { mutableStateOf(false) } + OutlinedButton( + onClick = { + showColorPicker = true + }, + colors = ButtonDefaults.outlinedButtonColors( + containerColor = Color(color) + ), + shape = RoundedCornerShape(4.dp), + modifier = Modifier + .fillMaxHeight() + .aspectRatio(1f) + .semantics { + contentDescription = context.getString(R.string.create_collection_color) + } + ) { /* no content */ } + if (showColorPicker) { + CalendarColorPickerDialog( + onSelectColor = { color -> + onSetColor(color) + showColorPicker = false + }, + onDismiss = { showColorPicker = false } + ) + } + } + + OutlinedTextField( + value = description, + onValueChange = onSetDescription, + label = { Text(stringResource(R.string.create_collection_description_optional)) }, + singleLine = true, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { onCreate() } + ), + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + ) + + var expanded by remember { mutableStateOf(false) } + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it }, + modifier = Modifier.padding(top = 8.dp) + ) { + OutlinedTextField( + label = { Text(stringResource(R.string.create_calendar_time_zone_optional)) }, + value = timeZone ?: stringResource(R.string.create_calendar_time_zone_none), + onValueChange = { /* read-only */ }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + readOnly = true, + modifier = Modifier + .fillMaxWidth() + .menuAnchor(MenuAnchorType.PrimaryNotEditable) + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.fillMaxHeight() + ) { + Text( + text = stringResource(R.string.create_calendar_time_zone_none), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .clickable { + onSelectTimeZone(null) + expanded = false + } + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 4.dp) + ) + + for (tz in timeZones) + Text( + text = tz.displayName, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .clickable { + onSelectTimeZone(tz.id) + expanded = false + } + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 4.dp) + ) + } + } + + Text( + stringResource(R.string.create_calendar_type), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 16.dp) + ) + CheckBoxRow( + label = stringResource(R.string.create_calendar_type_vevent), + value = supportVEVENT, + onValueChange = onSetSupportVEVENT + ) + CheckBoxRow( + label = stringResource(R.string.create_calendar_type_vtodo), + value = supportVTODO, + onValueChange = onSetSupportVTODO + ) + CheckBoxRow( + label = stringResource(R.string.create_calendar_type_vjournal), + value = supportVJOURNAL, + onValueChange = onSetSupportVJOURNAL + ) + + HomeSetSelection( + homeSet = selectedHomeSet, + homeSets = homeSets, + onSelectHomeSet = onSelectHomeSet, + modifier = Modifier.padding(vertical = 8.dp) + ) + + Text( + stringResource(R.string.create_calendar_maybe_not_supported), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(vertical = 8.dp) + ) + + Button( + onClick = onCreate, + enabled = canCreate + ) { + Text(stringResource(R.string.create_calendar)) + } + } + } + } + } +} + +@Composable +fun CheckBoxRow( + label: String, + value: Boolean, + onValueChange: (Boolean) -> Unit +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.clickable { onValueChange(!value) } + ) { + Checkbox( + checked = value, + onCheckedChange = onValueChange + ) + Text( + label, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f) + ) + } +} + +@Composable +@Preview +fun CreateCalendarScreenPreview() { + CreateCalendarScreen( + timeZones = listOf( + CreateCalendarModel.TimeZoneInfo( + id = "Europe/Vienna", + displayName = "Vienna (Europe)" + ) + ), + timeZone = "Europe/Vienna", + + homeSets = listOf( + HomeSet( + id = 0, + serviceId = 0, + personal = true, + url = "https://example.com/some/homeset".toHttpUrl() + ) + ) + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/GetBindableHomeSetsFromServiceUseCase.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/GetBindableHomeSetsFromServiceUseCase.kt new file mode 100644 index 0000000..f4e4be7 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/GetBindableHomeSetsFromServiceUseCase.kt @@ -0,0 +1,29 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import at.bitfire.davdroid.db.HomeSet +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.repository.DavHomeSetRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import javax.inject.Inject + +class GetBindableHomeSetsFromServiceUseCase @Inject constructor( + val homeSetRepository: DavHomeSetRepository +) { + + @OptIn(ExperimentalCoroutinesApi::class) + operator fun invoke(serviceFlow: Flow): Flow> = + serviceFlow.flatMapLatest { service -> + if (service == null) + flowOf(emptyList()) + else + homeSetRepository.getBindableByServiceFlow(service.id) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/GetServiceCollectionPagerUseCase.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/GetServiceCollectionPagerUseCase.kt new file mode 100644 index 0000000..e4c2a46 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/GetServiceCollectionPagerUseCase.kt @@ -0,0 +1,72 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.map +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.CollectionType +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.repository.DavCollectionRepository +import at.bitfire.davdroid.settings.Settings +import at.bitfire.davdroid.settings.SettingsManager +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +/** + * Gets a list of collections for a service and type, optionally filtered by "show only personal" setting. + * + * Takes the "force read-only address books" setting into account: if set, all address books will have "forceReadOnly" set. + */ +class GetServiceCollectionPagerUseCase @Inject constructor( + val collectionRepository: DavCollectionRepository, + val settings: SettingsManager +) { + + companion object { + const val PAGER_SIZE = 20 + } + + val forceReadOnlyAddressBooksFlow = settings.getBooleanFlow(Settings.FORCE_READ_ONLY_ADDRESSBOOKS, false) + + + @OptIn(ExperimentalCoroutinesApi::class) + operator fun invoke( + serviceFlow: Flow, + @CollectionType collectionType: String, + showOnlyPersonalFlow: Flow + ): Flow> = + combine(serviceFlow, showOnlyPersonalFlow, forceReadOnlyAddressBooksFlow) { service, onlyPersonal, forceReadOnlyAddressBooks -> + service?.let { service -> + val dataFlow = Pager( + config = PagingConfig(PAGER_SIZE), + pagingSourceFactory = { + if (onlyPersonal == true) + collectionRepository.pagePersonalByServiceAndType(service.id, collectionType) + else + collectionRepository.pageByServiceAndType(service.id, collectionType) + } + ).flow + + // set "forceReadOnly" for every address book if requested + if (forceReadOnlyAddressBooks && collectionType == Collection.TYPE_ADDRESSBOOK) + dataFlow.map { pagingData -> + pagingData.map { collection -> + collection.copy(forceReadOnly = true) + } + } + else + dataFlow + } ?: flowOf(PagingData.empty()) + }.flatMapLatest { it } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/HomeSetSelection.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/HomeSetSelection.kt new file mode 100644 index 0000000..811072c --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/HomeSetSelection.kt @@ -0,0 +1,123 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +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 androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.HomeSet +import okhttp3.HttpUrl.Companion.toHttpUrl + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeSetSelection( + homeSet: HomeSet?, + homeSets: List, + onSelectHomeSet: (HomeSet) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true +) { + Column(modifier) { + // select first home set if none is selected + LaunchedEffect(homeSets) { + if (homeSet == null) + homeSets.firstOrNull()?.let(onSelectHomeSet) + } + + var expanded by remember { mutableStateOf(false) } + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it } + ) { + OutlinedTextField( + label = { Text(stringResource(R.string.create_collection_home_set)) }, + value = homeSet?.title() ?: "", + onValueChange = { /* read-only */ }, + readOnly = true, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) + }, + modifier = Modifier + .fillMaxWidth() + .menuAnchor(MenuAnchorType.PrimaryNotEditable) + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + Column(Modifier.padding(horizontal = 8.dp) + ) { + for (item in homeSets) { + Column( + modifier = Modifier + .clickable(enabled = enabled) { + onSelectHomeSet(item) + expanded = false + } + .padding(vertical = 8.dp) + ) { + Text( + text = item.title(), + style = MaterialTheme.typography.bodyLarge + ) + Text( + text = item.url.encodedPath, + style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } + } + } + } + } +} + +@Composable +@Preview +fun HomeSetSelection_Preview() { + val homeSets = listOf( + HomeSet( + id = 0, + serviceId = 0, + personal = true, + url = "https://example.com/homeset/first".toHttpUrl() + ), + HomeSet( + id = 0, + serviceId = 0, + personal = true, + url = "https://example.com/homeset/second".toHttpUrl() + ) + ) + HomeSetSelection( + homeSet = homeSets.last(), + homeSets = homeSets, + onSelectHomeSet = {} + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/RenameAccountDialog.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/RenameAccountDialog.kt new file mode 100644 index 0000000..11bd5de --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/RenameAccountDialog.kt @@ -0,0 +1,98 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DriveFileRenameOutline +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +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 androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import at.bitfire.davdroid.R + +@Composable +fun RenameAccountDialog( + oldName: String, + onRenameAccount: (newName: String) -> Unit = {}, + onDismiss: () -> Unit = {} +) { + var accountName by remember { mutableStateOf(TextFieldValue(oldName, selection = TextRange(oldName.length))) } + + AlertDialog( + onDismissRequest = onDismiss, + icon = { Icon(Icons.Default.DriveFileRenameOutline, contentDescription = null) }, + title = { Text(stringResource(R.string.account_rename)) }, + text = { Column { + Text( + stringResource(R.string.account_rename_new_name_description), + modifier = Modifier.padding(bottom = 8.dp) + ) + + val focusRequester = remember { FocusRequester() } + TextField( + value = accountName, + onValueChange = { accountName = it }, + label = { Text(stringResource(R.string.account_rename_new_name)) }, + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + onRenameAccount(accountName.text) + } + ), + modifier = Modifier.focusRequester(focusRequester) + ) + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + }}, + confirmButton = { + Button( + onClick = { + onRenameAccount(accountName.text) + }, + enabled = oldName != accountName.text + ) { + Text(stringResource(R.string.account_rename_rename)) + } + }, + dismissButton = { + OutlinedButton(onClick = onDismiss) { + Text(stringResource(android.R.string.cancel)) + } + } + ) +} + +@Composable +@Preview +fun RenameAccountDialog_Preview() { + RenameAccountDialog("Account Name") +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsActivity.kt new file mode 100644 index 0000000..a0c5001 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsActivity.kt @@ -0,0 +1,54 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import android.accounts.Account +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.ui.res.stringResource +import androidx.core.app.TaskStackBuilder +import at.bitfire.davdroid.R +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class WifiPermissionsActivity: AppCompatActivity() { + + companion object { + const val EXTRA_ACCOUNT = "account" + } + + private val account by lazy { intent.getParcelableExtra(EXTRA_ACCOUNT) ?: throw IllegalArgumentException("EXTRA_ACCOUNT must be set") } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + WifiPermissionsScreen( + backgroundPermissionOptionLabel = + if (Build.VERSION.SDK_INT >= 30) + packageManager.backgroundPermissionOptionLabel.toString() + else + stringResource(R.string.wifi_permissions_background_location_permission_label), + onEnableLocationService = { + val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) + if (intent.resolveActivity(packageManager) != null) + startActivity(intent) + }, + onNavUp = ::onSupportNavigateUp + ) + } + } + + override fun supportShouldUpRecreateTask(targetIntent: Intent) = true + + override fun onPrepareSupportNavigateUpTaskStack(builder: TaskStackBuilder) { + builder.editIntentAt(builder.intentCount - 1)?.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsModel.kt new file mode 100644 index 0000000..cd3ff01 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsModel.kt @@ -0,0 +1,29 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import android.content.Context +import android.content.IntentFilter +import android.location.LocationManager +import androidx.core.content.getSystemService +import androidx.core.location.LocationManagerCompat +import androidx.lifecycle.ViewModel +import at.bitfire.davdroid.util.broadcastReceiverFlow +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +class WifiPermissionsModel @Inject constructor( + @ApplicationContext context: Context +): ViewModel() { + + private val locationManager = context.getSystemService()!! + + val locationEnabled = broadcastReceiverFlow(context, IntentFilter(LocationManager.MODE_CHANGED_ACTION), immediate = true) + .map { LocationManagerCompat.isLocationEnabled(locationManager) } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsScreen.kt new file mode 100644 index 0000000..73d039f --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsScreen.kt @@ -0,0 +1,266 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.account + +import android.Manifest +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.Help +import androidx.compose.material.icons.filled.CloudOff +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.AppTheme +import at.bitfire.davdroid.ui.ExternalUris +import at.bitfire.davdroid.ui.ExternalUris.withStatParams +import at.bitfire.davdroid.ui.composable.PermissionSwitchRow +import at.bitfire.davdroid.util.PermissionUtils + +@Composable +fun WifiPermissionsScreen( + model: WifiPermissionsModel = viewModel(), + backgroundPermissionOptionLabel: String, + onEnableLocationService: (Boolean) -> Unit, + onNavUp: () -> Unit +) { + val locationServiceEnabled by model.locationEnabled.collectAsStateWithLifecycle(false) + WifiPermissionsScreen( + backgroundPermissionOptionLabel = backgroundPermissionOptionLabel, + locationServiceEnabled = locationServiceEnabled, + onEnableLocationService = onEnableLocationService, + onNavUp = onNavUp + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WifiPermissionsScreen( + backgroundPermissionOptionLabel: String, + locationServiceEnabled: Boolean, + onEnableLocationService: (Boolean) -> Unit, + onNavUp: () -> Unit +) { + AppTheme { + Scaffold( + topBar = { + TopAppBar( + navigationIcon = { + IconButton(onClick = onNavUp) { + Icon( + Icons.AutoMirrored.Default.ArrowBack, + stringResource(R.string.navigate_up) + ) + } + }, + title = { Text(stringResource(R.string.wifi_permissions_label)) }, + actions = { + val uriHandler = LocalUriHandler.current + IconButton(onClick = { + uriHandler.openUri( + ExternalUris.Homepage.baseUrl.buildUpon() + .appendPath(ExternalUris.Homepage.PATH_FAQ) + .appendPath(ExternalUris.Homepage.PATH_FAQ_LOCATION_PERMISSION) + .withStatParams("WifiPermissionsScreen") + .build().toString() + ) + }) { + Icon(Icons.AutoMirrored.Default.Help, stringResource(R.string.help)) + } + } + ) + } + ) { padding -> + Box(modifier = Modifier.padding(padding)) { + WifiPermissionsScreenContent( + backgroundPermissionOptionLabel = backgroundPermissionOptionLabel, + locationServiceEnabled = locationServiceEnabled, + onEnableLocationService = onEnableLocationService + ) + } + } + } +} + +@Composable +fun WifiPermissionsScreenContent( + backgroundPermissionOptionLabel: String, + locationServiceEnabled: Boolean, + onEnableLocationService: (Boolean) -> Unit +) { + Column( + Modifier + .padding(8.dp) + .verticalScroll(rememberScrollState())) { + + // Disclaimer + Row { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + stringResource( + R.string.wifi_permissions_background_location_disclaimer, stringResource( + R.string.app_name) + ), + style = MaterialTheme.typography.bodyMedium, + + ) + Text( + stringResource( + R.string.wifi_permissions_background_location_disclaimer2, stringResource( + R.string.app_name) + ), + style = MaterialTheme.typography.bodyMedium, + ) + } + Icon(Icons.Default.CloudOff, null, modifier = Modifier.padding(8.dp)) + } + + HorizontalDivider(Modifier.padding(vertical = 16.dp)) + + // Permission switches + Text( + stringResource(R.string.wifi_permissions_intro), + style = MaterialTheme.typography.bodyLarge + ) + + // Android 8.1+: location permission + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) + LocationPermission( + modifier = Modifier.padding(top = 16.dp) + ) + + // Android 10+: background location permission + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + BackgroundLocationPermission( + backgroundPermissionOptionLabel = backgroundPermissionOptionLabel, + modifier = Modifier.padding(top = 16.dp) + ) + + // Android 9+: location service + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + LocationService( + locationServiceEnabled = locationServiceEnabled, + modifier = Modifier.padding(top = 16.dp), + onEnableLocationService = onEnableLocationService + ) + + // If permissions have actively been denied + Text( + stringResource(R.string.permissions_app_settings_hint), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(top = 16.dp) + ) + val context = LocalContext.current + OutlinedButton( + modifier = Modifier.padding(top = 8.dp), + onClick = { PermissionUtils.showAppSettings(context) } + ) { + Text(stringResource(R.string.permissions_app_settings)) + } + } +} + +@Composable +fun LocationPermission( + modifier: Modifier = Modifier +) { + val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + Manifest.permission.ACCESS_FINE_LOCATION // since Android 10, fine location is required + else + Manifest.permission.ACCESS_COARSE_LOCATION // Android 8+: coarse location is enough + + PermissionSwitchRow( + text = stringResource(R.string.wifi_permissions_location_permission), + permissions = listOf(permission), + summaryWhenGranted = stringResource(R.string.wifi_permissions_location_permission_on), + summaryWhenNotGranted = stringResource(R.string.wifi_permissions_location_permission_off), + modifier = modifier + ) +} + +@RequiresApi(Build.VERSION_CODES.Q) +@Composable +fun BackgroundLocationPermission( + backgroundPermissionOptionLabel: String, + modifier: Modifier = Modifier +) { + PermissionSwitchRow( + text = stringResource(R.string.wifi_permissions_background_location_permission), + permissions = listOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION), + summaryWhenGranted = stringResource(R.string.wifi_permissions_background_location_permission_on, backgroundPermissionOptionLabel), + summaryWhenNotGranted = stringResource(R.string.wifi_permissions_background_location_permission_off, backgroundPermissionOptionLabel), + modifier = modifier + ) +} + +@Composable +fun LocationService( + locationServiceEnabled: Boolean, + modifier: Modifier = Modifier, + onEnableLocationService: (Boolean) -> Unit +) { + Row(modifier.fillMaxWidth()) { + Column(Modifier.weight(1f)) { + Text( + stringResource(R.string.wifi_permissions_location_enabled), + style = MaterialTheme.typography.bodyLarge + ) + Text( + stringResource( + if (locationServiceEnabled) + R.string.wifi_permissions_location_enabled_on + else + R.string.wifi_permissions_location_enabled_off + ), + style = MaterialTheme.typography.bodyMedium + ) + } + Switch( + checked = locationServiceEnabled, + onCheckedChange = onEnableLocationService + ) + } +} + +@Composable +@Preview +fun WifiPermissionsScreen_Preview() { + AppTheme { + WifiPermissionsScreen( + backgroundPermissionOptionLabel = stringResource(R.string.wifi_permissions_background_location_permission_label), + locationServiceEnabled = true, + onEnableLocationService = {}, + onNavUp = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/ActionCard.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/ActionCard.kt new file mode 100644 index 0000000..1c8f2cf --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/ActionCard.kt @@ -0,0 +1,80 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.composable + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.NotificationAdd +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun ActionCard( + modifier: Modifier = Modifier, + icon: ImageVector? = null, + actionText: String? = null, + onAction: () -> Unit = {}, + content: @Composable () -> Unit +) { + Card(Modifier + .fillMaxWidth() + .then(modifier) + ) { + Column(Modifier + .padding(top = 8.dp, start = 8.dp, end = 8.dp) + .fillMaxWidth(), + ) { + ProvideTextStyle(MaterialTheme.typography.bodyLarge) { + if (icon != null) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Icon(icon, "", Modifier + .align(Alignment.Top) + .padding(8.dp)) + content() + } + else + content() + } + + if (actionText != null) + OutlinedButton( + onClick = onAction, + modifier = Modifier.padding(vertical = 8.dp) + ) { + Text(actionText) + } + } + } +} + +@Composable +@Preview +fun ActionCard_Sample() { + ActionCard( + icon = Icons.Default.NotificationAdd, + actionText = "Some Action" + ) { + Column { + Text("Some Content. Some Content. Some Content. Some Content. ") + Text("Other Content. Other Content. Other Content. Other Content. Other Content. Other Content. Other Content. ", style = MaterialTheme.typography.bodyMedium) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/Assistant.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/Assistant.kt new file mode 100644 index 0000000..a0fb398 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/Assistant.kt @@ -0,0 +1,73 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.composable + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun Assistant( + nextLabel: String? = null, + nextEnabled: Boolean = true, + isLoading: Boolean = false, + onNext: () -> Unit = {}, + content: @Composable () -> Unit +) { + Column(Modifier.fillMaxSize()) { + if (isLoading) + ProgressBar( + Modifier + .fillMaxWidth() + .padding(bottom = 8.dp)) + + Column( + Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .weight(1f)) { + content() + } + + BottomAppBar( + modifier = Modifier + .fillMaxWidth() + .imePadding(), + actions = { + if (nextLabel != null) + Button( + enabled = nextEnabled, + onClick = onNext, + modifier = Modifier + .padding(horizontal = 8.dp) + .wrapContentSize(Alignment.CenterEnd) + ) { + Text(nextLabel) + } + } + ) + } +} + +@Composable +@Preview +fun Assistant_Preview_InScaffold() { + Assistant(nextLabel = "Next") { + Text("Some Content") + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/Boxes.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/Boxes.kt new file mode 100644 index 0000000..009ef67 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/Boxes.kt @@ -0,0 +1,37 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun PixelBoxes( + colors: Array, + modifier: Modifier = Modifier +) { + Row(modifier) { + for (color in colors) + Box(Modifier.padding(4.dp)) { + Box(Modifier + .background(color) + .size(12.dp)) + } + } +} + +@Composable +@Preview +fun PixelBoxes_Sample() { + PixelBoxes(arrayOf(Color.Magenta, Color.Yellow, Color.Cyan)) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/CardWithImage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/CardWithImage.kt new file mode 100644 index 0000000..d23fe8d --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/CardWithImage.kt @@ -0,0 +1,146 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.composable + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.TabletAndroid +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import at.bitfire.davdroid.R + +@Composable +fun CardWithImage( + title: String, + modifier: Modifier = Modifier, + image: Painter? = null, + imageContentDescription: String? = null, + imageAlignment: Alignment = Alignment.Center, + imageContentScale: ContentScale = ContentScale.Crop, + message: String? = null, + subtitle: String? = null, + icon: ImageVector? = null, + iconContentDescription: String? = null, + content: @Composable ColumnScope.() -> Unit = {} +) { + Card(modifier) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + image?.let { + Image( + painter = it, + contentDescription = imageContentDescription, + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 126.dp), + contentScale = imageContentScale, + alignment = imageAlignment + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + icon?.let { + Icon( + imageVector = it, + contentDescription = iconContentDescription, + modifier = Modifier + .size(44.dp) + .padding(end = 12.dp) + ) + } + + Column(Modifier.fillMaxWidth()) { + Text( + text = title, + modifier = Modifier.fillMaxWidth(), + style = MaterialTheme.typography.titleLarge + ) + subtitle?.let { + Text( + text = it, + modifier = Modifier.fillMaxWidth(), + style = MaterialTheme.typography.titleMedium + ) + } + } + } + message?.let { + Text( + text = it, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + style = MaterialTheme.typography.bodyLarge + ) + } + + content() + } + } + } +} + +@Preview +@Composable +fun CardWithImage_Preview() { + CardWithImage( + image = painterResource(R.drawable.intro_tasks), + title = "Demo card", + message = "This is the message to be displayed under the title, but before the content." + ) +} + +@Preview +@Composable +fun CardWithImage_Preview_WithIconAndSubtitleAndContent() { + CardWithImage( + title = "Demo card", + icon = Icons.Default.TabletAndroid, + subtitle = "Subtitle", + message = "This is the message to be displayed under the title, but before the content." + ) { + Text("Content") + } +} + +@Preview +@Composable +fun CardWithImage_Preview_WithIconAndContentNoMessage() { + CardWithImage( + title = "Demo card", + icon = Icons.Default.TabletAndroid + ) { + Text("Content") + } +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/ExceptionInfoDialog.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/ExceptionInfoDialog.kt new file mode 100644 index 0000000..2682273 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/ExceptionInfoDialog.kt @@ -0,0 +1,100 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.composable + +import android.accounts.Account +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Error +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.compose.ui.unit.dp +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.DebugInfoActivity +import okhttp3.HttpUrl +import java.io.IOException + +@Composable +fun ExceptionInfoDialog( + exception: Throwable, + account: Account? = null, + remoteResource: HttpUrl? = null, + onDismiss: () -> Unit +) { + val context = LocalContext.current + + val titleRes = when (exception) { + is HttpException -> R.string.exception_httpexception + is IOException -> R.string.exception_ioexception + else -> R.string.exception + } + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text( + text = stringResource(titleRes) + ) + }, + icon = { + Icon(Icons.Rounded.Error, null) + }, + text = { + val message = if (exception is HttpException) { + when (exception.statusCode) { + 403 -> context.getString(R.string.debug_info_http_403_description) + 404 -> context.getString(R.string.debug_info_http_404_description) + 405 -> context.getString(R.string.debug_info_http_405_description) + in 500..599 -> context.getString(R.string.debug_info_http_5xx_description) + else -> null + } + } else null + Text( + text = message ?: "${exception::class.java.name}\n${exception.localizedMessage}" + ) + }, + dismissButton = { + OutlinedButton( + onClick = { + val intent = DebugInfoActivity.IntentBuilder(context).withCause(exception) + if (account != null) + intent.withAccount(account) + if (remoteResource != null) + intent.withRemoteResource(remoteResource) + context.startActivity(intent.build()) + } + ) { + Text(stringResource(R.string.exception_show_details)) + } + }, + confirmButton = { + Button(onClick = onDismiss) { + Text(stringResource(android.R.string.ok)) + } + } + ) +} + +@Composable +@Preview +fun ExceptionInfoDialog_Preview() { + ExceptionInfoDialog( + exception = Exception("Test exception"), + onDismiss = {} + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/InputDialogs.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/InputDialogs.kt new file mode 100644 index 0000000..6b2bc54 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/InputDialogs.kt @@ -0,0 +1,197 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.composable + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog + +@Composable +fun EditTextInputDialog( + title: String, + initialValue: String? = null, + inputLabel: String? = null, + passwordField: Boolean = false, + keyboardType: KeyboardType = if (passwordField) KeyboardType.Password else KeyboardType.Text, + onValueEntered: (String) -> Unit = {}, + onDismiss: () -> Unit = {}, +) { + val state = rememberTextFieldState( + initialText = initialValue ?: "", + initialSelection = TextRange(initialValue?.length ?: 0) + ) + + val confirmEnabled = state.text != initialValue + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text( + title, + style = MaterialTheme.typography.bodyLarge + ) + }, + text = { + val focusRequester = remember { FocusRequester() } + if (passwordField) + PasswordTextField( + password = state, + labelText = inputLabel, + modifier = Modifier.focusRequester(focusRequester) + ) + else + TextField( + label = { inputLabel?.let { Text(it) } }, + state = state, + keyboardOptions = KeyboardOptions( + keyboardType = keyboardType, + imeAction = ImeAction.Done + ), + onKeyboardAction = { + if (confirmEnabled) { + onValueEntered(state.text.toString()) + onDismiss() + } + }, + modifier = Modifier.focusRequester(focusRequester) + ) + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + }, + confirmButton = { + Button( + onClick = { + onValueEntered(state.text.toString()) + onDismiss() + }, + enabled = confirmEnabled + ) { + Text(stringResource(android.R.string.ok)) + } + }, + dismissButton = { + OutlinedButton( + onClick = onDismiss + ) { + Text(stringResource(android.R.string.cancel)) + } + } + ) +} + +@Composable +@Preview +fun EditTextInputDialog_Preview() { + EditTextInputDialog( + title = "Enter Some Text", + inputLabel = "Some Label", + initialValue = "initial value" + ) +} + +@Composable +@Preview +fun EditTextInputDialog_Preview_Password() { + EditTextInputDialog( + title = "New Password", + passwordField = true, + initialValue = "some password" + ) +} + + +@Composable +fun MultipleChoiceInputDialog( + title: String, + namesAndValues: List>, + initialValue: String? = null, + onValueSelected: (String) -> Unit = {}, + onDismiss: () -> Unit = {}, +) { + Dialog(onDismissRequest = onDismiss) { + Card { + Column { + Text( + title, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) + + LazyColumn(Modifier.padding(8.dp)) { + items( + count = namesAndValues.size, + key = { index -> namesAndValues[index].second }, + itemContent = { index -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + val (name, value) = namesAndValues[index] + RadioButton( + selected = value == initialValue, + onClick = { + onValueSelected(value) + onDismiss() + } + ) + Text( + name, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .weight(1f) + .clickable { + onValueSelected(value) + onDismiss() + } + ) + } + } + ) + } + } + } + } +} + +@Composable +@Preview +fun MultipleChoiceInputDialog_Preview() { + MultipleChoiceInputDialog( + title = "Some Title", + namesAndValues = listOf( + "Some Name" to "Some Value", + "Some Other Name" to "Some Other Value" + ) + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/PasswordTextField.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/PasswordTextField.kt new file mode 100644 index 0000000..a340ba5 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/PasswordTextField.kt @@ -0,0 +1,136 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.composable + +import android.net.Uri +import androidx.compose.foundation.focusGroup +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.KeyboardActionHandler +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.TextObfuscationMode +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedSecureTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.text.HtmlCompat +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.ExternalUris +import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString + +@Composable +fun PasswordTextField( + password: TextFieldState, + labelText: String?, + modifier: Modifier = Modifier, + leadingIcon: @Composable (() -> Unit)? = null, + keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + onKeyboardAction: KeyboardActionHandler? = null, + enabled: Boolean = true, + isError: Boolean = false +) { + var passwordVisible by remember { mutableStateOf(false) } + + Column { + OutlinedSecureTextField( + state = password, + label = labelText?.let { { Text(it) } }, + leadingIcon = leadingIcon, + isError = isError, + enabled = enabled, + modifier = modifier.focusGroup(), + keyboardOptions = keyboardOptions, + onKeyboardAction = onKeyboardAction, + textObfuscationMode = if (passwordVisible) TextObfuscationMode.Visible else TextObfuscationMode.RevealLastTyped, + trailingIcon = { + IconButton( + enabled = enabled, + onClick = { passwordVisible = !passwordVisible } + ) { + if (passwordVisible) + Icon(Icons.Default.VisibilityOff, stringResource(R.string.login_password_hide)) + else + Icon(Icons.Default.Visibility, stringResource(R.string.login_password_show)) + } + } + ) + Text( + modifier = Modifier.padding(vertical = 8.dp), + text = HtmlCompat.fromHtml( + stringResource( + R.string.settings_app_password_hint, + appPasswordHelpUrl().toString() + ), + 0 + ).toAnnotatedString() + ) + } +} + +fun appPasswordHelpUrl(): Uri = ExternalUris.Manual.baseUrl.buildUpon() + .appendPath(ExternalUris.Manual.PATH_INTRODUCTION) + .fragment(ExternalUris.Manual.FRAGMENT_AUTHENTICATION_METHODS) + .build() + + +@Composable +@Preview +fun PasswordTextField_Sample() { + PasswordTextField( + password = rememberTextFieldState(""), + labelText = "labelText", + enabled = true, + isError = false, + ) +} + +@Composable +@Preview +fun PasswordTextField_Sample_Filled() { + PasswordTextField( + password = rememberTextFieldState("password"), + labelText = "labelText", + enabled = true, + isError = false, + ) +} + +@Composable +@Preview +fun PasswordTextField_Sample_Error() { + PasswordTextField( + password = rememberTextFieldState("password"), + labelText = "labelText", + enabled = true, + isError = true, + ) +} + +@Composable +@Preview +fun PasswordTextField_Sample_Disabled() { + PasswordTextField( + password = rememberTextFieldState("password"), + labelText = "labelText", + enabled = false, + isError = false, + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/PermissionSwitchRow.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/PermissionSwitchRow.kt new file mode 100644 index 0000000..ad6a6f5 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/PermissionSwitchRow.kt @@ -0,0 +1,144 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.composable + +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import at.bitfire.davdroid.ui.AppTheme +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberMultiplePermissionsState + +@Composable +fun PermissionSwitchRow( + text: String, + allPermissionsGranted: Boolean, + summaryWhenGranted: String, + summaryWhenNotGranted: String, + modifier: Modifier = Modifier, + fontWeight: FontWeight = FontWeight.Normal, + onLaunchRequest: () -> Unit +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = text, + modifier = Modifier.fillMaxWidth(), + fontWeight = fontWeight, + style = MaterialTheme.typography.bodyLarge + ) + Text( + text = if (allPermissionsGranted) summaryWhenGranted else summaryWhenNotGranted, + modifier = Modifier.fillMaxWidth(), + style = MaterialTheme.typography.bodyMedium + ) + } + Switch( + checked = allPermissionsGranted, + thumbContent = if (allPermissionsGranted) { + { + Icon( + Icons.Default.Check, + contentDescription = null, + modifier = Modifier.padding(4.dp) + ) + } + } else null, + onCheckedChange = { checked -> + if (checked) { + onLaunchRequest() + } + } + ) + } +} + +@Composable +@OptIn(ExperimentalPermissionsApi::class) +fun PermissionSwitchRow( + text: String, + permissions: List, + summaryWhenGranted: String, + summaryWhenNotGranted: String, + modifier: Modifier = Modifier, + fontWeight: FontWeight = FontWeight.Normal +) { + if (LocalInspectionMode.current) { + // preview + PermissionSwitchRow( + text = text, + fontWeight = fontWeight, + summaryWhenGranted = summaryWhenGranted, + summaryWhenNotGranted = summaryWhenNotGranted, + allPermissionsGranted = false, + onLaunchRequest = {}, + modifier = modifier + ) + return + } + + val state = rememberMultiplePermissionsState(permissions = permissions.toList()) + PermissionSwitchRow( + text = text, + fontWeight = fontWeight, + summaryWhenGranted = summaryWhenGranted, + summaryWhenNotGranted = summaryWhenNotGranted, + allPermissionsGranted = state.allPermissionsGranted, + onLaunchRequest = state::launchMultiplePermissionRequest, + modifier = modifier + ) +} + +@Preview +@Composable +fun PermissionSwitchRow_Preview_NotGranted() { + AppTheme { + PermissionSwitchRow( + text = "Contacts", + allPermissionsGranted = false, + summaryWhenGranted = "Granted", + summaryWhenNotGranted = "Not granted", + onLaunchRequest = {} + ) + } +} + +@Preview +@Preview(uiMode = UI_MODE_NIGHT_YES) +@Composable +fun PermissionSwitchRow_Preview_Granted() { + AppTheme { + Surface { + PermissionSwitchRow( + text = "Contacts", + allPermissionsGranted = true, + summaryWhenGranted = "Granted", + summaryWhenNotGranted = "Not granted", + onLaunchRequest = {} + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/ProgressBar.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/ProgressBar.kt new file mode 100644 index 0000000..a452205 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/ProgressBar.kt @@ -0,0 +1,46 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.composable + +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProgressIndicatorDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap + + +@Composable +fun ProgressBar( + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colorScheme.secondary, + trackColor: Color = ProgressIndicatorDefaults.linearTrackColor, + strokeCap: StrokeCap = ProgressIndicatorDefaults.LinearStrokeCap +) { + LinearProgressIndicator( + modifier = modifier, + color = color, + trackColor = trackColor, + strokeCap = strokeCap + ) +} + +@Composable +fun ProgressBar( + progress: () -> Float, + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colorScheme.secondary, + trackColor: Color = ProgressIndicatorDefaults.linearTrackColor, + strokeCap: StrokeCap = ProgressIndicatorDefaults.LinearStrokeCap +) { + LinearProgressIndicator( + progress = progress, + modifier = modifier, + color = color, + trackColor = trackColor, + strokeCap = strokeCap + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/RadioButtons.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/RadioButtons.kt new file mode 100644 index 0000000..a7f2e45 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/RadioButtons.kt @@ -0,0 +1,97 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.composable + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import at.bitfire.davdroid.ui.AppTheme + +@Composable +fun RadioButtons( + options: List = listOf(), + initiallySelectedIdx: Int? = null, + onOptionSelected: (Int) -> Unit = { _ -> }, + optionTextPadding: PaddingValues = PaddingValues(10.dp), + modifier: Modifier = Modifier +) { + var selectedIdx by remember { mutableStateOf(initiallySelectedIdx) } + Column( + // Modifier.selectableGroup() is essential to ensure correct accessibility behavior + modifier = modifier.selectableGroup() + ) { + options.forEachIndexed { idx, text -> + Row( + Modifier + .fillMaxWidth() + .selectable( + selected = (selectedIdx == idx), + onClick = { + selectedIdx = idx + onOptionSelected(idx) + }, + role = Role.Companion.RadioButton + ) + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = (idx == selectedIdx), + onClick = null, // null recommended for accessibility with screen readers + ) + Text( + text = text, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(optionTextPadding) + ) + } + } + } +} + +@Preview +@Composable +private fun RadioButtons_Preview_NoInitialSelection() { + AppTheme { + RadioButtons( + options = listOf( + "Option 1", + "Option 2 is the longest of all the options, so we can see whether line breaks are not a problem.", + "Option 3") + ) + } +} + +@Preview +@Composable +private fun RadioButtons_Preview_InitialSelection() { + AppTheme { + RadioButtons( + options = listOf( + "Option 1", + "Option 2", + "Option 3" + ), + initiallySelectedIdx = 1 + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/RadioWithSwitch.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/RadioWithSwitch.kt new file mode 100644 index 0000000..f84c18d --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/RadioWithSwitch.kt @@ -0,0 +1,86 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.composable + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview + +/** + * Provides a radio button with a text, a switch at the end, and an optional summary to be shown + * under the main text. + * + * @param title The "proper" text of the Radio button. Shown in the middle of the row, between the + * radio button and the switch. + * @param summary If not `null`, shown below the title. Used to give more context or information. + * Supports formatting and interactions. + * @param isSelected Whether the item is currently selected. Refers to the radio button. + * @param isToggled Whether the switch is toggled. + * @param modifier Any modifiers to apply to the row. + * @param enabled Whether the radio button should be enabled. + * @param onSelected Gets called whenever the user requests this row to be enabled. Either by + * selecting the radio button or tapping the text. + * @param onToggled Gets called whenever the switch gets updated. Contains the checked status. + */ +@Composable +fun RadioWithSwitch( + title: String, + summary: (@Composable () -> Unit)?, + isSelected: Boolean, + isToggled: Boolean, + modifier: Modifier = Modifier, + enabled: Boolean = true, + onSelected: () -> Unit, + onToggled: (Boolean) -> Unit +) { + Row(modifier) { + RadioButton(selected = isSelected, onClick = onSelected, enabled = enabled) + + Column( + modifier = Modifier + .weight(1f) + .clickable(enabled = enabled, role = Role.RadioButton, onClick = onSelected) + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.fillMaxWidth() + ) + summary?.let { sum -> + ProvideTextStyle(MaterialTheme.typography.bodyMedium) { + sum() + } + } + } + + Switch( + checked = isToggled, + onCheckedChange = onToggled + ) + } +} + +@Preview +@Composable +private fun RadioWithSwitch_Preview() { + RadioWithSwitch( + title = "RadioWithSwitch Preview", + summary = { Text("An example summary") }, + isSelected = true, + isToggled = false, + onSelected = { }, + onToggled = { } + ) +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/SafeAndroidUriHandler.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/SafeAndroidUriHandler.kt new file mode 100644 index 0000000..5d262f3 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/SafeAndroidUriHandler.kt @@ -0,0 +1,29 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.composable + +import android.content.Context +import android.widget.Toast +import androidx.compose.ui.platform.AndroidUriHandler +import androidx.compose.ui.platform.UriHandler +import at.bitfire.davdroid.R +import java.util.logging.Level +import java.util.logging.Logger + +class SafeAndroidUriHandler( + val context: Context +): UriHandler { + + override fun openUri(uri: String) { + try { + AndroidUriHandler(context).openUri(uri) + } catch (e: Exception) { + Logger.getGlobal().log(Level.WARNING, "No browser available", e) + // no browser available + Toast.makeText(context, R.string.install_browser, Toast.LENGTH_LONG).show() + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/SelectClientCertificateCard.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/SelectClientCertificateCard.kt new file mode 100644 index 0000000..19b52ea --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/SelectClientCertificateCard.kt @@ -0,0 +1,97 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.composable + +import android.os.Build +import android.security.KeyChain +import androidx.activity.compose.LocalActivity +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import at.bitfire.davdroid.R +import kotlinx.coroutines.launch + +@Composable +fun SelectClientCertificateCard( + snackbarHostState: SnackbarHostState, + modifier: Modifier = Modifier, + enabled: Boolean = true, + suggestedAlias: String? = null, + chosenAlias: String?, + onAliasChosen: (String) -> Unit = {} +) { + Card(modifier = modifier) { + Column(Modifier.padding(8.dp)) { + Text( + if (!chosenAlias.isNullOrEmpty()) + stringResource(R.string.login_client_certificate_selected, chosenAlias) + else + stringResource(R.string.login_no_client_certificate_optional), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(8.dp) + ) + + val activity = LocalActivity.current + val scope = rememberCoroutineScope() + OutlinedButton( + enabled = enabled, + onClick = { + if (activity != null) + KeyChain.choosePrivateKeyAlias(activity, { alias -> + if (alias != null) + onAliasChosen(alias) + else { + // Show a Snackbar to add a certificate if no certificate was found + // API Versions < 29 does that itself + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + scope.launch { + if (snackbarHostState.showSnackbar( + message = activity.getString(R.string.login_no_certificate_found), + actionLabel = activity.getString(R.string.login_install_certificate), + duration = SnackbarDuration.Long + ) == SnackbarResult.ActionPerformed) + activity.startActivity(KeyChain.createInstallIntent()) + } + } + }, null, null, null, -1, suggestedAlias) + } + ) { + Text(stringResource(R.string.login_select_certificate)) + } + } + } +} + +@Composable +@Preview +fun SelectClientCertificateCard_Preview_CertSelected() { + SelectClientCertificateCard( + snackbarHostState = SnackbarHostState(), + suggestedAlias = "Test", + chosenAlias = "Test" + ) +} + +@Composable +@Preview +fun SelectClientCertificateCard_Preview_NothingSelected() { + SelectClientCertificateCard( + snackbarHostState = SnackbarHostState(), + suggestedAlias = null, + chosenAlias = null + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/Settings.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/Settings.kt new file mode 100644 index 0000000..07191e4 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/Settings.kt @@ -0,0 +1,192 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.composable + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Folder +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun SettingsHeader(divider: Boolean = false, content: @Composable () -> Unit) { + if (divider) + Divider(Modifier.padding(vertical = 8.dp)) + + Row( + Modifier + .padding(top = 16.dp, start = 52.dp, end = 16.dp, bottom = 8.dp) + .fillMaxWidth() + ) { + CompositionLocalProvider( + LocalTextStyle provides MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.secondary + ) + ) { + content() + } + } +} + +@Composable +@Preview +fun SettingsHeader_Sample() { + Column { + SettingsHeader(divider = true) { + Text("Some Settings Section") + } + } +} + + +@Composable +fun Setting( + icon: @Composable () -> Unit, + name: @Composable () -> Unit, + summary: String?, + end: @Composable () -> Unit = {}, + enabled: Boolean = true, + onClick: () -> Unit = {} +) { + var modifier = Modifier.fillMaxWidth() + modifier = if (enabled) + modifier.clickable(onClick = onClick) + else + modifier.alpha(.38f) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.padding(vertical = 8.dp) + ) { + Box( + modifier = Modifier + .width(44.dp) + .padding(4.dp), + contentAlignment = Alignment.Center + ) { + icon() + } + + Column( + Modifier + .padding(start = 8.dp) + .weight(1f) + ) { + ProvideTextStyle(MaterialTheme.typography.bodyLarge) { + name() + } + + if (summary != null) + Text(summary, style = MaterialTheme.typography.bodyMedium) + } + + end() + } +} + +@Composable +fun Setting( + name: String, + summary: String? = null, + icon: ImageVector? = null, + enabled: Boolean = true, + onClick: () -> Unit = {} +) { + Setting( + icon = { + if (icon != null) + Icon(icon, contentDescription = name) + }, + name = { + Text(name, style = MaterialTheme.typography.bodyLarge) + }, + summary = summary, + enabled = enabled, + onClick = onClick + ) +} + +@Composable +@Preview +fun Setting_Sample() { + Setting( + icon = Icons.Default.Folder, + name = "Setting", + summary = "Currently off" + ) +} + +@Composable +@Preview +fun Setting_Sample_Disabled() { + Setting( + icon = Icons.Default.Folder, + enabled = false, + name = "Setting", + summary = "Currently off" + ) +} + +@Composable +fun SwitchSetting( + checked: Boolean, + name: String, + summaryOn: String? = null, + summaryOff: String? = null, + enabled: Boolean = true, + icon: ImageVector? = null, + onCheckedChange: (Boolean) -> Unit = {} +) { + Setting( + icon = { + if (icon != null) + Icon(icon, name) + }, + name = { + Text(name) + }, + summary = if (checked) summaryOn else summaryOff, + end = { + Switch( + checked = checked, + enabled = enabled, + onCheckedChange = onCheckedChange, + modifier = Modifier.padding(horizontal = 4.dp) + ) + }, + enabled = enabled + ) { + onCheckedChange(!checked) + } +} + +@Composable +@Preview +fun SwitchSetting_Sample() { + SwitchSetting( + name = "Some Switched Setting", + checked = true, + summaryOn = "Currently on" + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/icon/CalendarImport.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/icon/CalendarImport.kt new file mode 100644 index 0000000..f956f4c --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/icon/CalendarImport.kt @@ -0,0 +1,67 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val Icons.Filled.CalendarImport: ImageVector + get() { + if (_CalendarImport != null) { + return _CalendarImport!! + } + _CalendarImport = ImageVector.Builder( + name = "Filled.CalendarImport", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + path(fill = SolidColor(Color.Black)) { + moveTo(12f, 12f) + lineTo(8f, 16f) + horizontalLineTo(11f) + verticalLineTo(22f) + horizontalLineTo(13f) + verticalLineTo(16f) + horizontalLineTo(16f) + moveTo(19f, 3f) + horizontalLineTo(18f) + verticalLineTo(1f) + horizontalLineTo(16f) + verticalLineTo(3f) + horizontalLineTo(8f) + verticalLineTo(1f) + horizontalLineTo(6f) + verticalLineTo(3f) + horizontalLineTo(5f) + curveTo(3.9f, 3f, 3f, 3.9f, 3f, 5f) + verticalLineTo(19f) + curveTo(3f, 20.11f, 3.9f, 21f, 5f, 21f) + horizontalLineTo(9f) + verticalLineTo(19f) + horizontalLineTo(5f) + verticalLineTo(8f) + horizontalLineTo(19f) + verticalLineTo(19f) + horizontalLineTo(15f) + verticalLineTo(21f) + horizontalLineTo(19f) + curveTo(20.11f, 21f, 21f, 20.11f, 21f, 19f) + verticalLineTo(5f) + curveTo(21f, 3.9f, 20.11f, 3f, 19f, 3f) + close() + } + }.build() + + return _CalendarImport!! + } + +@Suppress("ObjectPropertyName") +private var _CalendarImport: ImageVector? = null diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPage.kt new file mode 100644 index 0000000..1658d7d --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPage.kt @@ -0,0 +1,58 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.intro + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import androidx.activity.result.contract.ActivityResultContract +import androidx.compose.runtime.Composable +import androidx.core.net.toUri +import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPageModel.Companion.HINT_AUTOSTART_PERMISSION +import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPageModel.Companion.HINT_BATTERY_OPTIMIZATIONS +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class BatteryOptimizationsPage @Inject constructor( + @ApplicationContext val context: Context, + val settingsManager: SettingsManager +): IntroPage() { + + override fun getShowPolicy(): ShowPolicy { + // show fragment when: + // 1. DAVx5 is not whitelisted yet and "don't show anymore" has not been clicked, and/or + // 2a. evil manufacturer AND + // 2b. "don't show anymore" has not been clicked + return if ( + (!BatteryOptimizationsPageModel.isExempted(context) && settingsManager.getBooleanOrNull(HINT_BATTERY_OPTIMIZATIONS) != false) || + (BatteryOptimizationsPageModel.manufacturerWarning && settingsManager.getBooleanOrNull(HINT_AUTOSTART_PERMISSION) != false) + ) + ShowPolicy.SHOW_ALWAYS + else + ShowPolicy.DONT_SHOW + } + + @Composable + override fun ComposePage() { + BatteryOptimizationsPageContent() + } + + + @SuppressLint("BatteryLife") + object IgnoreBatteryOptimizationsContract: ActivityResultContract() { + override fun createIntent(context: Context, input: String): Intent { + return Intent( + android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + "package:$input".toUri() + ) + } + + override fun parseResult(resultCode: Int, intent: Intent?): Unit? { + return null + } + } + +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageContent.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageContent.kt new file mode 100644 index 0000000..6864442 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageContent.kt @@ -0,0 +1,225 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.intro + +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.compose.animation.AnimatedVisibility +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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.Checkbox +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import at.bitfire.davdroid.BuildConfig +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.AppTheme +import at.bitfire.davdroid.ui.ExternalUris +import at.bitfire.davdroid.ui.ExternalUris.withStatParams +import java.util.Locale + +@Composable +fun BatteryOptimizationsPageContent( + model: BatteryOptimizationsPageModel = viewModel() +) { + val ignoreBatteryOptimizationsResultLauncher = rememberLauncherForActivityResult( + BatteryOptimizationsPage.IgnoreBatteryOptimizationsContract + ) { + model.checkBatteryOptimizations() + } + + val hintBatteryOptimizations by model.hintBatteryOptimizations.collectAsStateWithLifecycle(false) + val uiState = model.uiState + LaunchedEffect(uiState) { + if (uiState.shouldBeExempted && !uiState.isExempted) + ignoreBatteryOptimizationsResultLauncher.launch(BuildConfig.APPLICATION_ID) + } + + val hintAutostartPermission by model.hintAutostartPermission.collectAsStateWithLifecycle(false) + BatteryOptimizationsPageContent( + dontShowBattery = hintBatteryOptimizations == false, + onChangeDontShowBattery = model::updateHintBatteryOptimizations, + isExempted = uiState.isExempted, + shouldBeExempted = uiState.shouldBeExempted, + onChangeShouldBeExempted = model::updateShouldBeExempted, + dontShowAutostart = hintAutostartPermission == false, + onChangeDontShowAutostart = model::updateHintAutostartPermission, + manufacturerWarning = BatteryOptimizationsPageModel.manufacturerWarning + ) +} + +@Composable +fun BatteryOptimizationsPageContent( + dontShowBattery: Boolean, + onChangeDontShowBattery: (Boolean) -> Unit = {}, + isExempted: Boolean, + shouldBeExempted: Boolean, + onChangeShouldBeExempted: (Boolean) -> Unit = {}, + dontShowAutostart: Boolean, + onChangeDontShowAutostart: (Boolean) -> Unit = {}, + manufacturerWarning: Boolean +) { + val uriHandler = LocalUriHandler.current + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + Card( + modifier = Modifier.padding(8.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.intro_battery_title), + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.weight(1f) + ) + Switch( + checked = shouldBeExempted, + onCheckedChange = { + // Only accept click events if not whitelisted + if (!isExempted) { + onChangeShouldBeExempted(it) + } + }, + enabled = !dontShowBattery + ) + } + Text( + text = stringResource( + R.string.intro_battery_text, + stringResource(R.string.app_name) + ), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 12.dp) + ) + AnimatedVisibility(visible = !isExempted) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = dontShowBattery, + onCheckedChange = { onChangeDontShowBattery(dontShowBattery) }, + enabled = !isExempted + ) + Text( + text = stringResource(R.string.intro_battery_dont_show), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .clickable { onChangeDontShowBattery(dontShowBattery) } + ) + } + } + } + } + if (manufacturerWarning) { + Card( + modifier = Modifier + .padding(8.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = stringResource( + R.string.intro_autostart_title, + Build.MANUFACTURER.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } + ), + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.fillMaxWidth() + ) + Text( + text = stringResource(R.string.intro_autostart_text), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 12.dp) + ) + OutlinedButton( + onClick = { + uriHandler.openUri( + ExternalUris.Homepage.baseUrl.buildUpon() + .appendPath(ExternalUris.Homepage.PATH_FAQ) + .appendPath(ExternalUris.Homepage.PATH_FAQ_SYNC_NOT_RUN) + .appendQueryParameter( + "manufacturer", + Build.MANUFACTURER.lowercase(Locale.ROOT) + ) + .withStatParams("BatteryOptimizationsPage") + .build().toString() + ) + } + ) { + Text(stringResource(R.string.intro_more_info)) + } + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = dontShowAutostart, + onCheckedChange = { onChangeDontShowAutostart(dontShowAutostart) } + ) + Text( + text = stringResource(R.string.intro_autostart_dont_show), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .clickable { onChangeDontShowAutostart(dontShowAutostart) } + ) + } + } + } + } + Text( + text = stringResource( + R.string.intro_leave_unchecked, + stringResource(R.string.app_settings_reset_hints) + ), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + } +} + +@Preview(showBackground = true, showSystemUi = true) +@Composable +private fun BatteryOptimizationsContent_Preview() { + AppTheme { + BatteryOptimizationsPageContent( + dontShowBattery = true, + isExempted = false, + shouldBeExempted = true, + dontShowAutostart = false, + manufacturerWarning = true + ) + } +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageModel.kt new file mode 100644 index 0000000..e05374e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageModel.kt @@ -0,0 +1,118 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.intro + +import android.content.Context +import android.content.IntentFilter +import android.os.Build +import android.os.PowerManager +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.core.content.getSystemService +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.bitfire.davdroid.BuildConfig +import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPageModel.Companion.evilManufacturers +import at.bitfire.davdroid.util.PermissionUtils +import at.bitfire.davdroid.util.broadcastReceiverFlow +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.launch +import java.util.Locale +import javax.inject.Inject + +@HiltViewModel +class BatteryOptimizationsPageModel @Inject constructor( + @ApplicationContext val context: Context, + private val settings: SettingsManager +): ViewModel() { + + companion object { + + /** + * Whether the request for whitelisting from battery optimizations shall be shown. + * If this setting is true or null/not set, the notice shall be shown. Only if this + * setting is false, the notice shall not be shown. + */ + const val HINT_BATTERY_OPTIMIZATIONS = "hint_BatteryOptimizations" + + /** + * Whether the autostart permission notice shall be shown. If this setting is true + * or null/not set, the notice shall be shown. Only if this setting is false, the notice + * shall not be shown. + * + * Type: Boolean + */ + const val HINT_AUTOSTART_PERMISSION = "hint_AutostartPermissions" + + /** + * List of manufacturers which are known to restrict background processes or otherwise + * block synchronization. + * + * See https://www.davx5.com/faq/synchronization-is-not-run-as-expected for why this is evil. + * See https://github.com/jaredrummler/AndroidDeviceNames/blob/master/json/ for manufacturer values. + */ + private val evilManufacturers = arrayOf( + "asus", "lenovo", "letv", "meizu", "nokia", + "oneplus", "oppo", "sony", "vivo", "wiko", "xiaomi", "zte") + + /** + * Whether the device has been produced by an evil manufacturer. + * + * Always true for debug builds (to test the UI). + * + * @see evilManufacturers + */ + val manufacturerWarning = + (evilManufacturers.contains(Build.MANUFACTURER.lowercase(Locale.ROOT)) || BuildConfig.DEBUG) + + fun isExempted(context: Context) = + context.getSystemService()!!.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID) + } + + data class UiState( + val shouldBeExempted: Boolean = true, + val isExempted: Boolean = false + ) + + var uiState by mutableStateOf(UiState()) + private set + + val hintBatteryOptimizations = settings.getBooleanFlow(HINT_BATTERY_OPTIMIZATIONS) + + val hintAutostartPermission = settings.getBooleanFlow(HINT_AUTOSTART_PERMISSION) + + init { + viewModelScope.launch { + broadcastReceiverFlow(context, IntentFilter(PermissionUtils.ACTION_POWER_SAVE_WHITELIST_CHANGED), immediate = true).collect { + checkBatteryOptimizations() + } + } + } + + fun checkBatteryOptimizations() { + val exempted = isExempted(context) + uiState = uiState.copy(shouldBeExempted = exempted, isExempted = exempted) + + // if DAVx5 is whitelisted, always show a reminder as soon as it's not whitelisted anymore + if (exempted) + settings.remove(HINT_BATTERY_OPTIMIZATIONS) + } + + fun updateShouldBeExempted(value: Boolean) { + uiState = uiState.copy(shouldBeExempted = value) + } + + fun updateHintBatteryOptimizations(value: Boolean) { + settings.putBoolean(HINT_BATTERY_OPTIMIZATIONS, value) + } + + fun updateHintAutostartPermission(value: Boolean) { + settings.putBoolean(HINT_AUTOSTART_PERMISSION, value) + } + +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt new file mode 100644 index 0000000..97d919f --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt @@ -0,0 +1,72 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.intro + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.BackHandler +import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContract +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.rememberCoroutineScope +import at.bitfire.davdroid.ui.AppTheme +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class IntroActivity : AppCompatActivity() { + + val model by viewModels() + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val pages = model.pages + + setContent { + AppTheme { + val scope = rememberCoroutineScope() + val pagerState = rememberPagerState { pages.size } + + BackHandler { + if (pagerState.settledPage == 0) { + setResult(Activity.RESULT_CANCELED) + finish() + } else scope.launch { + pagerState.animateScrollToPage(pagerState.settledPage - 1) + } + } + + IntroScreen( + pages = pages, + pagerState = pagerState, + onDonePressed = { + setResult(Activity.RESULT_OK) + finish() + } + ) + } + } + } + + + /** + * For launching the [IntroActivity]. Result is `true` when the user cancelled the intro. + */ + object Contract: ActivityResultContract() { + override fun createIntent(context: Context, input: Unit?): Intent = + Intent(context, IntroActivity::class.java) + + override fun parseResult(resultCode: Int, intent: Intent?): Boolean { + return resultCode == Activity.RESULT_CANCELED + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroModel.kt new file mode 100644 index 0000000..b2ec1a6 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroModel.kt @@ -0,0 +1,50 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.intro + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import java.util.logging.Logger +import javax.inject.Inject + +@HiltViewModel +class IntroModel @Inject constructor( + introPageFactory: IntroPageFactory, + private val logger: Logger +): ViewModel() { + + private val introPages = introPageFactory.introPages + + val pages: List by lazy { + calculatePages() + } + + + private fun calculatePages(): List { + for (page in introPages) + logger.fine("Found intro page ${page::class.java} with order ${page.getShowPolicy()}") + + // Calculate which intro pages shall be shown + val activePages: Map = introPages + .associateWith { page -> + page.getShowPolicy().also { policy -> + logger.fine("IntroActivity: found intro page ${page::class.java} with $policy") + } + } + .filterValues { it != IntroPage.ShowPolicy.DONT_SHOW } + + // Show intro screen when there's at least one page that shall [always] be shown + val anyShowAlways = activePages.values.any { it == IntroPage.ShowPolicy.SHOW_ALWAYS } + return if (anyShowAlways) { + val pages = mutableListOf() + activePages.filterValues { it != IntroPage.ShowPolicy.DONT_SHOW }.forEach { page, _ -> + pages += page + } + pages + } else + emptyList() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroPage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroPage.kt new file mode 100644 index 0000000..fe267a7 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroPage.kt @@ -0,0 +1,43 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.intro + +import androidx.compose.runtime.Composable + +abstract class IntroPage { + + enum class ShowPolicy { + DONT_SHOW, + SHOW_ALWAYS, + SHOW_ONLY_WITH_OTHERS + } + + /** + * Whether insets are handled by [ComposePage]. + * + * If `true`, [ComposePage] must add top/side insets for edge-to-edge layout itself. Bottom insets are handled by the bottom bar. + * If `false`, [IntroScreen] will apply all insets to give [ComposePage] a safe content area. + */ + open val customTopInsets: Boolean = false + + /** + * Used to determine whether an intro page of this type (for instance, + * the [BatteryOptimizationsPage]) should be shown. + * + * @return Order with which an instance of this page type shall be created and shown. Possible values: + * + * * < 0: only show the page when there is at least one other page with positive order (lower numbers are shown first) + * * [DONT_SHOW] (0): don't show the page + * * ≥ 0: show the page (lower numbers are shown first) + */ + abstract fun getShowPolicy(): ShowPolicy + + /** + * Composes this page. Will only be called when [getShowPolicy] is not [DONT_SHOW]. + */ + @Composable + abstract fun ComposePage() + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroPageFactory.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroPageFactory.kt new file mode 100644 index 0000000..09f1fca --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroPageFactory.kt @@ -0,0 +1,11 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.intro + +interface IntroPageFactory { + + val introPages: Array + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroScreen.kt new file mode 100644 index 0000000..a1efb0b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroScreen.kt @@ -0,0 +1,286 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.intro + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +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.Color +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.AppTheme +import at.bitfire.davdroid.ui.M3ColorScheme +import kotlinx.coroutines.launch + +@Composable +fun IntroScreen( + pages: List, + pagerState: PagerState = rememberPagerState { pages.size }, + onDonePressed: () -> Unit +) { + val scope = rememberCoroutineScope() + + Scaffold( + bottomBar = { + Box( + modifier = Modifier + .fillMaxWidth() + .background(M3ColorScheme.primaryLight) + // consume bottom and side insets of safe drawing area, like BottomAppBar + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom)) + .height(90.dp) + ) { + PositionIndicator( + index = pagerState.currentPage, + max = pages.size, + modifier = Modifier + .fillMaxHeight() + .padding(horizontal = 128.dp) + .align(Alignment.Center) + .fillMaxWidth(), + selectedIndicatorColor = MaterialTheme.colorScheme.onPrimary, + unselectedIndicatorColor = MaterialTheme.colorScheme.tertiary, + indicatorSize = 15f + ) + + ButtonWithIcon( + icon = if (pagerState.currentPage + 1 == pagerState.pageCount) { + Icons.Default.Check + } else { + Icons.AutoMirrored.Default.ArrowForward + }, + contentDescription = stringResource(R.string.intro_next), + modifier = Modifier + .padding(end = 16.dp) + .align(Alignment.CenterEnd), + color = M3ColorScheme.tertiaryLight + ) { + if (pagerState.currentPage + 1 == pagerState.pageCount) { + onDonePressed() + } else scope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + } + } + }, + contentWindowInsets = WindowInsets(0) + ) { paddingValues -> + Column(modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + HorizontalPager(state = pagerState) { idxPage -> + val page = pages[idxPage] + Box( + modifier = if (page.customTopInsets) + Modifier // ComposePage() handles insets itself + else + // consume top and horizontal sides of safe drawing padding (like TopAppBar) + // bottom is handled by the bottom bar + Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)) + ) { + page.ComposePage() + } + } + } + } +} + +@Preview( + showSystemUi = true, + showBackground = true +) +@Composable +fun IntroScreen_Preview() { + AppTheme { + IntroScreen( + listOf( + object : IntroPage() { + override fun getShowPolicy(): ShowPolicy = ShowPolicy.SHOW_ALWAYS + + @Composable + override fun ComposePage() { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + ) { + Text("Some Text") + } + } + }, + object : IntroPage() { + override fun getShowPolicy(): ShowPolicy = ShowPolicy.SHOW_ALWAYS + + @Composable + override fun ComposePage() { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.primary) + ) { + Text("Some Text") + } + } + } + ), + onDonePressed = {} + ) + } +} + + +@Composable +fun PositionIndicator( + index: Int, + max: Int, + modifier: Modifier = Modifier, + selectedIndicatorColor: Color = MaterialTheme.colorScheme.tertiary, + unselectedIndicatorColor: Color = contentColorFor(selectedIndicatorColor), + indicatorSize: Float = 20f, + indicatorPadding: Float = 20f +) { + val selectedPosition by animateFloatAsState( + targetValue = index.toFloat(), + label = "position" + ) + + Canvas(modifier = modifier) { + // idx * indicatorSize * 2 + idx * indicatorPadding + indicatorSize + // idx * (indicatorSize * 2 + indicatorPadding) + indicatorSize + val padding = indicatorSize * 2 + indicatorPadding + + val totalWidth = indicatorSize * 2 * max + indicatorPadding * (max - 1) + translate( + left = size.width / 2 - totalWidth / 2 + ) { + for (idx in 0 until max) { + drawCircle( + color = unselectedIndicatorColor, + radius = indicatorSize, + center = Offset( + x = idx * padding + indicatorSize, + y = size.height / 2 + ) + ) + } + + drawCircle( + color = selectedIndicatorColor, + radius = indicatorSize, + center = Offset( + x = selectedPosition * padding + indicatorSize, + y = size.height / 2 + ) + ) + } + } +} + +@Preview( + showBackground = true, + backgroundColor = 0xff000000 +) +@Composable +fun PositionIndicator_Preview() { + var index by remember { mutableIntStateOf(0) } + + PositionIndicator( + index = index, + max = 5, + modifier = Modifier + .width(200.dp) + .height(50.dp) + .clickable { if (index == 4) index = 0 else index++ } + ) +} + + +@Composable +fun ButtonWithIcon( + icon: ImageVector, + contentDescription: String?, + modifier: Modifier = Modifier, + size: Dp = 56.dp, + color: Color = MaterialTheme.colorScheme.tertiary, + contentColor: Color = contentColorFor(backgroundColor = color), + onClick: () -> Unit +) { + Surface( + color = color, + contentColor = contentColor, + modifier = modifier + .size(size) + .aspectRatio(1f), + onClick = onClick, + shape = CircleShape + ) { + AnimatedContent( + targetState = icon, + label = "Button Icon" + ) { + Icon( + imageVector = it, + contentDescription = contentDescription, + modifier = Modifier.padding(12.dp) + ) + } + } +} + +@Preview +@Composable +fun ButtonWithIcon_Preview() { + AppTheme { + ButtonWithIcon( + icon = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = null + ) { } + } +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourcePage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourcePage.kt new file mode 100644 index 0000000..98fc0c2 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourcePage.kt @@ -0,0 +1,159 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.intro + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewmodel.compose.viewModel +import at.bitfire.davdroid.R +import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.ui.AppTheme +import at.bitfire.davdroid.ui.ExternalUris +import at.bitfire.davdroid.ui.ExternalUris.withStatParams +import at.bitfire.davdroid.ui.composable.CardWithImage +import at.bitfire.davdroid.ui.composable.RadioButtons +import dagger.hilt.android.lifecycle.HiltViewModel +import java.util.logging.Logger +import javax.inject.Inject + +class OpenSourcePage @Inject constructor( + private val settingsManager: SettingsManager +): IntroPage() { + + override fun getShowPolicy(): ShowPolicy { + return if (System.currentTimeMillis() > (settingsManager.getLongOrNull(Model.SETTING_NEXT_DONATION_POPUP) ?: 0)) + ShowPolicy.SHOW_ALWAYS + else + ShowPolicy.DONT_SHOW + } + + @Composable + override fun ComposePage() { + Page() + } + + @Composable + private fun Page(model: Model = viewModel()) { + OpenSourcePage( + dontShowForMonthsOptions = model.donationPopupIntervalOptions, + onDontShowForMonths = model::setDontShowForMonths + ) + } + + @HiltViewModel + class Model @Inject constructor( + private val settings: SettingsManager, + private val logger: Logger + ): ViewModel() { + + companion object { + const val SETTING_NEXT_DONATION_POPUP = "time_nextDonationPopup" + } + + /** + * Possible number of months (30 days) to hide the donation popup for. + */ + val donationPopupIntervalOptions = listOf(1, 3, 9) + + /** + * Set the next time the donation popup should be shown. + * + * @param months Number of months (30 days) to hide the donation popup for. + */ + fun setDontShowForMonths(months: Int) { + logger.info("Setting next donation popup to $months months") + val oneMonth = 30*86400000L // 30 days (~ 1 month) + val nextReminder = oneMonth * months + System.currentTimeMillis() + settings.putLong(SETTING_NEXT_DONATION_POPUP, nextReminder) + } + + } + +} + +@Composable +fun OpenSourcePage( + dontShowForMonthsOptions: List, + onDontShowForMonths: (Int) -> Unit = {} +) { + val uriHandler = LocalUriHandler.current + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(8.dp) + ) { + CardWithImage( + title = stringResource(R.string.intro_open_source_title), + image = painterResource(R.drawable.intro_open_source), + imageContentScale = ContentScale.Inside, + message = stringResource( + R.string.intro_open_source_text, + stringResource(R.string.app_name) + ), + modifier = Modifier.padding(vertical = 8.dp) + ) { + OutlinedButton( + modifier = Modifier.padding(top = 8.dp, bottom = 16.dp), + onClick = { + uriHandler.openUri( + ExternalUris.Homepage.baseUrl.buildUpon() + .appendPath(ExternalUris.Homepage.PATH_OPEN_SOURCE) + .withStatParams("OpenSourcePage") + .build() + .toString() + ) + } + ) { + Text( + stringResource(R.string.intro_open_source_details) + ) + } + + Text( + text = stringResource(R.string.intro_open_source_dont_show), + style = MaterialTheme.typography.bodyLarge + ) + RadioButtons( + options = dontShowForMonthsOptions.map { months -> + pluralStringResource(R.plurals.intro_open_source_dont_show_months, months, months) + }, + onOptionSelected = { idx -> + val months = dontShowForMonthsOptions[idx] + onDontShowForMonths(months) + }, + modifier = Modifier.padding(bottom = 12.dp) + ) + + } + } +} + +@Preview +@Composable +fun OpenSourcePagePreview() { + AppTheme { + OpenSourcePage( + dontShowForMonthsOptions = listOf(1, 3, 9) + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroPage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroPage.kt new file mode 100644 index 0000000..080d6bf --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroPage.kt @@ -0,0 +1,41 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.intro + +import android.content.Context +import androidx.compose.runtime.Composable +import at.bitfire.davdroid.ui.PermissionsModel +import at.bitfire.davdroid.ui.PermissionsScreen +import at.bitfire.davdroid.util.PermissionUtils +import at.bitfire.davdroid.util.PermissionUtils.CALENDAR_PERMISSIONS +import at.bitfire.davdroid.util.PermissionUtils.CONTACT_PERMISSIONS +import at.bitfire.ical4android.TaskProvider +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class PermissionsIntroPage @Inject constructor( + @ApplicationContext private val context: Context +): IntroPage() { + + var model: PermissionsModel? = null + + override fun getShowPolicy(): ShowPolicy { + // show PermissionsFragment as intro fragment when no permissions are granted + val permissions = CONTACT_PERMISSIONS + CALENDAR_PERMISSIONS + + TaskProvider.PERMISSIONS_JTX + + TaskProvider.PERMISSIONS_OPENTASKS + + TaskProvider.PERMISSIONS_TASKS_ORG + return if (PermissionUtils.haveAnyPermission(context, permissions)) + ShowPolicy.DONT_SHOW + else + ShowPolicy.SHOW_ALWAYS + } + + @Composable + override fun ComposePage() { + PermissionsScreen() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroPage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroPage.kt new file mode 100644 index 0000000..839146e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroPage.kt @@ -0,0 +1,31 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.intro + +import androidx.compose.runtime.Composable +import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.sync.TasksAppManager +import at.bitfire.davdroid.ui.TasksCard +import at.bitfire.davdroid.ui.TasksModel +import javax.inject.Inject + +class TasksIntroPage @Inject constructor( + private val settingsManager: SettingsManager, + private val tasksAppManager: TasksAppManager +): IntroPage() { + + override fun getShowPolicy(): ShowPolicy { + return if (tasksAppManager.currentProvider() != null || settingsManager.getBooleanOrNull(TasksModel.HINT_OPENTASKS_NOT_INSTALLED) == false) + ShowPolicy.DONT_SHOW + else + ShowPolicy.SHOW_ALWAYS + } + + @Composable + override fun ComposePage() { + TasksCard() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/WelcomePage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/WelcomePage.kt new file mode 100644 index 0000000..4863b2f --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/WelcomePage.kt @@ -0,0 +1,170 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.intro + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeContentPadding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.AppTheme +import at.bitfire.davdroid.ui.M3ColorScheme + +class WelcomePage: IntroPage() { + + override val customTopInsets: Boolean = true + + override fun getShowPolicy() = ShowPolicy.SHOW_ONLY_WITH_OTHERS + + @Composable + override fun ComposePage() { + if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) + ContentLandscape() + else + ContentPortrait() + } + + + @Composable + private fun ContentPortrait() { + Column( + modifier = Modifier + .fillMaxSize() + .background(color = M3ColorScheme.primaryLight) // fill background color edge-to-edge + .safeContentPadding() + ) { + Image( + painter = painterResource(R.drawable.ic_launcher_foreground), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .padding(top = 48.dp) + .weight(2f) + ) + + Text( + text = stringResource(R.string.intro_slogan1), + color = Color.White, + style = MaterialTheme.typography.bodyMedium.copy(fontSize = 34.sp), + lineHeight = 38.sp, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 16.dp) + ) + + Text( + text = stringResource(R.string.intro_slogan2), + color = Color.White, + style = MaterialTheme.typography.labelLarge.copy(fontSize = 48.sp), + lineHeight = 52.sp, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 32.dp) + ) + + Spacer(modifier = Modifier.weight(0.1f)) + } + } + + @Composable + @Preview( + device = "id:3.7in WVGA (Nexus One)", + showSystemUi = true + ) + fun Preview_ContentPortrait_Light() { + AppTheme(darkTheme = false) { + ContentPortrait() + } + } + + @Composable + @Preview( + device = "id:3.7in WVGA (Nexus One)", + showSystemUi = true + ) + fun Preview_ContentPortrait_Dark() { + AppTheme(darkTheme = true) { + ContentPortrait() + } + } + + + @Preview( + showSystemUi = true, + device = "id:medium_tablet" + ) + @Composable + private fun ContentLandscape() { + AppTheme { + Row( + modifier = Modifier + .fillMaxSize() + .background(color = MaterialTheme.colorScheme.primary) + .safeContentPadding(), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(R.drawable.ic_launcher_foreground), + contentDescription = null, + modifier = Modifier + .fillMaxHeight() + .weight(1f) + ) + + Column( + modifier = Modifier + .padding(horizontal = 32.dp) + .weight(2f) + ) { + Text( + text = stringResource(R.string.intro_slogan1), + color = Color.White, + style = MaterialTheme.typography.bodyMedium.copy(fontSize = 34.sp), + lineHeight = 38.sp, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Text( + text = stringResource(R.string.intro_slogan2), + color = Color.White, + style = MaterialTheme.typography.labelLarge.copy(fontSize = 48.sp), + lineHeight = 52.sp, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(top = 16.dp) + .fillMaxWidth() + ) + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsPage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsPage.kt new file mode 100644 index 0000000..3260b1e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsPage.kt @@ -0,0 +1,236 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import android.accounts.Account +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Email +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.RadioButton +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +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.platform.LocalContext +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.PopupProperties +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.composable.Assistant +import at.bitfire.vcard4android.GroupMethod + +@Composable +fun AccountDetailsPage( + snackbarHostState: SnackbarHostState, + onAccountCreated: (Account) -> Unit, + model: LoginScreenModel = viewModel() +) { + val uiState by model.accountDetailsUiState.collectAsStateWithLifecycle() + uiState.createdAccount?.let(onAccountCreated) + + val context = LocalContext.current + LaunchedEffect(uiState.couldNotCreateAccount) { + if (uiState.couldNotCreateAccount) { + snackbarHostState.showSnackbar(context.getString(R.string.login_account_not_added)) + model.resetCouldNotCreateAccount() + } + } + + AccountDetailsPageContent( + accountName = uiState.accountName, + suggestedAccountNames = uiState.suggestedAccountNames, + accountNameAlreadyExists = uiState.accountNameExists, + onUpdateAccountName = { model.updateAccountName(it) }, + showApostropheWarning = uiState.showApostropheWarning, + groupMethod = uiState.groupMethod, + groupMethodReadOnly = uiState.groupMethodReadOnly, + onUpdateGroupMethod = { model.updateGroupMethod(it) }, + onCreateAccount = { model.createAccount() }, + creatingAccount = uiState.creatingAccount + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AccountDetailsPageContent( + suggestedAccountNames: Set, + accountName: String, + accountNameAlreadyExists: Boolean, + onUpdateAccountName: (String) -> Unit = {}, + showApostropheWarning: Boolean, + groupMethod: GroupMethod, + groupMethodReadOnly: Boolean, + onUpdateGroupMethod: (GroupMethod) -> Unit = {}, + onCreateAccount: () -> Unit = {}, + creatingAccount: Boolean +) { + Assistant( + nextLabel = stringResource(R.string.login_finish), + onNext = onCreateAccount, + nextEnabled = !creatingAccount && accountName.isNotBlank() && !accountNameAlreadyExists, + isLoading = creatingAccount + ) { + Column(Modifier.padding(8.dp)) { + var expanded by remember { mutableStateOf(false) } + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it } + ) { + val offerDropdown = suggestedAccountNames.isNotEmpty() + OutlinedTextField( + value = accountName, + onValueChange = onUpdateAccountName, + label = { Text(stringResource(R.string.login_account_name)) }, + isError = accountNameAlreadyExists, + supportingText = + if (accountNameAlreadyExists) { + { Text(stringResource(R.string.login_account_name_already_taken)) } + } else + null, + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email + ), + trailingIcon = if (offerDropdown) { + { ExposedDropdownMenuDefaults.TrailingIcon(expanded) } + } else null, + modifier = Modifier + .menuAnchor(MenuAnchorType.PrimaryEditable) + .fillMaxWidth() + ) + + if (offerDropdown) + DropdownMenu( // ExposedDropdownMenu takes focus away from the text field when expanded + expanded = expanded, + onDismissRequest = { expanded = false }, + properties = PopupProperties(focusable = false) // prevent focus from being taken away + ) { + for (name in suggestedAccountNames) + DropdownMenuItem( + text = { Text(name) }, + onClick = { + onUpdateAccountName(name) + expanded = false + } + ) + } + } + + // apostrophe warning + if (showApostropheWarning) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 16.dp) + ) { + Icon( + Icons.Default.Warning, + contentDescription = null, + modifier = Modifier.padding(top = 8.dp, end = 8.dp, bottom = 8.dp) + ) + Text( + stringResource(R.string.login_account_avoid_apostrophe), + style = MaterialTheme.typography.bodyLarge + ) + } + + // email address info + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 16.dp) + ) { + Icon( + Icons.Default.Email, + contentDescription = null, + modifier = Modifier.padding(top = 8.dp, end = 8.dp, bottom = 8.dp) + ) + Text( + stringResource(R.string.login_account_name_info), + style = MaterialTheme.typography.bodyLarge + ) + } + + // group type selector + Text( + stringResource(R.string.login_account_contact_group_method), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 16.dp) + ) + val groupMethodNames = stringArrayResource(R.array.settings_contact_group_method_entries) + val groupMethodValues = stringArrayResource(R.array.settings_contact_group_method_values).map { GroupMethod.valueOf(it) } + for ((name, method) in groupMethodNames.zip(groupMethodValues)) { + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = groupMethod == method, + enabled = !groupMethodReadOnly, + onClick = { onUpdateGroupMethod(method) } + ) + + var modifier = Modifier.padding(vertical = 4.dp) + if (!groupMethodReadOnly) + modifier = modifier.clickable(onClick = { onUpdateGroupMethod(method) }) + Text( + name, + style = MaterialTheme.typography.bodyLarge, + modifier = modifier + ) + } + } + } + } +} + +@Composable +@Preview +fun AccountDetailsPage_Content_Preview() { + AccountDetailsPageContent( + suggestedAccountNames = setOf("name1", "name2@example.com"), + accountName = "account@example.com", + accountNameAlreadyExists = false, + showApostropheWarning = false, + groupMethod = GroupMethod.GROUP_VCARDS, + groupMethodReadOnly = false, + creatingAccount = true + ) +} + +@Composable +@Preview +fun AccountDetailsPage_Content_Preview_With_Apostrophe() { + AccountDetailsPageContent( + suggestedAccountNames = setOf("name1", "name2@example.com"), + accountName = "account'example.com", + accountNameAlreadyExists = true, + showApostropheWarning = true, + groupMethod = GroupMethod.CATEGORIES, + groupMethodReadOnly = true, + creatingAccount = false + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AdvancedLogin.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AdvancedLogin.kt new file mode 100644 index 0000000..20a0d3c --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AdvancedLogin.kt @@ -0,0 +1,213 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import android.net.Uri +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.Folder +import androidx.compose.material.icons.filled.Password +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.text.HtmlCompat +import androidx.hilt.navigation.compose.hiltViewModel +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.ExternalUris +import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString +import at.bitfire.davdroid.ui.composable.Assistant +import at.bitfire.davdroid.ui.composable.PasswordTextField +import at.bitfire.davdroid.ui.composable.SelectClientCertificateCard + +object AdvancedLogin : LoginType { + + override val title: Int + get() = R.string.login_type_advanced + + override val helpUrl: Uri? + get() = null + + + @Composable + override fun LoginScreen( + snackbarHostState: SnackbarHostState, + initialLoginInfo: LoginInfo, + onLogin: (LoginInfo) -> Unit + ) { + val model: AdvancedLoginModel = hiltViewModel( + creationCallback = { factory: AdvancedLoginModel.Factory -> + factory.create(loginInfo = initialLoginInfo) + } + ) + + val uiState = model.uiState + AdvancedLoginScreen( + snackbarHostState = snackbarHostState, + url = uiState.url, + onSetUrl = model::setUrl, + username = uiState.username, + onSetUsername = model::setUsername, + password = uiState.password, + certAlias = uiState.certAlias, + onSetCertAlias = model::setCertAlias, + canContinue = uiState.canContinue, + onLogin = { + onLogin(uiState.asLoginInfo()) + } + ) + } + +} + +@Composable +fun AdvancedLoginScreen( + snackbarHostState: SnackbarHostState, + url: String, + onSetUrl: (String) -> Unit = {}, + username: String, + onSetUsername: (String) -> Unit = {}, + password: TextFieldState, + certAlias: String, + onSetCertAlias: (String) -> Unit = {}, + canContinue: Boolean, + onLogin: () -> Unit = {} +) { + val focusRequester = remember { FocusRequester() } + + Assistant( + nextLabel = stringResource(R.string.login_login), + nextEnabled = canContinue, + onNext = onLogin + ) { + Column(modifier = Modifier.padding(8.dp)) { + Text( + stringResource(R.string.login_type_advanced), + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + + val manualUrl = ExternalUris.Manual.baseUrl.buildUpon() + .appendPath(ExternalUris.Manual.PATH_ACCOUNTS_COLLECTIONS) + .fragment(ExternalUris.Manual.FRAGMENT_SERVICE_DISCOVERY) + .build() + val urlInfo = HtmlCompat.fromHtml(stringResource(R.string.login_base_url_info, manualUrl), HtmlCompat.FROM_HTML_MODE_COMPACT) + Text( + text = urlInfo.toAnnotatedString(), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 16.dp) + ) + + OutlinedTextField( + value = url, + onValueChange = onSetUrl, + label = { Text(stringResource(R.string.login_base_url)) }, + placeholder = { Text("dav.example.com/path") }, + singleLine = true, + leadingIcon = { + Icon(Icons.Default.Folder, null) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Uri, + imeAction = ImeAction.Next + ), + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester) + ) + + OutlinedTextField( + value = username, + onValueChange = onSetUsername, + label = { Text(stringResource(R.string.login_user_name_optional)) }, + singleLine = true, + leadingIcon = { + Icon(Icons.Default.AccountCircle, null) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Next + ), + modifier = Modifier.fillMaxWidth() + ) + + PasswordTextField( + password = password, + labelText = stringResource(R.string.login_password_optional), + leadingIcon = { + Icon(Icons.Default.Password, null) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ), + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(16.dp)) + + SelectClientCertificateCard( + snackbarHostState = snackbarHostState, + suggestedAlias = null, + chosenAlias = certAlias, + onAliasChosen = onSetCertAlias + ) + } + } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } +} + +@Composable +@Preview +fun AdvancedLoginScreen_Preview_Empty() { + AdvancedLoginScreen( + snackbarHostState = SnackbarHostState(), + url = "", + username = "", + password = rememberTextFieldState(""), + certAlias = "", + canContinue = false + ) +} + +@Composable +@Preview +fun AdvancedLoginScreen_Preview_AllFilled() { + AdvancedLoginScreen( + snackbarHostState = SnackbarHostState(), + url = "dav.example.com", + username = "someuser", + password = rememberTextFieldState("password"), + certAlias = "someCert", + canContinue = true + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AdvancedLoginModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AdvancedLoginModel.kt new file mode 100644 index 0000000..4ffd2f6 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AdvancedLoginModel.kt @@ -0,0 +1,82 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import at.bitfire.davdroid.settings.Credentials +import at.bitfire.davdroid.util.DavUtils.toURIorNull +import at.bitfire.davdroid.util.SensitiveString.Companion.toSensitiveString +import at.bitfire.davdroid.util.trimToNull +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel + +@HiltViewModel(assistedFactory = AdvancedLoginModel.Factory::class) +class AdvancedLoginModel @AssistedInject constructor( + @Assisted val initialLoginInfo: LoginInfo, +): ViewModel() { + + @AssistedFactory + interface Factory { + fun create(loginInfo: LoginInfo): AdvancedLoginModel + } + + data class UiState( + val url: String = "", + val username: String = "", + val password: TextFieldState = TextFieldState(), + val certAlias: String = "" + ) { + + val urlWithPrefix = + if (url.startsWith("http://") || url.startsWith("https://")) + url + else + "https://$url" + val uri = urlWithPrefix.toURIorNull() + + val canContinue = uri != null + + fun asLoginInfo() = LoginInfo( + baseUri = uri, + credentials = Credentials( + username = username.trimToNull(), + password = password.text.trimToNull()?.toSensitiveString(), + certificateAlias = certAlias.trimToNull() + ) + ) + + } + + var uiState by mutableStateOf(UiState()) + private set + + init { + uiState = uiState.copy( + url = initialLoginInfo.baseUri?.toString()?.removePrefix("https://") ?: "", + username = initialLoginInfo.credentials?.username ?: "", + password = TextFieldState(initialLoginInfo.credentials?.password?.asString() ?: ""), + certAlias = initialLoginInfo.credentials?.certificateAlias ?: "" + ) + } + + fun setUrl(url: String) { + uiState = uiState.copy(url = url) + } + + fun setUsername(username: String) { + uiState = uiState.copy(username = username) + } + + fun setCertAlias(certAlias: String) { + uiState = uiState.copy(certAlias = certAlias) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/DetectResourcesPage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/DetectResourcesPage.kt new file mode 100644 index 0000000..82cbe4d --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/DetectResourcesPage.kt @@ -0,0 +1,184 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CloudOff +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.compose.ui.unit.dp +import androidx.core.text.HtmlCompat +import androidx.lifecycle.viewmodel.compose.viewModel +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.DebugInfoActivity +import at.bitfire.davdroid.ui.ExternalUris +import at.bitfire.davdroid.ui.ExternalUris.withStatParams +import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString +import at.bitfire.davdroid.ui.composable.ProgressBar + +@Composable +fun DetectResourcesPage( + model: LoginScreenModel = viewModel() +) { + val uiState = model.detectResourcesUiState + DetectResourcesPageContent( + loading = uiState.loading, + foundNothing = uiState.foundNothing, + encountered401 = uiState.encountered401, + logs = uiState.logs + ) +} + +@Composable +fun DetectResourcesPageContent( + loading: Boolean, + foundNothing: Boolean, + encountered401: Boolean, + logs: String? +) { + Column(Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { + if (loading) + DetectResourcesPageContent_InProgress() + else if (foundNothing) + DetectResourcesPageContent_NothingFound( + encountered401 = encountered401, + logs = logs + ) + } +} + +@Composable +@Preview +fun DetectResourcesPageContent_InProgress() { + Column(Modifier.fillMaxWidth()) { + ProgressBar( + //color = MaterialTheme.colors.secondary, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp)) + + Column(Modifier.padding(8.dp)) { + Text( + stringResource(R.string.login_configuration_detection), + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(bottom = 16.dp) + ) + Text( + stringResource(R.string.login_querying_server), + style = MaterialTheme.typography.bodyLarge + ) + } + } +} + +@Composable +fun DetectResourcesPageContent_NothingFound( + encountered401: Boolean, + logs: String? +) { + Column(Modifier.padding(8.dp)) { + Text( + stringResource(R.string.login_configuration_detection), + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(bottom = 16.dp) + ) + + Card(Modifier.fillMaxWidth()) { + Column(Modifier.padding(8.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.CloudOff, contentDescription = null, modifier = Modifier.padding(end = 8.dp)) + Text( + stringResource(R.string.login_no_service), + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.weight(1f) + ) + } + + Text( + stringResource(R.string.login_no_service_info), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 16.dp, bottom = 8.dp) + ) + + val urlServices = ExternalUris.Homepage.baseUrl.buildUpon() + .appendPath(ExternalUris.Homepage.PATH_TESTED_SERVICES) + .withStatParams("DetectResourcesPage") + .build() + val testedServices = HtmlCompat.fromHtml( + stringResource(R.string.login_see_tested_services, urlServices), + HtmlCompat.FROM_HTML_MODE_COMPACT + ).toAnnotatedString() + Text( + text = testedServices, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(vertical = 8.dp) + ) + + if (encountered401) + Text( + stringResource(R.string.login_check_credentials), + modifier = Modifier.padding(vertical = 8.dp), + style = MaterialTheme.typography.bodyLarge + ) + + if (logs != null && logs.isNotEmpty()) { + Text( + stringResource(R.string.login_logs_available), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(vertical = 8.dp) + ) + + val context = LocalContext.current + Button( + onClick = { + val intent = DebugInfoActivity.IntentBuilder(context) + .withLogs(logs) + .build() + context.startActivity(intent) + } + ) { + Text(stringResource(R.string.login_view_logs)) + } + } + } + } + } +} + +@Composable +@Preview +fun DetectResourcesPageContent_NothingFound() { + DetectResourcesPageContent_NothingFound( + encountered401 = false, + logs = "SOME LOGS" + ) +} + +@Composable +@Preview +fun DetectResourcesPage_NothingFound_401() { + DetectResourcesPageContent_NothingFound( + encountered401 = true, + logs = "" + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/EmailLogin.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/EmailLogin.kt new file mode 100644 index 0000000..1ec8687 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/EmailLogin.kt @@ -0,0 +1,162 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import android.net.Uri +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Email +import androidx.compose.material.icons.filled.Password +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.text.HtmlCompat +import androidx.hilt.navigation.compose.hiltViewModel +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.ExternalUris +import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString +import at.bitfire.davdroid.ui.composable.Assistant +import at.bitfire.davdroid.ui.composable.PasswordTextField + +object EmailLogin : LoginType { + + override val title: Int + get() = R.string.login_type_email + + override val helpUrl: Uri? + get() = null + + + @Composable + override fun LoginScreen( + snackbarHostState: SnackbarHostState, + initialLoginInfo: LoginInfo, + onLogin: (LoginInfo) -> Unit + ) { + val model: EmailLoginModel = hiltViewModel( + creationCallback = { factory: EmailLoginModel.Factory -> + factory.create(loginInfo = initialLoginInfo) + } + ) + + val uiState = model.uiState + EmailLoginScreen( + email = uiState.email, + onSetEmail = model::setEmail, + password = uiState.password, + canContinue = uiState.canContinue, + onLogin = { onLogin(uiState.asLoginInfo()) } + ) + } + +} + + +@Composable +fun EmailLoginScreen( + email: String, + onSetEmail: (String) -> Unit = {}, + password: TextFieldState, + canContinue: Boolean, + onLogin: () -> Unit = {} +) { + val focusRequester = remember { FocusRequester() } + + Assistant( + nextLabel = stringResource(R.string.login_login), + nextEnabled = canContinue, + onNext = onLogin + ) { + Column(Modifier.padding(8.dp)) { + Text( + stringResource(R.string.login_type_email), + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + + val manualUrl = ExternalUris.Manual.baseUrl.buildUpon() + .appendPath(ExternalUris.Manual.PATH_ACCOUNTS_COLLECTIONS) + .fragment(ExternalUris.Manual.FRAGMENT_SERVICE_DISCOVERY) + .build() + val emailInfo = HtmlCompat.fromHtml(stringResource(R.string.login_email_address_info, manualUrl), HtmlCompat.FROM_HTML_MODE_COMPACT) + Text( + text = emailInfo.toAnnotatedString(), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 16.dp) + ) + + OutlinedTextField( + value = email, + onValueChange = onSetEmail, + label = { Text(stringResource(R.string.login_email_address)) }, + singleLine = true, + leadingIcon = { + Icon(Icons.Default.Email, null) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Next + ), + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester) + ) + + PasswordTextField( + password = password, + labelText = stringResource(R.string.login_password), + leadingIcon = { + Icon(Icons.Default.Password, null) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ), + onKeyboardAction = { + if (canContinue) + onLogin() + }, + modifier = Modifier.fillMaxWidth() + ) + } + } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } +} + + +@Composable +@Preview +fun EmailLoginScreen_Preview() { + EmailLoginScreen( + email = "test@example.com", + password = rememberTextFieldState(""), + canContinue = false + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/EmailLoginModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/EmailLoginModel.kt new file mode 100644 index 0000000..5c8ceae --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/EmailLoginModel.kt @@ -0,0 +1,64 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import at.bitfire.davdroid.settings.Credentials +import at.bitfire.davdroid.util.DavUtils.toURIorNull +import at.bitfire.davdroid.util.SensitiveString.Companion.toSensitiveString +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel + +@HiltViewModel(assistedFactory = EmailLoginModel.Factory::class) +class EmailLoginModel @AssistedInject constructor( + @Assisted val initialLoginInfo: LoginInfo +): ViewModel() { + + @AssistedFactory + interface Factory { + fun create(loginInfo: LoginInfo): EmailLoginModel + } + + data class UiState( + val email: String = "", + val password: TextFieldState = TextFieldState() + ) { + val uri = "mailto:$email".toURIorNull() + + val canContinue // we have to use get() because password is not immutable + get() = uri != null && password.text.toString().isNotEmpty() + + fun asLoginInfo(): LoginInfo { + return LoginInfo( + baseUri = uri, + credentials = Credentials( + username = email, + password = password.text.toSensitiveString() + ) + ) + } + } + + var uiState by mutableStateOf(UiState()) + private set + + init { + uiState = uiState.copy( + email = initialLoginInfo.credentials?.username ?: "", + password = TextFieldState(initialLoginInfo.credentials?.password?.asString() ?: "") + ) + } + + fun setEmail(email: String) { + uiState = uiState.copy(email = email) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/FastmailLogin.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/FastmailLogin.kt new file mode 100644 index 0000000..f3c286d --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/FastmailLogin.kt @@ -0,0 +1,184 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import android.content.ActivityNotFoundException +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Email +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.ExternalUris +import at.bitfire.davdroid.ui.ExternalUris.withStatParams +import java.util.logging.Level +import java.util.logging.Logger + +object FastmailLogin : LoginType { + + override val title: Int + get() = R.string.login_fastmail + + override val helpUrl: Uri + get() = ExternalUris.Homepage.baseUrl.buildUpon() + .appendPath(ExternalUris.Homepage.PATH_TESTED_SERVICES) + .appendPath("fastmail") + .withStatParams("LoginTypeFastmail") + .build() + + + @Composable + override fun LoginScreen( + snackbarHostState: SnackbarHostState, + initialLoginInfo: LoginInfo, + onLogin: (LoginInfo) -> Unit + ) { + val model: FastmailLoginModel = hiltViewModel( + creationCallback = { factory: FastmailLoginModel.Factory -> + factory.create(loginInfo = initialLoginInfo) + } + ) + + val uiState = model.uiState + LaunchedEffect(uiState.result) { + if (uiState.result != null) { + onLogin(uiState.result) + model.resetResult() + } + } + + LaunchedEffect(uiState.error) { + if (uiState.error != null) + snackbarHostState.showSnackbar(uiState.error) + } + + // contract to open the browser for authentication + val authRequestContract = rememberLauncherForActivityResult(model.authorizationContract()) { authResponse -> + if (authResponse != null) + model.authenticate(authResponse) + else + model.authCodeFailed() + } + + FastmailLoginScreen( + email = uiState.email, + onSetEmail = model::setEmail, + canContinue = uiState.canContinue, + onLogin = { + if (uiState.canContinue) { + val authRequest = model.signIn() + + try { + authRequestContract.launch(authRequest) + } catch (e: ActivityNotFoundException) { + Logger.getGlobal().log(Level.WARNING, "Couldn't start OAuth intent", e) + model.signInFailed() + } + } + } + ) + } +} + +@Composable +fun FastmailLoginScreen( + email: String, + onSetEmail: (String) -> Unit = {}, + canContinue: Boolean, + onLogin: () -> Unit = {} +) { + val context = LocalContext.current + val uriHandler = LocalUriHandler.current + + Column( + Modifier + .padding(8.dp) + .verticalScroll(rememberScrollState()) + ) { + Text( + stringResource(R.string.login_fastmail), + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(vertical = 8.dp) + ) + + val focusRequester = remember { FocusRequester() } + OutlinedTextField( + email, + singleLine = true, + onValueChange = onSetEmail, + leadingIcon = { + Icon(Icons.Default.Email, null) + }, + label = { Text(stringResource(R.string.login_fastmail_account)) }, + placeholder = { Text("example@fastmail.com") }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Next + ), + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .focusRequester(focusRequester) + ) + LaunchedEffect(Unit) { + if (email.isEmpty()) + focusRequester.requestFocus() + } + + Button( + enabled = canContinue, + onClick = { onLogin() }, + modifier = Modifier + .padding(top = 8.dp) + .wrapContentSize() + ) { + Text(stringResource(R.string.login_fastmail_sign_in)) + } + } +} + +@Composable +@Preview(showBackground = true) +fun FastmailLoginScreen_Preview_Empty() { + FastmailLoginScreen( + email = "", + canContinue = false + ) +} + +@Composable +@Preview(showBackground = true) +fun FastmailLoginScreen_Preview_WithDefaultEmail() { + FastmailLoginScreen( + email = "example@gmail.com", + canContinue = true + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/FastmailLoginModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/FastmailLoginModel.kt new file mode 100644 index 0000000..96ef0d4 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/FastmailLoginModel.kt @@ -0,0 +1,114 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import android.content.Context +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.bitfire.davdroid.R +import at.bitfire.davdroid.network.OAuthFastmail +import at.bitfire.davdroid.network.OAuthIntegration +import at.bitfire.davdroid.settings.Credentials +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.launch +import net.openid.appauth.AuthorizationResponse +import net.openid.appauth.AuthorizationService +import java.util.Locale +import java.util.logging.Level +import java.util.logging.Logger + +@HiltViewModel(assistedFactory = FastmailLoginModel.Factory::class) +class FastmailLoginModel @AssistedInject constructor( + @Assisted val initialLoginInfo: LoginInfo, + private val authService: AuthorizationService, + @ApplicationContext val context: Context, + private val logger: Logger +) : ViewModel() { + + @AssistedFactory + interface Factory { + fun create(loginInfo: LoginInfo): FastmailLoginModel + } + + override fun onCleared() { + authService.dispose() + } + + + data class UiState( + val email: String = "", + val error: String? = null, + + /** login info (set after successful login) */ + val result: LoginInfo? = null + ) { + val canContinue = email.isNotEmpty() + val emailWithDomain = if (email.contains("@")) email else "$email@fastmail.com" + } + + var uiState by mutableStateOf(UiState()) + private set + + init { + uiState = uiState.copy( + email = initialLoginInfo.credentials?.username ?: "", + error = null, + result = null + ) + } + + fun setEmail(email: String) { + uiState = uiState.copy(email = email) + } + + + fun authorizationContract() = OAuthIntegration.AuthorizationContract(authService) + + fun signIn() = + OAuthFastmail.signIn( + email = uiState.emailWithDomain, + locale = Locale.getDefault().toLanguageTag() + ) + + fun signInFailed() { + uiState = uiState.copy(error = context.getString(R.string.install_browser)) + } + + fun authenticate(authResponse: AuthorizationResponse) { + viewModelScope.launch { + try { + val credentials = Credentials(authState = OAuthIntegration.authenticate(authService, authResponse)) + + // success, provide login info to continue + uiState = uiState.copy( + result = LoginInfo( + baseUri = OAuthFastmail.baseUri, + credentials = credentials, + suggestedAccountName = uiState.emailWithDomain + ) + ) + } catch (e: Exception) { + logger.log(Level.WARNING, "Fastmail authentication failed", e) + uiState = uiState.copy(error = e.message) + } + } + } + + fun authCodeFailed() { + uiState = uiState.copy(error = context.getString(R.string.login_oauth_couldnt_obtain_auth_code)) + } + + fun resetResult() { + uiState = uiState.copy(result = null) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/GoogleLogin.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/GoogleLogin.kt new file mode 100644 index 0000000..3dad746 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/GoogleLogin.kt @@ -0,0 +1,254 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import android.content.ActivityNotFoundException +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Email +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.text.HtmlCompat +import androidx.hilt.navigation.compose.hiltViewModel +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.ExternalUris +import at.bitfire.davdroid.ui.ExternalUris.withStatParams +import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString +import at.bitfire.davdroid.ui.setup.GoogleLogin.GOOGLE_POLICY_URL +import java.util.logging.Level +import java.util.logging.Logger + +object GoogleLogin : LoginType { + + override val title: Int + get() = R.string.login_type_google + + override val helpUrl: Uri + get() = ExternalUris.Homepage.baseUrl.buildUpon() + .appendPath(ExternalUris.Homepage.PATH_TESTED_SERVICES) + .appendPath("google") + .withStatParams(javaClass.name) + .build() + + + // Google API Services User Data Policy + const val GOOGLE_POLICY_URL = + "https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes" + + + @Composable + override fun LoginScreen( + snackbarHostState: SnackbarHostState, + initialLoginInfo: LoginInfo, + onLogin: (LoginInfo) -> Unit + ) { + val model: GoogleLoginModel = hiltViewModel( + creationCallback = { factory: GoogleLoginModel.Factory -> + factory.create(loginInfo = initialLoginInfo) + } + ) + + val uiState = model.uiState + LaunchedEffect(uiState.result) { + if (uiState.result != null) { + onLogin(uiState.result) + model.resetResult() + } + } + + LaunchedEffect(uiState.error) { + if (uiState.error != null) + snackbarHostState.showSnackbar(uiState.error) + } + + // contract to open the browser for authentication + val authRequestContract = rememberLauncherForActivityResult(model.authorizationContract()) { authResponse -> + if (authResponse != null) + model.authenticate(authResponse) + else + model.authCodeFailed() + } + + GoogleLoginScreen( + email = uiState.email, + onSetEmail = model::setEmail, + customClientId = uiState.customClientId, + onSetCustomClientId = model::setCustomClientId, + canContinue = uiState.canContinue, + onLogin = { + if (uiState.canContinue) { + val authRequest = model.signIn() + + try { + authRequestContract.launch(authRequest) + } catch (e: ActivityNotFoundException) { + Logger.getGlobal().log(Level.WARNING, "Couldn't start OAuth intent", e) + model.signInFailed() + } + } + } + ) + } +} + +@Composable +fun GoogleLoginScreen( + email: String, + onSetEmail: (String) -> Unit = {}, + customClientId: String, + onSetCustomClientId: (String) -> Unit = {}, + canContinue: Boolean, + onLogin: () -> Unit = {} +) { + val context = LocalContext.current + + Column( + Modifier + .padding(8.dp) + .verticalScroll(rememberScrollState()) + ) { + Text( + stringResource(R.string.login_type_google), + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(vertical = 8.dp) + ) + + val focusRequester = remember { FocusRequester() } + OutlinedTextField( + email, + singleLine = true, + onValueChange = onSetEmail, + leadingIcon = { + Icon(Icons.Default.Email, null) + }, + label = { Text(stringResource(R.string.login_google_account)) }, + placeholder = { Text("example@gmail.com") }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Next + ), + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .focusRequester(focusRequester) + ) + LaunchedEffect(Unit) { + if (email.isEmpty()) + focusRequester.requestFocus() + } + + OutlinedTextField( + customClientId, + singleLine = true, + onValueChange = onSetCustomClientId, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { onLogin() } + ), + label = { Text(stringResource(R.string.login_google_client_id)) }, + placeholder = { Text("[...].apps.googleusercontent.com") }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + ) + + Button( + enabled = canContinue, + onClick = { onLogin() }, + modifier = Modifier + .padding(top = 8.dp) + .wrapContentSize() + ) { + Image( + painter = painterResource(R.drawable.google_g_logo), + contentDescription = stringResource(R.string.login_google), + modifier = Modifier.size(18.dp) + ) + Text( + text = stringResource(R.string.login_google), + modifier = Modifier.padding(start = 12.dp) + ) + } + + Spacer(Modifier.padding(8.dp)) + + val privacyPolicyUrl = ExternalUris.Homepage.baseUrl.buildUpon() + .appendPath(ExternalUris.Homepage.PATH_PRIVACY) + .withStatParams(javaClass.name) + .build() + val privacyPolicyNote = HtmlCompat.fromHtml( + stringResource( + R.string.login_google_client_privacy_policy, + context.getString(R.string.app_name), + privacyPolicyUrl.toString() + ), 0 + ).toAnnotatedString() + Text( + text = privacyPolicyNote, + style = MaterialTheme.typography.bodyMedium + ) + + val limitedUseNote = HtmlCompat.fromHtml( + stringResource(R.string.login_google_client_limited_use, context.getString(R.string.app_name), GOOGLE_POLICY_URL), 0 + ).toAnnotatedString() + Text( + text = limitedUseNote, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(top = 12.dp) + ) + } +} + +@Composable +@Preview(showBackground = true) +fun GoogleLoginScreen_Preview_Empty() { + GoogleLoginScreen( + email = "", + customClientId = "", + canContinue = false + ) +} + +@Composable +@Preview(showBackground = true) +fun GoogleLoginScreen_Preview_WithDefaultEmail() { + GoogleLoginScreen( + email = "example@gmail.com", + customClientId = "some-client-id", + canContinue = true + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/GoogleLoginModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/GoogleLoginModel.kt new file mode 100644 index 0000000..be274ef --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/GoogleLoginModel.kt @@ -0,0 +1,130 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import android.accounts.AccountManager +import android.content.Context +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.bitfire.davdroid.R +import at.bitfire.davdroid.network.OAuthGoogle +import at.bitfire.davdroid.network.OAuthIntegration +import at.bitfire.davdroid.settings.Credentials +import at.bitfire.davdroid.util.trimToNull +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.launch +import net.openid.appauth.AuthorizationResponse +import net.openid.appauth.AuthorizationService +import java.util.Locale +import java.util.logging.Level +import java.util.logging.Logger + +@HiltViewModel(assistedFactory = GoogleLoginModel.Factory::class) +class GoogleLoginModel @AssistedInject constructor( + @Assisted val initialLoginInfo: LoginInfo, + private val authService: AuthorizationService, + @ApplicationContext val context: Context, + private val logger: Logger +): ViewModel() { + + @AssistedFactory + interface Factory { + fun create(loginInfo: LoginInfo): GoogleLoginModel + } + + override fun onCleared() { + authService.dispose() + } + + + data class UiState( + val email: String = "", + val customClientId: String = "", + val error: String? = null, + + /** login info (set after successful login) */ + val result: LoginInfo? = null + ) { + val canContinue = email.isNotEmpty() + val emailWithDomain = if (email.contains("@")) email else "$email@gmail.com" + } + + var uiState by mutableStateOf(UiState()) + private set + + init { + uiState = uiState.copy( + email = initialLoginInfo.credentials?.username ?: findGoogleAccount() ?: "", + error = null, + result = null + ) + } + + fun setEmail(email: String) { + uiState = uiState.copy(email = email) + } + + fun setCustomClientId(clientId: String) { + uiState = uiState.copy(customClientId = clientId) + } + + + fun authorizationContract() = OAuthIntegration.AuthorizationContract(authService) + + fun signIn() = + OAuthGoogle.signIn( + email = uiState.emailWithDomain, + customClientId = uiState.customClientId.trimToNull(), + locale = Locale.getDefault().toLanguageTag() + ) + + fun signInFailed() { + uiState = uiState.copy(error = context.getString(R.string.install_browser)) + } + + fun authenticate(authResponse: AuthorizationResponse) { + viewModelScope.launch { + try { + val credentials = Credentials(authState = OAuthIntegration.authenticate(authService, authResponse)) + + // success, provide login info to continue + uiState = uiState.copy( + result = LoginInfo( + baseUri = OAuthGoogle.baseUri(uiState.emailWithDomain), + credentials = credentials, + suggestedAccountName = uiState.emailWithDomain + ) + ) + } catch (e: Exception) { + logger.log(Level.WARNING, "Google authentication failed", e) + uiState = uiState.copy(error = e.message) + } + } + } + + fun authCodeFailed() { + uiState = uiState.copy(error = context.getString(R.string.login_oauth_couldnt_obtain_auth_code)) + } + + fun resetResult() { + uiState = uiState.copy(result = null) + } + + private fun findGoogleAccount(): String? { + val accountManager = AccountManager.get(context) + return accountManager + .getAccountsByType("com.google") + .map { it.name } + .firstOrNull() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginActivity.kt new file mode 100644 index 0000000..90bd6c0 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginActivity.kt @@ -0,0 +1,150 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import at.bitfire.davdroid.settings.Credentials +import at.bitfire.davdroid.ui.account.AccountActivity +import at.bitfire.davdroid.util.SensitiveString.Companion.toSensitiveString +import dagger.hilt.android.AndroidEntryPoint +import java.net.URI +import java.net.URISyntaxException +import java.util.logging.Logger +import javax.inject.Inject + +/** + * Activity to initially connect to a server and create an account. + * Fields for server/user data can be pre-filled with extras in the Intent. + */ +@AndroidEntryPoint +class LoginActivity @Inject constructor(): AppCompatActivity() { + + @Inject lateinit var loginTypesProvider: LoginTypesProvider + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val (initialLoginType, skipLoginTypePage) = loginTypesProvider.intentToInitialLoginType(intent) + + setContent { + LoginScreen( + initialLoginType = initialLoginType, + skipLoginTypePage = skipLoginTypePage, + initialLoginInfo = loginInfoFromIntent(intent), + onNavUp = { onSupportNavigateUp() }, + onFinish = { newAccount -> + finish() + + newAccount?.let { newAccount -> + val intent = Intent(this, AccountActivity::class.java) + intent.putExtra(AccountActivity.EXTRA_ACCOUNT, newAccount) + startActivity(intent) + } + } + ) + } + } + + companion object { + + /** + * When set, "login by URL" will be activated by default, and the URL field will be set to this value. + * When not set, "login by email" will be activated by default. + */ + const val EXTRA_URL = "url" + + /** + * When set, and {@link #EXTRA_PASSWORD} is set too, the user name field will be set to this value. + * When set, and {@link #EXTRA_URL} is not set, the email address field will be set to this value. + */ + const val EXTRA_USERNAME = "username" + + /** + * When set, the password field will be set to this value. + */ + const val EXTRA_PASSWORD = "password" + + /** + * When set, Nextcloud Login Flow will be used. + */ + const val EXTRA_LOGIN_FLOW = "loginFlow" + + + /** + * Extracts login information from given intent, validates it and returns it in [LoginInfo]. + * + * @param intent Contains base url, username and password. + * @return Extracted login info. Contains null values if given info is invalid. + */ + fun loginInfoFromIntent(intent: Intent): LoginInfo { + var givenUri: String? = null + var givenUsername: String? = null + var givenPassword: String? = null + + // extract URI or email and optionally username/password from Intent data + val logger = Logger.getGlobal() + intent.data?.normalizeScheme()?.let { uri -> + val realScheme = when (uri.scheme) { + // replace caldav[s]:// and carddav[s]:// with http[s]:// + "caldav", "carddav" -> "http" + "caldavs", "carddavs", "davx5" -> "https" + + // keep these + "http", "https", "mailto" -> uri.scheme + + // unknown scheme + else -> null + } + + when (realScheme) { + "http", "https" -> { + // extract user info + uri.userInfo?.split(':')?.let { userInfo -> + givenUsername = userInfo.getOrNull(0) + givenPassword = userInfo.getOrNull(1) + } + + // use real scheme, drop user info and fragment + givenUri = try { + URI(realScheme, null, uri.host, uri.port, uri.path, uri.query, null).toString() + } catch (_: URISyntaxException) { + logger.warning("Couldn't construct URI from login Intent data: $uri") + null + } + } + + "mailto" -> + givenUsername = uri.schemeSpecificPart + } + } + + if (givenUri == null) + givenUri = intent.getStringExtra(EXTRA_URL) + + // always prefer username/password from the extras + if (intent.hasExtra(EXTRA_USERNAME)) + givenUsername = intent.getStringExtra(EXTRA_USERNAME) + if (intent.hasExtra(EXTRA_PASSWORD)) + givenPassword = intent.getStringExtra(EXTRA_PASSWORD) + + return LoginInfo( + baseUri = try { + URI(givenUri) + } catch (_: Exception) { + null + }, + credentials = Credentials( + username = givenUsername, + password = givenPassword?.toSensitiveString() + ) + ) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginDetailsPage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginDetailsPage.kt new file mode 100644 index 0000000..6cf99f8 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginDetailsPage.kt @@ -0,0 +1,25 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.lifecycle.viewmodel.compose.viewModel + +@Composable +fun LoginDetailsPage( + snackbarHostState: SnackbarHostState, + model: LoginScreenModel = viewModel() +) { + val uiState = model.loginDetailsUiState + uiState.loginType.LoginScreen( + snackbarHostState = snackbarHostState, + initialLoginInfo = uiState.loginInfo, + onLogin = { loginInfo -> + model.updateLoginInfo(loginInfo) + model.navToNextPage() + } + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginInfo.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginInfo.kt new file mode 100644 index 0000000..385a0ad --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginInfo.kt @@ -0,0 +1,19 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import at.bitfire.davdroid.settings.Credentials +import at.bitfire.vcard4android.GroupMethod +import java.net.URI + +data class LoginInfo( + val baseUri: URI? = null, + val credentials: Credentials? = null, + + val suggestedAccountName: String? = null, + + /** group method that should be pre-selected */ + val suggestedGroupMethod: GroupMethod = GroupMethod.GROUP_VCARDS +) \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginScreen.kt new file mode 100644 index 0000000..c0c32e1 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginScreen.kt @@ -0,0 +1,136 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import android.accounts.Account +import android.net.Uri +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.Help +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.AppTheme +import at.bitfire.davdroid.ui.ExternalUris +import at.bitfire.davdroid.ui.ExternalUris.withStatParams + +@Composable +fun LoginScreen( + initialLoginInfo: LoginInfo = LoginInfo(), + skipLoginTypePage: Boolean = false, + initialLoginType: LoginType = UrlLogin, + onNavUp: () -> Unit, + onFinish: (Account?) -> Unit +) { + val model: LoginScreenModel = hiltViewModel { factory: LoginScreenModel.Factory -> + factory.create(initialLoginType, skipLoginTypePage, initialLoginInfo) + } + + // handle back/up navigation + BackHandler { + model.navBack() + } + if (model.finish) { + onFinish(null) + return + } + + // get specific help URL from current login type (may be null → show "tested with" page) + val loginType = model.loginTypeUiState.loginType + + LoginScreenContent( + page = model.page, + helpUri = loginType.helpUrl, + onNavUp = onNavUp, + onFinish = onFinish + ) +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun LoginScreenContent( + page: LoginScreenModel.Page, + helpUri: Uri?, + onNavUp: () -> Unit = {}, + onFinish: (newAccount: Account?) -> Unit = {} +) { + val snackbarHostState = remember { SnackbarHostState() } + AppTheme { + Scaffold( + topBar = { + TopAppBar( + navigationIcon = { + IconButton(onClick = onNavUp) { + Icon( + Icons.AutoMirrored.Default.ArrowBack, + stringResource(R.string.navigate_up) + ) + } + }, + title = { + Text(stringResource(R.string.login_title)) + }, + actions = { + val uriHandler = LocalUriHandler.current + val specificHelpUri = helpUri ?: ExternalUris.Homepage.baseUrl.buildUpon() + .appendPath(ExternalUris.Homepage.PATH_TESTED_SERVICES) + .withStatParams("LoginScreen") + .build() + IconButton(onClick = { + // show tested-with page + uriHandler.openUri(specificHelpUri.toString()) + }) { + Icon(Icons.AutoMirrored.Default.Help, stringResource(R.string.help)) + } + } + ) + }, + snackbarHost = { SnackbarHost(snackbarHostState) } + ) { padding -> + Box( + Modifier + .fillMaxSize() + .padding(top = padding.calculateTopPadding()) + ) { + + when (page) { + LoginScreenModel.Page.LoginType -> + LoginTypePage(snackbarHostState = snackbarHostState) + + LoginScreenModel.Page.LoginDetails -> + LoginDetailsPage(snackbarHostState = snackbarHostState) + + LoginScreenModel.Page.DetectResources -> + DetectResourcesPage() + + LoginScreenModel.Page.AccountDetails -> + AccountDetailsPage( + snackbarHostState = snackbarHostState, + onAccountCreated = { account -> + onFinish(account) + } + ) + } + + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginScreenModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginScreenModel.kt new file mode 100644 index 0000000..f1b16d6 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginScreenModel.kt @@ -0,0 +1,326 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import android.accounts.Account +import android.content.Context +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.bitfire.davdroid.di.DefaultDispatcher +import at.bitfire.davdroid.repository.AccountRepository +import at.bitfire.davdroid.servicedetection.DavResourceFinder +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.vcard4android.GroupMethod +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.runInterruptible +import kotlinx.coroutines.withContext +import java.util.logging.Logger + +@HiltViewModel(assistedFactory = LoginScreenModel.Factory::class) +class LoginScreenModel @AssistedInject constructor( + @Assisted val initialLoginType: LoginType, + @Assisted val skipLoginTypePage: Boolean, + @Assisted val initialLoginInfo: LoginInfo, + private val accountRepository: AccountRepository, + @ApplicationContext val context: Context, + @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher, + private val logger: Logger, + val loginTypesProvider: LoginTypesProvider, + private val resourceFinderFactory: DavResourceFinder.Factory, + settingsManager: SettingsManager +): ViewModel() { + + @AssistedFactory + interface Factory { + fun create( + initialLoginType: LoginType, + skipLoginTypePage: Boolean, + initialLoginInfo: LoginInfo + ): LoginScreenModel + } + + enum class Page { + LoginType, + LoginDetails, + DetectResources, + AccountDetails + } + + private val startPage = if (skipLoginTypePage) + Page.LoginDetails + else + Page.LoginType + + var page by mutableStateOf(startPage) + private set + + var finish by mutableStateOf(false) + private set + + + // navigation events + + fun navToNextPage() { + when (page) { + Page.LoginType -> { + // continue to login details + loginDetailsUiState = loginDetailsUiState.copy( + loginType = loginTypeUiState.loginType + ) + page = Page.LoginDetails + } + + Page.LoginDetails -> { + // continue to resource detection + loginInfo = loginDetailsUiState.loginInfo + page = Page.DetectResources + + detectResources() + } + + Page.DetectResources -> { + // continue to account details + val emails = foundConfig?.calDAV?.emails.orEmpty().toSet() + val initialAccountName = emails.firstOrNull() + ?: loginInfo.suggestedAccountName + ?: loginInfo.credentials?.username + ?: loginInfo.baseUri?.host + ?: "" + updateAccountNameAndEmails(initialAccountName, emails) + updateGroupMethod(loginInfo.suggestedGroupMethod) + page = Page.AccountDetails + } + + Page.AccountDetails -> { + // last page + } + } + } + + fun navBack() { + when (page) { + Page.LoginType -> + finish = true + + Page.LoginDetails -> + if (loginTypesProvider.maybeNonInteractive) + finish = true + else + page = Page.LoginType + + Page.DetectResources -> { + cancelResourceDetection() + page = Page.LoginDetails + } + + Page.AccountDetails -> + page = Page.LoginDetails + } + } + + + // UI element state – first page: login type + + data class LoginTypeUiState( + val loginType: LoginType + ) + + var loginTypeUiState by mutableStateOf(LoginTypeUiState(loginType = initialLoginType)) + private set + + fun selectLoginType(loginType: LoginType) { + loginTypeUiState = loginTypeUiState.copy(loginType = loginType) + loginDetailsUiState = loginDetailsUiState.copy(loginType = loginType) + } + + + // UI element state – second page: login details + + // base URI and credentials + private var loginInfo: LoginInfo = initialLoginInfo + + data class LoginDetailsUiState( + val loginType: LoginType, + val loginInfo: LoginInfo + ) + + var loginDetailsUiState by mutableStateOf(LoginDetailsUiState( + loginType = initialLoginType, + loginInfo = loginInfo + )) + private set + + fun updateLoginInfo(loginInfo: LoginInfo) { + loginDetailsUiState = loginDetailsUiState.copy(loginInfo = loginInfo) + } + + + // UI element state – third page: detect resources + + data class DetectResourcesUiState( + val loading: Boolean = false, + val foundNothing: Boolean = false, + val encountered401: Boolean = false, + val logs: String? = null + ) + + var detectResourcesUiState by mutableStateOf(DetectResourcesUiState()) + private set + + private var foundConfig: DavResourceFinder.Configuration? = null + private var detectResourcesJob: Job? = null + + private fun detectResources() { + detectResourcesUiState = detectResourcesUiState.copy(loading = true) + detectResourcesJob = viewModelScope.launch { + val result = withContext(Dispatchers.IO) { + runInterruptible { + resourceFinderFactory.create(loginInfo.baseUri!!, loginInfo.credentials).use { finder -> + finder.findInitialConfiguration() + } + } + } + + if (result.calDAV != null || result.cardDAV != null) { + foundConfig = result + navToNextPage() + + } else { + foundConfig = null + detectResourcesUiState = detectResourcesUiState.copy( + loading = false, + foundNothing = true, + encountered401 = result.encountered401, + logs = result.logs + ) + } + } + } + + private fun cancelResourceDetection() { + detectResourcesJob?.cancel() + } + + + // UI element state – last page: account details + + data class AccountDetailsUiState( + val accountName: String = "", + val suggestedAccountNames: Set = emptySet(), + val accountNameExists: Boolean = false, + val groupMethod: GroupMethod = GroupMethod.GROUP_VCARDS, + val groupMethodReadOnly: Boolean = false, + val creatingAccount: Boolean = false, + val createdAccount: Account? = null, + val couldNotCreateAccount: Boolean = false + ) { + val showApostropheWarning = accountName.contains('\'') || accountName.contains('"') + } + + private val forcedGroupMethod = settingsManager + .getStringFlow(AccountSettings.KEY_CONTACT_GROUP_METHOD) + .map { groupMethodName -> + // map group method name to GroupMethod + if (groupMethodName != null) + try { + GroupMethod.valueOf(groupMethodName) + } catch (e: IllegalArgumentException) { + logger.warning("Invalid forced group method: $groupMethodName") + null + } + else + null + } + + // backing field that is combined with dynamic content for the resulting UI State + private var _accountDetailsUiState = MutableStateFlow(AccountDetailsUiState()) + val accountDetailsUiState = combine(_accountDetailsUiState, forcedGroupMethod) { uiState, method -> + // set group type to read-only if group method is forced + var combinedState = uiState.copy(groupMethodReadOnly = method != null) + + // apply forced group method, if applicable + if (method != null) + combinedState = combinedState.copy(groupMethod = method) + + combinedState + }.stateIn(viewModelScope, SharingStarted.Lazily, _accountDetailsUiState.value) + + fun updateAccountName(accountName: String) { + _accountDetailsUiState.update { currentState -> + currentState.copy( + accountName = accountName, + accountNameExists = accountRepository.exists(accountName) + ) + } + } + + fun updateAccountNameAndEmails(accountName: String, emails: Set) { + _accountDetailsUiState.update { currentState -> + currentState.copy( + accountName = accountName, + accountNameExists = accountRepository.exists(accountName), + suggestedAccountNames = emails + ) + } + } + + fun updateGroupMethod(groupMethod: GroupMethod) { + _accountDetailsUiState.update { currentState -> + currentState.copy(groupMethod = groupMethod) + } + } + + fun resetCouldNotCreateAccount() { + _accountDetailsUiState.update { currentState -> + currentState.copy(couldNotCreateAccount = false) + } + } + + fun createAccount() { + _accountDetailsUiState.update { currentState -> + currentState.copy(creatingAccount = true) + } + + viewModelScope.launch { + val account = withContext(defaultDispatcher) { + accountRepository.createBlocking( + accountDetailsUiState.value.accountName, + loginInfo.credentials, + foundConfig!!, + accountDetailsUiState.value.groupMethod + ) + } + + _accountDetailsUiState.update { currentState -> + if (account != null) + currentState.copy(createdAccount = account) + else + currentState.copy( + creatingAccount = false, + couldNotCreateAccount = true + ) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginType.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginType.kt new file mode 100644 index 0000000..88de996 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginType.kt @@ -0,0 +1,25 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import android.net.Uri +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable + +interface LoginType { + + val title: Int + + /** Optional URL to a provider-specific help page. */ + val helpUrl: Uri? + + @Composable + fun LoginScreen( + snackbarHostState: SnackbarHostState, + initialLoginInfo: LoginInfo, + onLogin: (LoginInfo) -> Unit + ) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginTypePage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginTypePage.kt new file mode 100644 index 0000000..1d5dcfb --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginTypePage.kt @@ -0,0 +1,32 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.lifecycle.viewmodel.compose.viewModel + +@Composable +fun LoginTypePage( + snackbarHostState: SnackbarHostState, + model: LoginScreenModel = viewModel() +) { + val uiState = model.loginTypeUiState + + // show login type selection page + model.loginTypesProvider.LoginTypePage( + snackbarHostState = snackbarHostState, + selectedLoginType = uiState.loginType, + onSelectLoginType = { loginType -> + model.selectLoginType(loginType) + }, + setInitialLoginInfo = { loginInfo -> + model.updateLoginInfo(loginInfo) + }, + onContinue = { + model.navToNextPage() + } + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginTypesProvider.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginTypesProvider.kt new file mode 100644 index 0000000..6faa126 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginTypesProvider.kt @@ -0,0 +1,39 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import android.content.Intent +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable + +interface LoginTypesProvider { + + data class LoginAction( + val loginType: LoginType, + val skipLoginTypePage: Boolean + ) + + val defaultLoginType: LoginType + + /** + * Which login type to use and whether to skip the login type page. Used for Nextcloud login + * flow and may be used for other intent started flows. + */ + fun intentToInitialLoginType(intent: Intent): LoginAction = LoginAction(defaultLoginType, false) + + /** Whether the [LoginTypePage] may be non-interactive. This causes it to be skipped in back navigation. */ + val maybeNonInteractive: Boolean + get() = false + + @Composable + fun LoginTypePage( + snackbarHostState: SnackbarHostState, + selectedLoginType: LoginType, + onSelectLoginType: (LoginType) -> Unit, + setInitialLoginInfo: (LoginInfo) -> Unit, + onContinue: () -> Unit + ) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLogin.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLogin.kt new file mode 100644 index 0000000..8080137 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLogin.kt @@ -0,0 +1,240 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import android.content.Intent +import android.net.Uri +import android.provider.Browser +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.browser.customtabs.CustomTabsIntent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Cloud +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import androidx.core.os.bundleOf +import androidx.hilt.navigation.compose.hiltViewModel +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.ExternalUris +import at.bitfire.davdroid.ui.ExternalUris.withStatParams +import at.bitfire.davdroid.ui.UiUtils.haveCustomTabs +import at.bitfire.davdroid.ui.composable.Assistant +import at.bitfire.davdroid.ui.composable.ProgressBar +import kotlinx.coroutines.launch + +object NextcloudLogin : LoginType { + + override val title: Int + get() = R.string.login_type_nextcloud + + override val helpUrl: Uri + get() = ExternalUris.Homepage.baseUrl.buildUpon() + .appendPath(ExternalUris.Homepage.PATH_TESTED_SERVICES) + .appendPath("nextcloud") + .withStatParams(javaClass.simpleName) + .build() + + + @Composable + override fun LoginScreen( + snackbarHostState: SnackbarHostState, + initialLoginInfo: LoginInfo, + onLogin: (LoginInfo) -> Unit + ) { + val model: NextcloudLoginModel = hiltViewModel( + creationCallback = { factory: NextcloudLoginModel.Factory -> + factory.create(loginInfo = initialLoginInfo) + } + ) + + val context = LocalContext.current + val checkResultCallback = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + model.onReturnFromBrowser() + } + + val uiState = model.uiState + LaunchedEffect(uiState.loginUrl) { + if (uiState.loginUrl != null) { + val loginUri = uiState.loginUrl.toString().toUri() + + if (haveCustomTabs(context)) { + // Custom Tabs are available + @Suppress("DEPRECATION") + val browser = CustomTabsIntent.Builder() + .build() + browser.intent.data = loginUri + browser.intent.putExtra( + Browser.EXTRA_HEADERS, + bundleOf("Accept-Language" to Locale.current.toLanguageTag()) + ) + checkResultCallback.launch(browser.intent) + } else { + // fallback: launch normal browser + val browser = Intent(Intent.ACTION_VIEW, loginUri) + browser.addCategory(Intent.CATEGORY_BROWSABLE) + if (browser.resolveActivity(context.packageManager) != null) { + checkResultCallback.launch(browser) + } else + this@LaunchedEffect.launch { + snackbarHostState.showSnackbar(context.getString(R.string.install_browser)) + } + } + } + } + + // continue to resource detection when result is set in model + LaunchedEffect(uiState.result) { + if (uiState.result != null) { + onLogin(uiState.result) + model.resetResult() + } + } + + NextcloudLoginScreen( + baseUrl = uiState.baseUrl, + onUpdateBaseUrl = { model.updateBaseUrl(it) }, + canContinue = uiState.canContinue, + inProgress = uiState.inProgress, + error = uiState.error, + onLogin = { model.startLoginFlow() } + ) + } + +} + +@Composable +fun NextcloudLoginScreen( + baseUrl: String, + onUpdateBaseUrl: (String) -> Unit = {}, + canContinue: Boolean, + inProgress: Boolean, + error: String? = null, + onLogin: () -> Unit = {} +) { + Assistant( + nextLabel = stringResource(R.string.login_login), + nextEnabled = canContinue, + onNext = onLogin + ) { + if (inProgress) + ProgressBar( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + + Column(modifier = Modifier.padding(8.dp)) { + Text( + stringResource(R.string.login_nextcloud_login_with_nextcloud), + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(vertical = 8.dp) + ) + + Column { + Text( + stringResource(R.string.login_nextcloud_login_flow_text), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 8.dp) + ) + + val focusRequester = remember { FocusRequester() } + OutlinedTextField( + value = baseUrl, + onValueChange = onUpdateBaseUrl, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + .focusRequester(focusRequester), + enabled = !inProgress, + leadingIcon = { + Icon(Icons.Default.Cloud, null) + }, + label = { + Text(stringResource(R.string.login_nextcloud_login_flow_server_address)) + }, + placeholder = { Text("cloud.example.com") }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Uri, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { onLogin() } + ), + singleLine = true + ) + LaunchedEffect(Unit) { + if (baseUrl.isEmpty()) + focusRequester.requestFocus() + } + + if (error != null) + Card(Modifier.fillMaxWidth()) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(8.dp) + ) { + Icon( + Icons.Default.Warning, + contentDescription = null, + modifier = Modifier.padding(end = 4.dp) + ) + Text( + error, + style = MaterialTheme.typography.bodyLarge + ) + } + } + } + } + } +} + +@Composable +@Preview +fun NextcloudLoginScreen_Preview() { + NextcloudLoginScreen( + baseUrl = "cloud.example.com", + canContinue = true, + inProgress = false, + error = null + ) +} + +@Composable +@Preview +fun NextcloudLoginScreen_Preview_InProgressError() { + NextcloudLoginScreen( + baseUrl = "cloud.example.com", + canContinue = true, + inProgress = true, + error = "Some Error" + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLoginModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLoginModel.kt new file mode 100644 index 0000000..bee5072 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLoginModel.kt @@ -0,0 +1,180 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import android.content.Context +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.bitfire.davdroid.network.NextcloudLoginFlow +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.launch +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import java.util.logging.Level +import java.util.logging.Logger + +@HiltViewModel(assistedFactory = NextcloudLoginModel.Factory::class) +class NextcloudLoginModel @AssistedInject constructor( + @Assisted val initialLoginInfo: LoginInfo, + @ApplicationContext val context: Context, + private val logger: Logger, + private val loginFlow: NextcloudLoginFlow +): ViewModel() { + + @AssistedFactory + interface Factory { + fun create(loginInfo: LoginInfo): NextcloudLoginModel + } + + /*companion object { + const val STATE_POLL_URL = "poll_url" + const val STATE_TOKEN = "token" + }*/ + + data class UiState( + val baseUrl: String = "", + val inProgress: Boolean = false, + val error: String? = null, + + /** URL to open in the browser (set during Login Flow) */ + val loginUrl: HttpUrl? = null, + + /** login info (set after successful login) */ + val result: LoginInfo? = null + ) { + + val baseHttpUrl: HttpUrl? = run { + val baseUrlWithPrefix = + if (baseUrl.startsWith("http://") || baseUrl.startsWith("https://")) + baseUrl + else + "https://$baseUrl" + + baseUrlWithPrefix.toHttpUrlOrNull() + } + + val canContinue = !inProgress && baseHttpUrl != null + + } + + var uiState by mutableStateOf(UiState()) + private set + + init { + val baseUri = initialLoginInfo.baseUri + if (baseUri != null) + uiState = uiState.copy( + baseUrl = baseUri.toString() + .removePrefix("https://") + .removeSuffix(NextcloudLoginFlow.FLOW_V1_PATH) + .removeSuffix(NextcloudLoginFlow.FLOW_V2_PATH) + .removeSuffix(NextcloudLoginFlow.DAV_PATH) + ) + + uiState = uiState.copy( + error = null, + result = null + ) + } + + fun updateBaseUrl(baseUrl: String) { + uiState = uiState.copy(baseUrl = baseUrl) + } + + // Login flow state + /*private var pollUrl: HttpUrl? + get() = state.get(STATE_POLL_URL)?.toHttpUrlOrNull() + set(value) { + state[STATE_POLL_URL] = value.toString() + } + private var token: String? + get() = state.get(STATE_TOKEN) + set(value) { + state[STATE_TOKEN] = value + }*/ + + override fun onCleared() { + loginFlow.close() + } + + + /** + * Starts the Login Flow. + */ + fun startLoginFlow() { + val baseUrl = uiState.baseHttpUrl + if (uiState.inProgress || baseUrl == null) + return + + uiState = uiState.copy( + inProgress = true, + error = null + ) + + viewModelScope.launch { + try { + val loginUrl = loginFlow.initiate(baseUrl) + + uiState = uiState.copy( + loginUrl = loginUrl, + inProgress = false + ) + + } catch (e: Exception) { + logger.log(Level.WARNING, "Initiating Login Flow failed", e) + + uiState = uiState.copy( + inProgress = false, + error = e.toString() + ) + } + } + } + + /** + * Called when the custom tab / browser activity is finished. If memory is low, our + * [NextcloudLogin] and its model have been cleared in the meanwhile. So if + * we need certain data from the model, we have to make sure that these data are retained when the + * model is cleared (saved state). + */ + fun onReturnFromBrowser() = viewModelScope.launch { + // Login Flow has been started in browser by UI, should not be started again + uiState = uiState.copy( + loginUrl = null, + inProgress = true + ) + + val loginInfo = try { + loginFlow.fetchLoginInfo() + } catch (e: Exception) { + logger.log(Level.WARNING, "Fetching login info failed", e) + uiState = uiState.copy( + inProgress = false, + error = e.toString() + ) + return@launch + } + + uiState = uiState.copy( + inProgress = false, + result = loginInfo + ) + } + + fun resetResult() { + uiState = uiState.copy( + loginUrl = null, + result = null + ) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/UrlLogin.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/UrlLogin.kt new file mode 100644 index 0000000..9a5291b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/UrlLogin.kt @@ -0,0 +1,184 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import android.net.Uri +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.Folder +import androidx.compose.material.icons.filled.Password +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.text.HtmlCompat +import androidx.hilt.navigation.compose.hiltViewModel +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.ExternalUris +import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString +import at.bitfire.davdroid.ui.composable.Assistant +import at.bitfire.davdroid.ui.composable.PasswordTextField + +object UrlLogin : LoginType { + + override val title + get() = R.string.login_type_url + + override val helpUrl: Uri? + get() = null + + @Composable + override fun LoginScreen( + snackbarHostState: SnackbarHostState, + initialLoginInfo: LoginInfo, + onLogin: (LoginInfo) -> Unit + ) { + val model: UrlLoginModel = hiltViewModel( + creationCallback = { factory: UrlLoginModel.Factory -> + factory.create(loginInfo = initialLoginInfo) + } + ) + + val uiState = model.uiState + UrlLoginScreen( + url = uiState.url, + onSetUrl = model::setUrl, + username = uiState.username, + onSetUsername = model::setUsername, + password = uiState.password, + canContinue = uiState.canContinue, + onLogin = { + if (uiState.canContinue) + onLogin(uiState.asLoginInfo()) + } + ) + } + +} + +@Composable +fun UrlLoginScreen( + url: String, + onSetUrl: (String) -> Unit = {}, + username: String, + onSetUsername: (String) -> Unit = {}, + password: TextFieldState, + canContinue: Boolean, + onLogin: () -> Unit = {} +) { + val focusRequester = remember { FocusRequester() } + + Assistant( + nextLabel = stringResource(R.string.login_login), + nextEnabled = canContinue, + onNext = onLogin + ) { + Column(modifier = Modifier.padding(8.dp)) { + Text( + stringResource(R.string.login_type_url), + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + + val manualUrl = ExternalUris.Manual.baseUrl.buildUpon() + .appendPath(ExternalUris.Manual.PATH_ACCOUNTS_COLLECTIONS) + .fragment(ExternalUris.Manual.FRAGMENT_SERVICE_DISCOVERY) + .build() + val urlInfo = HtmlCompat.fromHtml(stringResource(R.string.login_base_url_info, manualUrl), HtmlCompat.FROM_HTML_MODE_COMPACT) + Text( + text = urlInfo.toAnnotatedString(), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 16.dp) + ) + + OutlinedTextField( + value = url, + onValueChange = onSetUrl, + label = { Text(stringResource(R.string.login_base_url)) }, + placeholder = { Text("dav.example.com/path") }, + singleLine = true, + leadingIcon = { + Icon(Icons.Default.Folder, null) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Uri, + imeAction = ImeAction.Next + ), + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester) + ) + + OutlinedTextField( + value = username, + onValueChange = onSetUsername, + label = { Text(stringResource(R.string.login_user_name)) }, + singleLine = true, + leadingIcon = { + Icon(Icons.Default.AccountCircle, null) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Next + ), + modifier = Modifier.fillMaxWidth() + ) + + PasswordTextField( + password = password, + labelText = stringResource(R.string.login_password), + leadingIcon = { + Icon(Icons.Default.Password, null) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ), + onKeyboardAction = { + if (canContinue) + onLogin() + }, + modifier = Modifier.fillMaxWidth() + ) + } + } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } +} + +@Composable +@Preview +fun UrlLoginScreen_Preview() { + UrlLoginScreen( + url = "https://example.com", + username = "user", + password = rememberTextFieldState(""), + canContinue = false + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/UrlLoginModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/UrlLoginModel.kt new file mode 100644 index 0000000..7f0de6d --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/UrlLoginModel.kt @@ -0,0 +1,77 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import at.bitfire.davdroid.settings.Credentials +import at.bitfire.davdroid.util.DavUtils.toURIorNull +import at.bitfire.davdroid.util.SensitiveString.Companion.toSensitiveString +import at.bitfire.davdroid.util.trimToNull +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel + +@HiltViewModel(assistedFactory = UrlLoginModel.Factory::class) +class UrlLoginModel @AssistedInject constructor( + @Assisted val initialLoginInfo: LoginInfo +): ViewModel() { + + @AssistedFactory + interface Factory { + fun create(loginInfo: LoginInfo): UrlLoginModel + } + + data class UiState( + val url: String = "", + val username: String = "", + val password: TextFieldState = TextFieldState() + ) { + + val urlWithPrefix = + if (url.startsWith("http://") || url.startsWith("https://")) + url + else + "https://$url" + val uri = urlWithPrefix.trim().toURIorNull() + + val canContinue // we have to use get() because password is not immutable + get() = uri != null && username.isNotEmpty() && password.text.toString().isNotEmpty() + + fun asLoginInfo(): LoginInfo = + LoginInfo( + baseUri = uri, + credentials = Credentials( + username = username.trimToNull(), + password = password.text.toString().trimToNull()?.toSensitiveString() + ) + ) + + } + + var uiState by mutableStateOf(UiState()) + private set + + init { + uiState = UiState( + url = initialLoginInfo.baseUri?.toString()?.removePrefix("https://") ?: "", + username = initialLoginInfo.credentials?.username ?: "", + password = TextFieldState(initialLoginInfo.credentials?.password?.asString() ?: "") + ) + } + + fun setUrl(url: String) { + uiState = uiState.copy(url = url) + } + + fun setUsername(username: String) { + uiState = uiState.copy(username = username) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivity.kt new file mode 100644 index 0000000..4f3d84d --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivity.kt @@ -0,0 +1,26 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.webdav + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class AddWebdavMountActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + AddWebdavMountScreen( + onNavUp = { onSupportNavigateUp() }, + onFinish = { finish() } + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountModel.kt new file mode 100644 index 0000000..c945122 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountModel.kt @@ -0,0 +1,108 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.webdav + +import android.content.Context +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.settings.Credentials +import at.bitfire.davdroid.util.SensitiveString.Companion.toSensitiveString +import at.bitfire.davdroid.util.trimToNull +import at.bitfire.davdroid.webdav.WebDavMountRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.launch +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import javax.inject.Inject + +@HiltViewModel +class AddWebdavMountModel @Inject constructor( + @ApplicationContext val context: Context, + val db: AppDatabase, + private val mountRepository: WebDavMountRepository +): ViewModel() { + + data class UiState( + val isLoading: Boolean = false, + val success: Boolean = false, + val error: String? = null, + val displayName: String = "", + val url: String = "", + val username: String = "", + val password: TextFieldState = TextFieldState(), + val certificateAlias: String? = null + ) { + val urlWithPrefix = + if (url.startsWith("http://", true) || url.startsWith("https://", true)) + url + else + "https://$url" + val httpUrl = urlWithPrefix.toHttpUrlOrNull() + val canContinue = displayName.isNotBlank() && httpUrl != null + } + + var uiState by mutableStateOf(UiState()) + private set + + fun resetError() { + uiState = uiState.copy(error = null) + } + + fun setDisplayName(displayName: String) { + uiState = uiState.copy(displayName = displayName) + } + + fun setUrl(url: String) { + uiState = uiState.copy(url = url) + } + + fun setUsername(username: String) { + uiState = uiState.copy(username = username) + } + + fun setCertificateAlias(certAlias: String) { + uiState = uiState.copy(certificateAlias = certAlias) + } + + + fun addMount() { + if (uiState.isLoading) + return + val url = uiState.httpUrl ?: return + uiState = uiState.copy(isLoading = true) + + val displayName = uiState.displayName + val credentials = Credentials( + username = uiState.username.trimToNull(), + password = uiState.password.text.trimToNull()?.toSensitiveString(), + certificateAlias = uiState.certificateAlias + ) + + viewModelScope.launch { + var error: String? = null + try { + if (!mountRepository.addMount(url, displayName, credentials)) + error = context.getString(R.string.webdav_add_mount_no_support) + else { + uiState = uiState.copy(success = true) + + // refresh quota + mountRepository.refreshAllQuota() + } + } catch (e: Exception) { + error = e.localizedMessage + } + + uiState = uiState.copy(isLoading = false, error = error) + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountScreen.kt new file mode 100644 index 0000000..1e136af --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountScreen.kt @@ -0,0 +1,266 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.webdav + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.Help +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.Cloud +import androidx.compose.material.icons.filled.Password +import androidx.compose.material.icons.filled.Sell +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.AppTheme +import at.bitfire.davdroid.ui.composable.Assistant +import at.bitfire.davdroid.ui.composable.PasswordTextField +import at.bitfire.davdroid.ui.composable.SelectClientCertificateCard + +@Composable +fun AddWebdavMountScreen( + onNavUp: () -> Unit = {}, + onFinish: () -> Unit = {}, + model: AddWebdavMountModel = viewModel() +) { + val uiState = model.uiState + + if (uiState.success) { + onFinish() + return + } + + AppTheme { + AddWebDavMountScreen( + isLoading = uiState.isLoading, + error = uiState.error, + onResetError = model::resetError, + displayName = uiState.displayName, + onSetDisplayName = model::setDisplayName, + url = uiState.url, + onSetUrl = model::setUrl, + username = uiState.username, + onSetUsername = model::setUsername, + password = uiState.password, + certificateAlias = uiState.certificateAlias, + onSetCertificateAlias = model::setCertificateAlias, + canContinue = uiState.canContinue, + onAddMount = { model.addMount() }, + onNavUp = onNavUp + ) + } +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun AddWebDavMountScreen( + isLoading: Boolean, + error: String?, + onResetError: () -> Unit = {}, + displayName: String, + onSetDisplayName: (String) -> Unit = {}, + url: String, + onSetUrl: (String) -> Unit = {}, + username: String, + onSetUsername: (String) -> Unit = {}, + password: TextFieldState, + certificateAlias: String?, + onSetCertificateAlias: (String) -> Unit = {}, + canContinue: Boolean, + onAddMount: () -> Unit = {}, + onNavUp: () -> Unit = {} +) { + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(error) { + if (error != null) { + snackbarHostState.showSnackbar(error) + onResetError() + } + } + + Scaffold( + topBar = { + TopAppBar( + navigationIcon = { + IconButton(onClick = onNavUp) { + Icon(Icons.AutoMirrored.Default.ArrowBack, stringResource(R.string.navigate_up)) + } + }, + title = { Text(stringResource(R.string.webdav_add_mount_title)) }, + actions = { + val uriHandler = LocalUriHandler.current + IconButton( + onClick = { + uriHandler.openUri(webdavMountsHelpUrl().toString()) + } + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Help, + contentDescription = stringResource(R.string.help) + ) + } + } + ) + }, + snackbarHost = { SnackbarHost(snackbarHostState) } + ) { paddingValues -> + Assistant( + nextLabel = stringResource(R.string.webdav_add_mount_add), + nextEnabled = canContinue && !isLoading, + isLoading = isLoading, + onNext = onAddMount + ) { + Column( + modifier = Modifier + .padding(paddingValues) + .padding(8.dp) + ) { + val focusRequester = remember { FocusRequester() } + + Text( + text = stringResource(R.string.webdav_add_mount_mountpoint_displayname), + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + + OutlinedTextField( + label = { Text(stringResource(R.string.webdav_add_mount_url)) }, + leadingIcon = { Icon(Icons.Default.Cloud, contentDescription = null) }, + placeholder = { Text("dav.example.com") }, + value = url, + onValueChange = onSetUrl, + singleLine = true, + enabled = !isLoading, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Uri + ), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + .focusRequester(focusRequester) + ) + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + OutlinedTextField( + label = { Text(stringResource(R.string.webdav_add_mount_display_name)) }, + value = displayName, + onValueChange = onSetDisplayName, + singleLine = true, + leadingIcon = { + Icon(Icons.Default.Sell, null) + }, + enabled = !isLoading, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + ) + + Text( + text = stringResource(R.string.webdav_add_mount_authentication), + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + OutlinedTextField( + label = { Text(stringResource(R.string.login_user_name_optional)) }, + value = username, + onValueChange = onSetUsername, + singleLine = true, + leadingIcon = { + Icon(Icons.Default.AccountCircle, null) + }, + enabled = !isLoading, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Email + ), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + PasswordTextField( + password = password, + labelText = stringResource(R.string.login_password_optional), + enabled = !isLoading, + leadingIcon = { + Icon(Icons.Default.Password, null) + }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done + ), + onKeyboardAction = { + // can only be called when not loading + if (canContinue) + onAddMount() + }, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + SelectClientCertificateCard( + snackbarHostState = snackbarHostState, + enabled = !isLoading, + chosenAlias = certificateAlias, + onAliasChosen = onSetCertificateAlias, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + } + } + } +} + +@Composable +@Preview +fun AddWebDavMountScreen_Preview() { + AppTheme { + AddWebDavMountScreen( + isLoading = true, + error = null, + displayName = "Test", + url = "https://example.com", + username = "user", + password = rememberTextFieldState("password"), + certificateAlias = null, + canContinue = true + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsActivity.kt new file mode 100644 index 0000000..4c47088 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsActivity.kt @@ -0,0 +1,29 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.webdav + +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class WebdavMountsActivity: AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + WebdavMountsScreen( + onAddWebdavMount = { + startActivity(Intent(this, AddWebdavMountActivity::class.java)) + }, + onNavUp = { onSupportNavigateUp() } + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsModel.kt new file mode 100644 index 0000000..dab513c --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsModel.kt @@ -0,0 +1,62 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.webdav + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.bitfire.davdroid.db.WebDavMount +import at.bitfire.davdroid.webdav.WebDavMountRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class WebdavMountsModel @Inject constructor( + private val mountRepository: WebDavMountRepository +): ViewModel() { + + private val mounts = mountRepository.getAllFlow() + + // UI state + val mountInfos = mountRepository.getAllWithRootFlow() + var refreshingQuota by mutableStateOf(false) + private set + + init { + // refresh quota as soon as mounts are available + viewModelScope.launch { + mounts.collect { + refreshQuota() + } + } + } + + /** + * Refreshes quota of all mounts (causes progress bar to be shown during refresh). + */ + fun refreshQuota() { + if (refreshingQuota) + return + refreshingQuota = true + + viewModelScope.launch { + mountRepository.refreshAllQuota() + refreshingQuota = false + } + } + + /** + * Removes the mountpoint locally (= deletes connection information). + */ + fun remove(mount: WebDavMount) { + viewModelScope.launch { + mountRepository.delete(mount) + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsScreen.kt new file mode 100644 index 0000000..1a3d94d --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsScreen.kt @@ -0,0 +1,423 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.webdav + +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.DocumentsContract +import android.text.format.Formatter +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.Help +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +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.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.app.ShareCompat +import androidx.core.text.HtmlCompat +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.WebDavMount +import at.bitfire.davdroid.db.WebDavMountWithQuota +import at.bitfire.davdroid.ui.AppTheme +import at.bitfire.davdroid.ui.ExternalUris +import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString +import at.bitfire.davdroid.ui.composable.ProgressBar +import at.bitfire.davdroid.util.DavUtils +import kotlinx.coroutines.delay +import okhttp3.HttpUrl + +@Composable +fun WebdavMountsScreen( + onAddWebdavMount: () -> Unit, + onNavUp: () -> Unit, + model: WebdavMountsModel = viewModel() +) { + val mountInfos by model.mountInfos.collectAsStateWithLifecycle(emptyList()) + + AppTheme { + WebdavMountsScreen( + mountInfos = mountInfos, + refreshingQuota = model.refreshingQuota, + onRefreshQuota = { + model.refreshQuota() + }, + onAddMount = onAddWebdavMount, + onRemoveMount = { mount -> + model.remove(mount) + }, + onNavUp = onNavUp + ) + } +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun WebdavMountsScreen( + mountInfos: List, + refreshingQuota: Boolean = false, + onRefreshQuota: () -> Unit = {}, + onAddMount: () -> Unit = {}, + onRemoveMount: (WebDavMount) -> Unit = {}, + onNavUp: () -> Unit = {} +) { + val uriHandler = LocalUriHandler.current + + var isRefreshing by remember { mutableStateOf(false) } + LaunchedEffect(isRefreshing) { + if (isRefreshing) { + delay(300) + isRefreshing = false + } + } + + Scaffold( + topBar = { + TopAppBar( + navigationIcon = { + IconButton( + onClick = onNavUp + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null + ) + } + }, + title = { Text(stringResource(R.string.webdav_mounts_title)) }, + actions = { + IconButton( + onClick = { + uriHandler.openUri(webdavMountsHelpUrl().toString()) + } + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Help, + contentDescription = stringResource(R.string.help) + ) + } + } + ) + }, + floatingActionButton = { + FloatingActionButton( + onClick = onAddMount, + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = stringResource(R.string.webdav_add_mount_add) + ) + } + } + ) { paddingValues -> + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = { isRefreshing = true; onRefreshQuota() }, + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + if (mountInfos.isEmpty()) + HintText() + else { + Column { + if (refreshingQuota) + ProgressBar( + modifier = Modifier + .fillMaxWidth() + .height(4.dp)) + else + Spacer(Modifier.height(4.dp)) + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 8.dp) + ) { + items(mountInfos, key = { it.mount.id }, contentType = { "mount" }) { + WebdavMountsItem( + info = it, + onRemoveMount = onRemoveMount + ) + } + } + } + } + } + } +} + +@Composable +fun HintText() { + Column( + modifier = Modifier + .fillMaxSize() + .wrapContentSize(align = Alignment.Center) + .padding(horizontal = 16.dp) + ) { + Text( + text = stringResource(R.string.webdav_mounts_empty), + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + ) + + val text = HtmlCompat.fromHtml( + stringResource( + R.string.webdav_add_mount_empty_more_info, + webdavMountsHelpUrl().toString() + ), + 0 + ).toAnnotatedString() + Text( + text = text, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.fillMaxWidth() + ) + } +} + +@Composable +fun WebdavMountsItem( + info: WebDavMountWithQuota, + onRemoveMount: (WebDavMount) -> Unit = {}, +) { + var showingDialog by remember { mutableStateOf(false) } + if (showingDialog) { + AlertDialog( + onDismissRequest = { showingDialog = false }, + title = { Text(stringResource(R.string.webdav_remove_mount_title)) }, + text = { Text(stringResource(R.string.webdav_remove_mount_text)) }, + confirmButton = { + Button( + onClick = { + onRemoveMount(info.mount) + } + ) { + Text(stringResource(R.string.dialog_remove)) + } + }, + dismissButton = { + OutlinedButton( + onClick = { showingDialog = false } + ) { + Text(stringResource(R.string.dialog_deny)) + } + } + ) + } + + ElevatedCard( + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = info.mount.name, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + style = MaterialTheme.typography.bodyLarge + ) + Text( + text = info.mount.url.toString(), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace) + ) + + val quotaUsed = info.quotaUsed + val quotaAvailable = info.quotaAvailable + if (quotaUsed != null && quotaAvailable != null) { + val quotaTotal = quotaUsed + quotaAvailable + val progress = quotaUsed.toFloat() / quotaTotal + ProgressBar( + progress = { progress }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + ) + val context = LocalContext.current + Text( + text = stringResource( + R.string.webdav_mounts_quota_used_available, + Formatter.formatFileSize(context, quotaUsed), + Formatter.formatFileSize(context, quotaAvailable) + ), + modifier = Modifier.fillMaxWidth() + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val context = LocalContext.current + + val browser = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) { result -> + result.data?.data?.let { uri -> + ShareCompat.IntentBuilder(context) + .setType(DavUtils.MIME_TYPE_ACCEPT_ALL) + .addStream(uri) + .startChooser() + } + } + + Button( + onClick = { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "*/*" + } + val uri = DocumentsContract.buildRootUri(context.getString(R.string.webdav_authority), info.mount.id.toString()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri) + } + browser.launch(intent) + } + ) { + Text( + text = stringResource(R.string.webdav_mounts_share_content) + ) + } + Spacer(Modifier.weight(1f)) + IconButton( + onClick = { showingDialog = true } + ) { + Icon( + imageVector = Icons.Filled.Delete, + contentDescription = stringResource(R.string.webdav_mounts_unmount) + ) + } + } + } + } +} + +@Composable +@Preview +fun WebdavMountsScreen_Preview_Empty() { + AppTheme { + WebdavMountsScreen( + mountInfos = emptyList(), + refreshingQuota = false + ) + } +} + +@Composable +@Preview +fun WebdavMountsScreen_Preview_TwoMounts() { + AppTheme { + WebdavMountsScreen( + mountInfos = listOf( + WebDavMountWithQuota( + mount = WebDavMount( + id = 0, + name = "Preview Webdav Mount 1", + url = HttpUrl.Builder() + .scheme("https") + .host("example.com") + .build() + ), + quotaAvailable = 1024 * 1024 * 1024, + quotaUsed = 512 * 1024 * 1024 + ), + WebDavMountWithQuota( + mount = WebDavMount( + id = 1, + name = "Preview Webdav Mount 2", + url = HttpUrl.Builder() + .scheme("https") + .host("example.com") + .build() + ), + quotaAvailable = 1024 * 1024 * 1024, + quotaUsed = 512 * 1024 * 1024 + ) + ), + refreshingQuota = true + ) + } +} + +@Composable +@Preview +fun WebdavMountsItem_Preview() { + AppTheme { + WebdavMountsItem( + info = WebDavMountWithQuota( + mount = WebDavMount( + id = 0, + name = "Preview Webdav Mount", + url = HttpUrl.Builder() + .scheme("https") + .host("example.com") + .build() + ), + quotaAvailable = 1024 * 1024 * 1024, + quotaUsed = 512 * 1024 * 1024 + ) + ) + } +} + + +fun webdavMountsHelpUrl(): Uri = ExternalUris.Manual.baseUrl.buildUpon() + .appendPath(ExternalUris.Manual.PATH_WEBDAV_MOUNTS) + .build() diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/CalendarColorPickerDialog.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/CalendarColorPickerDialog.kt new file mode 100644 index 0000000..ff0d2ab --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/CalendarColorPickerDialog.kt @@ -0,0 +1,62 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.widget + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import at.bitfire.synctools.icalendar.Css3Color + +@OptIn(ExperimentalLayoutApi::class) +@Composable +@Preview +fun CalendarColorPickerDialog( + onSelectColor: (color: Int) -> Unit = {}, + onDismiss: () -> Unit = {} +) { + val colors = remember { + Css3Color.entries.sortedBy { css3Color -> + Color(css3Color.argb).luminance() + } + } + + Dialog(onDismissRequest = onDismiss) { + Card(Modifier.verticalScroll(rememberScrollState())) { + FlowRow(Modifier.padding(8.dp)) { + for (color in colors) { + Box(Modifier.padding(2.dp)) { + Box(Modifier + .background(color = Color(color.argb), shape = CircleShape) + .clickable { onSelectColor(color.argb) } + .size(32.dp) + .padding(8.dp) + .semantics { + contentDescription = color.name + } + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/IconSyncButtonWidget.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/IconSyncButtonWidget.kt new file mode 100644 index 0000000..16f283b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/IconSyncButtonWidget.kt @@ -0,0 +1,84 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.widget + +import android.content.Context +import android.widget.Toast +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.glance.ColorFilter +import androidx.glance.GlanceId +import androidx.glance.GlanceModifier +import androidx.glance.Image +import androidx.glance.ImageProvider +import androidx.glance.LocalContext +import androidx.glance.action.clickable +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.cornerRadius +import androidx.glance.appwidget.provideContent +import androidx.glance.background +import androidx.glance.layout.Alignment +import androidx.glance.layout.Box +import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.size +import androidx.glance.unit.ColorProvider +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.M3ColorScheme +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent + +/** + * A widget with a "Sync all" button displaying just an icon to indicate the action. + */ +class IconSyncButtonWidget : GlanceAppWidget() { + + // Hilt over @AndroidEntryPoint is not available for widgets + @EntryPoint + @InstallIn(SingletonComponent::class) + interface SyncButtonWidgetEntryPoint { + fun model(): SyncWidgetModel + } + + + override suspend fun provideGlance(context: Context, id: GlanceId) { + // initial data + val entryPoint = EntryPointAccessors.fromApplication(context) + val model = entryPoint.model() + + // will be called when the widget is updated + provideContent { + WidgetContent(model) + } + } + + @Composable + private fun WidgetContent(model: SyncWidgetModel) { + val context = LocalContext.current + + Box( + modifier = GlanceModifier + .size(50.dp) + .background(ColorProvider(M3ColorScheme.primaryLight)) + .cornerRadius(25.dp) + .clickable { + model.requestSync() + Toast.makeText(context, R.string.sync_started, Toast.LENGTH_SHORT).show() + }, + contentAlignment = Alignment.Center, + ) { + Image( + provider = ImageProvider(R.drawable.ic_sync), + contentDescription = context.getString(R.string.widget_sync_all_accounts), + modifier = GlanceModifier.fillMaxSize().size(32.dp), + colorFilter = ColorFilter.tint( + ColorProvider(M3ColorScheme.onPrimaryLight) + ) + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/IconSyncButtonWidgetReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/IconSyncButtonWidgetReceiver.kt new file mode 100644 index 0000000..1b5f2b3 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/IconSyncButtonWidgetReceiver.kt @@ -0,0 +1,12 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.widget + +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetReceiver + +class IconSyncButtonWidgetReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget: GlanceAppWidget = IconSyncButtonWidget() +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/LabeledSyncButtonWidget.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/LabeledSyncButtonWidget.kt new file mode 100644 index 0000000..859a6a6 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/LabeledSyncButtonWidget.kt @@ -0,0 +1,100 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.widget + +import android.content.Context +import android.widget.Toast +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.glance.ColorFilter +import androidx.glance.GlanceId +import androidx.glance.GlanceModifier +import androidx.glance.Image +import androidx.glance.ImageProvider +import androidx.glance.LocalContext +import androidx.glance.action.clickable +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.cornerRadius +import androidx.glance.appwidget.provideContent +import androidx.glance.background +import androidx.glance.layout.Alignment +import androidx.glance.layout.Row +import androidx.glance.layout.fillMaxWidth +import androidx.glance.layout.padding +import androidx.glance.layout.size +import androidx.glance.text.Text +import androidx.glance.text.TextDefaults +import androidx.glance.unit.ColorProvider +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.M3ColorScheme +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent + +/** + * A widget with a "Sync all" button displaying an icon and a label. + */ +class LabeledSyncButtonWidget : GlanceAppWidget() { + + // Hilt over @AndroidEntryPoint is not available for widgets + @EntryPoint + @InstallIn(SingletonComponent::class) + interface SyncButtonWidgetEntryPoint { + fun model(): SyncWidgetModel + } + + + override suspend fun provideGlance(context: Context, id: GlanceId) { + // initial data + val entryPoint = EntryPointAccessors.fromApplication(context) + val model = entryPoint.model() + + // will be called when the widget is updated + provideContent { + WidgetContent(model) + } + } + + @Composable + private fun WidgetContent(model: SyncWidgetModel) { + val context = LocalContext.current + + Row( + modifier = GlanceModifier + .fillMaxWidth() + .background(ColorProvider(M3ColorScheme.primaryLight)) + .cornerRadius(16.dp) + .padding(4.dp) + .clickable { + model.requestSync() + Toast.makeText(context, R.string.sync_started, Toast.LENGTH_SHORT).show() + }, + verticalAlignment = Alignment.CenterVertically + ) { + val onPrimary = ColorProvider(M3ColorScheme.onPrimaryLight) + Image( + provider = ImageProvider(R.drawable.ic_sync), + contentDescription = context.getString(R.string.widget_sync_all_accounts), + modifier = GlanceModifier + .padding(vertical = 8.dp, horizontal = 8.dp) + .size(32.dp), + colorFilter = ColorFilter.tint(onPrimary) + ) + Text( + text = context.getString(R.string.widget_sync_all), + modifier = GlanceModifier + .defaultWeight() + .padding(end = 8.dp), + style = TextDefaults.defaultTextStyle.copy( + color = onPrimary, + fontSize = 16.sp + ) + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/LabeledSyncButtonWidgetReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/LabeledSyncButtonWidgetReceiver.kt new file mode 100644 index 0000000..a32d7f3 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/LabeledSyncButtonWidgetReceiver.kt @@ -0,0 +1,12 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.widget + +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetReceiver + +class LabeledSyncButtonWidgetReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget: GlanceAppWidget = LabeledSyncButtonWidget() +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/SyncWidgetModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/SyncWidgetModel.kt new file mode 100644 index 0000000..376e9b2 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/SyncWidgetModel.kt @@ -0,0 +1,28 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.widget + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.bitfire.davdroid.repository.AccountRepository +import at.bitfire.davdroid.sync.worker.SyncWorkerManager +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import javax.inject.Inject + +class SyncWidgetModel @Inject constructor( + private val accountRepository: AccountRepository, + @ApplicationContext val context: Context, + private val syncWorkerManager: SyncWorkerManager +): ViewModel() { + + fun requestSync() = viewModelScope.launch(Dispatchers.Default) { + for (account in accountRepository.getAll()) + syncWorkerManager.enqueueOneTimeAllAuthorities(account, manual = true) + } + +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/BroadcastReceiverFlow.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/BroadcastReceiverFlow.kt new file mode 100644 index 0000000..1f38c44 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/BroadcastReceiverFlow.kt @@ -0,0 +1,80 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.util + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import androidx.core.content.ContextCompat +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import java.util.logging.Logger + +/** + * Creates a flow that emits the respective [Intent] when a broadcast is received. + * + * @param context the context to register the receiver with + * @param filter specifies which broadcasts shall be received + * @param flags flags to pass to [Context.registerReceiver] (usually [ContextCompat.RECEIVER_EXPORTED] or + * [ContextCompat.RECEIVER_NOT_EXPORTED]; `null` if only system broadcasts are received) + * @param immediate if `true`, send an empty [Intent] as first value + * + * @return cold flow of [Intent]s + */ +@SuppressLint("UnspecifiedRegisterReceiverFlag") +fun broadcastReceiverFlow( + context: Context, + filter: IntentFilter, + flags: Int? = null, + immediate: Boolean +): Flow = callbackFlow { + val logger = Logger.getGlobal() + + val receiver = object: BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + logger.fine("broadcastReceiverFlow received $intent") + trySend(intent) + } + } + + // register receiver + var filterDump = filter.toString() + filter.dump({ filterDump = it }, "") + logger.fine("Registering broadcast receiver for $filterDump (flags=$flags)") + if (flags != null) + ContextCompat.registerReceiver(context, receiver, filter, null, null, flags) + else + context.registerReceiver(receiver, filter) + + // send empty Intent as first value, if requested + if (immediate) + trySend(Intent()) + + // wait until flow is cancelled, then clean up + awaitClose { + logger.fine("Unregistering broadcast receiver for $filterDump") + context.unregisterReceiver(receiver) + } +} + +/** + * Creates a flow that emits the Intent when a package is added, changed or removed. + * + * @param context the context to register the receiver with + * @param immediate if `true`, send an empty [Intent] as first value + * + * @return cold flow of [Intent]s + */ +fun packageChangedFlow(context: Context, immediate: Boolean = true): Flow { + val filter = IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply { + addAction(Intent.ACTION_PACKAGE_CHANGED) + addAction(Intent.ACTION_PACKAGE_REMOVED) + addDataScheme("package") + } + return broadcastReceiverFlow(context = context, filter = filter, immediate = immediate) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/DavUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/DavUtils.kt new file mode 100644 index 0000000..818f399 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/DavUtils.kt @@ -0,0 +1,93 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.util + +import okhttp3.HttpUrl +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaType +import java.net.URI +import java.net.URISyntaxException +import java.util.Locale + +/** + * Some WebDAV and HTTP network utility methods. + */ +object DavUtils { + + const val MIME_TYPE_ACCEPT_ALL = "*/*" + + val MEDIA_TYPE_JCARD = "application/vcard+json".toMediaType() + val MEDIA_TYPE_OCTET_STREAM = "application/octet-stream".toMediaType() + val MEDIA_TYPE_VCARD = "text/vcard".toMediaType() + + /** + * Builds an HTTP `Accept` header that accepts anything (*/*), but optionally + * specifies a preference. + * + * @param preferred preferred MIME type (optional) + * + * @return `media-range` for `Accept` header that accepts anything, but prefers [preferred] (if it was specified) + */ + fun acceptAnything(preferred: MediaType?): String = + if (preferred != null) + "$preferred, $MIME_TYPE_ACCEPT_ALL;q=0.8" + else + MIME_TYPE_ACCEPT_ALL + + @Suppress("FunctionName") + fun ARGBtoCalDAVColor(colorWithAlpha: Int): String { + val alpha = (colorWithAlpha shr 24) and 0xFF + val color = colorWithAlpha and 0xFFFFFF + return String.format(Locale.ROOT, "#%06X%02X", color, alpha) + } + + + // extension methods + + val HttpUrl.lastSegment: String + get() = pathSegments.lastOrNull { it.isNotEmpty() } ?: "/" + + /** + * Returns parent URL (parent folder). Always with trailing slash + */ + fun HttpUrl.parent(): HttpUrl { + if (pathSegments.size == 1 && pathSegments[0] == "") + // already root URL + return this + + val builder = newBuilder() + + if (pathSegments[pathSegments.lastIndex] == "") { + // URL ends with a slash ("/some/thing/" -> ["some","thing",""]), remove two segments ("" at lastIndex and "thing" at lastIndex - 1) + builder.removePathSegment(pathSegments.lastIndex) + builder.removePathSegment(pathSegments.lastIndex - 1) + } else + // URL doesn't end with a slash ("/some/thing" -> ["some","thing"]), remove one segment ("thing" at lastIndex) + builder.removePathSegment(pathSegments.lastIndex) + + // append trailing slash + builder.addPathSegment("") + + return builder.build() + } + + /** + * Compares MIME type and subtype of two MediaTypes. Does _not_ compare parameters + * like `charset` or `version`. + * + * @param other MediaType to compare with + * + * @return *true* if type and subtype match; *false* if they don't + */ + fun MediaType.sameTypeAs(other: MediaType) = + type == other.type && subtype == other.subtype + + fun String.toURIorNull(): URI? = try { + URI(this) + } catch (_: URISyntaxException) { + null + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt new file mode 100644 index 0000000..437a703 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt @@ -0,0 +1,158 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.util + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.location.LocationManager +import android.net.Uri +import android.os.Build +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService +import androidx.core.location.LocationManagerCompat +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import at.bitfire.davdroid.BuildConfig +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberMultiplePermissionsState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import java.util.logging.Logger + +object PermissionUtils { + + /** There's an undocumented intent that is sent when the battery optimization whitelist changes. */ + const val ACTION_POWER_SAVE_WHITELIST_CHANGED = "android.os.action.POWER_SAVE_WHITELIST_CHANGED" + + val CONTACT_PERMISSIONS = arrayOf( + Manifest.permission.READ_CONTACTS, + Manifest.permission.WRITE_CONTACTS + ) + val CALENDAR_PERMISSIONS = arrayOf( + Manifest.permission.READ_CALENDAR, + Manifest.permission.WRITE_CALENDAR + ) + + val WIFI_SSID_PERMISSIONS = + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> + arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION) + else -> + arrayOf() + } + + /** + * Checks whether all conditions to access the current WiFi's SSID are met: + * + * 1. location permissions ([WIFI_SSID_PERMISSIONS]) granted (Android 8.1+) + * 2. location enabled (Android 9+) + * + * @return *true* if SSID can be obtained; *false* if the SSID will be or something like that + */ + fun canAccessWifiSsid(context: Context): Boolean { + // before Android 8.1, SSIDs are always readable + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) + return true + + val locationAvailable = + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) + true // Android <9 doesn't require active location services + else + context.getSystemService()?.let { locationManager -> + LocationManagerCompat.isLocationEnabled(locationManager) + } ?: /* location feature not available on this device */ false + + return havePermissions(context, WIFI_SSID_PERMISSIONS) && + locationAvailable + } + + /** + * Returns a live state of whether all conditions to access the current WiFi's SSID are met: + * + * 1. location permissions ([WIFI_SSID_PERMISSIONS]) granted (Android 8.1+) + * 2. location enabled (Android 9+) + * + * @return `true` if SSID can be obtained reliably; `false` otherwise (SSID will be "unknown" or something like that) + */ + @Composable + @OptIn(ExperimentalPermissionsApi::class) + fun rememberCanAccessWifiSsid(): State { + // before Android 8.1, SSIDs are always readable + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) + return remember { mutableStateOf(true) } + + val locationAvailableFlow = + // Android 9+: dynamically check whether Location is enabled + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + locationEnabledFlow(LocalContext.current) + else + // Android <9 doesn't require active Location to read the SSID + flowOf(true) + val locationAvailable by locationAvailableFlow.collectAsStateWithLifecycle(false) + + val permissions = rememberMultiplePermissionsState(WIFI_SSID_PERMISSIONS.toList()) + + return remember { + derivedStateOf { + locationAvailable && permissions.allPermissionsGranted + } + } + } + + private fun locationEnabledFlow(context: Context): Flow = + broadcastReceiverFlow( + context, + IntentFilter(LocationManager.MODE_CHANGED_ACTION), + null, + immediate = true + ).map { + val locationManager = context.getSystemService()!! + LocationManagerCompat.isLocationEnabled(locationManager) + } + + /** + * Checks whether at least one of the given permissions is granted. + * + * @param context context to check + * @param permissions array of permissions to check + * + * @return whether at least one of [permissions] is granted + */ + fun haveAnyPermission(context: Context, permissions: Array) = + permissions.any { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED } + + /** + * Checks whether all given permissions are granted. + * + * @param context context to check + * @param permissions array of permissions to check + * + * @return whether all [permissions] are granted + */ + fun havePermissions(context: Context, permissions: Array) = + permissions.all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED } + + fun showAppSettings(context: Context) { + val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", BuildConfig.APPLICATION_ID, null)) + if (intent.resolveActivity(context.packageManager) != null) + context.startActivity(intent) + else + Logger.getGlobal().warning("App settings Intent not resolvable") + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/SensitiveString.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/SensitiveString.kt new file mode 100644 index 0000000..6912fe6 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/SensitiveString.kt @@ -0,0 +1,69 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.util + +/** + * Wrapper for passwords and other sensitive strings so that they're not directly [String]s, + * so that they're less likely to be used in clear-text unintentionally, like being printed in logs + * by [Any.toString]. + * + * This class does not address the issue that clear-text passwords are stored in memory. This problem + * could only be reduced if we would consequently store and process only encrypted passwords, with the + * exception of some "providePassword" method that provides the clear-text password for a lambda function as + * [CharArray] and wipes out the array values after usage. + * + * See also: + * + * - https://stackoverflow.com/a/8889285 + * - https://javaee.github.io/security-api/apidocs/javax/security/enterprise/credential/Password.html and + * https://javaee.github.io/security-api/apidocs/javax/security/enterprise/credential/UsernamePasswordCredential.html + */ +class SensitiveString private constructor( + private val data: String +) { + + /** + * Returns the sensitive string as a [CharArray]. + * + * _Be careful when using it (for instance, don't print its content unintentionally)._ + */ + fun asCharArray() = data.toCharArray() + + /** + * Returns the sensitive string as an immutable [String]. + * + * _Be careful when using it (for instance, don't print it unintentionally)._ + */ + fun asString() = data + + + // make comparable by data + + override fun equals(other: Any?) = + if (other is SensitiveString) + data == other.data + else + false + + override fun hashCode() = data.hashCode() + + + /** + * Overrides [toString] so that it doesn't expose the clear-text string (password). + */ + override fun toString() = "*****" + + + companion object { + + fun CharArray.toSensitiveString() = + SensitiveString(this.concatToString()) + + fun CharSequence.toSensitiveString() = + SensitiveString(this.toString()) + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/StringUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/StringUtils.kt new file mode 100644 index 0000000..90626e2 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/StringUtils.kt @@ -0,0 +1,15 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.util + +import com.google.common.base.Strings + +fun CharSequence?.trimToNull() = Strings.emptyToNull(this?.trim()?.toString()) + +fun String.withTrailingSlash() = + if (this.endsWith('/')) + this + else + "$this/" \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/CredentialsStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/CredentialsStore.kt new file mode 100644 index 0000000..4114548 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/CredentialsStore.kt @@ -0,0 +1,75 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav + +import android.content.Context +import androidx.annotation.StringDef +import androidx.core.content.edit +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import at.bitfire.davdroid.settings.Credentials +import at.bitfire.davdroid.util.SensitiveString.Companion.toSensitiveString +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class CredentialsStore @Inject constructor( + @ApplicationContext context: Context +) { + + @Retention(AnnotationRetention.SOURCE) + @StringDef( + HAS_CREDENTIALS, + USER_NAME, + PASSWORD, + CERTIFICATE_ALIAS + ) + annotation class KeyName + + companion object { + const val HAS_CREDENTIALS = "has_credentials" + const val USER_NAME = "user_name" + const val PASSWORD = "password" + const val CERTIFICATE_ALIAS = "certificate_alias" + } + + private val masterKey = MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + private val prefs = EncryptedSharedPreferences.create(context, "webdav_credentials", masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM) + + + fun getCredentials(mountId: Long): Credentials? { + if (!prefs.getBoolean(keyName(mountId, HAS_CREDENTIALS), false)) + return null + + return Credentials( + prefs.getString(keyName(mountId, USER_NAME), null), + prefs.getString(keyName(mountId, PASSWORD), null)?.toSensitiveString(), + prefs.getString(keyName(mountId, CERTIFICATE_ALIAS), null) + ) + } + + fun setCredentials(mountId: Long, credentials: Credentials?) { + prefs.edit { + if (credentials != null) + putBoolean(keyName(mountId, HAS_CREDENTIALS), true) + .putString(keyName(mountId, USER_NAME), credentials.username) + .putString(keyName(mountId, PASSWORD), credentials.password?.asString()) + .putString(keyName(mountId, CERTIFICATE_ALIAS), credentials.certificateAlias) + else + remove(keyName(mountId, HAS_CREDENTIALS)) + .remove(keyName(mountId, USER_NAME)) + .remove(keyName(mountId, PASSWORD)) + .remove(keyName(mountId, CERTIFICATE_ALIAS)) + } + } + + + private fun keyName(mountId: Long, @KeyName name: String) = + "$mountId.$name" + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt new file mode 100644 index 0000000..b2b80a8 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt @@ -0,0 +1,106 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav + +import android.graphics.Point +import android.os.CancellationSignal +import android.provider.DocumentsProvider +import at.bitfire.davdroid.webdav.operation.CopyDocumentOperation +import at.bitfire.davdroid.webdav.operation.CreateDocumentOperation +import at.bitfire.davdroid.webdav.operation.DeleteDocumentOperation +import at.bitfire.davdroid.webdav.operation.IsChildDocumentOperation +import at.bitfire.davdroid.webdav.operation.MoveDocumentOperation +import at.bitfire.davdroid.webdav.operation.OpenDocumentOperation +import at.bitfire.davdroid.webdav.operation.OpenDocumentThumbnailOperation +import at.bitfire.davdroid.webdav.operation.QueryChildDocumentsOperation +import at.bitfire.davdroid.webdav.operation.QueryDocumentOperation +import at.bitfire.davdroid.webdav.operation.QueryRootsOperation +import at.bitfire.davdroid.webdav.operation.RenameDocumentOperation +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent + +/** + * Provides functionality on WebDav documents. + * + * Hilt constructor injection can't be used for content providers because SingletonComponent + * may not ready yet when the content provider is created. So we use an explicit EntryPoint. + * + * Note: A DocumentsProvider is a ContentProvider and thus has no well-defined lifecycle. It + * is created by Android when it's first accessed and then stays in memory until the process + * is killed. + */ +class DavDocumentsProvider: DocumentsProvider() { + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface DavDocumentsProviderEntryPoint { + fun copyDocumentOperation(): CopyDocumentOperation + fun createDocumentOperation(): CreateDocumentOperation + fun deleteDocumentOperation(): DeleteDocumentOperation + fun isChildDocumentOperation(): IsChildDocumentOperation + fun moveDocumentOperation(): MoveDocumentOperation + fun openDocumentOperation(): OpenDocumentOperation + fun openDocumentThumbnailOperation(): OpenDocumentThumbnailOperation + fun queryChildDocumentsOperation(): QueryChildDocumentsOperation + fun queryDocumentOperation(): QueryDocumentOperation + fun queryRootsOperation(): QueryRootsOperation + fun renameDocumentOperation(): RenameDocumentOperation + } + + private val entryPoint: DavDocumentsProviderEntryPoint by lazy { + EntryPointAccessors.fromApplication(context!!) + } + + + override fun onCreate() = true + + /* Note: shutdown() is NOT called automatically by Android; a content provider lives until + the process is killed. */ + + + /*** query ***/ + + override fun queryRoots(projection: Array?) = + entryPoint.queryRootsOperation().invoke(projection) + + override fun queryDocument(documentId: String, projection: Array?) = + entryPoint.queryDocumentOperation().invoke(documentId, projection) + + override fun queryChildDocuments(parentDocumentId: String, projection: Array?, sortOrder: String?) = + entryPoint.queryChildDocumentsOperation().invoke(parentDocumentId, projection, sortOrder) + + override fun isChildDocument(parentDocumentId: String, documentId: String) = + entryPoint.isChildDocumentOperation().invoke(parentDocumentId, documentId) + + + /*** copy/create/delete/move/rename ***/ + + override fun copyDocument(sourceDocumentId: String, targetParentDocumentId: String) = + entryPoint.copyDocumentOperation().invoke(sourceDocumentId, targetParentDocumentId) + + override fun createDocument(parentDocumentId: String, mimeType: String, displayName: String): String? = + entryPoint.createDocumentOperation().invoke(parentDocumentId, mimeType, displayName) + + override fun deleteDocument(documentId: String) = + entryPoint.deleteDocumentOperation().invoke(documentId) + + override fun moveDocument(sourceDocumentId: String, sourceParentDocumentId: String, targetParentDocumentId: String) = + entryPoint.moveDocumentOperation().invoke(sourceDocumentId, sourceParentDocumentId, targetParentDocumentId) + + override fun renameDocument(documentId: String, displayName: String): String? = + entryPoint.renameDocumentOperation().invoke(documentId, displayName) + + + /*** read/write ***/ + + override fun openDocument(documentId: String, mode: String, signal: CancellationSignal?) = + entryPoint.openDocumentOperation().invoke(documentId, mode, signal) + + override fun openDocumentThumbnail(documentId: String, sizeHint: Point, signal: CancellationSignal?) = + entryPoint.openDocumentThumbnailOperation().invoke(documentId, sizeHint, signal) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/DavHttpClientBuilder.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/DavHttpClientBuilder.kt new file mode 100644 index 0000000..9f6e8d0 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/DavHttpClientBuilder.kt @@ -0,0 +1,49 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav + +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.network.MemoryCookieStore +import okhttp3.CookieJar +import okhttp3.logging.HttpLoggingInterceptor +import javax.inject.Inject +import javax.inject.Provider + +class DavHttpClientBuilder @Inject constructor( + private val credentialsStore: CredentialsStore, + private val httpClientBuilder: Provider, +) { + + /** + * Creates an HTTP client that can be used to access resources in the given mount. + * + * @param mountId ID of the mount to access + * @param logBody whether to log the body of HTTP requests (disable for potentially large files) + */ + fun build(mountId: Long, logBody: Boolean = true): HttpClient { + val cookieStore = cookieStores.getOrPut(mountId) { + MemoryCookieStore() + } + val builder = httpClientBuilder.get() + .loggerInterceptorLevel(if (logBody) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.HEADERS) + .setCookieStore(cookieStore) + + credentialsStore.getCredentials(mountId)?.let { credentials -> + builder.authenticate(host = null, getCredentials = { credentials }) + } + + return builder.build() + } + + + companion object { + + /** in-memory cookie stores (one per mount ID) that are available until the content + * provider (= process) is terminated */ + private val cookieStores = mutableMapOf() + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/DocumentProviderUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/DocumentProviderUtils.kt new file mode 100644 index 0000000..19cb461 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/DocumentProviderUtils.kt @@ -0,0 +1,90 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav + +import android.app.AuthenticationRequiredException +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import android.provider.DocumentsContract.buildChildDocumentsUri +import android.provider.DocumentsContract.buildRootsUri +import android.webkit.MimeTypeMap +import androidx.core.app.TaskStackBuilder +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.webdav.WebdavMountsActivity +import java.io.FileNotFoundException + +object DocumentProviderUtils { + + const val MAX_DISPLAYNAME_TO_MEMBERNAME_ATTEMPTS = 5 + + internal fun displayNameToMemberName(displayName: String, appendNumber: Int = 0): String { + val safeName = displayName.filterNot { it.isISOControl() } + + if (appendNumber != 0) { + val extension: String? = MimeTypeMap.getFileExtensionFromUrl(displayName) + if (extension != null) { + val baseName = safeName.removeSuffix(".$extension") + return "${baseName}_$appendNumber.$extension" + } else + return "${safeName}_$appendNumber" + } else + return safeName + } + + internal fun notifyFolderChanged(context: Context, parentDocumentId: Long?) { + if (parentDocumentId != null) + context.contentResolver.notifyChange( + buildChildDocumentsUri( + context.getString(R.string.webdav_authority), + parentDocumentId.toString() + ), + null + ) + } + + internal fun notifyFolderChanged(context: Context, parentDocumentId: String) { + context.contentResolver.notifyChange( + buildChildDocumentsUri( + context.getString(R.string.webdav_authority), + parentDocumentId + ), + null + ) + } + + internal fun notifyMountsChanged(context: Context) { + context.contentResolver.notifyChange( + buildRootsUri(context.getString(R.string.webdav_authority)), + null) + } + +} + +internal fun HttpException.throwForDocumentProvider(context: Context, ignorePreconditionFailed: Boolean = false) { + when (statusCode) { + 401 -> { + if (Build.VERSION.SDK_INT >= 26) { + val intent = Intent(context, WebdavMountsActivity::class.java) + throw AuthenticationRequiredException( + this, + TaskStackBuilder.create(context) + .addNextIntentWithParentStack(intent) + .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + ) + } + } + 404 -> + throw FileNotFoundException() + 412 -> + if (ignorePreconditionFailed) + return + } + + // re-throw + throw this +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/DocumentSortByMapper.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/DocumentSortByMapper.kt new file mode 100644 index 0000000..a873521 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/DocumentSortByMapper.kt @@ -0,0 +1,81 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav + +import android.provider.DocumentsContract.Document +import at.bitfire.davdroid.db.WebDavDocument +import java.util.logging.Logger +import javax.inject.Inject + +class DocumentSortByMapper @Inject constructor( + private val logger: Logger +) { + + /** + * Contains a map that maps the column names from the documents provider to [WebDavDocument]. + */ + private val columnsMap = mapOf( + Document.COLUMN_DOCUMENT_ID to "id", + Document.COLUMN_DISPLAY_NAME to "displayName", + Document.COLUMN_MIME_TYPE to "mimeType", + Document.COLUMN_SIZE to "size", + Document.COLUMN_LAST_MODIFIED to "lastModified" + ) + + /** + * Maps an incoming `orderBy` column from a [android.content.ContentProvider] query to + * a validated SQL ORDER BY-clause for [WebDavDocument]s. + * + * @param orderBy orderBy of content provider documents + * + * @return value of the ORDER BY-clause, like "name ASC" + */ + fun mapContentProviderToSql(orderBy: String): String { + val requestedFields = orderBy + // Split by commas to divide each order column + .split(',') + // Trim any leading or trailing spaces + .map { it.trim() } + + // Map incoming orderBy fields to a list of pairs of column and direction (true for ASC), like + // [ Pair("displayName", Boolean), … ] + val requestedCriteria = mutableListOf>() + for (field in requestedFields) { + val idx = field.indexOfFirst { it == ' ' } + if (idx == -1) + // no whitespace, only name → use ASC as default, like in SQL + requestedCriteria += field to true + else { + // whitespace, name and sort order + val name = field.substring(0, idx) + val directionStr = field.substring(idx).trim() + val ascending = directionStr.equals("ASC", true) + requestedCriteria += name to ascending + } + } + + // If displayName doesn't appear in sort order, append it in case that the other criteria generate + // the same order. For instance, if files are ordered by ascending size and all have 0 bytes, then + // another order by name is useful. + if (!requestedCriteria.any { it.first == Document.COLUMN_DISPLAY_NAME }) + requestedCriteria += Document.COLUMN_DISPLAY_NAME to true + + // Generate SQL + val sqlSortBy = mutableListOf() // list of valid SQL ORDER BY elements like "displayName ASC" + for ((requestedColumn, ascending) in requestedCriteria) { + // Only take columns that are registered in the columns map + val sqlFieldName = columnsMap[requestedColumn] + if (sqlFieldName == null) { + logger.warning("Ignoring unknown column in sortOrder: $requestedColumn") + continue + } + + // Finally, convert the column name from document to room, including the sort direction. + sqlSortBy += "$sqlFieldName ${if (ascending) "ASC" else "DESC"}" + } + return sqlSortBy.joinToString(", ") + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/DocumentState.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/DocumentState.kt new file mode 100644 index 0000000..0a8d0cf --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/DocumentState.kt @@ -0,0 +1,25 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav + +import java.time.Instant + +data class DocumentState( + val eTag: String? = null, + val lastModified: Instant? = null +) { + + init { + if (eTag == null && lastModified == null) + throw IllegalArgumentException("Either ETag or Last-Modified is required") + } + + override fun toString() = + if (eTag != null) + "eTag=$eTag" + else + "lastModified=$lastModified" + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/DocumentsCursor.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/DocumentsCursor.kt new file mode 100644 index 0000000..06cc25e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/DocumentsCursor.kt @@ -0,0 +1,40 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav + +import android.database.MatrixCursor +import android.os.Bundle +import android.provider.DocumentsContract + +class DocumentsCursor(columns: Array): MatrixCursor(columns) { + + private val documentsExtras = Bundle(1) + + override fun getExtras() = documentsExtras + + + var error: String? + get() = documentsExtras.getString(DocumentsContract.EXTRA_ERROR) + set(value) = documentsExtras.putString(DocumentsContract.EXTRA_ERROR, value) + + var info: String? + get() = documentsExtras.getString(DocumentsContract.EXTRA_INFO) + set(value) = documentsExtras.putString(DocumentsContract.EXTRA_INFO, value) + + var loading: Boolean + get() = documentsExtras.getBoolean(DocumentsContract.EXTRA_LOADING, false) + set(value) = documentsExtras.putBoolean(DocumentsContract.EXTRA_LOADING, value) + + + fun addRow(bundle: Bundle) { + newRow().also { row -> + for (entry in bundle.keySet()) { + val value = bundle.get(entry) + row.add(entry, value) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/HeadResponse.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/HeadResponse.kt new file mode 100644 index 0000000..db221dc --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/HeadResponse.kt @@ -0,0 +1,67 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav + +import androidx.annotation.WorkerThread +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.HttpUtils +import at.bitfire.dav4jvm.property.webdav.GetETag +import at.bitfire.davdroid.network.HttpClient +import okhttp3.HttpUrl +import java.time.Instant + +/** + * Represents the information that was retrieved via a HEAD request before + * accessing the file. + */ +data class HeadResponse( + val size: Long? = null, + val eTag: String? = null, + val lastModified: Instant? = null, + + val supportsPartial: Boolean? = null +) { + + companion object { + + @WorkerThread + fun fromUrl(client: HttpClient, url: HttpUrl): HeadResponse { + var size: Long? = null + var eTag: String? = null + var lastModified: Instant? = null + var supportsPartial: Boolean? = null + + DavResource(client.okHttpClient, url).head { response -> + response.header("ETag", null)?.let { + val getETag = GetETag(it) + if (!getETag.weak) + eTag = getETag.eTag + } + response.header("Last-Modified", null)?.let { + lastModified = HttpUtils.parseDate(it) + } + response.headers["Content-Length"]?.let { + size = it.toLong() + } + response.headers["Accept-Ranges"]?.let { acceptRangesStr -> + val acceptRanges = acceptRangesStr.split(',').map { it.trim().lowercase() } + when { + acceptRanges.contains("none") -> supportsPartial = false + acceptRanges.contains("bytes") -> supportsPartial = true + } + } + } + return HeadResponse(size, eTag, lastModified, supportsPartial) + } + + } + + fun toDocumentState(): DocumentState? = + if (eTag != null || lastModified != null) + DocumentState(eTag, lastModified) + else + null + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/PagingReader.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/PagingReader.kt new file mode 100644 index 0000000..800522f --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/PagingReader.kt @@ -0,0 +1,131 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav + +import androidx.annotation.RequiresApi +import com.google.common.cache.LoadingCache +import java.io.IOException +import java.util.logging.Logger +import kotlin.math.min + +/** + * Splits a resource into pages (segments) so that read accesses can be cached per page. + * + * For instance, if [fileSize] is 3 MB and [pageSize] is 2 MB, multiple read requests within the + * first 2 MB will cause only the first page (0 – 2 MB) to be loaded once and then fulfilled + * from within the cache. For requests between 2 MB and 3 MB, the second page (and in this case last) + * is loaded and used. + * + * @param fileSize file size (must not change between read operations) + * @param pageSize page size (big enough to cache efficiently, small enough to avoid unnecessary traffic and spare memory) + * @param pageCache [LoadingCache] that loads page content from the actual data source + */ +@RequiresApi(26) +class PagingReader( + private val fileSize: Long, + private val pageSize: Int, + private val pageCache: LoadingCache +) { + + val logger: Logger = Logger.getLogger(javaClass.name) + + + /** + * Represents a loaded page (meta information + data). + */ + class CachedPage( + val idx: Long, + val start: Long, + val end: Long, + val data: ByteArray + ) + + /** currently loaded page */ + private var currentPage: CachedPage? = null + + /** + * Reads a given number of bytes from a given position. + * + * Will split the request into multiple page access operations, if necessary. + * + * @param offset starting position + * @param size number of bytes to read + * @param dst destination where data are read into + * + * @return number of bytes read (may be smaller than [size] if the file is not that big) + */ + fun read(offset: Long, size: Int, dst: ByteArray): Int { + // input validation + if (offset > fileSize) + throw IndexOutOfBoundsException() + var remaining = min(size.toLong(), fileSize - offset).toInt() + + var transferred = 0 + while (remaining > 0) { + val nrBytes = readPage(offset + transferred, remaining, dst, transferred) + if (nrBytes == 0) // EOF + break + transferred += nrBytes + remaining -= nrBytes + } + + return transferred + } + + /** + * Tries to read a given number of bytes from a given position, but stays + * within one page – it will not read across two pages. + * + * This method will determine the page that contains [position] and read only + * from this page. + * + * This method is synchronized so that no concurrent modifications of [currentPage] + * and no concurrent calls to [pageCache] will be made. + * + * @param position starting position + * @param size number of bytes requested + * @param dst destination where data are read into + * @param dstOffset starting offset within destination array + * + * @return number of bytes read (may be less than [size] when the page ends before); + * 0 guarantees that there are no more bytes (EOF) + */ + @Synchronized + fun readPage(position: Long, size: Int, dst: ByteArray, dstOffset: Int): Int { + logger.fine("read(position=$position, size=$size, dstOffset=$dstOffset)") + + // read max. 1 page + val pgIdx = position / pageSize + val page = currentPage?.takeIf { it.idx == pgIdx } ?: run { + val pgStart = pgIdx * pageSize + val pgEnd = min((pgIdx + 1) * pageSize, fileSize) + val pgSize = (pgEnd - pgStart).toInt() + + val pageData = + if (pgSize == 0) + ByteArray(0) // don't load 0-byte pages + else + pageCache.get(RandomAccessCallback.PageIdentifier(offset = pgStart, size = pgSize)) + if (pageData.size != pgSize) + throw IOException("Couldn't fetch whole file segment (expected $pgSize bytes, got ${pageData.size} bytes)") + + val newPage = CachedPage(pgIdx, pgStart, pgEnd, pageData) + currentPage = newPage + newPage + } + + val pgSize = (page.end - page.start).toInt() + logger.fine("pgIdx=${page.idx}, pgStart=${page.start}, pgEnd=${page.end}, pgSize=$pgSize") + + val inPageStart = (position - page.start).toInt() + val len = min(pgSize - inPageStart, size) // use the remaining number of bytes in the page, or less if less were requested + logger.fine("inPageStart=$inPageStart, len=$len") + + System.arraycopy(page.data, inPageStart, dst, dstOffset, len) + + return len + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallback.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallback.kt new file mode 100644 index 0000000..a4c391b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallback.kt @@ -0,0 +1,229 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav + +import android.content.Context +import android.os.Handler +import android.os.HandlerThread +import android.os.ParcelFileDescriptor +import android.os.ProxyFileDescriptorCallback +import android.os.storage.StorageManager +import android.system.ErrnoException +import android.system.OsConstants +import androidx.annotation.RequiresApi +import androidx.core.content.getSystemService +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.HttpUtils +import at.bitfire.dav4jvm.exception.DavException +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.util.DavUtils +import com.google.common.cache.CacheBuilder +import com.google.common.cache.CacheLoader +import com.google.common.cache.LoadingCache +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.runInterruptible +import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.MediaType +import java.io.InterruptedIOException +import java.util.logging.Logger +import javax.annotation.WillClose + +@RequiresApi(26) +class RandomAccessCallback @AssistedInject constructor( + @Assisted @WillClose private val httpClient: HttpClient, + @Assisted private val url: HttpUrl, + @Assisted private val mimeType: MediaType?, + @Assisted headResponse: HeadResponse, + @Assisted private val externalScope: CoroutineScope, + @ApplicationContext private val context: Context, + private val logger: Logger +): ProxyFileDescriptorCallback() { + + companion object { + + /** + * WebDAV resources will be read in chunks of this size (or less at the end of the file). + */ + const val MAX_PAGE_SIZE = 2 * 1024*1024 // 2 MB + + } + + @AssistedFactory + interface Factory { + fun create(httpClient: HttpClient, url: HttpUrl, mimeType: MediaType?, headResponse: HeadResponse, externalScope: CoroutineScope): RandomAccessCallback + } + + data class PageIdentifier( + val offset: Long, + val size: Int + ) + + private val dav = DavResource(httpClient.okHttpClient, url) + + private val fileSize = headResponse.size ?: throw IllegalArgumentException("Can only be used with given file size") + private val documentState = headResponse.toDocumentState() ?: throw IllegalArgumentException("Can only be used with ETag/Last-Modified") + + private val pageLoader = PageLoader(externalScope) + private val pageCache: LoadingCache = CacheBuilder.newBuilder() + .maximumSize(10) // don't cache more than 10 entries (MAX_PAGE_SIZE each) + .softValues() // use SoftReference for the page contents so they will be garbage-collected if memory is needed + .build(pageLoader) // fetch actual content using pageLoader + + /** This thread will be used for I/O operations like [onRead]. Using the main looper would cause ANRs. */ + private val ioThread = HandlerThread("WebDAV I/O").apply { + start() + } + + private val pagingReader = PagingReader(fileSize, MAX_PAGE_SIZE, pageCache) + + + // file descriptor + + /** + * Returns a random-access file descriptor that can be used in a DocumentsProvider. + */ + fun fileDescriptor(): ParcelFileDescriptor { + val storageManager = context.getSystemService()!! + val ioHandler = Handler(ioThread.looper) + return storageManager.openProxyFileDescriptor(ParcelFileDescriptor.MODE_READ_ONLY, this, ioHandler) + } + + + // implementation + + override fun onFsync() { /* not used */ } + + override fun onGetSize(): Long = runBlockingFd("onGetFileSize") { + logger.fine("onGetFileSize $url") + fileSize + } + + override fun onRead(offset: Long, size: Int, data: ByteArray) = runBlockingFd("onRead") { + logger.fine("onRead $url $offset $size") + pagingReader.read(offset, size, data) + } + + override fun onWrite(offset: Long, size: Int, data: ByteArray): Int { + logger.fine("onWrite $url $offset $size") + // ranged write requests not supported by WebDAV (yet) + throw ErrnoException("onWrite", OsConstants.EROFS) + } + + override fun onRelease() { + logger.fine("onRelease") + + // free resources + ioThread.quitSafely() + httpClient.close() + } + + + // scope / cancellation + + /** + * Runs blocking in [externalScope]. + * + * Exceptions (including [CancellationException]) are wrapped in an [ErrnoException], as expected by the file + * descriptor / Storage Access Framework. + * + * @param functionName name of the operation, passed to [ErrnoException] in case of cancellation + */ + private fun runBlockingFd(functionName: String, block: () -> T): T = + runBlocking { + try { + externalScope.async { + block() + }.await() + } catch (e: CancellationException) { + logger.warning("Random file access cancelled in $functionName, throwing ErrnoException(EINTR)") + throw ErrnoException(functionName, OsConstants.EINTR, e) + } catch (e: Throwable) { + throw e.toErrNoException("onRead") + } + } + + private fun Throwable.toErrNoException(functionName: String) = + ErrnoException( + functionName, + when (this) { + is HttpException -> + when (statusCode) { + 403 -> OsConstants.EPERM + 404 -> OsConstants.ENOENT + else -> OsConstants.EIO + } + is IndexOutOfBoundsException -> OsConstants.ENXIO // no such [device or] address, see man lseek (2) + is InterruptedIOException -> OsConstants.EINTR + is PartialContentNotSupportedException -> OsConstants.EOPNOTSUPP + else -> OsConstants.EIO + }, + this + ) + + + /** + * Responsible for loading (= downloading) a single page from the WebDAV resource. + * + * @param scope cancellable scope the loader runs in (loader cancels I/O) when this scope is cancelled + */ + inner class PageLoader( + private val scope: CoroutineScope, + private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO + ): CacheLoader() { + + override fun load(key: PageIdentifier) = runBlocking { + scope.async(ioDispatcher) { + loadAsync(key) + }.await() + } + + private suspend fun loadAsync(key: PageIdentifier): ByteArray { + val offset = key.offset + val size = key.size + logger.fine("Loading page $url $offset/$size") + + val ifMatch: Headers = + documentState.eTag?.let { eTag -> + Headers.headersOf("If-Match", "\"$eTag\"") + } ?: documentState.lastModified?.let { lastModified -> + Headers.headersOf("If-Unmodified-Since", HttpUtils.formatDate(lastModified)) + } ?: throw DavException("ETag/Last-Modified required for random access") + + return runInterruptible { // network I/O that should be cancelled by Thread interruption + var result: ByteArray? = null + dav.getRange( + DavUtils.acceptAnything(preferred = mimeType), + offset, + size, + ifMatch + ) { response -> + if (response.code == 200) // server doesn't support ranged requests + throw PartialContentNotSupportedException() + else if (response.code != 206) + throw HttpException(response) + + result = response.body.bytes() + } + result ?: throw DavException("No response body") + } + } + + } + + + class PartialContentNotSupportedException: Exception() + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallbackWrapper.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallbackWrapper.kt new file mode 100644 index 0000000..2d2ab9e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallbackWrapper.kt @@ -0,0 +1,88 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav + +import android.os.ProxyFileDescriptorCallback +import android.system.ErrnoException +import android.system.OsConstants +import androidx.annotation.RequiresApi +import at.bitfire.davdroid.network.HttpClient +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import okhttp3.HttpUrl +import okhttp3.MediaType + +/** + * Use this wrapper to ensure that all memory is released as soon as [onRelease] is called. + * + * - (2021/12/02) Currently Android's `StorageManager.openProxyFileDescriptor` has a memory leak: + * the given callback is registered in `com.android.internal.os.AppFuseMount` (which adds it to + * a [Map]), but is not unregistered anymore. So it stays in the memory until the whole mount + * is unloaded. See https://issuetracker.google.com/issues/208788568. + * - (2024/08/24) [Fixed in Android.](https://android.googlesource.com/platform/frameworks/base/+/e7dbf78143ba083af7a8ecadd839a9dbf6f01655%5E%21/#F0) + * + * **All fields of objects of this class must be set to `null` when [onRelease] is called!** + * Otherwise they will leak memory. + * + * @param httpClient HTTP client ([RandomAccessCallbackWrapper] is responsible to close it) + */ +@RequiresApi(26) +class RandomAccessCallbackWrapper @AssistedInject constructor( + @Assisted httpClient: HttpClient, + @Assisted url: HttpUrl, + @Assisted mimeType: MediaType?, + @Assisted headResponse: HeadResponse, + @Assisted externalScope: CoroutineScope, + callbackFactory: RandomAccessCallback.Factory +): ProxyFileDescriptorCallback() { + + @AssistedFactory + interface Factory { + fun create(httpClient: HttpClient, url: HttpUrl, mimeType: MediaType?, headResponse: HeadResponse, externalScope: CoroutineScope): RandomAccessCallbackWrapper + } + + + // callback reference + + /** + * This field is initialized with a strong reference to the callback. It is cleared when + * [onRelease] is called so that the garbage collector can remove the actual [RandomAccessCallback]. + */ + private var callbackRef: RandomAccessCallback? = + callbackFactory.create(httpClient, url, mimeType, headResponse, externalScope) + + private fun requireCallback(functionName: String): RandomAccessCallback = + callbackRef ?: throw ErrnoException(functionName, OsConstants.EBADF) + + + // non-interface delegates + + fun fileDescriptor() = + requireCallback("fileDescriptor").fileDescriptor() + + + // delegating implementation of ProxyFileDescriptorCallback + + override fun onFsync() { /* not used */ } + + override fun onGetSize() = + requireCallback("onGetSize").onGetSize() + + override fun onRead(offset: Long, size: Int, data: ByteArray) = + requireCallback("onRead").onRead(offset, size, data) + + override fun onWrite(offset: Long, size: Int, data: ByteArray) = + requireCallback("onWrite").onWrite(offset, size, data) + + override fun onRelease() { + requireCallback("onRelease").onRelease() + + // remove reference to allow garbage collection + callbackRef = null + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt new file mode 100644 index 0000000..50cb1f4 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt @@ -0,0 +1,137 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav + +import android.os.ParcelFileDescriptor +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.davdroid.di.IoDispatcher +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.util.DavUtils +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runInterruptible +import okhttp3.HttpUrl +import okhttp3.MediaType +import okhttp3.RequestBody +import okio.BufferedSink +import java.io.IOException +import java.util.logging.Level +import java.util.logging.Logger +import javax.annotation.WillClose + +/** + * @param client HTTP client ([StreamingFileDescriptor] is responsible to close it) + */ +class StreamingFileDescriptor @AssistedInject constructor( + @Assisted @WillClose private val client: HttpClient, + @Assisted private val url: HttpUrl, + @Assisted private val mimeType: MediaType?, + @Assisted private val externalScope: CoroutineScope, + @Assisted private val finishedCallback: OnSuccessCallback, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val logger: Logger +) { + + @AssistedFactory + interface Factory { + fun create(client: HttpClient, url: HttpUrl, mimeType: MediaType?, externalScope: CoroutineScope, finishedCallback: OnSuccessCallback): StreamingFileDescriptor + } + + val dav = DavResource(client.okHttpClient, url) + var transferred: Long = 0 + + fun download() = doStreaming(false) + fun upload() = doStreaming(true) + + private fun doStreaming(upload: Boolean): ParcelFileDescriptor { + val (readFd, writeFd) = ParcelFileDescriptor.createReliablePipe() + + var success = false + externalScope.launch { + try { + if (upload) + uploadNow(readFd) + else + downloadNow(writeFd) + + success = true + } catch (e: HttpException) { + logger.log(Level.WARNING, "HTTP error when opening remote file", e) + writeFd.closeWithError("${e.statusCode} ${e.message}") + } catch (e: Exception) { + logger.log(Level.INFO, "Couldn't serve file (not necessarily an error)", e) + writeFd.closeWithError(e.message) + } finally { + // close pipe + try { + readFd.close() + writeFd.close() + } catch (_: IOException) {} + + client.close() + finishedCallback.onFinished(transferred, success) + } + } + + return if (upload) + writeFd + else + readFd + } + + /** + * Downloads a WebDAV resource. + * + * @param writeFd destination file descriptor (could for instance represent a local file) + */ + private suspend fun downloadNow(writeFd: ParcelFileDescriptor) = runInterruptible(ioDispatcher) { + dav.get(DavUtils.acceptAnything(preferred = mimeType), null) { response -> + response.body.use { body -> + if (response.isSuccessful) { + ParcelFileDescriptor.AutoCloseOutputStream(writeFd).use { destination -> + body.byteStream().use { source -> + transferred += source.copyTo(destination) + } + logger.finer("Downloaded $transferred byte(s) from $url") + } + + } else + writeFd.closeWithError("${response.code} ${response.message}") + } + } + } + + /** + * Uploads a WebDAV resource. + * + * @param readFd source file descriptor (could for instance represent a local file) + */ + private suspend fun uploadNow(readFd: ParcelFileDescriptor) = runInterruptible(ioDispatcher) { + val body = object: RequestBody() { + override fun contentType(): MediaType? = mimeType + override fun isOneShot() = true + override fun writeTo(sink: BufferedSink) { + ParcelFileDescriptor.AutoCloseInputStream(readFd).use { input -> + transferred += input.copyTo(sink.outputStream()) + logger.finer("Uploaded $transferred byte(s) to $url") + } + } + } + DavResource(client.okHttpClient, url).put(body) { + // upload successful + } + } + + + fun interface OnSuccessCallback { + fun onFinished(transferred: Long, success: Boolean) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/WebDavMountRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/WebDavMountRepository.kt new file mode 100644 index 0000000..def00da --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/WebDavMountRepository.kt @@ -0,0 +1,151 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav + +import android.content.Context +import android.provider.DocumentsContract +import androidx.annotation.VisibleForTesting +import at.bitfire.dav4jvm.DavResource +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.WebDavMount +import at.bitfire.davdroid.di.IoDispatcher +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.settings.Credentials +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.delay +import kotlinx.coroutines.runInterruptible +import kotlinx.coroutines.withContext +import okhttp3.HttpUrl +import javax.inject.Inject +import javax.inject.Provider + +class WebDavMountRepository @Inject constructor( + @ApplicationContext private val context: Context, + private val db: AppDatabase, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val httpClientBuilder: Provider +) { + + private val mountDao = db.webDavMountDao() + private val documentDao = db.webDavDocumentDao() + + /** authority of our WebDAV document provider ([DavDocumentsProvider]) */ + private val authority = context.getString(R.string.webdav_authority) + + /** + * Checks whether an HTTP endpoint supports WebDAV and if it does, adds it as a new WebDAV mount. + * + * @param url URL of the HTTP endpoint + * @param displayName display name of the mount + * @param credentials credentials to use for the mount + * + * @return `true` if the mount was added successfully, `false` if the endpoint doesn't support WebDAV + */ + suspend fun addMount( + url: HttpUrl, + displayName: String, + credentials: Credentials? + ): Boolean { + val webdavUrl = hasWebDav(url, credentials) + if (webdavUrl == null) + return false + + // create in database + val mount = WebDavMount( + url = webdavUrl, + name = displayName + ) + val id = db.webDavMountDao().insert(mount) + + // store credentials + val credentialsStore = CredentialsStore(context) + credentialsStore.setCredentials(id, credentials) + + // notify content URI listeners + DocumentProviderUtils.notifyMountsChanged(context) + + return true + } + + suspend fun delete(mount: WebDavMount) { + // remove mount from database + mountDao.deleteAsync(mount) + + // remove credentials, too + CredentialsStore(context).setCredentials(mount.id, null) + + // notify content URI listeners + DocumentProviderUtils.notifyMountsChanged(context) + } + + fun getAllFlow() = mountDao.getAllFlow() + + fun getAllWithRootFlow() = mountDao.getAllWithQuotaFlow() + + suspend fun refreshAllQuota() { + val resolver = context.contentResolver + + withContext(ioDispatcher) { + // query root document of each mount to refresh quota + mountDao.getAll().forEach { mount -> + documentDao.getOrCreateRoot(mount).let { root -> + var loading = true + while (loading) { + val rootDocumentUri = DocumentsContract.buildChildDocumentsUri(authority, root.id.toString()) + resolver.query(rootDocumentUri, null, null, null, null)?.use { cursor -> + loading = cursor.extras.getBoolean(DocumentsContract.EXTRA_LOADING) + } + + if (loading) // still loading, wait a bit + delay(100) + } + } + } + } + } + + + // helpers + + /** + * Checks whether WebDAV is supported at given URL with given credentials + * and returns the resulting if following a few redirects. + * + * @param url The URL to check + * @param credentials The credentials to use for the request + * @return The URL at which WebDAV support was found + */ + @VisibleForTesting + internal suspend fun hasWebDav( + url: HttpUrl, + credentials: Credentials? + ): HttpUrl? = withContext(ioDispatcher) { + val validVersions = arrayOf("1", "2", "3") + + val builder = httpClientBuilder.get() + + if (credentials != null) + builder.authenticate( + host = null, + getCredentials = { credentials } + ) + + var webdavUrl: HttpUrl? = null + builder.build().use { httpClient -> + val dav = DavResource(httpClient.okHttpClient, url) + runInterruptible { + dav.options(followRedirects = true) { davCapabilities, response -> + if (davCapabilities.any { it in validVersions }) + webdavUrl = dav.location + } + } + } + + webdavUrl + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/cache/DiskCache.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/cache/DiskCache.kt new file mode 100644 index 0000000..8bb2f34 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/cache/DiskCache.kt @@ -0,0 +1,106 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav.cache + +import java.io.File +import java.util.logging.Logger + +/** + * Disk-based cache that maps [String]s to [ByteArray]s. + * + * @param cacheDir directory where to put cache files + * @param maxSize max. total cache size (approximately, may be exceeded for some time) + */ +class DiskCache( + val cacheDir: File, + val maxSize: Long +) { + + companion object { + /** + * after how many cache writes [trim] is called + */ + const val CLEANUP_RATE = 15 + } + + private val logger = Logger.getGlobal() + private var writeCounter: Int = 0 + + init { + if (!cacheDir.isDirectory) + if (!cacheDir.mkdirs()) + throw IllegalArgumentException("Couldn't create cache in $cacheDir") + } + + /** + * Gets the file that contains the given key. If the key is not in the cache, the value is being generated from the + * callback, stored in the cache and the backing file is returned. + * + * It's not guaranteed that the file still exists when you're using it! For instance, it may have already + * been removed to keep the cache in size. + * + * @param key key of the cached entry + * @param generate callback that generates the value + * + * @return the file that contains the value + */ + fun getFileOrPut(key: String, generate: () -> ByteArray?): File? { + synchronized(this) { + val file = File(cacheDir, key) + if (file.exists()) { + logger.fine("Cache hit: $key") + return file + } else { + logger.fine("Cache miss: $key → generating") + val result = generate() ?: return null + + file.outputStream().use { output -> + output.write(result) + } + + if (writeCounter++.mod(CLEANUP_RATE) == 0) + trim() + + return file + } + } + } + + + @Synchronized + fun clear() { + cacheDir.listFiles()?.forEach { entry -> + entry.delete() + } + } + + @Synchronized + fun entries(): Int { + return cacheDir.listFiles()!!.size + } + + fun keys(): Array = cacheDir.list()!! + + /** + * Trims the cache to keep it smaller than [maxSize]. + */ + @Synchronized + fun trim(): Int { + var removed = 0 + logger.fine("Trimming disk cache to $maxSize bytes") + + val files = cacheDir.listFiles()!!.toMutableList() + files.sortBy { file -> file.lastModified() } // sort by modification time (ascending) + + while (files.sumOf { file -> file.length() } > maxSize) { + val file = files.removeAt(0) // take first (= oldest) file + logger.finer("Removing $file") + file.delete() + removed++ + } + return removed + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/cache/ThumbnailCache.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/cache/ThumbnailCache.kt new file mode 100644 index 0000000..db79d41 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/cache/ThumbnailCache.kt @@ -0,0 +1,72 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav.cache + +import android.content.Context +import android.graphics.Point +import android.os.Build +import android.os.storage.StorageManager +import android.text.format.Formatter +import androidx.core.content.getSystemService +import at.bitfire.davdroid.db.WebDavDocument +import com.google.common.hash.Hashing +import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.File +import java.util.logging.Logger +import javax.inject.Inject + +/** + * Simple disk cache for image thumbnails. + */ +class ThumbnailCache @Inject constructor( + @ApplicationContext context: Context, + logger: Logger +) { + + val storage: DiskCache + + init { + val storageManager = context.getSystemService()!! + val cacheDir = File(context.cacheDir, "webdav/thumbnail") + val maxBytes = if (Build.VERSION.SDK_INT >= 26) + storageManager.getCacheQuotaBytes(storageManager.getUuidForPath(cacheDir)) / 2 + else + 50 * 1024*1024 // 50 MB + logger.info("Initializing WebDAV thumbnail cache with ${Formatter.formatFileSize(context, maxBytes)}") + + storage = DiskCache(cacheDir, maxBytes) + } + + + fun get(docKey: WebDavDocument.CacheKey, sizeHint: Point, generate: () -> ByteArray?): File? { + val key = Key(docKey, sizeHint) + return storage.getFileOrPut(key.asString(), generate) + } + + @Suppress("UnstableApiUsage") + data class Key( + val document: WebDavDocument.CacheKey, + val size: Point + ) { + + fun asString(): String { + val hf = Hashing.sha256().newHasher() + + hf.putLong(document.docId) + document.documentState.eTag?.let { + hf.putString(it, Charsets.UTF_8) + } + document.documentState.lastModified?.let { + hf.putLong(it.toEpochMilli()) + } + hf.putInt(size.x) + hf.putInt(size.y) + + return hf.hash().toString() + } + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/CopyDocumentOperation.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/CopyDocumentOperation.kt new file mode 100644 index 0000000..a031edd --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/CopyDocumentOperation.kt @@ -0,0 +1,77 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav.operation + +import android.content.Context +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.WebDavDocument +import at.bitfire.davdroid.di.IoDispatcher +import at.bitfire.davdroid.webdav.DavHttpClientBuilder +import at.bitfire.davdroid.webdav.DocumentProviderUtils +import at.bitfire.davdroid.webdav.throwForDocumentProvider +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.runInterruptible +import java.io.FileNotFoundException +import java.util.logging.Logger +import javax.inject.Inject + +class CopyDocumentOperation @Inject constructor( + @ApplicationContext private val context: Context, + private val db: AppDatabase, + private val httpClientBuilder: DavHttpClientBuilder, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val logger: Logger +) { + + private val documentDao = db.webDavDocumentDao() + + operator fun invoke(sourceDocumentId: String, targetParentDocumentId: String): String = runBlocking { + logger.fine("WebDAV copyDocument $sourceDocumentId $targetParentDocumentId") + val srcDoc = documentDao.get(sourceDocumentId.toLong()) ?: throw FileNotFoundException() + val dstFolder = documentDao.get(targetParentDocumentId.toLong()) ?: throw FileNotFoundException() + val name = srcDoc.name + + if (srcDoc.mountId != dstFolder.mountId) + throw UnsupportedOperationException("Can't COPY between WebDAV servers") + + httpClientBuilder.build(srcDoc.mountId).use { client -> + val dav = DavResource(client.okHttpClient, srcDoc.toHttpUrl(db)) + val dstUrl = dstFolder.toHttpUrl(db).newBuilder() + .addPathSegment(name) + .build() + + try { + runInterruptible(ioDispatcher) { + dav.copy(dstUrl, false) { + // successfully copied + } + } + } catch (e: HttpException) { + e.throwForDocumentProvider(context) + } + + val dstDocId = documentDao.insertOrReplace( + WebDavDocument( + mountId = dstFolder.mountId, + parentId = dstFolder.id, + name = name, + isDirectory = srcDoc.isDirectory, + displayName = srcDoc.displayName, + mimeType = srcDoc.mimeType, + size = srcDoc.size + ) + ).toString() + + DocumentProviderUtils.notifyFolderChanged(context, targetParentDocumentId) + + /* return */ dstDocId + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/CreateDocumentOperation.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/CreateDocumentOperation.kt new file mode 100644 index 0000000..bd5e3b3 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/CreateDocumentOperation.kt @@ -0,0 +1,89 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav.operation + +import android.content.Context +import android.provider.DocumentsContract.Document +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.WebDavDocument +import at.bitfire.davdroid.di.IoDispatcher +import at.bitfire.davdroid.webdav.DavHttpClientBuilder +import at.bitfire.davdroid.webdav.DocumentProviderUtils +import at.bitfire.davdroid.webdav.DocumentProviderUtils.displayNameToMemberName +import at.bitfire.davdroid.webdav.throwForDocumentProvider +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.runInterruptible +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody +import java.io.FileNotFoundException +import java.util.logging.Logger +import javax.inject.Inject + +class CreateDocumentOperation @Inject constructor( + @ApplicationContext private val context: Context, + private val db: AppDatabase, + private val httpClientBuilder: DavHttpClientBuilder, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val logger: Logger +) { + + private val documentDao = db.webDavDocumentDao() + + operator fun invoke(parentDocumentId: String, mimeType: String, displayName: String): String? = runBlocking { + logger.fine("WebDAV createDocument $parentDocumentId $mimeType $displayName") + val parent = documentDao.get(parentDocumentId.toLong()) ?: throw FileNotFoundException() + val createDirectory = mimeType == Document.MIME_TYPE_DIR + + var docId: Long? + httpClientBuilder.build(parent.mountId).use { client -> + for (attempt in 0..DocumentProviderUtils.MAX_DISPLAYNAME_TO_MEMBERNAME_ATTEMPTS) { + val newName = displayNameToMemberName(displayName, attempt) + val parentUrl = parent.toHttpUrl(db) + val newLocation = parentUrl.newBuilder() + .addPathSegment(newName) + .build() + val doc = DavResource(client.okHttpClient, newLocation) + try { + runInterruptible(ioDispatcher) { + if (createDirectory) + doc.mkCol(null) { + // directory successfully created + } + else + doc.put(RequestBody.EMPTY, ifNoneMatch = true) { + // document successfully created + } + } + + docId = documentDao.insertOrReplace( + WebDavDocument( + mountId = parent.mountId, + parentId = parent.id, + name = newName, + isDirectory = createDirectory, + mimeType = mimeType.toMediaTypeOrNull(), + eTag = null, + lastModified = null, + size = if (createDirectory) null else 0 + ) + ) + + DocumentProviderUtils.notifyFolderChanged(context, parentDocumentId) + + return@runBlocking docId.toString() + } catch (e: HttpException) { + e.throwForDocumentProvider(context, ignorePreconditionFailed = true) + } + } + } + + null + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/DeleteDocumentOperation.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/DeleteDocumentOperation.kt new file mode 100644 index 0000000..9736e90 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/DeleteDocumentOperation.kt @@ -0,0 +1,55 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav.operation + +import android.content.Context +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.di.IoDispatcher +import at.bitfire.davdroid.webdav.DavHttpClientBuilder +import at.bitfire.davdroid.webdav.DocumentProviderUtils +import at.bitfire.davdroid.webdav.throwForDocumentProvider +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.runInterruptible +import java.io.FileNotFoundException +import java.util.logging.Logger +import javax.inject.Inject + +class DeleteDocumentOperation @Inject constructor( + @ApplicationContext private val context: Context, + private val db: AppDatabase, + private val httpClientBuilder: DavHttpClientBuilder, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val logger: Logger +) { + + private val documentDao = db.webDavDocumentDao() + + operator fun invoke(documentId: String) = runBlocking { + logger.fine("WebDAV removeDocument $documentId") + val doc = documentDao.get(documentId.toLong()) ?: throw FileNotFoundException() + + httpClientBuilder.build(doc.mountId).use { client -> + val dav = DavResource(client.okHttpClient, doc.toHttpUrl(db)) + try { + runInterruptible(ioDispatcher) { + dav.delete { + // successfully deleted + } + } + logger.fine("Successfully removed") + documentDao.delete(doc) + + DocumentProviderUtils.notifyFolderChanged(context, doc.parentId) + } catch (e: HttpException) { + e.throwForDocumentProvider(context) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/IsChildDocumentOperation.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/IsChildDocumentOperation.kt new file mode 100644 index 0000000..e1df2ac --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/IsChildDocumentOperation.kt @@ -0,0 +1,38 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav.operation + +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.WebDavDocument +import java.io.FileNotFoundException +import java.util.logging.Logger +import javax.inject.Inject + +class IsChildDocumentOperation @Inject constructor( + db: AppDatabase, + private val logger: Logger +) { + + private val documentDao = db.webDavDocumentDao() + + operator fun invoke(parentDocumentId: String, documentId: String): Boolean { + logger.fine("WebDAV isChildDocument $parentDocumentId $documentId") + val parent = documentDao.get(parentDocumentId.toLong()) ?: throw FileNotFoundException() + + var iter: WebDavDocument? = documentDao.get(documentId.toLong()) ?: throw FileNotFoundException() + while (iter != null) { + val currentParentId = iter.parentId + if (currentParentId == parent.id) + return true + + iter = if (currentParentId != null) + documentDao.get(currentParentId) + else + null + } + return false + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/MoveDocumentOperation.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/MoveDocumentOperation.kt new file mode 100644 index 0000000..c9faa3f --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/MoveDocumentOperation.kt @@ -0,0 +1,66 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav.operation + +import android.content.Context +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.di.IoDispatcher +import at.bitfire.davdroid.webdav.DavHttpClientBuilder +import at.bitfire.davdroid.webdav.DocumentProviderUtils +import at.bitfire.davdroid.webdav.throwForDocumentProvider +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.runInterruptible +import java.io.FileNotFoundException +import java.util.logging.Logger +import javax.inject.Inject + +class MoveDocumentOperation @Inject constructor( + @ApplicationContext private val context: Context, + private val db: AppDatabase, + private val httpClientBuilder: DavHttpClientBuilder, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val logger: Logger +) { + + private val documentDao = db.webDavDocumentDao() + + operator fun invoke(sourceDocumentId: String, sourceParentDocumentId: String, targetParentDocumentId: String): String = runBlocking { + logger.fine("WebDAV moveDocument $sourceDocumentId $sourceParentDocumentId $targetParentDocumentId") + val doc = documentDao.get(sourceDocumentId.toLong()) ?: throw FileNotFoundException() + val dstParent = documentDao.get(targetParentDocumentId.toLong()) ?: throw FileNotFoundException() + + if (doc.mountId != dstParent.mountId) + throw UnsupportedOperationException("Can't MOVE between WebDAV servers") + + val newLocation = dstParent.toHttpUrl(db).newBuilder() + .addPathSegment(doc.name) + .build() + + httpClientBuilder.build(doc.mountId).use { client -> + val dav = DavResource(client.okHttpClient, doc.toHttpUrl(db)) + try { + runInterruptible(ioDispatcher) { + dav.move(newLocation, false) { + // successfully moved + } + } + + documentDao.update(doc.copy(parentId = dstParent.id)) + + DocumentProviderUtils.notifyFolderChanged(context, sourceParentDocumentId) + DocumentProviderUtils.notifyFolderChanged(context, targetParentDocumentId) + } catch (e: HttpException) { + e.throwForDocumentProvider(context) + } + } + + doc.id.toString() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/OpenDocumentOperation.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/OpenDocumentOperation.kt new file mode 100644 index 0000000..ad0f014 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/OpenDocumentOperation.kt @@ -0,0 +1,115 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav.operation + +import android.content.Context +import android.os.Build +import android.os.CancellationSignal +import android.os.ParcelFileDescriptor +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.di.IoDispatcher +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.webdav.DavHttpClientBuilder +import at.bitfire.davdroid.webdav.DocumentProviderUtils +import at.bitfire.davdroid.webdav.HeadResponse +import at.bitfire.davdroid.webdav.RandomAccessCallbackWrapper +import at.bitfire.davdroid.webdav.StreamingFileDescriptor +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.cancel +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.runInterruptible +import okhttp3.HttpUrl +import java.io.FileNotFoundException +import java.util.logging.Logger +import javax.inject.Inject + +class OpenDocumentOperation @Inject constructor( + @ApplicationContext private val context: Context, + private val db: AppDatabase, + private val httpClientBuilder: DavHttpClientBuilder, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val logger: Logger, + private val randomAccessCallbackWrapperFactory: RandomAccessCallbackWrapper.Factory, + private val streamingFileDescriptorFactory: StreamingFileDescriptor.Factory +) { + + private val documentDao = db.webDavDocumentDao() + + operator fun invoke(documentId: String, mode: String, signal: CancellationSignal?): ParcelFileDescriptor = runBlocking { + logger.fine("WebDAV openDocument $documentId $mode $signal") + + val doc = documentDao.get(documentId.toLong()) ?: throw FileNotFoundException() + val url = doc.toHttpUrl(db) + val client = httpClientBuilder.build(doc.mountId, logBody = false) + + val readOnlyMode = when (mode) { + "r" -> true + "w", "wt" -> false + else -> throw UnsupportedOperationException("Mode $mode not supported by WebDAV") + } + + val accessScope = CoroutineScope(SupervisorJob()) + signal?.setOnCancelListener { + logger.fine("Cancelling WebDAV access to $url") + accessScope.cancel() + } + + val fileInfo = accessScope.async { + headRequest(client, url) + }.await() + logger.fine("Received file info: $fileInfo") + + // RandomAccessCallback.Wrapper / StreamingFileDescriptor are responsible for closing httpClient + return@runBlocking if ( + androidSupportsRandomAccess && + readOnlyMode && // WebDAV doesn't support random write access (natively) + fileInfo.size != null && // file descriptor must return a useful value on getFileSize() + (fileInfo.eTag != null || fileInfo.lastModified != null) && // we need a method to determine when the document changes during access + fileInfo.supportsPartial == true // WebDAV server must advertise random access + ) { + logger.fine("Creating RandomAccessCallback for $url") + val accessor = randomAccessCallbackWrapperFactory.create(client, url, doc.mimeType, fileInfo, accessScope) + accessor.fileDescriptor() + + } else { + logger.fine("Creating StreamingFileDescriptor for $url") + val fd = streamingFileDescriptorFactory.create(client, url, doc.mimeType, accessScope) { transferred, success -> + // called when transfer is finished + if (!success) + return@create + + val now = System.currentTimeMillis() + if (!readOnlyMode /* write access */) { + // write access, update file size + documentDao.update(doc.copy(size = transferred, lastModified = now)) + } + + DocumentProviderUtils.notifyFolderChanged(context, doc.parentId) + } + + if (readOnlyMode) + fd.download() + else + fd.upload() + } + } + + private suspend fun headRequest(client: HttpClient, url: HttpUrl): HeadResponse = runInterruptible(ioDispatcher) { + HeadResponse.fromUrl(client, url) + } + + + companion object { + + /** openProxyFileDescriptor (required for random access) exists since Android 8.0 */ + val androidSupportsRandomAccess = Build.VERSION.SDK_INT >= 26 + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/OpenDocumentThumbnailOperation.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/OpenDocumentThumbnailOperation.kt new file mode 100644 index 0000000..d267ed4 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/OpenDocumentThumbnailOperation.kt @@ -0,0 +1,126 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav.operation + +import android.content.Context +import android.content.res.AssetFileDescriptor +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Point +import android.media.ThumbnailUtils +import android.net.ConnectivityManager +import android.os.CancellationSignal +import android.os.ParcelFileDescriptor +import androidx.core.content.getSystemService +import at.bitfire.dav4jvm.DavResource +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.di.IoDispatcher +import at.bitfire.davdroid.webdav.DavHttpClientBuilder +import at.bitfire.davdroid.webdav.cache.ThumbnailCache +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.cancel +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.runInterruptible +import kotlinx.coroutines.withTimeout +import java.io.ByteArrayOutputStream +import java.io.FileNotFoundException +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject +import kotlin.use + +class OpenDocumentThumbnailOperation @Inject constructor( + @ApplicationContext private val context: Context, + private val db: AppDatabase, + private val httpClientBuilder: DavHttpClientBuilder, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val logger: Logger, + private val thumbnailCache: ThumbnailCache +) { + + private val documentDao = db.webDavDocumentDao() + + operator fun invoke(documentId: String, sizeHint: Point, signal: CancellationSignal?): AssetFileDescriptor? { + logger.info("openDocumentThumbnail documentId=$documentId sizeHint=$sizeHint signal=$signal") + + // don't download the large images just to create a thumbnail on metered networks + val connectivityManager = context.getSystemService()!! + if (connectivityManager.isActiveNetworkMetered) + return null + + if (signal == null) { + logger.warning("openDocumentThumbnail without cancellationSignal causes too much problems, please fix calling app") + return null + } + val accessScope = CoroutineScope(SupervisorJob()) + signal.setOnCancelListener { + logger.fine("Cancelling thumbnail generation for $documentId") + accessScope.cancel() + } + + val doc = documentDao.get(documentId.toLong()) ?: throw FileNotFoundException() + + val docCacheKey = doc.cacheKey() + if (docCacheKey == null) { + logger.warning("openDocumentThumbnail won't generate thumbnails when document state (ETag/Last-Modified) is unknown") + return null + } + + val thumbFile = thumbnailCache.get(docCacheKey, sizeHint) { + // create thumbnail + val job = accessScope.async { + withTimeout(THUMBNAIL_TIMEOUT_MS) { + httpClientBuilder.build(doc.mountId, logBody = false).use { client -> + val url = doc.toHttpUrl(db) + val dav = DavResource(client.okHttpClient, url) + var result: ByteArray? = null + runInterruptible(ioDispatcher) { + dav.get("image/*", null) { response -> + response.body.byteStream().use { data -> + BitmapFactory.decodeStream(data)?.let { bitmap -> + val thumb = ThumbnailUtils.extractThumbnail(bitmap, sizeHint.x, sizeHint.y) + val baos = ByteArrayOutputStream() + thumb.compress(Bitmap.CompressFormat.JPEG, 95, baos) + result = baos.toByteArray() + } + } + } + } + result + } + } + } + + try { + runBlocking { + job.await() + } + } catch (e: Exception) { + logger.log(Level.WARNING, "Couldn't generate thumbnail", e) + null + } + } + + if (thumbFile != null) + return AssetFileDescriptor( + ParcelFileDescriptor.open(thumbFile, ParcelFileDescriptor.MODE_READ_ONLY), + 0, thumbFile.length() + ) + + return null + } + + + companion object { + + const val THUMBNAIL_TIMEOUT_MS = 15000L + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/QueryChildDocumentsOperation.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/QueryChildDocumentsOperation.kt new file mode 100644 index 0000000..74157e6 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/QueryChildDocumentsOperation.kt @@ -0,0 +1,214 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav.operation + +import android.content.Context +import android.provider.DocumentsContract.Document +import android.provider.DocumentsContract.buildChildDocumentsUri +import at.bitfire.dav4jvm.DavCollection +import at.bitfire.dav4jvm.Response +import at.bitfire.dav4jvm.property.webdav.CurrentUserPrivilegeSet +import at.bitfire.dav4jvm.property.webdav.DisplayName +import at.bitfire.dav4jvm.property.webdav.GetContentLength +import at.bitfire.dav4jvm.property.webdav.GetContentType +import at.bitfire.dav4jvm.property.webdav.GetETag +import at.bitfire.dav4jvm.property.webdav.GetLastModified +import at.bitfire.dav4jvm.property.webdav.QuotaAvailableBytes +import at.bitfire.dav4jvm.property.webdav.QuotaUsedBytes +import at.bitfire.dav4jvm.property.webdav.ResourceType +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.WebDavDocument +import at.bitfire.davdroid.db.WebDavDocumentDao +import at.bitfire.davdroid.di.IoDispatcher +import at.bitfire.davdroid.webdav.DavHttpClientBuilder +import at.bitfire.davdroid.webdav.DocumentSortByMapper +import at.bitfire.davdroid.webdav.DocumentsCursor +import dagger.Lazy +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.runInterruptible +import java.io.FileNotFoundException +import java.util.concurrent.ConcurrentHashMap +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject + +class QueryChildDocumentsOperation @Inject constructor( + @ApplicationContext private val context: Context, + private val db: AppDatabase, + private val documentSortByMapper: Lazy, + private val httpClientBuilder: DavHttpClientBuilder, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val logger: Logger +) { + + private val authority = context.getString(R.string.webdav_authority) + private val documentDao = db.webDavDocumentDao() + + private val backgroundScope = CoroutineScope(SupervisorJob()) + + operator fun invoke(parentDocumentId: String, projection: Array?, sortOrder: String?) = + synchronized(QueryChildDocumentsOperation::class.java) { + queryChildDocuments(parentDocumentId, projection, sortOrder) + } + + private fun queryChildDocuments( + parentDocumentId: String, + projection: Array?, + sortOrder: String? + ): DocumentsCursor { + logger.fine("WebDAV queryChildDocuments $parentDocumentId $projection $sortOrder") + val parentId = parentDocumentId.toLong() + val parent = documentDao.get(parentId) ?: throw FileNotFoundException() + + val columns = projection ?: arrayOf( + Document.COLUMN_DOCUMENT_ID, + Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_MIME_TYPE, + Document.COLUMN_FLAGS, + Document.COLUMN_SIZE, + Document.COLUMN_LAST_MODIFIED + ) + + // Register watcher + val result = DocumentsCursor(columns) + val notificationUri = buildChildDocumentsUri(authority, parentDocumentId) + result.setNotificationUri(context.contentResolver, notificationUri) + + // Dispatch worker querying for the children and keep track of it + val running = runningQueryChildren.getOrPut(parentId) { + backgroundScope.launch { + queryChildren(parent) + // Once the query is done, set query as finished (not running) + runningQueryChildren[parentId] = false + // .. and notify - effectively calling this method again + context.contentResolver.notifyChange(notificationUri, null) + } + true + } + + if (running) // worker still running + result.loading = true + else // remove worker from list if done + runningQueryChildren.remove(parentId) + + // Prepare SORT BY clause + val mapper = documentSortByMapper.get() + val sqlSortBy = if (sortOrder != null) + mapper.mapContentProviderToSql(sortOrder) + else + WebDavDocumentDao.DEFAULT_ORDER + + // Regardless of whether the worker is done, return the children we already have + val children = documentDao.getChildren(parentId, sqlSortBy) + for (child in children) { + val bundle = child.toBundle(parent) + result.addRow(bundle) + } + + return result + } + + /** + * Finds children of given parent [WebDavDocument]. After querying, it + * updates existing children, adds new ones or removes deleted ones. + * + * There must never be more than one running instance per [parent]! + * + * @param parent folder to search for children + */ + internal suspend fun queryChildren(parent: WebDavDocument) { + val oldChildren = documentDao.getChildren(parent.id).associateBy { it.name }.toMutableMap() // "name" of file/folder must be unique + val newChildrenList = hashMapOf() + + val parentUrl = parent.toHttpUrl(db) + httpClientBuilder.build(parent.mountId).use { client -> + val folder = DavCollection(client.okHttpClient, parentUrl) + + try { + runInterruptible(ioDispatcher) { + folder.propfind(1, *DAV_FILE_FIELDS) { response, relation -> + logger.fine("$relation $response") + + val resource: WebDavDocument = + when (relation) { + Response.HrefRelation.SELF -> // it's about the parent + parent + + Response.HrefRelation.MEMBER -> // it's about a member + WebDavDocument(mountId = parent.mountId, parentId = parent.id, name = response.hrefName()) + + else -> { + // we didn't request this; log a warning and ignore it + logger.warning("Ignoring unexpected $response $relation in $parentUrl") + return@propfind + } + } + + val updatedResource = resource.copy( + isDirectory = response[ResourceType::class.java]?.types?.contains(ResourceType.COLLECTION) + ?: resource.isDirectory, + displayName = response[DisplayName::class.java]?.displayName, + mimeType = response[GetContentType::class.java]?.type, + eTag = response[GetETag::class.java]?.takeIf { !it.weak }?.eTag, + lastModified = response[GetLastModified::class.java]?.lastModified?.toEpochMilli(), + size = response[GetContentLength::class.java]?.contentLength, + mayBind = response[CurrentUserPrivilegeSet::class.java]?.mayBind, + mayUnbind = response[CurrentUserPrivilegeSet::class.java]?.mayUnbind, + mayWriteContent = response[CurrentUserPrivilegeSet::class.java]?.mayWriteContent, + quotaAvailable = response[QuotaAvailableBytes::class.java]?.quotaAvailableBytes, + quotaUsed = response[QuotaUsedBytes::class.java]?.quotaUsedBytes, + ) + + if (resource == parent) + documentDao.update(updatedResource) + else { + documentDao.insertOrUpdate(updatedResource) + newChildrenList[resource.name] = updatedResource + } + + // remove resource from known child nodes, because not found on server + oldChildren.remove(resource.name) + } + } + } catch (e: Exception) { + logger.log(Level.WARNING, "Couldn't query children", e) + } + } + + // Delete child nodes which were not rediscovered (deleted serverside) + for ((_, oldChild) in oldChildren) + documentDao.delete(oldChild) + } + + + companion object { + + val DAV_FILE_FIELDS = arrayOf( + ResourceType.NAME, + CurrentUserPrivilegeSet.NAME, + DisplayName.NAME, + GetETag.NAME, + GetContentType.NAME, + GetContentLength.NAME, + GetLastModified.NAME, + QuotaAvailableBytes.NAME, + QuotaUsedBytes.NAME, + ) + + /** List of currently active [queryChildDocuments] runners. + * + * Key: document ID (directory) for which children are listed. + * Value: whether the runner is still running (*true*) or has already finished (*false*). + */ + private val runningQueryChildren = ConcurrentHashMap() + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/QueryDocumentOperation.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/QueryDocumentOperation.kt new file mode 100644 index 0000000..ad3e104 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/QueryDocumentOperation.kt @@ -0,0 +1,55 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav.operation + +import android.database.Cursor +import android.provider.DocumentsContract.Document +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.webdav.DocumentsCursor +import kotlinx.coroutines.runBlocking +import java.io.FileNotFoundException +import java.util.logging.Logger +import javax.inject.Inject + +class QueryDocumentOperation @Inject constructor( + db: AppDatabase, + private val logger: Logger +) { + + private val documentDao = db.webDavDocumentDao() + private val mountDao = db.webDavMountDao() + + operator fun invoke(documentId: String, projection: Array?): Cursor { + logger.fine("WebDAV queryDocument $documentId ${projection?.joinToString("+")}") + + val doc = documentDao.get(documentId.toLong()) ?: throw FileNotFoundException() + val parent = doc.parentId?.let { parentId -> + documentDao.get(parentId) + } + + return DocumentsCursor(projection ?: arrayOf( + Document.COLUMN_DOCUMENT_ID, + Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_MIME_TYPE, + Document.COLUMN_FLAGS, + Document.COLUMN_SIZE, + Document.COLUMN_LAST_MODIFIED, + Document.COLUMN_ICON, + Document.COLUMN_SUMMARY + )).apply { + val bundle = doc.toBundle(parent) + logger.fine("queryDocument($documentId) = $bundle") + + // override display names of root documents + if (parent == null) { + val mount = runBlocking { mountDao.getById(doc.mountId) } + bundle.putString(Document.COLUMN_DISPLAY_NAME, mount.name) + } + + addRow(bundle) + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/QueryRootsOperation.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/QueryRootsOperation.kt new file mode 100644 index 0000000..da2b937 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/QueryRootsOperation.kt @@ -0,0 +1,65 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav.operation + +import android.content.Context +import android.database.Cursor +import android.database.MatrixCursor +import android.provider.DocumentsContract.Root +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.AppDatabase +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.runBlocking +import java.util.logging.Logger +import javax.inject.Inject + +class QueryRootsOperation @Inject constructor( + @ApplicationContext private val context: Context, + db: AppDatabase, + private val logger: Logger +) { + + private val documentDao = db.webDavDocumentDao() + private val mountDao = db.webDavMountDao() + + operator fun invoke(projection: Array?): Cursor { + logger.fine("WebDAV queryRoots") + val roots = MatrixCursor(projection ?: arrayOf( + Root.COLUMN_ROOT_ID, + Root.COLUMN_ICON, + Root.COLUMN_TITLE, + Root.COLUMN_FLAGS, + Root.COLUMN_DOCUMENT_ID, + Root.COLUMN_SUMMARY + )) + + runBlocking { + for (mount in mountDao.getAll()) { + val rootDocument = documentDao.getOrCreateRoot(mount) + logger.info("Root ID: $rootDocument") + + roots.newRow().apply { + add(Root.COLUMN_ROOT_ID, mount.id) + add(Root.COLUMN_ICON, R.mipmap.ic_launcher) + add(Root.COLUMN_TITLE, context.getString(R.string.webdav_provider_root_title)) + add(Root.COLUMN_DOCUMENT_ID, rootDocument.id.toString()) + add(Root.COLUMN_SUMMARY, mount.name) + add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE or Root.FLAG_SUPPORTS_IS_CHILD) + + val quotaAvailable = rootDocument.quotaAvailable + if (quotaAvailable != null) + add(Root.COLUMN_AVAILABLE_BYTES, quotaAvailable) + + val quotaUsed = rootDocument.quotaUsed + if (quotaAvailable != null && quotaUsed != null) + add(Root.COLUMN_CAPACITY_BYTES, quotaAvailable + quotaUsed) + } + } + } + + return roots + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/RenameDocumentOperation.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/RenameDocumentOperation.kt new file mode 100644 index 0000000..26dd385 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/operation/RenameDocumentOperation.kt @@ -0,0 +1,67 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav.operation + +import android.content.Context +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.di.IoDispatcher +import at.bitfire.davdroid.webdav.DavHttpClientBuilder +import at.bitfire.davdroid.webdav.DocumentProviderUtils +import at.bitfire.davdroid.webdav.DocumentProviderUtils.displayNameToMemberName +import at.bitfire.davdroid.webdav.throwForDocumentProvider +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.runInterruptible +import java.io.FileNotFoundException +import java.util.logging.Logger +import javax.inject.Inject + +class RenameDocumentOperation @Inject constructor( + @ApplicationContext private val context: Context, + private val db: AppDatabase, + private val httpClientBuilder: DavHttpClientBuilder, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, + private val logger: Logger +) { + + private val documentDao = db.webDavDocumentDao() + + operator fun invoke(documentId: String, displayName: String): String? = runBlocking { + logger.fine("WebDAV renameDocument $documentId $displayName") + val doc = documentDao.get(documentId.toLong()) ?: throw FileNotFoundException() + + httpClientBuilder.build(doc.mountId).use { client -> + for (attempt in 0..DocumentProviderUtils.MAX_DISPLAYNAME_TO_MEMBERNAME_ATTEMPTS) { + val newName = displayNameToMemberName(displayName, attempt) + val oldUrl = doc.toHttpUrl(db) + val newLocation = oldUrl.newBuilder() + .removePathSegment(oldUrl.pathSegments.lastIndex) + .addPathSegment(newName) + .build() + try { + val dav = DavResource(client.okHttpClient, oldUrl) + runInterruptible(ioDispatcher) { + dav.move(newLocation, false) { + // successfully renamed + } + } + documentDao.update(doc.copy(name = newName)) + + DocumentProviderUtils.notifyFolderChanged(context, doc.parentId) + + return@runBlocking doc.id.toString() + } catch (e: HttpException) { + e.throwForDocumentProvider(context, true) + } + } + } + + null + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_storage_notify.xml b/app/src/main/res/drawable-anydpi/ic_storage_notify.xml new file mode 100644 index 0000000..12f67f0 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_storage_notify.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable-hdpi/ic_storage_notify.png b/app/src/main/res/drawable-hdpi/ic_storage_notify.png new file mode 100644 index 0000000000000000000000000000000000000000..12bbcb33fe77f52b40af4a46902ae67f280097c7 GIT binary patch literal 272 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBu6w#ThE&A8z50-|*@1^O!F=`N z9ge#jWe@V+@||_|5_jAOQ^6$#ZOoq?|FtZCzk*%1ZOWyqhQ@_5oBsB-JZRi_j=4;C z;@fhSC7Cx)E?dApQtvRutc`+}ePPDvcy_Z?g&Znjq4_E(7^NYC%lg0spFxBrVQ zIIASd`l?JJ^QFVdQ&MBb@0EN3vE&u=k literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_storage_notify.png b/app/src/main/res/drawable-xhdpi/ic_storage_notify.png new file mode 100644 index 0000000000000000000000000000000000000000..d57b3b4793274f13300a5376edbe6676302ce075 GIT binary patch literal 291 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtSDr48Ar*0NuN~w)Y{0>KAt%i3 zBimb!oNIY*y;Y9(O~Fw*-|ahv75;imd;C~IN$FPa%-+cJN3#;nU6Bck?oT>5h5zs- zhyBgRcvR)vQXA{odnLW>cuq6@<&!d=QX?_#{pP%dRvOQIej4QII~b;)vb+8wd!y8h z$uFY+?78_f{9Y_`@v@xeXb$@ahTcx40cU1@N{kSDQGQ{@!Y79}=v;Wez|i?*d*V$0 zPHvzflJkYWJV(>l0F=r&6M#)yVKrlyW$>}(EP{d;^Y<|RTUK;osZ9M-psyJ`UHx3v IIVCg!0I`03^Z)<= literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_storage_notify.png b/app/src/main/res/drawable-xxhdpi/ic_storage_notify.png new file mode 100644 index 0000000000000000000000000000000000000000..cf3a5370154d8a16ba1899765f6cab73ed5bdc5a GIT binary patch literal 467 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@Zgyv2V4UUY;uum9_x75jSF@vlOJaUn zUfNxke4+eB=9#*4eC7Ug?0wDi_28OIHW~+Coc4Bs0GVa0LY@nK-)4E}mE)1UxqTaU z)x_<2-`@Qb%*oob`PLmiwY-etme9v@uRbd?D0H5!yhiH%8nf-qozk~t60cUIZ`~>D zbLYY{j@8GkSD$^G@TkqqJtFp<`1|bIZ#rO8sssx2fx6^e?nny?s{j1EZ2#YV+nw+C zR^QgDfA;@5$7*&#umc{&+wSWp&+5~!k5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/google_g_logo.xml b/app/src/main/res/drawable/google_g_logo.xml new file mode 100644 index 0000000..993a5e7 --- /dev/null +++ b/app/src/main/res/drawable/google_g_logo.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_database_off.xml b/app/src/main/res/drawable/ic_database_off.xml new file mode 100644 index 0000000..ba5a0b8 --- /dev/null +++ b/app/src/main/res/drawable/ic_database_off.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_foreground_notify.xml b/app/src/main/res/drawable/ic_foreground_notify.xml new file mode 100644 index 0000000..7423be8 --- /dev/null +++ b/app/src/main/res/drawable/ic_foreground_notify.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..0cae730 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_sd_card_notify.xml b/app/src/main/res/drawable/ic_sd_card_notify.xml new file mode 100644 index 0000000..f284d1a --- /dev/null +++ b/app/src/main/res/drawable/ic_sd_card_notify.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..0bc3e11 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml new file mode 100644 index 0000000..d0def11 --- /dev/null +++ b/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_sync.xml b/app/src/main/res/drawable/ic_sync.xml new file mode 100644 index 0000000..aa3d8e7 --- /dev/null +++ b/app/src/main/res/drawable/ic_sync.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_sync_problem_notify.xml b/app/src/main/res/drawable/ic_sync_problem_notify.xml new file mode 100644 index 0000000..ed44b85 --- /dev/null +++ b/app/src/main/res/drawable/ic_sync_problem_notify.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_sync_shortcut.xml b/app/src/main/res/drawable/ic_sync_shortcut.xml new file mode 100644 index 0000000..f9efb87 --- /dev/null +++ b/app/src/main/res/drawable/ic_sync_shortcut.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_warning_notify.xml b/app/src/main/res/drawable/ic_warning_notify.xml new file mode 100644 index 0000000..ed23531 --- /dev/null +++ b/app/src/main/res/drawable/ic_warning_notify.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/intro_open_source.xml b/app/src/main/res/drawable/intro_open_source.xml new file mode 100644 index 0000000..b383085 --- /dev/null +++ b/app/src/main/res/drawable/intro_open_source.xml @@ -0,0 +1,222 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/intro_permissions.xml b/app/src/main/res/drawable/intro_permissions.xml new file mode 100644 index 0000000..21279cb --- /dev/null +++ b/app/src/main/res/drawable/intro_permissions.xml @@ -0,0 +1,347 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/intro_tasks.xml b/app/src/main/res/drawable/intro_tasks.xml new file mode 100644 index 0000000..79a38eb --- /dev/null +++ b/app/src/main/res/drawable/intro_tasks.xml @@ -0,0 +1,371 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/mastodon.xml b/app/src/main/res/drawable/mastodon.xml new file mode 100644 index 0000000..b2d15a6 --- /dev/null +++ b/app/src/main/res/drawable/mastodon.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/product_logomark_cloud_messaging_full_color.xml b/app/src/main/res/drawable/product_logomark_cloud_messaging_full_color.xml new file mode 100644 index 0000000..1353c33 --- /dev/null +++ b/app/src/main/res/drawable/product_logomark_cloud_messaging_full_color.xml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/shape_rounded_primary.xml b/app/src/main/res/drawable/shape_rounded_primary.xml new file mode 100644 index 0000000..c2f71da --- /dev/null +++ b/app/src/main/res/drawable/shape_rounded_primary.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/undraw_server_down.xml b/app/src/main/res/drawable/undraw_server_down.xml new file mode 100644 index 0000000..a2659ed --- /dev/null +++ b/app/src/main/res/drawable/undraw_server_down.xml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/widget_preview_icon_sync_button.xml b/app/src/main/res/layout/widget_preview_icon_sync_button.xml new file mode 100644 index 0000000..f5996b6 --- /dev/null +++ b/app/src/main/res/layout/widget_preview_icon_sync_button.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/widget_preview_labeled_sync_button.xml b/app/src/main/res/layout/widget_preview_labeled_sync_button.xml new file mode 100644 index 0000000..24a31be --- /dev/null +++ b/app/src/main/res/layout/widget_preview_labeled_sync_button.xml @@ -0,0 +1,28 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..f14eb87 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..c402e3feb9efa131b521651be72c2a40e2b73c80 GIT binary patch literal 3410 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..77e9d2fc2b06a484953880f13d171082c4baeb94 GIT binary patch literal 2239 zcmV;w2tfCVP)?mC^*`ade%b=Ge zRR0T7u8=?=q}#GeUbQfNW4=pYCqDl6jZBNpQfxFCBul#)SvVdpQx8y~LhBbuxGoxh zZO|_kjobLwjy_FS$Gf(+Ee2zieqC(}0k(FFhVMCS_BKUZL0BL^kR%!c(5b`GX~GafV6r#SCO6^0_5h*_mt3Cv zoEQ?4!^M{42oDYt64Cxc84@*tkqKE02v2a6R&cH10%xxkQ`JyRWBcX4@$X512mXvt znoZ)Jk$h487lfqrN(_3O+)<0s_fPIIt%O8;R4VaNsm$bgstsivtk_JQwbFMDo&+E~ zNr3FMJOHN%fBNyn@YfkVD7#OIlVV2ka?HCFpWDxo>JPAE^=h0eL$79_105p1uGz~A zS?lTFFQ$6}AyaefJatVqw3wTaB#C~3;lxHJ6C0_VB?*Zi4*LlSQE6-|Sx%d1@>zk; zRVO(Ww0t_B*CV4t(-gIU(OJ6nk;%+Sdkve#`z?%8 zfR{7S0SSrJ*mR`sdqS`9#i_sZ@zD*6@;o+W8cD%fUW-r&=*wp;dqf`3O<&Px5y#8- z^4V9L6t=mUOKCP(y%OOv1AXE^_J}+d+=qyQ(yd&nJL_ymhsBYqAB*vZxmOV`Wz@Zb z`sP~v{d7GBN_cE2lSj|v*qOZ^eNyye@KbBKV`}6^V|8a@)U-~We^n_OGffk0LBcSL}W-TDwX?JDL|{k zY;EOg{W-V&p3ot}W^r_*sB(35hmramCZ^71;q>1!YuqCI{d7E?^EwNru0RNx#kp(H zsI@Hm(MlvhmW5jp>vu2ZYW+F#CM@+yKvxm%E^ET;FL8CY8e(y8a!B z(Zlf5XlZOQFn;I^Ox9NFo3HUy_A+KeIsY}PO*)4o`=@vLv}*Hvw85YZvZ8AP5VJ;{AdZoT@(Ru{!wzl_atGm!(eLUY*Ev z>Ij14{J7g>gJ5Vd8gR&Vgm5on_PFN>4T@yluEi{w`3IiPd5y~260GeeB7$R(q~1Sx zmUc6(=H9p0UM=FR#Alc^{7Gkf%c0eLb?yLwTM;Eyhd6oh2(`DWSigG-{Q|?0xc7Y+ z7C)BfroD+I^}LTPIe&=F2UqpVO<6`gOhKjLOGSJ3<1e7qXgkOKM??p%-@ZictttSF zt@YI0I*-}f3P4fCKDHnIlgC`X!mCAm(^5ulLzS~VAUuH=bAOLo)j8Z%L{BIz|CDV< z)+)BreTB;mxQ9ScaQb~k!t~@RJTv7DR9#s6BBH2bA3Kiy*=NG89KAjd+_aW+F1ZV>lA-oY091u!+@|4~kdx1}9;xDgnBPt}0F@q;NZ5b)k7!Z-jKMLRB zY)uK^eqnb&LLwt&D)Y0K5gF3`QovUzzr;e|=EmI#83&(Mu0yK{AT55p)2{EI#INSP z$Hg0`DXBU{*_9I*Tk8}eqQm1EJt&LJp)-kpEVWzgYC{==V@CRXrttYz!a+Mb&#WTX zw2XJZ)A_mW5KlLK8f(@ zUdZVuIViaB7U!>j!@LQv5FQ-W%}1;8Cn{9mW1RY?T6P|Pi}I_*02G#g0>FZ4%e`dc z_GZpI^(wVQwYiX$+mDhnVm>o6o+U&V;nmpNEjKAB-O7o|&yi*2_PDV86A*$SvFTpL zQn3B?t{l6+PSyR`(*tCOprOWu5E6r9(ioMPO=9#gLW3d+)%8afGA-stn(j7GZ78Gc z$_cJDTRO-InO85fT+e76anl^_Qx2x{)utW$M#Y0vIXfm|@(_!!XChNZ~LK0xJ zIL>snDb|3^WLsTR*8J1{v4J50!5SnFw+p=?EII(Ap$*}9Fgs4Tcc`+M9S*xqyxP^? zjp0)(M-6h=<=;t286W~(`@O~kodjVAsY0a^>vyj`Tlyd)Kf?b#{tb;0Kv@pd_wfJ# N002ovPDHLkV1jYIJ0<`C literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..2aa7185580e3d47e30abd92c4694d9877e21e66d GIT binary patch literal 4391 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..01b53f26ed5b841272e24d2a269ce66c48f113d8 GIT binary patch literal 6521 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/resources.properties b/app/src/main/res/resources.properties new file mode 100644 index 0000000..4c0e06b --- /dev/null +++ b/app/src/main/res/resources.properties @@ -0,0 +1,2 @@ +# Set default locale +unqualifiedResLocale=en-US diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml new file mode 100644 index 0000000..e98d899 --- /dev/null +++ b/app/src/main/res/values-ar/strings.xml @@ -0,0 +1,212 @@ + + + + هذا الحساب ليس مسجلاً (لم يعد مسجلاً) + DAVx⁵ دفتر عناوين + أزل + ألغِ + تفعيل + يجب ملء هذا الحقل + مساعدة + العودة + شارك + عطبت قاعدة البيانات + أزيلت الحسابات المحلية كلها + تصحيح العلل + رسائل هامة أخرى + الرسائل غير المهمة + مزامنة + أخطاء المزامنة + الأخطاء الهامة التي توقف المزامنة ، نحو ردود الخادم غير المتوقعة + تحذيرات المزامنة + المشاكل غير الفادحة في المزامنة ، مثل بعض الملفات غير الصالحة + أخطاء الشبكة و عمليات الإدخال/الإخراج + أوقات المهل، مشاكل الاتصال ، ...الخ (مؤقتة في العادة) + + بياناتك. قرارك. + تحكَّم + الفواصل البينية للمزامنة + يجب السماح لـ %s بالعمل في الخلفية لتنجح المزامنة المجدولة. وإلا فإن آندرويد يمكنه إيقاف المزامنة في أية وقت. + لا أحتاج إلى جدولة المزامنة.* + توافق %s + أنهيت الإعدادت الإجبارية. توقف عن تذكيري أبداً.* + * اتركها مفرغة لتذكيرك لاحقا. يمكن إعادة ضبطها في إعدادات التطبيق / %s. + المزيد من المعلومات + jtx Board + دعم المهمات + إذا كان خادمك داعما للمهمات، يمكن مزامنتها مع تطبيق مهمات معتمد: + OpenTasks + يبدو أن تطويره متوقف منذ مدة +- غير منصوح به + Tasks.org + لا متجر تطبيقات متاحا + لا أحتاج دعم المهمات.* + برمجيات حرة المصدر + نحن سعداء باستخدامك تطبيقنا حر المصدر %s. التطوير والعناية والدعم كلها مهام صعبة. نرجوا أن تضع المساهمة معنا أو التبرع لنا في عين الاعتبار. سنكون ممتنين لمساهمتك أو تبرعك. + كيفية المساهمة أو التبرع + + الأذونات + %sيتطلب بعض الأذونات للعمل بكفء. + + + المكتبات + النسخة %1$s (%2$d) + يقدَّم هذا البرنامج دون أدنى مسؤولية. إنه برنامج حر، وندعوك لإعادة توزيعه حسب أحكام محددة. + + لا يمكن إنشاء ملف سجل + + مهايئ مزامنة CalDAV/CardDAV + حول / الترخيص + انطباعات المستخدمين عن البيتا + الإعدادات + أخبار وتحديثات + روابط خارجية + موقع الويب + دليل الاستخدام + الأسئلة الشائعة + + + فشل اكتشاف الخدمة + لم يتمكن التطبيق من تجديد قائمة المجموعة + + + الإعدادات + تصحيح العلل + عرض معلومات التصحيح + التسجيل المفصّل + التسجيل معطَّل + الاتصال + الأمن + عدم الثقة في شهادات النظام + هيئات توثيق النظام و تلك التي أضافها المستخدم لن تكون محل ثقة + هيئات توثيق النظام و تلك التي أضافها المستخدم ستكون محل ثقة (نوصي بهذا) + إعادة ضبط الشهادات (غير)الموثوقة + إعادة ضبط حالة الثقة لجميع الشهادات المخصصة + تمت إزالة جميع الشهادات المخصصة + واجهة المستخدم + إعدادات الإشعار + إدارة وسائل الإشعار وإعداداتها + إعادة ضبط التلميحات + إعادة تفعيل التلميحات التي أُبعِدت سابقاً + كل التلميحات ستظهر مرة أخرى + + CardDAV + CalDAV + Webcal + زامن الآن + إعدادات الحساب + إعادة تسمية الحساب + إعادة تسمية + اسم الحساب مأخوذ بالفعل + حذف الحساب + هل تريد حذف الحساب فعلاً ؟ + سيتم حذف كل النسخ المحليّة من دفاتر العناوين والتقاويم وقوائم المهام. + مزامنة هذه المجموعة + للقراءة فقط + تقويم + لم نجِد تطبيقاً قادراً على استخدام Webcal + تثبيت ICSx⁵ + + إضافة حساب + تسجيل الدخول + تسجيل الدخول بعنوان البريد + عنوان البريد الإلكتروني + مطلوب عنوان بريد إلكتروني صالح + كلمة المرور + تسجيل الدخول بعنوان URL واسم مستخدم + اسم المستخدم + URL الأساس + اختيار الشهادة + إضافة حساب + اسم الحساب + استخدم عنوان بريدك الإلكتروني اسماً للحساب لأن آندرويد يستخدم اسم الحساب في حقل المنظّم ORGANIZER للأحداث التي تنشئها. لايمكن أن تمتلك حسابين بالاسم نفسه. + طريقة مجموعة جهة الاتصال: + اسم الحساب مطلوب + اسم الحساب مأخوذ بالفعل + اكتشاف الضبط + يجري استعلام الخادم … يرجى الانتظار + لم نجِد خدمة CalDAV أو CardDAV. + + المزامنة + مدة مزامنة جهات الاتصال + يدوية فقط + كل %dدقائق + فور حدوث التغييرات المحليّة + مدة مزامنة التقاويم + مدة مزامنة المهام + + يدوياً فقط + كل 15 دقيقة + كل 30 دقيقة + كل ساعة + كل ساعتين + كل 4 ساعات + مرة في اليوم + + المزامنة فقط عبر WiFi + المزامنة مقصورة على اتصالات WiFi + نوع الاتصال غير مأخوذ في الاعتبار + تقييد SSID لـ WiFi + ستتم المزامنة فقط عبر %s + سيتم استخدام جميع اتصالات WiFi + أسماء (SSIDs) مفصولة بفواصل لشبكات WiFi المسموح الاتصال عبرها (اتركه فارغاً للسماح للكل) + المصادقة + اسم المستخدم + حدّث كلمة المرور المعمول بها في خادمك. + CalDAV + الحد الزمني للأحداث الماضية + ستتم مزامنة جميع الأحداث + + سيتم تجاهل الأحداث الأقدم من %d أيام + سيتم تجاهل الأحداث الأقدم من يوم واحد + سيتم تجاهل الأحداث الأقدم من %d أيام + سيتم تجاهل الأحداث الأقدم من %d أيام + سيتم تجاهل الأحداث الأقدم من %d أيام + سيتم تجاهل الأحداث الأقدم من %d أيام + + الأحداث التي جرت بعد عدد الأيام هذا في الماضي سيتم تجاهلها (يمكن أن يكون 0). أتركه فارغاً لمزامنة جميع الأحداث. + إدارة ألوان التقاويم + دعم ألوان الأحداث + CardDAV + طريقة مجموعة عنوان الاتصال + + إنشاء دفتر عناوين + إنشاء تقويم + المدخلات المحتملة للتقويم + أحداث + مهام + ملاحظات/يوميات + اللون + العنوان + مكان التخزين + إنشاء + + حذف المجموعة + المزامنة + العنوان + الوصف + + معلومات تصحيح العلل + نسخ عنوان URL + + حدث خطأ. + حدث خطأ HTTP. + حدث خطأ في الإدخال/الإخراج. + عرض التفاصيل + + المصادقة + اسم المستخدم + كلمة المرور + + أذونات DAVx⁵ + مطلوب أذونات إضافية + فشلت المصادقة (تحقق من بيانات تسجيل الدخول) + خطأ شبكة أو الإدخال/الإخراج - %s + خطأ خادم HTTP - %s + خطأ تخزين محلي - %s + استلام جهة اتصال غير صالحة من الخادم + استلام حدث غير صالح من الخادم + استلام مهمة غير صالحة من الخادم + جرى تجاهل مورد غير صالح واحد أو أكثر + + + diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml new file mode 100644 index 0000000..c75c466 --- /dev/null +++ b/app/src/main/res/values-bg/strings.xml @@ -0,0 +1,481 @@ + + + + Регистрацията (вече) не съществува + Адресник на DAVx⁵ + Не променяйте профила оттук! Вместо това използвайте приложението, за да управлявате профили. + Премахване + Премахване + Отказ + Включване + Задължително поле + Помощ + Придвижване нагоре + Меню настройки + Споделяне + Синхронизирането е започнало/изчаква + Повредено хранилище + Всички местни профили са премахнати. + Отстраняване на дефекти + Други важни съобщения + Съобщения за състоянието с нисък приоритет + Синхронизиране + Грешки при синхронизиране + Важни грешки, спиращи синхронизирането, като неочакван отговор от сървъра + Предупреждения от синхронизиране + Нефатални проблеми със синхронизацията като някои невалидни файлове + Грешки с входа/изхода и мрежата + Забавяния, прекъсвания и т.н (често са временни) + + Вашите данни. Вашият избор + Поемете контрол + Периодично синхронизиране + За да синхронизира периодично, %s се нуждае от разрешение да работи във фонов режим. В противен случай Android може да спре синхронизацията по всяко време. + Не желая периодично синхронизиране.* + %s съвместимост + Софтуера на телефона може да спре синхронизирането. Ако това се случва при вас, можете да решите проблема само ръчно. + Необходимите промени са направени. Без повторно напомняне.* + * Оставете без отметка за повторно напомняне. Може да бъде нулирано от настройките на приложението / %s. + Допълнителна информация + jtx Board + + Поддръжка на задачи + Ако сървърът поддържа задачи, те могат да бъдат синхронизирани с приложение за задачи: + OpenTasks + Изглежда не се разработва вече, не се препоръчва. + Tasks.org + не се поддържат.]]> + Няма достъпен магазин за приложения + Не се нуждая от поддръжка на задачи.* + Приложение с отворен код + Радваме се, че използвате %s – приложение с отворен код. Разработката, издръжката и поддръжката му са тежка работа. Молим ви да допринесете (има много начини) или да дарите. Вашият жест ще бъде високо оценен. + Как да допринеса или даря + Без напомняне за + + %d месец + %d месеца + + Напред + + Разрешения + За да работи нормално %s се нуждае от разрешения. + Всички от изброените по-долу + За да включите вички възможности, изберете това (препоръчано) + Всички разрешения са получени + Разрешения за контактите + Контактите не могат да бъдат синхронизирани (не се препоръчва) + Контактите могат да бъдат синхронизирани + Разрешения за календара + Календарите не могат да бъдат синхронизирани (не се препоръчва) + Календарите могат да бъдат синхронизирани + Разрешение за известия + Известията са изключени (непрепоръчително) + Известията са еключени + Разрешения за jtx Board + Разрешения за OpenTasks + Разрешения за Tasks + Няма синхронизация на задачи + Задачите могат да бъдат синхронизирани + Предпазване от нулиране на разрешенията + Разрешенията могат да бъдат нулирани автоматично (не се препоръчва) + Разрешенията не могат да бъдат нулирани автоматично + Докоснете Разрешения и махнете отметката от „Премахване на разрешенията, ако приложението не се използва“ + Ако някой превключвател не работи, използвайте настройките на приложението / Разрешения. + Настройки на приложението + + Разрешения за SSID на Wi-Fi + За достъп до името на текущата мрежа на Wi-Fi (SSID) трябва да бъдат изпълнени следните условия: + Разрешение за достъп до точното месоположение + Има разрешение за местоположението + Липсва разрешение за местоположението + Достъп до местоположението във фонов режим + Разрешаване във всички случаи + Разрешението за местоположението е зададено на: %s + Разрешението за местоположението не е зададено на: %s + %s използва разрешението за местоположение (само идентификатора на безжичната мрежа) единствено, за да ограничи синхронизирането само до безжична мрежа с определен идентификатор. Това се случва дори и когато приложението се изпълнява във фонов режим. + Данните за местоположението (само идентификатора на безжичната мрежа) се използват само на устройството без да бъдат изпращани никъде. + Винаги включено местоположение + Услугата за местоположението е включена + Услугата за местоположението е изключена + + Преводи + Библиотеки + Издание %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) и сътрудници + Тази програма се предлага с АБСОЛЮТНО НИКАКВА ГАРАНЦИЯ. Тя е свободен софтуер и можете да я разпространявате при определени условия. + + Не може да бъде създаден дневник + Действията на %s се записват в дневник + Преглед/споделяне + Изключване + + Адаптор за синхронизиране на CalDAV/CardDAV + Относно / лиценз + Обратна връзка към бета + Инсталиране на мрежов четец + Настройки + Новини и издания + Инструменти + Външни препратки + Страница + Ръководство + Често задавани въпроси + За организации + Общност + Подкрепа за проекта + Как да допринеса + Лични данни + Добре дошли при DAVx⁵! + Свържете се със сървър и поддържайте календарите и контактите си синхронизирани. + Синхронизиране на всички профили + + Известията са изключени. Няма да бъдете известявани за грешки при синхронизиране. + Автоматичното синхронизиране е изключено (липсва потвърдена връзка с интернет) + Управление на връзките + Включена е икономия на данни. Синхронизацията във фонов режим е ограничена. + Икономия на трафик + Включена е икономия на батерия. Синхронизацията може да е ограничена. + Икономия на батерия + Пространството за съхранение е малко. Андроид няма да синхронизира местните промени веднага, а по време на следващата редовна синхронизация. + Управление на хранилището + Липсва доставчик на календар + Да не би да сте изключили системното приложение „Съхранение в календар“? + Липсва доставчик на контакти + Да не би да сте изключили системното приложение „Хранилище на контакти“? + Управление на приложения + + Грешка при откриване на услугите + Грешка при презареждане + + Работи на преден план + На някои устройства това е необходимо, за да работи автоматичното синхронизиране. + + Настройки + Отстраняване на дефекти + Информация за отстраняване на дефекти + Преглед/споделяне на настройките и дневника + Подробен дневник + Дневникът е включен. Можете да го прегледате като част от информацията за отстраняване на дефекти. + Дневникът е изключен + Оптимизиране на батерията + Приложението е в белия списък (препоръчително) + Има ограничения за батерията (не препоръчително) + Връзка + Вид на сървъра на прокси + + Според системата + Без сървър на прокси + HTTP + SOCKS (за Orbot) + + Име на хоста на сървъра на прокси + Порт на сървъра на прокси + Защита + Разрешения за приложението + Преглед на необходимите за синхронизиране разрешения + Оттегляне на доверието в системните сертификати + Издатели на сертификати познати на системата или добавени от потребителя няма да бъдат доверени + Издателите на сертификатите познати на системата или добавени от потребителя ще бъдат доверени (препоръчително) + Когато е отметнато доверието от системните сертификати е оттеглено. Това означава, че вие ръчно трябва да приемете всеки сертификат (включително и при обновяване на сертификата на сървъра) в противен случай настройката на профил и синхронизирането няма да работят. + Нулиране на (не)доверените сертификати + Нулиране на доверието към всички потребителски сертификати + Всички потребителски сертификати са премахнати + Потребителски интерфейс + Настройки за съобщенията + Управлявление на каналите за съобщения и техните настройки + Избиране на тема + + Според системата + Светла + Тъмна + + Нулиране на подсказки + Всички затворени подсказки, ще бъдат показани отново + Всички подсказки ще бъдат показани отново + Интеграция + Приложението Tasks + Не е инсталирана съвместимо приложение за задачи. + UnifiedPush (експериментално) + Липсва (изключено) + Изберете дистрибутор + Не е инсталиран дистрибутор + Не е настроен сървър + В готовност за получаване на съобщения през %s + FCM (Google Play) + Отдалечено подадените съобщения са винаги шифровани. + + Профилът е премахнат + CardDAV + CalDAV + Webcal + За да бъдат синхронизирани тези списъци са необходими допълнителни разрешения. + Управление на разрешенията + Синхронизиране + Настройки на регистрацията + Преименуване на регистрация + Незапазените местни данни могат да бъдат отхвърлени. След преименуването е нужно синхронизиране. + Ново име на регистрацията + Преименуване + Това име на регистрация вече се използва + Грешка при преименуване + Премахване на регистрация + Да бъде ли премахната регистрацията? + Всички местни копия на адресници, календари и списъци със задачи ще бъдат премахнати. + синхронизиране на списък + само за четене + календар + контакти + дневник + задачи + Само лични + Презареждане на списъка + Абонаментите на Webcal могат да бъдат синхронизирани посредством друго приложение. + Липса приложение, поддържащо Webcal + Инсталиране на ICSx⁵ + + Нова регистрация + политиката за поверителност.]]> + Вход, независим от доставчик + Вход, специфичен за доставчика + Напред + Напред + Вход с електронна поща + Електронна поща + Необходим е действителен електронен адрес + Откриването на услугите става чрез записи в DNS и добре познати адреси.]]> + Парола + Скриване на паролата + Показване на паролата + Парола (по желание) + Вход с адрес и потребителско име + Потребителско име + Потребителско име (по желание) + Основен адрес + някои услуги могат да бъдат открити и чрез записи в DNS, и добре познати адреси.]]> + Избор на сертификат + Нова регистрация + Име на регистрация + Използването на апострофи (\') изглежда води до проблеми при някои устройства. + Използвайте адрес за електронна поща вместо име, защото Android ще го използва като адрес на организатора на събитията, които създавате. Не може да има две регистрации с еднакво име. + Метод за съхранение на групи от контакти: + Изисква се име на регистрацията + Това име на регистрация вече се използва + Профилът не може да бъде добавен + Готово + Вход за напреднали + Без клиентск сертификат (по желание) + Клиентски сертификат: %s + Не е намерен сертификат + Инсталиране на сертификат + Fastmail + Профил във Fastmail + Вход с Fastmail + Google Contacts / Calendar + Профил в Гугъл + Вход с Гугъл + Идентификатор на клиент (по желание) + Политика за поверителност.]]> + Политиката за потребителски данни на услугите на Google API, включително на Изискванията за ограничена употреба.]]> + Не може да бъде получен код за упълномощаване + Nextcloud + Вход с Nextcloud + По този начин ще се стартират стъпките за влизане в Nextcloud чрез мрежов четец. + Адрес на сървъра на Nextcloud + Вход + Адресът за вход не може да бъде получен + Данните за вход не могат да бъдат получени + Откриване на настройки + Изчакайте, запитване на сървъра… + Не са открити услуги на CalDAV или CardDAV. + Основата на адресът изглежда недостъпен за CalDAV/CardDAV адрес и откриването на услуги е неуспешно. + нашия списък с изпробвани услуги и техните основи на адреса.]]> + Също така отново проверете удостоверяването (обикновено потребителско име и парола). + Повече техническа информация има в дневника. + Преглед + + Синхронизиране + Интервал на синхронизиране на контактите + Само ръчно + На всеки %d минути и веднага при местна промяна + Интервал на синхронизиране на календарите + Интервал на синхронизиране на задачите + + Само ръчно + Всеки 15 минути + На 30 минути + Всеки час + На 2 часа + На 4 часа + Веднъж дневно + + Синхронизиране само през Wi-Fi + Синхронизацията е ограничена само при връзки през Wi-Fi + Видът на връзката не е от значение + Ограничения на Wi-Fi по SSID + Ще синхронизира само през %s + Ще бъдат използвани всички връзки по Wi-Fi + Разделени със запетая имена (SSID) на разрешените мрежи по Wi-Fi (празно за всички мрежи) + Ограниченията на Wi-Fi по SSID изискват допълнителни настройки + Управление + Използването на ВЧМ изисква връзка с интернет + VPN без работеща и проверена връзка с интернет не е достатъчен, за да се извършва синхронизация (препоръчително) + VPN без работеща и проверена връзка с интернет е достатъчен, за да се извършва синхронизация + Удостоверяване + Потребителско име + Парола или парола на приложение + парола на приложението.]]> + Нова парола + Променете паролата съгласно сървъра. + Повторно удостоверяване (OAuth) + При отменен достъп + Удостоверени сте + Клиентски сертификат + Не е избран или няма наличен сертификат + Инсталиране на сертификат + CalDAV + Ограничения за събития от миналото + Всички събития ще се синхронизират + + Събития от преди повече от един ден ще бъдат пренебрегнати + Събития от преди повече от %dдена ще бъдат пренебрегнати + + Събитията преди повече от зададения брой дни в миналото ще бъдат пренебрегнати (може да бъде 0). За да бъдат синхронизирани всички събития оставете празно. + Подразбирано напомняне + + Подразбирано напомняне една минута преди събитието + Подразбирано напомняне %d минути преди събитието + + Няма създадени подразбирани напомняния + Към събитията без напомняне ще бъде добавяно подразбирано напомняне: желания брой минути преди събитието. Оставете празно, за да изключите подразбираните напомняния. + Управление на цветовете на календара + Цветовете на календарите се нулират при всяко синхронизиране. + Цветовете на календарите могат да бъдат зададени от други приложения. + Поддръжка на цветове за събития + Цветовете на събитията се синхронизират. + Цветовете на събитията не се синхронизират. + CardDAV + Метод за групиране на контакти + + Групите са отделни vCards + Групите са категории във всеки контакт + + + Създаване на адресник + Създаване на адресник през CardDAV може да не е поддържано от сървъра. + Създаване на календар + Стандартен часови пояс (по желание) + + Възможни елементи на календара + Събития + Задачи + Бележки / дневник + Създаване на календар през CalDAV може да не е поддържано от сървъра. + Цвят + Заглавие + Местоположение на хранилището + Описание (по желание) + Създаване + + контакти + събития + задачи + Премахване + Списъкът (%s) и цялата му информация ще бъде безвъзвратно премахната, както от устройството, така и от сървъра. + Синхронизиране + Синхронизирането е включено + Синхронизирането е изключено + Само за четене + Само за четене (според сървъра) + Само за четене (според политиката) + Само за четене (на устройството) + Четене/писане + Заглавие + Описание + Собственик + Поддържане на Push + Сървърът обявява поддръжка на Push + Начало на абонамента %1$s, изтича на %2$s + Последно синхронизиране (%s) + Адрес (URL) + + Информация за отстраняване на дефекти + Архив на ZIP + Съдържа информацията за отстраняване на дефекти и дневниците + Споделете архива, за да го пренесете на компютър, да го изпратите по електронна поща или да го прикачите към билет за поддръжка. + Споделяне на архива + Информацията за отстраняване на дефекти е приложена към това съобщение (получаващото приложение трябва да поддръжа прикачени файлове). + Грешка на HTTP + Грешка на сървъра + Грешка на WebDAV + Грешка на входа/изхода + Заявката е отказана от сървъра. + Заявеният ресурс (вече) не съществува. + Сървърът не позволява този вид заявено действие. + Грешка на сървъра. Свържете се с поддръжката на сървъра. + Неочаквана грешка. Прегледайте дневника за отстраняване на грешки за повече подробности. + Подробности + Информацията за отстраняване на дефекта е събрана + Ресурси, имащи отношение + Свързани с проблема + Отдалечен ресурс: + Местен ресурс: + Дневници + Налични са подробни дневници + Преглед + Копиране на адреса + Съобщение за защита на личните данни + Дневниците и информацията за отстраняване на грешки могат да съдържат лична информация. Имайте го предвид, когато ги споделяте публично. + + Възникна грешка. + Възникна грешка на HTTP. + Възникна грешка с входа/изхода. + Подробности + + Дялове на WebDAV + Използвана квота: %1$s / налична: %2$s + Споделяне на съдържание + Демонтиране + Добавяне на дял на WebDAV + Получете директен достъп до файловете си в облака като монтирате дял на WebDAV. + как работят дяловете на WebDAV.]]> + Видимо име + Адрес на WebDAV + Недействителен адрес + Точка на монтиране и показвано име + Удостоверяване + Потребителско име + Парола + Потребителско име (по желание) + Парола (по желание) + Монтиране + Няма услуга на WebDAV на адреса + Премахване на точката на монтиране + Ще бъдат изгубени подробности за връзката, но файлове няма да бъдат премахвани. + Достъпва се файл на WebDAV + Изтегля се файл на WebDAV + Изпраща се файл на WebDAV + Дял на WebDAV + + Разрешения на DAVx⁵ + Необходими са допълнителни разрешения + Приложението %s е твърде старо + Минимално необходимо издание: %1$s + Грешка при удостоверяване (проверете данните за вход) + Грешка с входа/изхода или мрежата – %s + Грешка в сървъра на HTTP – %s + Грешка в местното хранилище – %s + Грешка (достигнат максимален брой опити) + Получен е недействителен контакт от сървъра + Получено е недействително събитие от сървъра + Получен е недействителен файл от сървъра + Пренебрегване на сбъркани данни + Чакаща синхронизация + Отдалечените данни са променени + + Синхронизирането всичко + Синхронизиране на всички профили + Бутон за синхронизиране с етикет + Бутон за синхронизиране с пиктограма + Докоснете, за да бъде извършено ръчно синхронизиране. + + diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml new file mode 100644 index 0000000..63d4bf5 --- /dev/null +++ b/app/src/main/res/values-ca/strings.xml @@ -0,0 +1,479 @@ + + + + El compte no existeix (eliminat) + Llibreta d’adreces del DAVx⁵ + No canvieu aquí el compte! Utilitzeu directament l\'aplicació per a gestionar els comptes. + Eliminar + Elimina + Cancel·la + Activa + Cal aquest camp + Ajuda + Navega cap amunt + Menú d\'opcions + Comparteix + La sincronització s\'ha iniciat/posat a la cua + Base de dades malmesa + S\'han eliminat localment tots els comptes. + Depurador + Altres missatges importants + Missatges d\'estat de baixa prioritat + Sincronització + Errors de sincronització + Errors importants que no permeten la sincronització, com respostes del servidor inesperades + Advertències de sincronització + Problemes de sincronització no fatals, com alguns fitxers invàlids + Xarxa i errors E/S + Temps d\'espera esgotats, errors de connexió, etc. (sovint temporals) + + Les teves dades. La teva elecció. + Pren el control. + Intervals de sincronització regulars + Per la sincronització en intervals regulars, cal que %s tingui permís per a executar-se en segon pla. En cas contrari, Android podria aturar la sincronització en qualsevol moment. + No necessito intervals de sincronització regulars.* + Compatibilitat amb %s + El microprogramari específic dels proveïdors pot bloquejar la sincronització. Si us afecta, només podeu resoldre-ho manualment. + He fet els ajustaments requerits. No m\'ho recordis més.* + * Deixar desmarcat per un recordatori més tard. Pot ser reinicialitzat a ajustaments /%s. + Més informació + jtx Board + + Suport de tasques + Si el vostre servidor permet les tasques, es poden sincronitzar amb una aplicació que admeti tasques: + OpenTasks + Sembla que ja no es desenvolupa, no es recomana. + Tasks.org + No s\'admeten algunes funcionalitats.]]> + No hi ha cap mercat d\'aplicacions disponible + No necessito suport per les tasques.* + Programari de codi obert + Ens alegra saber que utilitzes %s, que és programari de codi obert. El desenvolupament, manteniment i suport requereixen un gran treball. Si us plau, considera contribuir (hi ha moltes formes) o realitzar una donació. Seria molt apreciat! + Com contribuir/donar + No m\'ho recordis durant + + %d mes + %d mesos + + Següent + + Permisos + %s requereix permisos per a funcionar correctament. + Tots els següents + Utilitza això per a activar totes les funcions (recomanat) + Tots els permisos concedits + Permisos de contactes + Sense sincronització de contactes (no recomanat) + Sincronització de contactes possible + Permisos de calendari + Sense sincronització del calendari (no recomanat) + Sincronització del calendari possible + Permís de notificació + Notificacions desactivades (no recomanat) + Notificacions activades + Permisos de jtx Board + Permisos d\'OpenTasks + Permisos de Tasks + Sense sincronització de les tasques + Sincronització de les tasques possible + Mantenir els permisos + Els permisos podrien ser revocats automàticament (no recomanat) + Els permisos no seran revocats automàticament + Feu clic a Permisos > desmarqueu «Elimina els permisos si no s\'usa l\'aplicació» + Si un interruptor no funciona, utilitza l\'apartat de permisos de l\'aplicació dels ajustaments del telèfon. + Ajustaments de l\'aplicació + + Permisos de l\'SSID del Wi-Fi + Per tal de poder accedir al nom de la xarxa de Wi-Fi connectada (SSID), s\'han de complir els següents requisits: + Permís d\'ubicació precisa + Permís d\'ubicació concedit + Permís d\'ubicació rebutjat + Permís d\'ubicació en segon pla + Permetre tota l\'estona + El permís d\'ubicació s\'ha definit a: %s + El permís d\'ubicació no s\'ha definit a: %s + %s utilitza dades d\'ubicació (només SSID de WiFi) únicament per a restringir la sincronització a un SSID de WiFi específic. Això passarà fins i tot quan la sincronització s\'executi en segon pla. + Totes les dades d\'ubicació (només SSID de WiFi) només s\'utilitzen localment i no s\'envien enlloc. + Localització sempre habilitada + El servei de localització està habilitat + Servei de localització deshabilitat + + Traduccions + Biblioteques + Versió %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) i col·laboradors + Aquest programa és distribuït sense CAP MENA DE GARANTIA. És programari lliure, i està permesa la seva redistribució segons certes condicions. + + No s\'ha pogut crear el fitxer de registre + Iniciant a totes les %s activitats + Veure/compartir + Inhabilita + + Adaptador de sincronització CalDAV / CardDAV + Quant a/llicència + Opina sobre la Beta + Instal·leu un navegador web + Configuració + Novetats i actualitzacions + Eines + Enllaços externs + Pàgina web + Manual + Preguntes més freqüents + Per a organitzacions + Comunitat + Doneu suport al projecte + Com col·laborar + Política de privacitat + Us donem la benvinguda al DAVx⁵! + Connecteu-vos al vostre servidor i mantingueu els calendaris i contactes sincronitzats. + Sincronitza tots els comptes + + Notificacions desactivades. No es notificaran els errors de sincronització. + La sincronització automàtica no està activa (no hi ha cap connexió a Internet verificada). + Gestiona les connexions + S\'ha activat l\'estalviador de dades. La sincronització en segon pla està restringida. + Gestió de l\'estalviador de dades + S\'ha habilitat l\'estalvi de bateria. Es podria restringir la sincronització. + Gestiona l\'estalvi de bateria + Espai d\'emmagatzematge baix. L\'Android no sincronitzarà els canvis locals immediatament, sinó durant la pròxima sincronització normal. + Gestiona l\'emmagatzematge + Manca el proveïdor de calendari + Heu desactivat l\'aplicació del sistema «Emmagatzematge de calendari»? + Manca el proveïdor de contactes + Heu desactivat l\'aplicació del sistema «Emmagatzematge de contactes»? + Gestioneu les aplicacions + + Ha fallat la detecció del servei + No s\'ha pogut actualitzar la llista de col·leccions + + Executant-se en segon pla + En alguns dispositius, això és necessari per a la sincronització automàtica. + + Configuració + Depurador + Informació de depuració + Visualitza/comparteix els detalls de la configuració i els registres + Registre detallat + El registre està actiu. Podeu veure els registres com a part de la informació de depuració. + Registre inactiu + Optimització de la bateria + L\'aplicació està exempta (recomanat) + S\'apliquen restriccions de bateria (no recomanat) + Connexió + Tipus de servidor intermediari + + Predeterminat del sistema + Sense servidor intermediari + HTTP + SOCKS (per a l\'Orbot) + + Nom del servidor intermediari + Port del servidor intermediari + Seguretat + Permisos de l\'aplicació + Revisa els permisos necessaris per a la sincronització + Desconfia dels certificats del sistema + No es confiarà en les CA del sistema ni les afegides per l\'usuari + Es confiarà en les CA del sistema i les afegides per l\'usuari (recomanat) + Si aquest paràmetre està actiu, els certificats del sistema no es consideren fiables. Això vol dir que haureu d\'acceptar manualment cada certificat (també quan el servidor renovi el seu certificat) o la configuració del compte i la sincronització no funcionaran. + Restableix els certificats de (des)confiança + Restableix la confiança de tots els certificats personalitzats + Neteja tots els certificats personalitzats + Interfície d’usuari + Configuració de les notificacions + Gestiona els canals de notificació i la seva configuració + Selecció del tema + + Predeterminat del sistema + Clar + Fosc + + Reinicia els consells + Torna a habilitar els consells que s\'han ignorat anteriorment + Es mostraran de nou tots els consells + Integració + Aplicació de tasques + No s\'ha trobat cap aplicació de tasques compatible + UnifiedPush (experimental) + Sense (desactiva «push») + Trieu un distribuïdor + No s\'ha instal·lat cap distribuïdor de «push» + No s\'ha configurat cap punt final + Preparat per a rebre missatges «push» sobre %s + FCM (Google Play) + Els missatges «push» sempre són xifrats. + + S\'ha eliminat el compte + CardDAV + CalDAV + Webcal + Es requereixen permisos addicionals per a sincronitzar aquestes col·leccions. + Gestió dels permisos + Sincronitza ara + Configuració del compte + Canvia el nom del compte + Les dades locals sense desar es podrien descartar. Es requereix tornar a sincronitzar després de canviar el nom. + Nom nou del compte + Canvia el nom + Nom de compte existent + No s\'ha pogut canviar el nom del compte + Suprimeix el compte + Segur que vols suprimir el compte? + Se suprimiran totes les còpies locals de llibretes d\'adreces, calendaris i llistes de tasques. + Sincronitza aquesta col·lecció + només lectura + calendari + contactes + diari + tasques + Mostra només les personals + Recarregar llista + Les subscripcions Webcal es poden sincronitzar amb aplicacions externes. + No s\'ha trobat cap aplicació compatible amb Webcal + Instal·la ICSx⁵ + + Afegeix un compte + política de privadesa.]]> + Inici de sessió genèric + Inici de sessió específic de proveïdor + Continua + Inici de sessió + Inici de sessió amb una adreça de correu electrònic + Adreça de correu electrònic + Es requereix una adreça vàlida de correu electrònic + serveis es descobreixen utilitzant el registres de DNS i els URL ben coneguts.]]> + Contrasenya + Oculta la contrasenya + Mostra la contrasenya + Contrasenya (opcional) + Inici de sessió amb un URL i un nom d\'usuari/ària + Nom d\'usuari/ària + Nom d\'usuari (opcional) + URL base + serveis també es descobreixen utilitzant els registres de DNS i els URL ben coneguts.]]> + Selecciona el certificat + Afegeix un compte + Nom del compte + L\'ús d\'apòstrofs (\') sembla que provoca problemes en alguns dispositius. + Utilitzeu la vostra adreça de correu electrònic com a nom del compte perquè l\'Android utilitzarà el nom del compte com a camp ORGANITZADOR per als esdeveniments que creeu. No poden haver-hi dos comptes amb el mateix nom. + Mètode dels grups de contactes: + Nom del compte obligatori + Nom de compte existent + No s\'ha pogut afegir el compte + Finalitza + Inici de sessió avançat + Sense certificat del client (opcional) + Certificat del client: %s + No s\'ha trobat cap certificat + Instal·la un certificat + Fastmail + Compte de Fastmail + Inici de sessió amb el Fastmail + Contactes / Calendari de Google + Compte de Google + Inicia la sessió amb Google + ID de Client (opcional) + política de privadesa per als detalls.]]> + Política de dades d\'usuari dels serveis de l\'API de Google, incloent-hi els requisits d\'ús limitat.]]> + No s\'ha pogut obtenir el codi d\'autorització + Nextcloud + Inici de sessió amb Nextcloud + Això iniciarà el flux d\'inici de sessió del Nextcloud en un navegador web. + Adreça del servidor Nextcloud + Inicia la sessió + No s\'ha pogut obtenir l\'URL d\'inici de sessió + No s\'han pogut obtenir les dades d\'inici de sessió + Detecció de la configuració + Espereu, s\'està consultant el servidor… + No s\'ha pogut trobar cap servei CalDAV o CardDAV. + L\'URL base no sembla que sigui un URL accessible de CalDAV/CardDAV i la detecció del servei no ha estat correcta. + la nostra llista de serveis provats i les seves URL base.]]> + Comproveu també l\'autenticació (normalment nom d\'usuari i contrasenya). + Hi ha més informació tècnica disponible als registres. + Visualitza els registres + + Sincronització + Interval de sincronització dels contactes + Només a mà + Cada %d minut/s + immediatament en els canvis locals + Interval de sincronització dels calendaris + Interval de sincronització de les tasques + + Només a mà + Cada 15 minuts + Cada 30 minuts + Cada hora + Cada 2 hores + Cada 4 hores + Una vegada al dia + + Sincronitza només amb Wi-Fi + La sincronització està restringida a les connexions Wi-Fi + No es té en compte el tipus de connexió + Restricció per SSID de la Wi-Fi + Només sincronitzarà des de %s + S\'utilitzaran totes les connexions Wi-Fi + Noms (SSID) separats per comes de les xarxes Wi-Fi permeses (deixeu-ho en blanc per a totes) + La restricció per SSID de la Wi-Fi requereix una configuració addicional + Gestió + La VPN requereix una Internet subjacent + Una VPN sense connexió a Internet validada subjacent no és suficient per a executar la sincronització (recomanat) + Una VPN sense connexió a Internet validada subjacent és suficient per a executar la sincronització + Autentificació + Nom d\'usuari/ària + Contrasenya o contrasenya d\'aplicació + contrasenya d\'aplicació.]]> + Contrasenya nova + Actualitzeu la contrasenya segons el vostre servidor. + Torna a autoritzar (OAuth) + Usa quan s\'hagi revocat l\'accés + Autorització correcta + Certificat del client + No hi ha cap certificat disponible o seleccionat + Instal·la un certificat + CalDAV + Límit de temps dels esdeveniments passats + Tots els esdeveniments se sincronitzaran + + Els esdeveniments de més d\'un dia passat seran ignorats + Els esdeveniments de més de %d dies passats seran ignorats + + Els esdeveniments que superin aquest nombre de dies en el passat seran ignorats (poden ser 0). Deixeu-ho en blanc per a sincronitzar tots els esdeveniments. + Recordatori predeterminat + + Recordatori predeterminat d\'un minut abans de l\'esdeveniment + Recordatori predeterminat de %d minuts abans de l\'esdeveniment + + No es crearan recordatoris predeterminats + Si es creen recordatoris predeterminats per als esdeveniments sense recordatori: el nombre desitjat de minuts abans de l\'esdeveniment. Deixeu-ho en blanc per a desactivar els recordatoris predeterminats. + Gestiona els colors del calendari + Els colors del calendari es reinicialitzen a cada sincronització + Altres aplicacions poden establir els colors del calendari + Funcionament dels colors dels esdeveniments + Se sincronitzen els colors dels esdeveniments + No se sincronitzen els colors dels esdeveniments + CardDAV + Mètode dels grups de contactes + + Els grups són vCards separades + Els grups són categories per contacte + + + Crea una llibreta d\'adreces + La creació de llibretes d\'adreces sobre CardDAV pot no ser suportada pel servidor. + Crea un calendari + Fus horari predeterminat (opcional) + + Possibles entrades de calendari + Esdeveniments + Tasques + Notes/diari + La creació de calendaris sobre CalDAV pot no ser suportada pel servidor. + Color + Títol + Ubicació d’emmagatzematge + Descripció (opcional) + Crea + + contactes + esdeveniments + tasques + Suprimeix la col·lecció + Aquesta col·lecció (%s) i totes les seves dades seran eliminades localment i del servidor. + Sincronització + Sincronització habilitada + Sincronització deshabilitada + Només lectura + Només lectura (pel servidor) + Només de lectura (per política) + Només lectura (localment) + Lectura/escriptura + Títol + Descripció + Propietari + Suport de Push + El servidor té suport per a Push + Subscrit el %1$s, venç el %2$s + Última sincronització (%s) + Adreça (URL) + + Informació de depuració + Arxiu ZIP + Conté informació de depuració i registres + Compartiu l\'arxiu per a transferir-lo a un ordinador, per a enviar-lo per correu electrònic o per a adjuntar-lo a un tiquet de suport. + Comparteix l\'arxiu + Informació de depuració adjunta a aquest missatge (requereix el suport d\'adjunts de l\'aplicació de recepció). + Error HTTP + Error del servidor + Error del WebDAV + Error d\'E/S + El servidor no permet el tipus d\'operació sol·licitat. + S\'ha produït un problema a la banda del servidor. Poseu-vos en contacte amb l\'assistència del servidor. + S\'ha produït un error inesperat. Vegeu els detalls a la informació de depuració. + Vista dels detalls + S\'ha recopilat la informació de depuració + Recursos implicats + Relacionats amb el problema + Recurs remot: + Recurs local: + Registres + Hi ha registres detallats disponibles + Visualitza els registres + Copia l\'URL + Avís de privadesa + Els registres i la informació de depuració poden contenir informació privada. Tingueu en compte això quan ho compartiu públicament. + + S\'ha produït un error. + S\'ha produït un error HTTP. + S\'ha produït un error d\'E/S. + Mostra els detalls + + Muntatges WebDAV + Quota utilitzada: %1$s / disponible: %2$s + Comparteix el contingut + Desmunta + Afegeix un muntatge WebDAV + Accediu directament als fitxers en el núvol afegint un muntatge WebDAV! + com funcionen els muntatges WebDAV.]]> + Nom a mostrar + URL del WebDAV + URL no vàlid + Punt de muntatge i nom de visualització + Autentificació + Nom d\'usuari/ària + Contrasenya + Nom d\'usuari (opcional) + Contrasenya (opcional) + Afegeix un muntatge + No hi ha cap servei WebDAV en aquest URL + Elimina el punt de muntatge + Es perdran els detalls de la connexió, però no se suprimiran els fitxers. + S\'està accedint al fitxer WebDAV + S\'està baixant el fitxer WebDAV + S\'està pujant el fitxer WebDAV + Muntatge WebDAV + + Permisos del DAVx⁵ + Es requereixen permisos addicionals + %s és massa antiga + Versió mínima requerida: %1$s + L\'autenticació ha fallat (verifiqueu les credencials d\'inici de sessió) + Error de xarxa o d\'E/S: %s + Error del servidor HTTP: %s + Error d\'emmagatzematge local: %s + Error de programari (s\'ha arribat al màxim de reintents) + S\'ha rebut un contacte no vàlid del servidor + S\'ha rebut un esdeveniment no vàlid del servidor + S\'ha rebut una tasca no vàlida del servidor + S\'ha ignorat un o més recursos no vàlids + Sincronització pendent + Les dades remotes han canviat + + Sincronitza-ho tot + Sincronitza tots els comptes + Botó etiquetat de sincronització + Botó d\'icona de sincronització + Toqueu per a executar manualment la sincronització. + + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml new file mode 100644 index 0000000..fff0324 --- /dev/null +++ b/app/src/main/res/values-cs/strings.xml @@ -0,0 +1,414 @@ + + + + Účet (už) neexistuje + DAVx⁵ adresář kontaktů + Odstranit + Odebrat + Storno + Zapnout + Tuto kolonku je třeba vyplnit + Pomoc + Navigovat nahoru + Menu možností + Sdílet + Synchronizace spuštěna/přidána do fronty + Databáze rozbitá + Všechny účty byly lokálně odebrány. + Ladění + Ostatní důležité zprávy + Stavové zprávy nízké priority + Synchronizace + Chyby synchronizace + Důležité chyby, které zastaví synchronizaci, jako např. neočekávané odpovědi ze serveru + Varování ohledně synchronizace + Nefatální problémy při synchronizaci jako například některé neplatné soubory + Chyby sítě a vstupu/výstupu + Překročení časových limitů, problémy se spojením, atd. (často dočasné) + + Vaše data. Vaše volba. + Mějte pod svou kontrolou. + Synchronizace v pravidelných intervalech + Pokud chcete synchronizaci v pravidelných intervalech, je třeba %s povolit být spuštěné na pozadí. V opačném případě může systém Android synchronizaci kdykoli pozastavit. + Nepotřebuji synchronizaci v pravidelných intervalech.* + Kompatibilita %s + Potřebná nastavení provedena, už nepřipomínat.* + * Pokud chcete připomenout později, nezaškrtávejte. Je možné resetovat v nastavení aplikace / %s. + Další informace + jtx Board + + Podpora úkolů + Pokud jsou úkoly podporovány vámi využívaným serverem, je možné je synchronizovat pomocí podporované aplikace pro úkoly: + OpenTasks + Nezdá se už být vyvíjeno – nedoporučeno. + Tasks.org + Není k dispozici žádný obchod s aplikacemi + Nepotřebuji podporu pro úkoly.* + Opensource software + Jsme rádi, že %s používáte. Jde o opensource software. Vývoj, údržba a podpora je ale těžká práce. Prosím zvažte zapojení se (je mnoho způsobů jak) nebo podpoření vývoje darem. Bude to velmi oceněno! + Jak se zapojit / podpořit vývoj darem + Další + + Oprávnění + Aby fungovalo správně, %s vyžaduje oprávnění. + Vše níže uvedené + Pomocí tohoto zapněte všechny funkce (doporučeno) + Všechna oprávnění udělena + Oprávnění pro přístup ke kontaktům + Bez synchronizace kontaktů (nedoporučeno) + Synchronizace kontaktů možná + Oprávnění pro přístup ke kalendáři + Bez synchronizace kalendáře (nedoporučeno) + Synchronizace kalendáře možná + Oprávnění oznámení + Upozornění vypnuta (nedoporučeno) + Upozornění zapnuta + Oprávnění pro jtx Board + Oprávnění pro OpenTasks + Oprávnění pro úkoly + Žádné úlohy ohledně synchronizace + Synchronizace úkolů možná + Ponechat oprávnění + Oprávnění mohou být automaticky resetována (nedoporučeno) + Oprávnění nebudou resetována automaticky + Klikněte na Oprávnění > zrušte zaškrtnutí u „Odebrat oprávnění pokud aplikace dlouhodobě není používána“ + Pokud přepínač nefunguje, použijte nastavení aplikací / Oprávnění + Nastavení aplikace + + Oprávnění k přístupu k názvům (SSID) WiFi sítí + Aby bylo možné přistupovat k názvu (SSID) WiFi sítě, ke které jste připojení, je třeba, aby byly splněny tyto podmínky: + Oprávnění pro přístup k přesné poloze + Oprávnění pro přístup k poloze uděleno + Oprávnění pro přístup k poloze odepřeno + Oprávnění pro přístup k poloze na pozadí + Povolit napořád + Stav oprávnění polohy je: %s + Stav oprávnění polohy není: %s + Určování polohy vždy zapnuté + Služba určování polohy je zapnutá + Služba určování polohy je vypnutá + + Překlady + Knihovny + Verze %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) a přispěvatelé + Na tento program nejsou poskytovány ŽÁDNÉ ZÁRUKY. Jedná se o svobodný software a jeho šíření dál je vítáno, ovšem za podmínky, že stejné svobody zůstanou zachovány i všem dalším příjemcům. + + Nedaří se vytvořit soubor pro záznam událostí + Nyní je zaznamenáváno všech %s aktivit + Zobrazit/sdílet + Vypnout + + Adaptér synchronizace CalDAV/CardDAV + O aplikaci / Licence + Zpětná vazba k vývojové verzi + Nainstalujte si webový prohlížeč + Nastavení + Novinky a aktualizace + Nástroje + Externí odkazy + Webová stránka + Příručka + FAQ + Komunita + Podpořit projekt + Jak přispět + Ochrana soukromí + Synchronizovat všechny účty + + Upozornění vypnuta. Nebudete upozorněni na chyby při synchronizaci. + Spravovat spojení + Spořič dat povolen. Synchronizace na pozadí je omezena. + Spravovat spořič dat + Spořič baterie je povolen. Synchronizace může být omezena. + Spravovat spořič baterie + Málo volného místa v úložišti. Android nebude synchronizovat místní změny okamžitě, ale při další běžné synchronizaci. + Spravovat úložiště + + Vyhledání služby se nezdařilo + Nedaří se znovu načíst seznam sady + + Spuštěné v popředí + Na některých zařízeních je toto nezbytné aby fungovala automatická synchronizace. + + Nastavení + Ladění + Zobrazit ladící informace + Podrobnější zaznamenávání událostí + Zaznamenávání událostí je vypnuté + Optimalizace akumulátoru + Aplikace je vyjmuta (doporučeno) + Omezení baterie platí (nedoporučeno) + Připojení + Typ proxy + + Systémová výchozí + Bez proxy + HTTP + SOCKS (pro Orbot) + + Hostitel proxy + Port proxy + Zabezpečení + Oprávnění aplikace + Zkontrolujte oprávnění potřebná pro synchronizaci + Nedůvěřovat systémovým certifikátům + Systémovým a uživatelem přidaným CA nebude důvěřováno + Systémovým a uživatelem přidaným CA bude důvěřováno (doporučeno) + Resetovat (ne)důvěryhodné certifikáty + Resetovat důvěryhodnost všech uživatelsky určených certifikátů + Všechny vlastní certifikáty byly resetovány + Uživatelské prostředí + Nastavení oznamování + Spravovat kanály oznamování a jejich nastavení + Vybrat vzhled + + Výchozí systémový + Světlý + Tmavý + + Znovu zobrazovat rady + Znovu zobrazí rady, které jste už označili jako přečtené + Všechny rady budou znovu zobrazeny + Napojení + Aplikace pro úkoly + Nenalezena žádná kompatibilní aplikace pro úkoly + + CardDAV + CalDAV + Webcal + Jsou nezbytná další oprávnění pro synchronizaci těchto sad. + Spravovat oprávnění + Synchronizovat nyní + Nastavení účtu + Přejmenovat účet + Neuložená místní data mohou být ztracena. Po přejmenování je vyžadována nová synchronizace. + Název nového účtu + Přejmenovat + Tento název účtu už je používán někým jiným + Účet se nedaří přejmenovat + Smazat účet + Opravdu smazat účet? + Všechny místní kopie adresáře, kalendářů a úkolů budou smazány. + synchronizovat tuto sadu + pouze pro čtení + kalendář + žurnál + Zobrazit pouze osobní + Obnovit seznam + Odběry webcal mohou být synchronizovány externími aplikacemi. + Nenalezena žádná aplikace, která by umožňovala webcal + Nainstalovat ICSx⁵ + + Přidat účet + Obecné přihlášení + Přihlášení specifické pro poskytovatele + Pokračovat + Přihlášení + Přihlášení se e-mailovou adresou + E-mailová adresa + Vyžadován platný e-mail + Služby jsou objevovány pomocí záznamů DNS a dobře známých URL.]]> + Heslo + Skrýt heslo + Zobrazit heslo + Přihlášení s URL a uživatelským jménem + Uživatelské jméno + Základ URL + Vybrat certifikát + Přidat účet + Název účtu + Používání apostrofů (\') může způsobit problémy na některých zařízeních. + Pro jméno účtu použijte svou e-mailovou adresu, protože Android bude brát jméno účtu jako údaj pro ORGANIZÁTORA vytvořených událostí. Nelze mít dva účty stejného jména. + Metoda seskupování kontaktů: + Je třeba zadat název pro účet + Tento název účtu už je používán někým jiným + Pokročilé přihlášení + Certifikát klienta: %s + Nenalezen žádný certifikát + Instalovat certifikát + Google kontakty/kalendář + Google účet + Přihlásit se Googlem + Identif. klienta (volitelné) + Zásady ochrany osobních údajů.]]> + Zásadám dat služeb Google API, zahrnují Požadavky omezeného použití.]]> + Nelze získat autorizační kód + Nextcloud + Přihlásit pomocí Nextcloud + Toto spustí Nextcloud přihlášení ve webovém prohlížeči. + Adresa Nextcloud serveru + Přihlásit se + Nelze získat přihlašovací URL + Nelze získat přihlašovací data + Zjišťování nastavení + Chvíli strpení, probíhá dotazování serveru… + Nedaří se nalézt službu CalDAV nebo CardDAV. + našeho seznamu testovaných služeb a jejich základní URL.]]> + Prosíme též překontrolujte přihlašovací údaje (obvykle jméno a heslo). + Další technické informace jsou k dispozici v logu. + Zobrazit záznamy událostí + + Synchronizace + Interval synchronizace kontaktů + Pouze ručně + Každých %d minut a ihned při lokálních změnách + Interval synchronizace kalendáře + Interval synchronizace úkolů + + Pouze ručně + Každých 15 minut + Každých 30 minut + Každou hodinu + Každé 2 hodiny + Každé 4 hodiny + Jednou za den + + Synchronizovat pouze přes WiFi + Synchronizace omezena na WiFi připojení + Druh připojení není brán v potaz + Omezení na názvy (SSID) WiFi sítí + Bude synchronizovat pouze přes %s + Bude použito libovolné WiFi připojení + Čárkou oddělovaný seznam názvů (SSID) WiFi sítí, přes které synchronizovat (pokud omezovat nechcete, nevyplňujte) + Omezení na názvy (SSID) WiFi sítí vyžaduje další nastavení + Spravovat + VPN vyžaduje nadřazené internetové připojení + VPN bez ověřeného internetového připojení není dostatečné pro synchronizaci (doporučeno) + VPN bez ověřeného internetového připojení je dostatečné pro synchronizaci + Ověření + Uživatelské jméno + Nové heslo + Aktualizovat heslo dle svého serveru. + Certifikát klienta + Certifikát není vybrán nebo k dispozici + Instalovat certifikát + CalDAV + Časový limit pro staré události + Synchronizovat všechny události + + Ignorovat události starší než 1 den + Ignorovat události starší než %d dny + Ignorovat události starší než %d dnů + Ignorovat události starší než %d dny + + Události z minulosti starší, než vyznačený počet dnů, budou ignorovány (lze zadat 0). Pokud chcete synchronizovat všechny události, nevyplňujte. + Výchozí připomínka + + Výchozí připomínka jednu minutu před událostí + Výchozí připomínka %d minuty před událostí + Výchozí připomínka %d minut před událostí + Výchozí připomínka %d minuty před událostí + + Nejsou vytvořené žádné výchozí připomínky + Pokud mají být pro události bez připomínky vytvořeny výchozí připomínky: požadovaný počet minut před událostí. Pokud výchozí připomínky nechcete, nevyplňujte. + Spravovat barvy kalendářů + Barvy kalendáře jsou při každé synchronizaci resetovány + Barvy kalendáře je možné nastavovat ostatními aplikacemi + Podpora pro barvy událostí + Barvy událostí jsou synchronizovány + Barvy událostí nejsou synchronizovány + CardDAV + Metoda seskupování kontaktů + + Skupiny jsou zvlášt vCards vizitky + Skupiny jsou kategorie u jednotlivých kontaktů + + + Vytvořit adresář + Vytváření adresář kontaků přes CardDAV nemusí být serverem podporováno. + Vytvořit kalendář + + Možné položky kalendáře + Události + Úkoly + Poznámky / deník + Vytváření kalendáře přes CalCAV nemusí být serverem podporováno. + Barva + Nadpis + Umístění úložiště + Popis (volitelný) + Vytvořit + + Smazat sadu + Tato kolekce (%s) a její data budou navždy odstraněna, lokálně i na serveru. + Synchronizace + Synchronizace povolena + Synchronizace zakázána + Pouze pro čtení + Jen pro čtení (od serveru) + Jen pro čtení (jen lokálně) + Čtení/zápis + Nadpis + Popis + Vlastník + Podpora Push + Server propaguje podporu Push + Poslední synchronizace (%s) + Adresa (URL) + + Ladící informace + ZIP archiv + Obsahuje ladící informace a záznamy událostí + Nasdílet archiv pro účely přenosu do počítače, ze kterého odeslat e-mailem nebo přiložit k žádosti o technickou podporu. + Nasdílet archiv + Ladící informace připojené k této správě (vyžaduje podporu pro přílohy na straně přijímající aplikace). + Chyba HTTP + Chyba serveru + Chyba WebDAV + Chyba vstupu/výstupu + Zobrazit podrobnosti + Ladící informace byly shromážděny + Prostředky, kterých se týká + Související s problémem + Prostředek na protějšku: + Místní prostředek: + Záznamy událostí + Jsou k dispozici podrobnější záznamy událostí + Zobrazit záznamy událostí + Zkopírovat URL adresu + + Došlo k chybě. + Došlo k HTTP chybě. + Došlo k chybě vstupu/výstupu. + Zobrazit podrobnosti + + WebDAV připojení + Využitá kvóta: %1$s / k dispozici: %2$s + Obsah sdílení + Odpojit + Přidat WebDAV připojení + Přímo přistupujte ke svým souborům v cloudu přidáním WebDAV připojení! + Zobrazovaný název + WebDAV URL + Neplatná URL + Ověření + Uživatelské jméno + Heslo + Přidat připojení (mount) + Na této URL se nenachází žádná WebDAV služba + Odebrat přípojný bod + Podrobnosti o spojení budou ztraceny, ale soubory nebudou smazány. + Přistupuje se k WebDAV souboru + Stahuje se WebDAV soubor + Nahrává se WebDAV soubor + WebDAV připojení + + DAVx⁵ oprávnění + Vyžadována dodatečná oprávnění + Příliš stará verze %s + Nejnižší požadovaná verze: %1$s + Ověření se nezdařilo (zkontrolujte přihlašovací údaje) + Chyba sítě nebo vstupu/výstupu – %s + Chyba HTTP serveru – %s + Chyba místního úložiště – %s + Lehká chyba (maximální počet pokusů dosažen) + Ze serveru obdržen neplaný kontakt + Ze serveru obdržena neplatná událost + Ze serveru obdržen neplatný úkol + Ignoruje se jeden či více neplatný prostředků + + Synchronizovat vše + Synchronizovat všechny účty + + diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml new file mode 100644 index 0000000..8f94e28 --- /dev/null +++ b/app/src/main/res/values-da/strings.xml @@ -0,0 +1,416 @@ + + + + Konto findes ikke (længere) + DAVx⁵ adressebog + Foretag ikke ændringer i din konto her! Brug i stedet app\'en til direkte at håndtere konti. + Slet + Fjern + Annullér + Aktivér + Feltet er påkrævet + Hjælp + Navigér opad + Indstillinger + Del + Synkroniseringen er startet/sat i kø + Databasen er korrupt + Alle konti er fjernet lokalt. + Fejlfinding + Andre vigtige beskeder + Lav-prioritet statusbeskeder + Synkronisering + Synkroniseringsfejl + Vigtige fejl, såsom uventede serversvar, der stopper synkroniseringen + Synkroniseringsadvarsler + Ikke-kritiske synkroniseringproblemer såsom ugyldige filer + Netværks- og I/O-fejl + Timeouts, forbindelsesproblemer m.m. (ofte midlertidig) + + Dine data. Dit valg. + Tag kontrol + Regelmæssige synkroniseringsintervaller + For regelmæssige synkroniseringsintervaller, skal %s have tilladelse til at køre i baggrunden. Ellers kan Android til enhver tid pause synkronisering. + Jeg behøver ikke regelmæssige synkroniseringsintervaller.* + %s kompatibilitet + Jeg har udført de krævede indstillinger. Mind mig ikke om det mere. * + * Efterlad åben for senere påmindelser. Kan nulstilles i programindstillinger / %s. + Mere information + jtx Board + + Understøttelse af opgaver + Hvis din server understøtter opgaver, kan de blive synkroniseret med en understøttet opgave-app: + OpenTasks + Ser ikke ud til at blive udviklet længere - ikke anbefalet. + Tasks.org + Ingen app-store tilgængelig + Jeg behøver ikke opgaveunderstøttelse.* + Åben kilde program + Vi er glade for, at du bruger %s, som er åben-kilde software. Udvikling, vedligeholdelse og support er hårdt arbejde. Overvej at bidrage (der er mange måner) eller donere. Det ville være meget værdsat! + Sådan bidrager/donerer du + Næste + + Tilladelser + %s kræver tilladelser for at virke rigtig. + Alle de nedenstående + Brug denne for at aktivere alle funktioner (anbefales) + Alle tilladelser er givet + Tilladelser til kontakter + Ingen kontaktsynkronisering (anbefales ikke) + Kontaktsynkronisering mulig + Tilladelser til kalender + Ingen kalendersynkronisering (anbefales ikke) + Kalendersynkronisering mulig + Tilladelse til notifikationer + Notifikationer deaktiveret (anbefales ikke) + Notifikationer slået til + Tilladelser til jtx Board + Tilladelser til OpenTasks + Tilladelser til opgaver + Ingen opgavesynkronisering + Opgavesynkronisering mulig + Behold tilladelser + Tilladelser kan blive nulstillet automatisk (anbefales ikke) + Tilladelser vil ikke blive nulstillet automatisk + Tryk på tilladelser > fravælg \"Fjern tilladelser hvis program ikke bruges\" + Hvis det ikke virker at skifte, brug app-indstillinger / tilladelser. + App-indstillinger + + Tilladelser til WiFi SSID + For at tilgå nuværende trådløs navn (SSID), skal følgende betingelser være opfyldt: + Præcis placerings tilladelse + Placering rettighed tildelt + Placering rettighed afvist + Baggrund placerings-rettighed + Tillad altid + Lokaliseringstilladelse sat til: %s + Lokationstilladelse ikke sat til: %s + Placering altid aktiveret + Placering tjeneste er aktiveret + Placering tjeneste er deaktiveret + + Oversættelser + Biblioteker + Version %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) og bidragydere + Dette program leveres ABSOLUT UDEN GARANTI. Det er fri software, og du er velkommen til at videredistribuere det under visse betingelse. + + Kan ikke oprette log fil + Logger alle %s aktiviteter + Vis/del + Deaktivere + + CalDAV/CardDAV synkroniseringsadapter + Om / licens + Beta tilbagemelding + Installere netlæser + Opsætning + Nyheder & opdateringer + Værktøj + Eksterne henvisninger + Netsted + Manual + OSS + Fællesskab + Støt projektet + Sådan bidrager du + Privatlivs politik + Synkroniser alle konti + + Notifikationer slået fra. Du vil ikke blive gjort opmærksom på synkroniserings-fejl. + Aministrer forbindelser + Gemning af data slået til. Synkronisering i baggrunden er begrænset. + Administrer gemning af data + Batteribesparelse aktiveret. Synkronisering kan være forhindret. + Administrer batteribesparelse + 124 +Lav lagerplads. Android vil ikke synkronisere lokale ændringer med det samme, men under den næste almindelige synkronisering. + Administrer lagerplads + + Registrering af tjeneste kunne ikke foretages + Kunne ikke opdatere samling liste + + Kører i forgrund + På nogle enheder er dette nødvendigt for automatisk synkronisering. + + Indstillinger + Fejlsøgning + Vis fejlsøgnings information + Uddybende logning + Logning er deaktiveret + Batteri optimering + Appen er undtaget (anbefalet) + Batteribesparelse aktiv (anbefales ikke) + Forbindelse + Proxy type + + System standard + Ingen proxy + HTTP + SOCKS (til Orbot) + + Proxy værtsnavn + Proxy port + Sikkerhed + Program tilladelser + Gennemgå tilladelser krævet til synkronisering + Stol ikke på systemcertifikater + System og brugertilføjede CA\'er vil ikke blive betroet + System og brugertilføjede CA\'er vil blive betroet (anbefalet) + Nulstil betroede / ikke betroede certifikater + Nulstiller tilliden til brugerdefinerede certifikater + Alle brugerdefinerede certifikater er blevet rydet + Brugerflade + Notifikations indstillinger + Håndtér notifikationskanaler og deres opsætning + Vælg tema + + System standard + Lys + Mørk + + Nulstil vejledende pop op + Genaktivere tidligere lukket pop op + Al vejledning vil blive vist igen + Integration + Opgaver program + Der er ikke fundet et program der kan håndtere opgaver + + CardDAV + CalDAV + Webcal + Der kræves yderligere tilladelser for at synkronisere disse datasamlinger. + Administrer rettigheder + Synkronisere + Opsætning af konti + Omdøb konto + Ikke gemt lokal data kan blive afvist. Gen-synkronisering er påkrævet efter omdøbning. + Nyt kontonavn + Omdøbe + Konto navn er allerede i brug + Kunne ikke omdøbe konto + Slet konto + Slet konto? + Alle lokale kopier af addessebøger, kalendere og opgavelister vil blive slettet. + synkronisere samling + skrivebeskyttet + kalender + Kontakter + journal + Opgaver + Vis kun personlig + Opdater listen + Webcal-abonnementer kan synkroniseres med eksterne apps. + Der er ikke fundet noget program der kan håndtere Webcal. + Installere ICSx⁵ + + Tilføj konto + Generisk login + Provider-specifikt login + Fortsæt + Log ind + Log ind med e-mail adresse + E-mail adresse + Gyldig e-mail adresse påkrævet + Tjenester findes ved hjælp af DNS-records og velkendte URL\'er]]>. + Adgangskode + Skjul password + Vis kodeord + Log ind med URL og brugernavn + Brugernavn + Basis URL + tjenester opdages også ved hjælp af DNS-records og velkendte URL\'er]]>. + Vælge certifikat + Tilføj konto + Kontonavn + Brug af apostroffer (\') ser ud til at give problemer på nogle enheder. + Brug en e-mail adresse som kontonavn da Android bruger kontonavn til ORGANIZER-felt for oprettede aktiviteter. Man kan ikke have to konti med samme navn. + Gruppering af kontakter: + Kontonavn påkrævet + Konto navn er allerede i brug + Avanceret login + Klientcertifikat: %s + Intet certifikat fundet + Installere certifikat + Google Kontakter / Kalender + Google konto + Log ind med Google + Klient ID (valgfrit) + Privacy policy for detaljer.]]> + Google API Services User Data Policy, herunder kravene til begrænset brug.]]> + Kunne ikke hente autorisationskode + Nextcloud + Log ind med Nextcloud + Starter Nextcloud Login Flow i en webbrowser. + Nextcloud serveradresse + Log ind + Kunne ikke hente login-URL + Kunne ikke hente login-data + Check konfiguration + Vent, forespørger serveren… + Kunne ikke finde CalDAV- eller CardDAV-tjeneste. + Basis-URL\'en ser ikke ud til at være en tilgængelig CalDAV/CardDAV-URL, og tjenestegenkendelsen lykkedes ikke. + vores liste af testede tjenester og deres basis-URL\'er.]]> + Dobbelttjek også autentificeringen (oftest brugernavn og adgangskode). + Yderligere tekniske oplysninger findes i logfilerne. + Vis logfiler + + Synkronisering + Synkroniseringsinterval for kontakter + Kun manuelt + Hver %d minutter + øjeblikkeligt ved lokale ændringer + Synkroniseringsinterval for kalender + Synkroniseringsinterval for opgaver + + Kun manuelt + Hvert 15. minut + Hver halve time + Hver time + Hver 2. time + Hver 4. time + En gang om dagen + + Synkroniser kun over WiFi + Synkronisering er begrænset til WiFi-forbindelser + Forbindelsestypen har ingen betydning + WiFi SSID-begrænsning + Synkroniserer kun over %s + Alle WiFi-forbindelser vil blive anvendt + Kommaseparerede navne (SSID\'er) over tilladte WiFi-netværk (efterlad blank for at bruge alle) + Trådløs SSID begrænsning kræver yderligere opsætning + Håndtere + VPN kræver underliggende internet + VPN uden underliggende valideret internetforbindelse er ikke nok til at køre synkronisering (anbefales) + VPN uden underliggende valideret internetforbindelse er nok til at køre synkronisering + Adgangsgodkendelse + Brugernavn + Ny adgangskode + Opdater adgangskoden, så den svarer til din server. + Klientcertifikat + Intet certifikat tilgængeligt eller udvalgt + Installere certifikat + CalDAV + Tidsafgrænsning for tidligere begivenheder + Alle begivenheder vil blive synkroniseret + + Begivenheder ældre end en dag vil blive ignoreret + Begivenheder, der er mere end %d dage gamle, vil blive ignoreret + + Begivenheder, som er mere end dette antal dage gamle vil blive ignoreret (kan også være 0). Hvis feltet ikke er udfyldt, vil alle begivenheder blive synkroniseret. + Standard påmindelse + + Standard påmindelse 1 minut før hændelse + Standard påmindelse %d minutter før hændelse + + Ingen standard påmindelse oprettet + Hvis standard påmindelse skal oprettes for hændelse uden påmindelse: antal minutter før hændelse. Efterlad tom for at deaktivere standard påmindelse. + Administrer farver for kalender + Kalender farver nulstilles ved hver synkronisering + Kalender farver kan sættes fra andre programmer + Farver for begivenheder + Farver for begivenheder er synkroniseret + Farver for begivenheder er ikke synkroniseret + CardDAV + Gruppering af kontakter + + Grupper er særskilte vCards + Grupper er kategorier per kontakt + + + Opret adressebog + Oprettelse af adressebøger via CardDAV understøttes muligvis ikke af serveren. + Opret kalender + + Mulige kalenderposte + Begivenheder + Opgaver + Notater / journal + Kalenderoprettelse via CalDAV understøttes muligvis ikke af serveren. + Farve + Titel + Lager placering + Beskrivelse (valgfrit) + Opret + + Kontakter + Opgaver + Slet sæt + Denne samling (%s) og al dens data vil blive permanent fjernet, både lokalt og på serveren. + Synkronisering + Synkronisering aktiveret + Synkronisering deaktiveret + Skrivebeskyttet + Skrivebeskyttet (af server) + Skrivebeskyttet (kun lokalt) + Læs/skriv + Titel + Beskrivelse + Ejer + Sidste synkronisering (%s) + Adresse (URL) + + Debug-info + ZIP arkiv + Indeholder fejlsøgnings information og log + Del arkivet for at overføre det til en computer, sende det via e-mail eller vedhæfte til en support sag. + Del arkiv + Fejlsøge informationer vedhæftet denne meddelelse (kræver vedhæftning understøttelse hos modtagende program). + HTTP fejl + Server fejl + WebDAV fejl + Ind/ud fejl + Vis detaljer + Fejlsøgnings information er indsamlet + Involveret ressourcer + Relateret til problemet + Fjern ressource: + Lokal ressource: + Log + Uddybende log er tilgængelig + Vis logfiler + Kopiere URL + + Der er opstået en fejl. + Der er opstået en HTTP-fejl. + Der er opstået en I/O-fejl. + Vis detaljer + + WebDAV monteringspunkter + Mængde brugt: %1$s / tilgængelig: %2$s + Del indhold + Afmontere + Tilføj WebDAV monteringspunkt + Tilføj en WebDAV mount for at tilgå dine filer i skyen + Vis navn + WebDAV URL + Ugyldig URL + Adgangsgodkendelse + Brugernavn + Adgangskode + Tilføj monteringspunkt + Ingen WebDAV tjeneste på URL\'en + Fjern monteringspunkt + Forbindelses informationer vil gå tabt, men ingen filer slettes. + Tilgå WebDAV fil + Hent WebDAV fil + Overfør WebDAV fil + Montere WebDAV + + DAVx⁵-rettigheder + Yderligere adgang påkrævet + %s for gammel + Påkrævet version: %1$s + Login mislykkedes (check loginoplysninger) + Netværks- eller I/O-fejl - %s + HTTP-serverfejl - %s + Lokal lagringsfejl - %s + Blød fejl (maks forsøg nået) + Modtaget ugyldig kontakt fra server + Modtaget ugyldig begivenhed fra server + Modtaget ugyldig opgave fra server + Ignorere en eller flere ugyldige kilder + + Synkroniser alle + Synkroniser alle konti + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml new file mode 100644 index 0000000..13ae645 --- /dev/null +++ b/app/src/main/res/values-de/strings.xml @@ -0,0 +1,481 @@ + + + + Konto nicht (mehr) vorhanden + DAVx⁵-Adressbuch + Konto nicht hier ändern! Um Konten zu verwalten, stattdessen direkt die App nutzen. + Löschen + Entfernen + Abbrechen + Aktivieren + Feld wird benötigt + Hilfe + Aufwärts navigieren + Auswahlmenü + Teilen + Synchronisierung gestartet/eingereiht + Datenbank beschädigt + Alle Konten wurden lokal entfernt. + Fehlersuche + Andere wichtige Mitteilungen + Weniger wichtige Statusmitteilungen + Synchronisierung + Synchronisierungsfehler + Fehler, die zum Abbruch der Synchronisierung führen, wie z.B. unerwartete Serverantworten + Synchronisierungswarnungen + Nicht fatale Synchronisierungsprobleme wie bestimmte ungültige Dateien + Netzwerk- und E/A-Fehler + Zeitüberschreitungen, Verbindungsprobleme, usw. (oft vorübergehend) + + Dein Leben. Deine Daten. + Deine Entscheidung. + Regelmäßige Sync-Intervalle + Zur Synchronisierung in regelmäßigen Intervallen muss %s im Hintergrund laufen dürfen; ansonsten kann Android die Synchronisierung jederzeit aussetzen. + Ich brauche keine regelmäßigen Sync-Intervalle.* + %s-Kompatibilität + Herstellerspezifische Firmware blockiert möglicherweise die Synchronisierung. Wenn Sie davon betroffen sind, können Sie dies nur manuell beheben. + Ich habe die Einstellungen gemacht, nicht mehr erinnern.* + * Nicht anwählen, um später erinnert zu werden. Kann unter App-Einstellungen / %s zurückgesetzt werden. + Mehr Infos + jtx Board + + Unterstützung für Aufgaben + Falls der Server Aufgaben unterstützt, können sie mit einer unterstützten App synchronisiert werden: + OpenTasks + Wird anscheinend nicht weiterentwickelt – nicht empfohlen. + Tasks.org + werden nicht unterstützt.]]> + Kein App-Store verfügbar + Ich brauche keine Unterstützung für Aufgaben.* + Open-Source-Software + Wir freuen uns, dass Sie die Open-Source-Software %s verwenden. Entwicklung, Wartung und Support sind viel Arbeit. Ziehen Sie daher bitte in Betracht, mitzuhelfen (dazu gibt es viele Möglichkeiten) oder zu spenden. Vielen Dank! + Infos zum Mithelfen/Spenden + Nicht daran erinnern für + + %d Monat + %d Monate + + Weiter + + Berechtigungsverwaltung + %s benötigt Berechtigungen, um ordnungsgemäß zu funktionieren. + Alles darunter + Hiermit können alle Funktionen aktiviert werden (empfohlen) + Alle Berechtigungen gewährt + Kontakte-Berechtigungen + Keine Kontakte-Synchronisierung (nicht empfohlen) + Kontakte-Synchronisierung möglich + Kalender-Berechtigungen + Keine Kalender-Synchronisierung (nicht empfohlen) + Kalender-Synchronisierung möglich + Benachrichtigungsberechtigung + Benachrichtigungen deaktiviert (nicht empfohlen) + Benachrichtigungen aktiviert + jtx Board-Berechtigungen + OpenTasks-Berechtigungen + Tasks-Berechtigungen + Keine Aufgaben-Synchronisierung + Aufgaben-Synchronisierung möglich + Berechtigungen behalten + Berechtigungen können automatisch entzogen werden (nicht empfohlen) + Berechtigungen werden nicht automatisch entzogen + Berechtigungen > \"Berechtigungen entfernen, wenn die App nicht verwendet wird\" abwählen + Wenn ein Schalter nicht funktioniert, App-Einstellungen / Berechtigungen verwenden. + App-Einstellungen + + WLAN-SSID-Berechtigungen + Um auf den aktuellen WLAN-Namen (SSID) zugreifen zu können, müssen folgende Bedingungen erfüllt werden: + Exakter Standort-Berechtigung + Standort-Zugriff erlaubt + Standort-Zugriff verweigert + Hintergrund-Standort-Berechtigung + Immer zulassen + Standort-Zugriff eingestellt auf: %s + Standort-Zugriff nicht eingestellt auf: %s + %s benutzt Standortdaten (nur WLAN-SSID) ausschließlich, um die Synchronisierung auf ein bestimmtes WLAN zu beschränken. Dies geschieht auch dann, wenn die Synchronisierung im Hintergrund ausgeführt wird. + Alle Standortdaten (nur WLAN-SSID) werden nur lokal verwendet und nicht an Dritte weitergegeben. + Standort-Dienst immer aktiviert + Standort-Dienst aktiv + Standort-Dienst inaktiv + + Übersetzungen + Bibliotheken + Version %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) und Mitwirkende + Dieses Programm wird OHNE JEDE GEWÄHRLEISTUNG bereitgestellt. Es ist freie Software – Sie können es also unter bestimmten Bedingungen weiterverbreiten. + + Protokolldatei konnte nicht angelegt werden + Alle %s-Aktivitäten werden protokolliert + Anzeigen/teilen + Deaktivieren + + CalDAV/CardDAV-Sync-Adapter + Über / Lizenz + Beta-Rückmeldung + Bitte installieren Sie einen Web-Browser + Einstellungen + Aktuelles + Werkzeuge + Externe Links + Homepage + Handbuch + FAQ + Für Organisationen + Community + Projekt unterstützen + Einen Beitrag leisten + Datenschutzerklärung + Willkommen bei DAVx⁵! + Verbinden Sie sich mit Ihrem Server und synchronisieren Sie Ihre Kalender und Kontakte. + Alle Konten synchronisieren + + Benachrichtigungen deaktiviert. Sie werden nicht über Fehler bei der Synchronisierung informiert. + Automatische Synchronisation nicht aktiv (keine überprüfte Internetverbindung). + Verbindungen steuern + Datensparen aktiviert. Hintergrundsynchronisierung ist eingeschränkt. + Datensparen verwalten + Energiesparmodus aktiviert. Synchronisierung kann eingeschränkt sein. + Energieeinstellungen verwalten + Wenig Speicherplatz. Android wird lokale Änderungen nicht sofort synchronisieren, sondern bei der nächsten regulären Synchronisierung. + Speicherplatz verwalten + Kalender-Provider fehlt + Haben Sie die »Kalender«-System-App deaktiviert? + Kontakte-Provider fehlt + Haben Sie die »Kontakte«-System-App deaktiviert? + Apps verwalten + + Diensterkennung fehlgeschlagen + Ordnerliste konnte nicht aktualisiert werden + + Läuft im Vordergrund + Auf manchen Geräten für die automatische Synchronisierung benötigt + + Einstellungen + Fehlersuche + Informationen zur Fehlersuche + Einstellungsdetails und Logs anzeigen/teilen + Ausführliche Protokollierung + Logging ist aktiv. Sie können die Logs in den Debuginformationen anzeigen. + Keine Protokollierung + Akku-Optimierung + App ist ausgenommen (empfohlen) + Akku-Einschränkungen gelten (nicht empfohlen) + Verbindung + Proxy-Typ + + System-Standard + Kein Proxy + HTTP + SOCKS (für Orbot) + + Proxy-Rechnername + Proxy-Port + Sicherheit + App-Berechtigungen + Für die Synchronisierung benötigte Berechtigungen prüfen + Systemzertifikaten nicht vertrauen + System- und installierten CAs wird nicht vertraut + System- und installierten CAs wird vertraut (empfohlen) + Wenn diese Einstellung aktiv ist, werden Systemzertifikate als nicht vertrauenswürdig erachtet. Das bedeutet, dass Sie jedes Zertifikat (auch wenn der Server sein Zertifikat auffrischt) von Hand akzeptieren müssen; sonst werden Kontoeinrichtung und Synchronisierung nicht funktionieren. + Zertifikat-Vertrauen zurücksetzen + Setzt angenommene/abgelehnte Zertifikate zurück + Angenommene/abgelehnte Zertifikate zurückgesetzt + Oberfläche + Benachrichtigungseinstellungen + Benachrichtigungskanäle und -einstellungen verwalten + Aussehen wählen + + wie System + heller Stil + dunkler Stil + + Hinweise zurücksetzen + Hinweise, die deaktiviert wurden, wieder anzeigen + Alle Hinweise werden wieder angezeigt + Integration + Aufgaben-App + Keine kompatible Aufgaben-App gefunden + UnifiedPush (experimentell) + Keiner (Push deaktivieren) + Anbieter auswählen + Kein Push-Anbieter installiert + Kein Endpunkt konfiguriert + Bereit, Push-Mitteilungen über %s zu empfangen + FCM (Google Play) + Push-Nachrichten sind immer verschlüsselt. + + Konto wurde entfernt + CardDAV + CalDAV + Webcal + Für die Synchronisierung dieser Ordner sind zusätzliche Berechtigungen erforderlich. + Berechtigungen verwalten + Jetzt synchronisieren + Konto-Einstellungen + Konto umbenennen + Nicht gespeicherte lokale Daten können verloren gehen. Nach dem Umbenennen ist eine erneute Synchronisierung erforderlich. + Neuer Kontoname + Umbenennen + Kontoname bereits verwendet + Konto konnte nicht umbenannt werden + Konto löschen + Konto wirklich löschen? + Alle Adressbücher, Kalender und Aufgabenlisten werden vom Gerät (nicht am Server) gelöscht. + Diesen Ordner synchronisieren + schreibgeschützt + Kalender + Kontakte + Journal + Aufgaben + Nur eigene anzeigen + Liste aktualisieren + Webcal-Abonnements können mit externen Apps synchronisiert werden. + Keine Webcal-App gefunden + ICSx⁵ installieren + + Konto hinzufügen + Datenschutzbestimmungen.]]> + Allgemeine Anmeldung + Provider-spezifische Anmeldung + Fortfahren + Anmelden + Mit E-Mail-Adresse anmelden + E-Mail-Adresse + Gültige E-Mail-Adresse benötigt + Dienst-Erkennung erfolgt über DNS-Einträge und well-known-URLs.]]> + Passwort + Passwort ausblenden + Passwort anzeigen + Passwort (optional) + Mit URL und Benutzername anmelden + Benutzername + Benutzername (optional) + Basis-URL + Dienst-Erkennung über DNS-Einträge und well-known-URLs.]]> + Zertifikat auswählen + Konto hinzufügen + Kontoname + Das Verwenden von Apostrophen (\') scheint auf einigen Geräten Probleme zu verursachen. + Verwenden Sie Ihre E-Mail-Adresse als Kontonamen, da Android den Kontonamen als ORGANIZER einsetzt. Es kann allerdings keine zwei Konten mit dem gleichen Namen geben. + Kontaktgruppen-Methode: + Kontoname wird benötigt + Kontoname bereits verwendet + Konto konnte nicht hinzugefügt werden + Abschließen + Erweiterte Anmeldung + Kein Client-Zertifikat (optional) + Client-Zertifikat: %s + Kein Zertifikat gefunden + Zertifikat installieren + Fastmail + Fastmail-Konto + Mit Fastmail anmelden + Google-Kontakte / -Kalender + Google-Konto + Mit Google anmelden + Client-ID (optional) + Datenschutzrichtlinie für mehr Informationen.]]> + Google API Services Nutzerdaten-Richtlinie, inklusive der eingeschränkten Nutzungsbedingungen.]]> + Authentifizierungscode konnte nicht abgerufen werden + Nextcloud + Mit Nextcloud anmelden + Dadurch wird der Nextcloud-Anmeldevorgang in einem Webbrowser gestartet. + Nextcloud-Serveradresse + Anmeldung + Login-URL konnte nicht abgerufen werden + Anmeldedaten konnten nicht abgerufen werden + Ressourcen-Erkennung + Server wird abgefragt. Bitte warten … + Es konnte weder ein CalDAV- noch ein CardDAV-Dienst gefunden werden. + Die Basis-URL scheint keine erreichbare CalDAV/CardDAV-URL zu sein und die Diensterkennung war nicht erfolgreich. + unsere Liste der getesteten Dienste und deren Basis-URLs.]]> + Bitte überprüfen Sie auch die Authentifizierung (normalerweise Benutzername und Passwort). + Weitere technische Informationen sind in den Protokollen verfügbar. + Protokoll anzeigen + + Synchronisierung + Häufigkeit der Kontakte-Synchronisierung + Nur manuell + Alle %d Minuten + sofort bei lokalen Änderungen + Häufigkeit der Kalender-Synchronisierung + Häufigkeit der Aufgaben-Synchronisierung + + Nur manuell + Alle 15 Minuten + Alle 30 Minuten + Jede Stunde + Alle 2 Stunden + Alle 4 Stunden + Einmal am Tag + + Nur über WLAN synchronisieren + Synchronisierung nur bei aktiver WLAN-Verbindung + Verbindungstyp wird nicht beachtet + WLAN-SSID-Beschränkung + Synchronisierung nur über %s + Alle WLAN-Verbindungen werden verwendet + Erlaubte WLAN-Namen (SSIDs), mit Komma getrennt (leer lassen für alle) + WLAN-SSID-Einschränkung benötigt weitere Einstellungen + Verwalten + VPN erfordert zugrundeliegendes Internet + VPN ohne zugrundeliegende überprüfte Internetverbindung reicht für Synchronisierung nicht aus (empfohlen) + VPN ohne zugrundeliegende überprüfte Internetverbindung reicht für Synchronisierung aus + Anmeldeinformationen + Benutzername + Passwort oder App-Passwort + App-Passwort verwenden.]]> + Neues Passwort + Aktualisieren Sie Ihr Passwort gemäß den Server-Einstellungen. + Erneut authentifizieren (OAuth) + Verwenden, wenn der Zugriff widerrufen wurde + Authentifizierung erfolgreich + Client-Zertifikat + Kein Zertifikat verfügbar oder ausgewählt + Zertifikat installieren + CalDAV + Abrufbeschränkung vergangener Termine + Alle Termine werden synchronisiert + + Termine, die mehr als einen Tag in der Vergangenheit liegen, werden ignoriert + Termine, die mehr als %d Tage in der Vergangenheit liegen, werden ignoriert + + Termine, die mehr als diese Anzahl von Tagen in der Vergangenheit liegen, werden ignoriert (kann 0 sein). Feld leer lassen, um alle Termine zu synchronisieren. + Standard-Erinnerung + + Standard-Erinnerung eine Minute vor dem Ereignis + Standard-Erinnerung %d Minuten vor dem Ereignis + + Keine Standard-Erinnerungen + Wenn Standard-Erinnerungen für Termine ohne Erinnerung erzeugt werden sollen: gewünschte Anzahl der Minuten vor dem Ereignis. Leer lassen, um Standard-Erinnerungen zu deaktivieren. + Kalenderfarben verwalten + Kalenderfarben werden bei jeder Synchronisierung neu gesetzt + Kalenderfarben können von anderen Apps festgesetzt werden + Unterstützung für Terminfarben + Terminfarben werden synchronisiert + Terminfarben werden nicht synchronisiert + CardDAV + Kontaktgruppen-Methode + + Gruppen sind eigene vCards + Gruppen sind Kategorien der Kontakte + + + Adressbuch erstellen + Das Erstellen von Adressbüchern über CardDAV wird vom Server möglicherweise nicht unterstützt. + Kalender anlegen + Standard-Zeitzone (optional) + + Mögliche Kalendereinträge + Termine + Aufgaben + Notizen / Journal + Das Erstellen von Kalendern über CardDAV wird vom Server möglicherweise nicht unterstützt. + Farbe + Titel + Speicherort + Beschreibung (optional) + Erstellen + + Kontakte + Termine + Aufgaben + Ordner löschen + Dieser Ordner (%s) und alle enthaltenen Daten werden dauerhaft entfernt, sowohl lokal als auch auf dem Server. + Synchronisierung + Synchronisierung aktiviert + Synchronisierung deaktiviert + Schreibgeschützt + Schreibgeschützt (durch Server) + Schreibgeschützt (laut Richtlinie) + Schreibgeschützt (nur lokal) + Lesen/Schreiben + Titel + Beschreibung + Besitzer:in + Push-Unterstützung + Server bietet Push-Unterstützung + Um %1$s angemeldet, läuft ab %2$s + Letzte Synchronisierung (%s) + Adresse (URL) + + Informationen zur Fehlersuche + ZIP-Archiv + Beinhaltet Debug-Info und Logs + Das Archiv teilen, um es zu einem Rechner zu übertragen, per Email zu verschicken oder an ein Support-Ticket anzuhängen. + Archiv teilen + Debug-Informationen sind dieser Nachricht beigelegt (benötigt Unterstützung für Anhänge in der empfangenden App). + HTTP-Fehler + Serverfehler + WebDAV-Fehler + E/A-Fehler + Die Anfrage wurde vom Server abgelehnt. + Die angeforderte Ressource existiert nicht (mehr). + Der Server erlaubt die angeforderte Art der Operation nicht. + Es trat ein serverseitiges Problem auf. Wenden Sie sich bitte an den Server-Support. + Es trat ein unerwarteter Fehler auf. Einzelheiten dazu finden Sie in der Debug-Info. + Details anzeigen + Debug-Informationen wurden gesammelt + Beteiligte Ressourcen + Im Zusammenhang mit dem Problem + Entfernte Ressource: + Lokale Ressource: + Protokoll + Ausführliches Protokoll verfügbar + Logs anzeigen + URL kopieren + Datenschutzhinweis + Protokolle und Debug-Informationen können private Daten enthalten. Seien Sie sich dessen bewusst, wenn Sie diese öffentlich weitergeben. + + Ein Fehler ist aufgetreten. + Ein HTTP-Fehler ist aufgetreten. + Ein E/A-Fehler ist aufgetreten. + Details anzeigen + + WebDAV-Zugänge + Speicher belegt: %1$s / verfügbar: %2$s + Inhalt teilen + Aushängen + WebDAV-Zugang hinzufügen + Greifen Sie mit einem WebDAV-Zugang direkt auf Ihre Cloud-Dateien zu! + wie WebDAV-Zugänge funktionieren.]]> + Anzeigename + WebDAV-Adresse + Ungültige Adresse + Einhängepunkt und Anzeigename + Anmeldeinformationen + Anmeldename + Passwort + Benutzername (optional) + Passwort (optional) + Einhängen + Kein WebDAV-Dienst unter dieser Adresse + Einhängepunkt entfernen + Verbindungsdetails werden verloren gehen, es werden aber keine Dateien gelöscht. + WebDAV-Dateizugriff + WebDAV-Download + WebDAV-Upload + WebDAV-Zugang + + DAVx⁵-Berechtigungen + Zusätzliche Berechtigungen benötigt + %s zu alt + Benötigte Mindestversion: %1$s + Anmeldungsfehler (Login-Daten überprüfen) + Netzwerk- oder E/A-Fehler – %s + HTTP-Serverfehler – %s + Lokaler Speicherfehler – %s + Weicher Fehler (maximale Anzahl an Wiederholungen erreicht) + Ungültigen Kontakt vom Server erhalten + Ungültigen Termin vom Server erhalten + Ungültige Aufgabe vom Server erhalten + Eine/mehrere ungültige Ressourcen ignoriert + Synchronisierung ausstehend + Daten auf dem Server haben sich geändert + + Alles synchronisieren + Alle Konten synchronisieren + Beschriftete Sync-Taste + Sync-Taste-Symbol + Antippen, um die Synchronisierung manuell durchzuführen. + + diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml new file mode 100644 index 0000000..c90ce7c --- /dev/null +++ b/app/src/main/res/values-el/strings.xml @@ -0,0 +1,414 @@ + + + + Ο λογαριασμός δεν υπάρχει (πια) + Βιβλίο διευθύνσεων DAVx⁵ + Διαγραφή + Αφαίρεση + Ακύρωση + Ενεργοποίηση + Αυτό το πεδίο είναι απαραίτητο + Βοήθεια + Πλοήγηση προς τα πάνω + Μενού επιλογών + Διαμοιρασμός + Ο συγχρονισμός ξεκίνησε/αναμονή στην ουρά + Υπάρχει σφάλμα στην βάση δεδομένων + Όλοι οι λογαριασμοί έχουν διαγραφεί τοπικά + Αποσφαλμάτωση + Άλλα σημαντικά μηνύματα + Μηνύματα κατάστασης χαμηλής προτεραιότητας + Συγχρονισμός + Σφάλματα συγχρονισμού + Σημαντικά σφάλματα τα οποία σταματούν τον συγχρονισμό, όπως οι απροσδόκητες απαντήσεις του διακομιστή + Προειδοποιήσεις συγχρονισμού + Μη μοιραία προβλήματα συγχρονισμού όπως ορισμένα μη έγκυρα αρχεία + Σφάλματα δικτύου και I/O + Χρονικά όρια, προβλήματα σύνδεσης, κλπ. (συχνά προσωρινά) + + Τα δεδομένα σου. Οι επιλογές σου. + Πάρε τον έλεγχο. + Τακτά διαστήματα συγχρονισμού + Για συγχρονισμό σε τακτικά διαστήματα, πρέπει να επιτραπεί στο %s να λειτουργεί στο παρασκήνιο. Αλλιώς, το Android μπορεί να παύση τον συγχρονισμό ανά πάσα στιγμή. + Δεν χρειάζομαι τακτικά διαστήματα συγχρονισμού.* + %s Συμβατότητα + Έχω κάνει τις απαιτούμενες ρυθμίσεις. Δεν θέλω άλλες υπενθυμίσεις.* + Άφησε απενεργοποιημένες τις υπενθυμίσεις. Μπορούν να επαναφερθούν στις ρυθμίσεις εφαρμογών / %s. + Περισσότερες πληροφορίες + Πίνακας jtx + + Υποστήριξη εργασιών + Εάν οι εργασίες υποστηρίζονται από τον server σας, μπορούν να συγχρονιστούν με μια υποστηριζόμενη εφαρμογή εργασιών: + OpenTasks + Δεν φαίνεται να αναπτύσσεται πλέον - δεν συνιστάται. + Tasks.org + Δεν υπάρχει διαθέσιμο κατάστημα εφαρμογών + Δεν χρειάζομαι υποστήριξη εργασιών.* + Λογισμικό ανοικτού κώδικα + Χαιρόμαστε που χρησιμοποιείτε το %s, το οποίο είναι λογισμικό ανοικτού κώδικα. Η ανάπτυξη, η συντήρηση και η υποστήριξη είναι σκληρή δουλειά. Παρακαλούμε σκεφτείτε να συνεισφέρετε (υπάρχουν πολλοί τρόποι) ή να κάνετε μια δωρεά. Θα το εκτιμούσαμε ιδιαίτερα! + Πώς να συνεισφέρετε/δωρήσετε + + Δικαιώματα + Το %sχρειάζεται δικαιώματα για να λειτουργήσει σωστά. + Όλα τα παρακάτω + Χρησιμοποίησε αυτό για να ενεργοποιήσεις όλες τις λειτουργίες (προτείνεται) + Χορηγήθηκαν όλα τα δικαιώματα + Δικαιώματα επαφών + Μη συγχρονισμός επαφών (δεν προτείνεται) + Ο Συγχρονισμός επαφών είναι δυνατός + Δικαιώματα ημερολογίου + Μη συγχρονισμός ημερολογίου (δεν προτείνεται) + Ο Συγχρονισμός ημερολογίου είναι δυνατός + Άδεια ειδοποίησης + Οι ειδοποιήσεις είναι απενεργοποιημένες (δεν συνιστάται) + Ενεργοποιημένες ειδοποιήσεις + Πίνακας αδειών jtx + Δικαιώματα OpenTasks + Δικαιώματα εργασιών + Κανένας συγχρονισμός εργασιών + Ο Συγχρονισμός εργασιών είναι διαθέσιμος + Διατήρηση δικαιωμάτων + Τα δικαιώματα μπορούν να επαναφερθούν αυτόματα (δεν προτείνεται) + Τα δικαιώματα δεν θα επαναφερθούν αυτόματα + Κάντε κλικ στην επιλογή Άδειες > απενεργοποιήστε την επιλογή «Κατάργηση αδειών αν η εφαρμογή δεν χρησιμοποιείται» + Αν ο διακόπτης δεν λειτουργεί, χρησιμοποιήστε τις ρυθμίσεις της εφαρμογής / Άδειες. + Ρυθμίσεις εφαρμογής + + Δικαιώματα WiFi SSID + Για να είναι δυνατή η πρόσβαση σε αυτό το WiFi (SSID), χρειάζεται να πληρούνται οι παρακάτω προϋποθέσεις: + Δικαίωμα ακριβούς εντοπισμού + Ενεργοποιήθηκε το δικαίωμα εντοπισμού + Απενεργοποιήθηκε το δικαίωμα εντοπισμού + Δικαίωμα εντοπισμού στο παρασκήνιο + Να επιτρέπεται συνέχεια + Η άδεια τοποθεσίας έχει οριστεί σε: %s + Η άδεια τοποθεσίας δεν έχει οριστεί σε: %s + Η υπηρεσία εντοπισμού είναι πάντα ενεργοποιημένη + Η υπηρεσία εντοπισμού είναι ενεργοποιημένη + Η υπηρεσία εντοπισμού είναι απενεργοποιημένη + + Μεταφράσεις + Βιβλιοθήκες + Έκδοση %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) και συνεργάτες + Αυτό το πρόγραμμα συνοδεύεται χωρις ΚΑΜΙΑ ΕΓΓΥΗΣΗ. Είναι ελεύθερο λογισμικό και είστε ευπρόσδεκτοι να το αναδιανείμετε υπό ορισμένες προϋποθέσεις. + + Αδυναμία δημιουργίας αρχείου ιστορικού + Τώρα γίνεται η καταγραφή όλων των δραστηριοτήτων του %s + Προβολή/διαμοιρασμός + Απενεργοποίηση + + Προσαρμογέας συγχρονισμού CalDAV/CardDAV + Περί / άδεια χρήσης + Ανατροφοδότηση beta + Παρακαλούμε εγκαταστήστε έναν περιηγητή ιστού + Ρυθμίσεις + Νέα & ενημερώσεις + Εργαλεία + Εξωτερικοί σύνδεσμοι + Ιστότοπος + Χειροκίνητα + FAQ + Κοινότητα + Υποστηρίξτε το έργο + Πώς να συνεισφέρετε + Πολιτική απορρήτου + Συγχρονισμός όλων των λογαριασμών + + Οι ειδοποιήσεις είναι απενεργοποιημένες. Δεν θα λαμβάνετε ειδοποιήσεις για σφάλματα συγχρονισμού. + Διαχείριση συνδέσεων + Ενεργοποίηση της εξοικονόμησης δεδομένων. Ο συγχρονισμός στο παρασκήνιο είναι περιορισμένος. + Διαχείριση εξοικονόμησης δεδομένων + Ενεργοποιημένη εξοικονόμηση μπαταρίας. Ο συγχρονισμός μπορεί να είναι περιορισμένος. + Διαχείριση εξοικονόμησης μπαταρίας + Χαμηλός αποθηκευτικός χώρος. Το Android δεν θα συγχρονίσει τις τοπικές αλλαγές αμέσως, αλλά κατά τη διάρκεια του επόμενου τακτικού συγχρονισμού. + Διαχείριση αποθηκευτικού χώρου + + Αποτυχία ανίχνευσης υπηρεσίας + Αδυναμία ανανέωσης της λίστας συλλογής + + Εκτέλεση στο προσκήνιο + Σε κάποιες συσκευές, αυτό είναι απαραίτητο για τον αυτόματο συγχρονισμό + + Ρυθμίσεις + Αποσφαλμάτωση + Προβολή πληροφοριών αποσφαλμάτωσης + Λεπτομερής καταγραφή + Η καταγραφή είναι απενεργοποιημένη + Βελτιστοποίηση μπαταρίας + Η εφαρμογή εξαιρείται (συνιστάται) + Ισχύουν περιορισμοί για την μπαταρία (δεν συνιστάται) + Σύνδεση + Τύπος Proxy + + Προεπιλογή συστήματος + Χωρίς διαμεσολαβητή + HTTP + SOCKS (για το Orbot) + + Όνομα διακομιστή διαμεσολάβησης + Θύρα Proxy + Ασφάλεια + Δικαιώματα εφαρμογής + Αναθεώρηση των δικαιωμάτων που χρειάζονται για τον συγχρονισμό + Ακύρωση αξιοπιστίας πιστοποιητικών συστήματος + Τα πιστοποιητικά που προστέθηκαν από το σύστημα και τον χρήστη δεν θα είναι αξιόπιστα + Τα πιστοποιητικά που προστέθηκαν από το σύστημα και τον χρήστη θα είναι αξιόπιστα (συνιστάται) + Επαναφορά (μη)αξιόπιστων πιστοποιητικών + Επαναφέρει την εμπιστοσύνη όλων των προσαρμοσμένων πιστοποιητικών + Έχουν απαλειφθεί όλα τα προσαρμοσμένα πιστοποιητικά + Διεπαφή χρήστη + Ρυθμίσεις ειδοποιήσεων + Διαχείριση καναλιών ειδοποιήσεων και των ρυθμίσεών τους + Επιλογή θέματος + + Προεπιλογή Συστήματος + Φωτεινό + Σκοτεινό + + Επαναφορά συμβουλών + Ενεργοποιεί ξανά τις συμβουλές που έχουν απορριφθεί προηγουμένως + Όλες οι συμβουλές θα εμφανιστούν ξανά + Ενσωμάτωση + Εφαρμογή εργασιών + Δεν βρέθηκε συμβατή εφαρμογή εργασιών + + CardDAV + CalDAV + Webcal + Για το συγχρονισμό αυτών των συλλογών απαιτούνται πρόσθετα δικαιώματα. + Διαχείριση δικαιωμάτων + Συγχρονισμός τώρα + Ρυθμίσεις λογαριασμού + Μετονομασία λογαριασμού + Τα μη αποθηκευμένα τοπικά δεδομένα μπορούν να απορριφθούν. Μετά τη μετονομασία απαιτείται εκ νέου συγχρονισμός. + Νέο όνομα λογαριασμού + Μετονομασία + Το όνομα λογαριασμού έχει ήδη ληφθεί + Αδυναμία μετονομασίας λογαριασμού + Διαγραφή λογαριασμού + Θέλετε να διαγράψετε τον λογαριασμό; + Όλα τα τοπικά αντίγραφα των βιβλίων διευθύνσεων, ημερολογίων και λιστών εργασιών θα διαγραφούν. + συγχρονισμός αυτής της συλλογής + μόνο για ανάγνωση + ημερολόγιο + επαφές + ημερολόγιο + εργασίες + Εμφάνιση μόνο προσωπικών + Ανανέωση λίστας + Οι συνδρομές Webcal μπορούν να συγχρονιστούν με εξωτερικές εφαρμογές. + Δεν βρέθηκε εφαρμογή με δυνατότητα Webcal + Εγκατάσταση ICSx⁵ + + Προσθήκη λογαριασμού + Γενική είσοδος + Είσοδος για συγκεκριμένο πάροχο + Συνέχεια + Είσοδος + Είσοδος με την διεύθυνση email + Διεύθυνση email + Απαιτείται έγκυρη διεύθυνση email + Οι υπηρεσίες εντοπίζονται χρησιμοποιώντας εγγραφές DNS και γνωστές διευθύνσεις URL.]]> + Συνθηματικό + Απόκρυψη συνθηματικού + Εμφάνιση συνθηματικού + Είσοδος με την URL και το όνομα χρήστη + Όνομα χρήστη + Βασική URL + οι υπηρεσίες εντοπίζονται επίσης χρησιμοποιώντας εγγραφές DNS και γνωστές διευθύνσεις URL.]]> + Επιλογή πιστοποιητικού + Προσθήκη λογαριασμού + Όνομα λογαριασμού + Η χρήση των αποσιωπητικών (\') φαίνεται να προκαλεί προβλήματα σε ορισμένες συσκευές. + Χρησιμοποιήστε τη διεύθυνση ηλεκτρονικού ταχυδρομείου ως όνομα λογαριασμού, επειδή το Android θα χρησιμοποιεί το όνομα του λογαριασμού ως πεδίο ORGANIZER για τα συμβάντα που δημιουργείτε. Δεν μπορείτε να έχετε δύο λογαριασμούς με το ίδιο όνομα. + Μέθοδος ομάδας επαφών: + Απαιτείται όνομα λογαριασμού + Το όνομα λογαριασμού έχει ήδη ληφθεί + Σύνδεση για προχωρημένους + Πιστοποιητικό πελάτη: %s + Δεν βρέθηκε πιστοποιητικό + Εγκατάσταση πιστοποιητικού + Επαφές / Ημερολόγιο Google + Λογαριασμός Google + Συνδεθείτε με την Google + Αναγνωριστικό πελάτη (προαιρετικό) + πολιτική απορρήτου μας για λεπτομέρειες.]]> + Πολιτική Δεδομένων Χρήστη των Υπηρεσιών API της Google, συμπεριλαμβανομένων των απαιτήσεων περιορισμένης χρήσης.]]> + Αδυναμία λήψης κωδικού εξουσιοδότησης + Nextcloud + Συνδεθείτε με το Nextcloud + Αυτό θα ξεκινήσει τη ροή σύνδεσης στο Nextcloud σε ένα πρόγραμμα περιήγησης στο διαδίκτυο. + Διεύθυνση διακομιστή Nextcloud + Συνδεθείτε + Δεν ήταν δυνατή η λήψη του URL σύνδεσης + Δεν ήταν δυνατή η λήψη δεδομένων σύνδεσης + Ανίχνευση ρυθμίσεων + Περιμένετε, γίνεται ερώτημα στο διακομιστή... + Αδυναμία εύρεσης υπηρεσίας CalDAV ή CardDAV. + Η βασική διεύθυνση URL δεν φαίνεται να είναι προσβάσιμη διεύθυνση URL CalDAV/CardDAV και η ανίχνευση της υπηρεσίας δεν ήταν επιτυχής. + στον κατάλογο των δοκιμασμένων υπηρεσιών και των βασικών διευθύνσεων URL τους.]]> + Παρακαλούμε ελέγξτε επίσης δύο φορές τον έλεγχο ταυτότητας (συνήθως όνομα χρήστη και συνθηματικό). + Περαιτέρω τεχνικές πληροφορίες είναι διαθέσιμες στα αρχεία καταγραφής. + Προβολή ιστορικού + + Συγχρονισμός + Μεσοδιάστημα συγχρονισμού επαφών + Μόνο χειροκίνητα + Κάθε %d λεπτά + αμέσως στις τοπικές αλλαγές + Μεσοδιάστημα συγχρονισμού ημερολογίων + Μεσοδιάστημα συγχρονισμού εργασιών + + Μόνο χειροκίνητα + Κάθε 15 λεπτά + Κάθε 30 λεπτά + Κάθε ώρα + Κάθε 2 ώρες + Κάθε 4 ώρες + Μια φορά την ημέρα + + Συγχρονισμός μόνο μέσω WiFi + Ο συγχρονισμός περιορίζεται στις συνδέσεις WiFi + Ο τύπος σύνδεσης δεν λαμβάνεται υπόψη + Περιορισμός WiFi SSID + Θα γίνεται συγχρονισμός μόνο μέσω %s + Θα χρησιμοποιηθούν όλες οι συνδέσεις WiFi + Ονόματα που χωρίζονται με κόμμα (SSIDs) των επιτρεπόμενων δικτύων WiFi (αφήστε κενό για όλους) + Ο περιορισμός WiFi SSID χρειάζεται επιπλέον ρυθμίσεις + Διαχείριση + Το VPN απαιτεί υποκείμενο διαδίκτυο + Το VPN χωρίς υποκείμενη επικυρωμένη σύνδεση στο διαδίκτυο δεν αρκεί για την εκτέλεση του συγχρονισμού (συνιστάται) + Το VPN χωρίς υποκείμενη επικυρωμένη σύνδεση στο διαδίκτυο είναι αρκετό για την εκτέλεση του συγχρονισμού + Πιστοποίηση + Όνομα χρήστη + Νέο συνθηματικό + Ενημερώστε το συνθηματικό σύμφωνα με τον διακομιστή σας. + Πιστοποιητικό πελάτη + Δεν υπάρχει διαθέσιμο ή επιλεγμένο πιστοποιητικό + Εγκατάσταση πιστοποιητικού + CalDAV + Προθεσμία παρελθόντος συμβάντος + Όλα τα συμβάντα θα συγχρονίσουν + + Τα συμβάντα πέραν μιας ημέρας θα αγνοηθούν + Τα συμβάντα πέραν των %d ημερών θα αγνοηθούν + + Τα συμβάντα που υπερβαίνουν αυτόν τον αριθμό ημερών στο παρελθόν θα αγνοηθούν (μπορεί να είναι 0). Αφήστε κενό για συγχρονισμό όλων των συμβάντων. + Προεπιλεγμένη υπενθύμιση + + Προεπιλεγμένη υπενθύμιση ένα λεπτό πριν το συμβάν + Προεπιλεγμένη υπενθύμιση %dλεπτά πριν το συμβάν. + + Δεν δημιουργούνται προεπιλεγμένες υπενθυμίσεις + Εάν δημιουργούνται προεπιλεγμένες υπενθυμίσεις για συμβάντα χωρίς υπενθύμιση: επιθυμητός αριθμός λεπτών πριν από το συμβάν. Αφήστε κενό για να απενεργοποιήσετε τις προεπιλεγμένες υπενθυμίσεις. + Διαχείριση χρωμάτων ημερολογίου + Τα χρώματα των ημερολογίων επανέρχονται σε κάθε συγχρονισμό + Τα χρώματα ημερολογίων μπορούν να δηλωθούν από άλλες εφαρμογές + Υποστήριξη χρώματος στα συμβάντα + Τα χρώματα συμβάντων έχουν συγχρονιστεί + Τα χρώματα συμβάντων δεν έχουν συγχρονιστεί + CardDAV + Αλλαγή μεθόδου ομάδας + + Οι ομάδες είναι ξεχωριστές vCards + Οι ομάδες είναι κατηγορίες ανά επαφή + + + Δημιουργία βιβλίου διευθύνσεων + Η δημιουργία βιβλίου διευθύνσεων μέσω CardDAV ενδέχεται να μην υποστηρίζεται από το διακομιστή. + Δημιουργία ημερολογίου + + Πιθανές καταχωρήσεις ημερολογίου + Συμβάντα + Εργασίες + Σημειώσεις / ημερολόγιο + Η δημιουργία ημερολογίου μέσω CalDAV ενδέχεται να μην υποστηρίζεται από τον διακομιστή. + Χρώμα + Τίτλος + Τοποθεσία αποθήκευσης + Δημιουργία + + επαφές + εργασίες + Διαγραφή συλλογής + Αυτή η συλλογή (%s) και όλα τα δεδομένα της θα διαγραφούν μόνιμα, τόσο τοπικά όσο και από τον διακομιστή. + Συγχρονισμός + Ενεργοποιημένος συγχρονισμός + Απενεργοποιημένος συγχρονισμός + Μόνο για ανάγνωση + Μόνο για ανάγνωση (από τον διακομιστή) + Μόνο για ανάγνωση (μόνο τοπικά) + Ανάγνωση/εγγραφή + Τίτλος + Περιγραφή + Ιδιοκτήτης + Υποστήριξη Push + Ο διακομιστής διαφημίζει την υποστήριξη Push + Τελευταίος συγχρονισμός (%s) + Διεύθυνση (URL) + + Πληροφορίες αποσφαλμάτωσης + ZIP αρχείο αρχειοθέτησης + Περιέχει πληροφορίες εντοπισμού σφαλμάτων και αρχεία καταγραφής συστήματος + Μοιραστείτε το αρχείο για να το μεταφέρετε σε έναν υπολογιστή, να το στείλετε με email ή να το επισυνάψετε σε ένα εισιτήριο υποστήριξης. + Διαμοίρασε το αρχείο αρχειοθέτησης + Πληροφορίες εντοπισμού σφαλμάτων που επισυνάπτονται σε αυτό το μήνυμα (απαιτείται υποστήριξη συνημμένων από την εφαρμογή λήψης). + Σφάλμα HTTP + Σφάλμα διακομιστή + Σφάλμα WebDAV + Σφάλμα I/O + Προβολή λεπτομερειών + Έχουν συλλεχθεί πληροφορίες εντοπισμού σφαλμάτων + Εμπλεκόμενοι πόροι + Σχετικά με το πρόβλημα + Απομακρυσμένος πόρος: + Τοπικός πόρος: + Ιστορικό + Διατίθενται αναλυτικά αρχεία καταγραφής + Προβολή ιστορικού + Αντιγραφή URL + + Παρουσιάστηκε σφάλμα. + Παρουσιάστηκε σφάλμα HTTP. + Παρουσιάστηκε σφάλμα I/O. + Εμφάνιση λεπτομερειών + + Βάσεις WebDAV + Χώρος αποθήκευσης: %1$s / διαθέσιμα: %2$s + Κοινόχρηστο περιεχόμενο + Αποπροσάρτηση + Προσθήκη βάσης WebDAV + Απευθείας πρόσβαση στα αρχεία σας στο cloud δημιουργώντας μία προσάρτηση WebDAV + Εμφανιζόμενο όνομα + WebDAV URL + Μη έγκυρο URL + Πιστοποίηση + Όνομα χρήστη + Κωδικός πρόσβασης + Προσθήκη βάσης + Δεν υπάρχει υπηρεσία WebDAV σε αυτό το URL + Αφαίρεση σημείου προσάρτησης + Τα στοιχεία της σύνδεσης θα χαθούν, αλλά δεν θα διαγραφούν αρχεία. + Πρόσβαση σε αρχείο WebDAV + Kαταφόρτωση αρχείου WebDAV + Ανέβασμα αρχείου WebDAV + Bάση WebDAV + + Δικαιώματα DAVx⁵ + Απαιτούνται πρόσθετα δικαιώματα + Παλιά έκδοση %s + Ελάχιστη απαιτούμενη έκδοση: %1$s + Ο έλεγχος ταυτότητας απέτυχε (ελέγξτε τα διαπιστευτήρια σύνδεσης) + Σφάλμα δικτύου ή I/O – %s + Σφάλμα διακομιστής HTTP – %s + Σφάλμα τοπικού αποθηκευτικού χώρου – %s + Σφάλμα (φτάσατε στον μέγιστο αριθμό επαναλήψεων) + Ελήφθη μη έγκυρη επαφή από το διακομιστή + Ελήφθη μη έγκυρο συμβάν από το διακομιστή + Έλαβε μη έγκυρη εργασία από το διακομιστή + Αγνόηση ενός ή περισσοτέρων μη έγκυρων πόρων + + Συγχρονισμός όλων + Συγχρονισμός όλων των λογαριασμών + + diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml new file mode 100644 index 0000000..3ce147b --- /dev/null +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -0,0 +1,359 @@ + + + + Account does not exist (anymore) + DAVx⁵ Address book + Remove + Cancel + Enable + This field is required + Help + Share + Database corrupted + All accounts have been removed locally. + Debugging + Other important messages + Low-priority status messages + Synchronisation + Synchronisation errors + Important errors which stop synchronisation like unexpected server replies + Synchronisation warnings + Non-fatal synchronisation problems like certain invalid files + Network and I/O errors + Timeouts, connection problems, etc. (often temporary) + + Your data. Your choice. + Take control. + Regular sync intervals + For synchronisation at regular intervals, %s must be allowed to run in the background. Otherwise, Android may pause synchronisation at any time. + I don\'t need regular sync intervals.* + %s compatibility + I have done the required settings. Don\'t remind me anymore.* + * Leave unchecked to be reminded later. Can be reset in app settings / %s. + More information + jtx Board + + Tasks support + If tasks are supported by your server, they can be synchronised with a supported tasks app: + OpenTasks + Doesn\'t seem to be developed anymore – not recommended. + Tasks.org + No app store available + I don\'t need tasks support.* + Open-source software + We\'re happy that you use %s, which is open-source software. Development, maintenance and support are hard work. Please consider contributing (there are many ways) or a donation. It would be highly appreciated! + How to contribute/donate + + Permissions + %s requires permissions to work properly. + All of the below + Use this to enable all features (recommended) + All permissions granted + Contacts permissions + No contact sync (not recommended) + Contact sync possible + Calendar permissions + No calendar sync (not recommended) + Calendar sync possible + Notification permission + Notifications disabled (not recommended) + Notifications enabled + jtx Board permissions + OpenTasks permissions + Tasks permissions + No task sync + Task sync possible + Keep permissions + Permissions may be reset automatically (not recommended) + Permissions won\'t be reset automatically + Click Permissions > uncheck \"Remove permissions if app isn\'t used\" + If a switch doesn\'t work, use app settings / Permissions. + App settings + + WiFi SSID permissions + To be able to access the current WiFi name (SSID), these conditions must be met: + Precise location permission + Location permission granted + Location permission denied + Background location permission + Allow all the time + Location always enabled + Location service is enabled + Location service is disabled + + Translations + Libraries + Version %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) and contributors + This program comes with ABSOLUTELY NO WARRANTY. It is free software, and you are welcome to redistribute it under certain conditions. + + Couldn\'t create log file + Now logging all %s activities + View/share + Disable + + CalDAV/CardDAV Sync Adapter + About / License + Beta feedback + Please install a Web browser + Settings + News & updates + Tools + External links + Web site + Manual + FAQ + Community + Privacy policy + Sync all accounts + + Notifications disabled. You won\'t be notified about sync errors. + Manage connections + Data saver enabled. Background synchronisation is restricted. + Manage data saver + Storage space low. Android will not sync local changes immediately, but during the next regular sync. + Manage storage + + Service detection failed + Couldn\'t refresh collection list + + Running in foreground + On some devices, this is necessary for automatic synchronisation. + + Settings + Debugging + Show debug info + Verbose logging + Logging is disabled + Battery optimisation + Connection + Proxy type + + System default + No proxy + HTTP + SOCKS (for Orbot) + + Proxy host name + Proxy port + Security + App permissions + Review permissions required for synchronisation + Distrust system certificates + System and user-added CAs won\'t be trusted + System and user-added CAs will be trusted (recommended) + Reset (un)trusted certificates + Resets trust of all custom certificates + All custom certificates have been cleared + User interface + Notification settings + Manage notification channels and their settings + Select theme + + System default + Light + Dark + + Reset hints + Re-enables hints which have been dismissed previously + All hints will be shown again + Integration + Tasks app + No compatible tasks app found + + CardDAV + CalDAV + Webcal + Synchronise now + Account settings + Rename account + Rename + Account name already taken + Couldn\'t rename account + Delete account + Really delete account? + All local copies of address books, calendars and task lists will be deleted. + synchronise this collection + read-only + calendar + journal + Show only personal + No Webcal-capable app found + Install ICSx⁵ + + Add account + Login + Login with email address + Email address + Valid email address required + Password + Login with URL and user name + User name + Base URL + Select certificate + Add account + Account name + Use your email address as account name because Android will use the account name as ORGANISER field for events you create. You can\'t have two accounts with the same name. + Contact group method: + Account name required + Account name already taken + No certificate found + Install certificate + Google Contacts / Calendar + Google account + Sign in with Google + Client ID (optional) + Privacy policy for details.]]> + Google API Services User Data Policy, including the Limited Use requirements.]]> + Couldn\'t obtain authorisation code + Nextcloud + Login with Nextcloud + This will start the Nextcloud Login Flow in a Web browser. + Nextcloud server address + Sign in + Couldn\'t obtain login URL + Couldn\'t obtain login data + Configuration detection + Please wait, querying server… + Couldn\'t find CalDAV or CardDAV service. + View logs + + Synchronisation + Contacts sync. interval + Only manually + Every %d minutes + immediately on local changes + Calendars sync. interval + Tasks sync. interval + + Only manually + Every 15 minutes + Every 30 minutes + Every hour + Every 2 hours + Every 4 hours + Once a day + + Sync over WiFi only + Synchronisation is restricted to WiFi connections + Connection type is not taken into consideration + WiFi SSID restriction + Will only sync over %s + All WiFi connections will be used + Comma-separated names (SSIDs) of allowed WiFi networks (leave blank for all) + WiFi SSID restriction requires further settings + Manage + VPN requires underlying Internet + VPN without underlying validated Internet connection is not enough to run synchronisation (recommended) + VPN without underlying validated Internet connection is enough to run synchronisation + Authentication + User name + Update the password according to your server. + Install certificate + CalDAV + Past event time limit + All events will be synchronised + + Events more than one day in the past will be ignored + Events more than %d days in the past will be ignored + + Events which are more than this number of days in the past will be ignored (may be 0). Leave blank to synchronise all events. + Default reminder + + Default reminder one minute before event + Default reminder %d minutes before event + + No default reminders are created + If default reminders shall be created for events without reminder: the desired number of minutes before the event. Leave blank to disable default reminders. + Manage calendar colours + Calendar colours are reset at each sync + Calendar colours can be set by other apps + Event colour support + Event colours are synced + Event colours are not synced + CardDAV + Contact group method + + Groups are separate vCards + Groups are per-contact categories + + + Create address book + Create calendar + Possible calendar entries + Events + Tasks + Notes / journal + Colour + Title + Storage location + Create + + Delete collection + Synchronisation + Title + Description + + Debug info + ZIP archive + Contains debug info and logs + Share the archive to transfer it to a computer, to send it by email or to attach it to a support ticket. + Share archive + Debug info attached to this message (requires attachment support of the receiving app). + HTTP Error + Server Error + WebDAV Error + I/O Error + View details + Debug info have been collected + Involved resources + Related to the problem + Remote resource: + Local resource: + Logs + Verbose logs are available + View logs + Copy URL + + An error has occurred. + An HTTP error has occurred. + An I/O error has occurred. + Show details + + WebDAV mounts + Quota used: %1$s / available: %2$s + Share content + Unmount + Add WebDAV mount + Directly access your cloud files by adding a WebDAV mount! + Display name + WebDAV URL + Invalid URL + Authentication + User name + Password + Add mount + No WebDAV service at this URL + Remove mount point + Connection details will be lost, but no files will be deleted. + Accessing WebDAV file + Downloading WebDAV file + Uploading WebDAV file + WebDAV mount + + DAVx⁵ permissions + Additional permissions required + %s too old + Minimum required version: %1$s + Authentication failed (check login credentials) + Network or I/O error – %s + HTTP server error – %s + Local storage error – %s + Soft error (max retries reached) + Received invalid contact from server + Received invalid event from server + Received invalid task from server + Ignoring one or more invalid resources + + Sync all accounts + + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml new file mode 100644 index 0000000..051b2d7 --- /dev/null +++ b/app/src/main/res/values-es/strings.xml @@ -0,0 +1,418 @@ + + + + La cuenta (ya) no existe + Agenda DAVx⁵ + Eliminar + Eliminar + Cancelar + Activar + Este campo es requerido + Ayuda + Navegar hacia arriba + Menú de opciones + Compartir + Sincronización iniciada/programada + Base de datos corrompida + Toda las cuentas han sido eliminadas localmente. + Depuración + Otros mensajes importantes + Mensajes de baja prioridad + Sincronización + Errores de sincronización + Errores importantes que detienen la sincronización como respuestas inesperadas del servidor + Advertencias de sincronización + Problemas de sincronización no-fatales como ciertos archivos inválidos + Errores de Red y E/S + Timeouts, problemas de conección, etc. (muchas veces temporal) + + Sus datos. Su elección. + Tome el control. + Sincronización a intervalos regulares + Para sincronizar a intervalos regulares, se tiene que permitir a %sejecutarse como tarea de fondo. En caso contrario, Android podría pausar la sincronización en cualquier instante. + No necesito la sincronización a intervalos regulares.* + Compatibilidad %s + No tengo los ajustes requeridos. No volver a recordar.* + * Déjelo desmarcado para que se le recuerde más tarde. Se puede reconfigurar en los ajustes de la aplicación / %s + Información adicional + Tablero jtx + + Soporte de tareas + Si las tareas son compatibles con tu servidor, pueden sincronizarse con una aplicación de tareas compatible: + OpenTasks + Al parecer ya no tiene soporte – no se recomienda. + Tasks.org + Ninguna tienda de aplicaciones disponible + No necesito soporte para tareas.* + Software open-source + Nos complace que use%s, que es software open-source. Desarrollar, mantener y asistir a usuarios es un trabajo duro. Por favor, considere contribuir (hay muchas maneras) o hacer una donación. ¡Se agradecería mucho! + Cómo contribuir o donar + Siguiente + + Permisos + %s necesita permisos para funcionar correctamente. + Todos los siguientes + Usa esto para activar todas las características (recomendado) + Todos los permisos concedidos + Permisos de contactos + No sincronizar contactos (no recomendado) + Sincronización de contactos permitida + Permisos de calendario + Sin sincronización de calendario (no recomendado) + Sincronización de calendario permitida + Permiso de notificaciones + Notificaciones desactivadas (no recomendado) + Notificaciones habilitadas + Permisos de tablero jtx + Permisos de OpenTasks + Permisos de las tareas + Sin sincronización de tareas + Sincronización de tareas permitidas + Mantener permisos + Los permisos pueden restablecerse automáticamente (no recomendado) + Los permisos no se restablecerán automáticamente + Haga clic en Permisos > desmaque \"Quitar permisos si la aplicación no se utiliza\" + Si un interruptor no funciona usa Configuraciones de la app / Permisos. + Configuraciones de la app + + Permisos del SSID WiFi + Para poder acceder al nombre del WiFi actual (SSID), deben cumplirse estas condiciones: + Permiso de ubicación precisa + Permiso de ubicación concedido + Permiso de ubicación denegado + Permiso de ubicación en segundo plano + Permitir todo el tiemp + Acceso a la ubicación concedido para: %s + Acceso a la ubicación no concedido para: %s + Ubicación siempre activada + El servicio de ubicación está activado + El servicio de ubicación está desactivado + + Traducciones + Bibliotecas + Versión %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) y colaboradores + Este programa viene sin NINGÚN TIPO DE GARANTÍA. Es software libre, y cualquier contribución es bienvenida y redistribuida bajo ciertas condiciones. + + No se puede crear el fichero del registro. + Ahora se registran todas las activdades %s + Ver/compartir + Desactivar + + Adaptador de sincronización CalDAV/CardDAV + Acerca de / Licencia + Retroalimentación Beta + Por favor, instale un navegador web + Ajustes + Noticias y actualizaciones + Herramientas + Enlaces externos + Sitio web + Manual + Preguntas frequentes + Comunidad + Da soporte al proyecto + Cómo contribuir o donar + Reglamento de privacidad + Sincronizar todas las cuentas + + Notificaciones deshabilitadas. No serás notificado de errores de sincronización. + Administrar conexiones + Ahorro de datos habilitado. Sincronización en segundo plano restringida. + Administrar ahorro de datos + Ahorro de batería habilitado. La sincronización puede estar restringida. + Administrar ahorro de batería + Poco espacio de almacenamiento disponible. Android no sincronizará los cambios hechos localmente de manera inmediata, pero sí lo hará en la siguiente sincronización programada. + Administrar almacenamiento + + Falló la detección del servicio + No se pudo refrescar lista de colección + + Funcionando en primer plano. + En algunos dispositivos, esto es necesario para la sincronización automática. + + Ajustes + Depuración + Mostrar la información de depuración + Registro extendido + El registro está deshabilitado + Optimización de batería + La app está exenta (recomendado) + Se aplican restricciones de batería (no se recomienda) + Conexión + Tipo de proxy + + Por defecto del sistema + Sin proxy + HTTP + SOCKS (para Orbot) + + Nombre de proxy anfitrión + Puerto proxy + Seguridad + Permisos de la aplicación + Revisar los permisos necesarios para la sincronización + Invalidar los certificados del sistema + Los CA del sistema y los añadidos por el usuario no serán válidos + Los CA del sistema y los añadidos por el usuario serán usados y de confianza (recomendado) + Reiniciar certificados (in)validados + Reinicia la validez de todos los certificados particulares + Todos los certificados particulares han sido limpiados + Interfaz de usuario + Ajustes de notificación + Administrar notificación de canales y sus ajustes + Seleccionar tema + + Por defecto del sistema + Claro + Oscuro + + Restablecer advertencias + Habilita las advertencias que han sido rechazadas con anterioridad + Todas las advertencias se mostrarán nuevamente + Integración + Aplicación de tareas + No se han encontrado aplicaciones de tareas compatibles + + CardDAV + CalDAV + Webcal + Se requieren permisos adicionales para sincronizar estas colecciones. + Administrar permisos + Sincronizar ahora + Ajustes de cuenta + Renombrar cuenta + Los datos locales no guardados pueden ser perdidos. Es necesario volver a sincronizar después de renombrar. + Nuevo nombre de cuenta + Renombrar + El nombre de la cuenta ya está siendo utilizado + No se puede renombrar la cuenta + Eliminar cuenta + ¿Seguro que deseas eliminar la cuenta? + Todas las copias locales de tus contactos, calendarios y tareas serán eliminadas. + sincronizar ésta colección + solo lectura + calendario + contactos + diario + tareas + Mostrar solo personal + Recargar lista + Las subscripciones webcal pueden ser sincronizadas con apps externas. + No se encontró aplicación para administrar Webcal + Instalar ICSx⁵ + + Añadir cuenta + Inicio de sesión genérico + Inicio de sesión de proveedor + Continuar + Registrar + Acceder con cuenta de correo + Dirección de correo + Se requiere una dirección de correo válida + Los servicios son detectados usando registros DNS y URLs well-known.]]> + Contraseña + Esconder contraseña + Mostrar contraseña + Acceder con URL y nombre de usuario + Nombre de usuario + URL base + los servicios también son detectados usando registros DNS y URLs well-known.]]> + Seleccionar un certificado + Añadir cuenta + Nombre de cuenta + El uso de comillas (\') puede causar problemas en algunos dispositivos. + Usa tu dirección de correo como nombre de cuenta puesto que Android usará el nombre de la cuenta como campo de \"organizador\" en los eventos que cree. No puedes tener dos cuentas con el mismo nombre. + Método de contacto de grupo: + Nombre de cuenta requerido + El nombre de la cuenta ya está siendo utilizado + Inicio de sesión avanzado + Certificado de cliente: %s + No se ha encontrado ningún certificado + Instalar certificado + Google Contacts / Calendar + Cuenta de Google + Iniciar Sesión con Google + ID de cliente (opcional) + Política de privacidad para más información.]]> + Política de Datos de Usuario de los Servicios de las API de Google, incluyendo los requisitos de Uso Limitado.]]> + No se ha podido obtener el código de autorización + Nextcloud + Iniciar sesión con Nextcloud + Esto iniciará el Login Flow de Nextcloud en un navegador web. + Dirección del servidor Nextcloud + Iniciar sesión + No se ha podido obtener la URL de inicio de sesión + No se han podido obtener los datos de inicio de sesión + Detectar configuración + Por favor espera, consultando al servidor… + No se pudo encontrar el servicio CalDAV o CardDAV. + La URL base no parece ser una URL de CalDAV/CardDAV accesible, por lo que la detección de servicios no fue fructuosa. + lista de servicios probados y sus URL base.]]> + Por favor, también comprueba la autenticación (normalmente nombre de usuario y contraseña). + Hay más información técnica disponible en los registros. + Ver registros + + Sincronización + Intervalo de sincronización de contactos + Solo manualmente + Cada %d minutos + inmediatamente con cambios locales + Intervalo de sincronización de calendarios + Intervalo de sincronizacion de Tasks + + Solo manualmente + Cada 15 minutos + Cada 30 minutos + Cada hora + Cada 2 horas + Cada 4 horas + Una vez al dia + + Sincronizar sólo sobre WiFi + La sincronización está restringida a conexiones WiFi + Tipo de conexión no tenido en cuenta + Restricción WiFi SSID + Solo se sincronizará a través de %s + Todas las conexiones WiFi serán usadas + Nombres separados por comas (SSIDs) de redes WiFi permitidas (deje vacío para todas) + La restricción del SSID WiFi requiere más ajustes + Administrar + VPN requiere una conexión a Internet. + VPN sin una conexión a Internet válida no es suficiente para ejecutar la sincronización (recomendado) + VPN sin una conexión a Internet válida es suficiente para ejecutar la sincronización. + Autenticación + Nombre de usuario + Nueva contraseña + Actualiza la contraseña de acuerdo a tu servidor. + Certificado de cliente + No hay ninguna certificado disponible o seleccionado + Instalar certificado + CalDAV + Límite de tiempo de eventos pasados + Todos los eventos serán sincronizados + + Los eventos anteriores a un día serán ignorados + Los eventos anteriores a %d días serán ignorados + Los eventos anteriores a %d días serán ignorados + + Los eventos anteriores a este número de días serán ignorados (puede ser 0). Deja en blanco el campo para sincronizar todos los eventos. + Recordatorio por defecto + + Recordatorio por defecto un minuto antes del evento + Recordatorio por defecto %dminutos antes del evento + Recordatorio por defecto %dminutos antes del evento + + No se han creado recordatorios por defecto + Si se crearán recordatorios por defecto para los eventos que no los tengan: el número de minutos antes del evento. Déjelo en blanco para deshabilitar los recordatorios por defecto. + Colores de calendario + Los colores del calendario se restablecen en cada sincronización + Los colores del calendario pueden ser establecidos por otras aplicaciones + Soporte de colores en eventos + Los colores de los eventos están sincronizados + Los colores de los eventos no están sincronizados + CardDAV + Método de contacto de grupo + + Los grupos son vCards separadas + Los grupos son categorías de cada contacto + + + Crear nueva agenda + La creación de agendas de contactos sobre CardDAV puede no ser soportada por el servidor. + Crear calendario + + Posibles entradas de calendario + Eventos + Tareas + Notas / jornal + La creación de calendarios sobre CalDAV puede no ser soportada por el servidor. + Color + Título + Ubicación del almacenamiento + Descripción (opcional) + Crear + + contactos + tareas + Eliminar colección + La colección (%s) y todos sus datos serán eliminadas permanentemente, tanto localmente como del servidor. + Sincronización + Sincronización habilitada + Sincronización deshabilitada + Solo lectura + Solo lectura (desde el servidor) + Solo lectura (localmente) + Lectura/escritura + Título + Descripción + Propietario + Soporte de Push + El servidor tiene soporte para Push + Última sincronización (%s) + Dirección (URL) + + Información de depuración + Archivo ZIP + Contiene información de depuración y registro + Comparte el archivo para transferirlo al ordenador, para enviarlo por email o para adjuntarlo a un ticket de soporte. + Compartir archivo + Información de depuración adjunta a este mensaje (requiere soporte de adjuntos de la aplicación receptora). + Error HTTP + Error del servidor + Error de WebDAV + Error de E/S + Ver detalles + Se ha recogido la información de depuración + Recursos implicados + Relacionado con el problema + Recurso remoto: + Recurso local: + Registros + Los registros verbosos están disponibles + Ver registros + Copiar URL + + Ocurrió un error. + Ha ocurrido un error HTTP. + Ha ocurrido un error I/O. + Mostrar detalles + + Montajes WebDAV + Cuota usada: %1$s/ disponible: %2$s + Compartir contenido + Desmontar + Agregar montaje WebDAV + Accede directamente a tus archivos en la nube agregando un punto de montaje WebDAV + Mostrar nombre + URL WebDAV + URL inválida + Autenticación + Nombre de usuario + Contraseña + Agregar montaje + No hay un servicio WebDAV en esta URL + Eliminar punto de montaje + Los detalles de la conexión se perderán pero ningún archivo será eliminado. + Accediendo al fichero WebDAV + Descargando el fichero WebDAV + Subiendo fichero WebDAV + Montaje WebDAV + + Permisos de DAVx⁵ + Permisos adicionales requeridos + %s muy antiguo + Mínima versión requerida: %1$s + Falló la autenticación (revise credenciales de inicio de sesión) + Error de red o E/S – %s + Error de servidor – %s + Error de almacenamiento local – %s + Error no crítico (se ha llegado al número máximo de intentos) + Contacto inválido recibido del servidor + Evento inválido recibido del servidor + Tarea inválida recibidas del servidor + Ignorando uno o más recursos inválidos + + Sincronizar todos + Sincronizar todas las cuentas + + diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml new file mode 100644 index 0000000..6328e92 --- /dev/null +++ b/app/src/main/res/values-et/strings.xml @@ -0,0 +1,480 @@ + + + + Kasutajakontot ei leidu (enam) + DAVx⁵ aadressiraamat + Palun ära muuda kasutajakontot siin! Selle asemel pruugi kasutajakontode halduseks otseselt rakendust. + Kustuta + Eemalda + Katkesta + Võta kasutusele + See väli on kohustuslik + Abiteave + Liigu üles + Valikute menüü + Jaga + Sünkroniseerimine algas või on tööde järjekorras + Andmebaas on vigane + Kõik kasutajakontod on kohalikust seadmest eemaldatud + Silumine ja veaotsing + Muud olulised sõnumid + Väheolulised olekuteated + Sünkroniseerimine + Sünkroniseerimisvead + Olulised vead, mis peatavad sünkroniseerimise, nagu näiteks ootamatud päringuvastused serverist + Sünkroniseerimishoiatused + Vähetõsised sünkroniseerimisteated näiteks vigaste failide kohta + Võrgu- ja sisend/väljundvead + Ühenduste aegumine ja muud sarnased probleemid (tihti ajutised) + + Sinu andmed. Sinu valik. + Sina otsustad. + Regulaarne sünkroniseerimisvälp + Selleks, et sünkroniseerimine soovitud ajavahemike järel toimiks taustateenusena, vajab %s õigust töötada taustal. Vastasel juhul võib Android igal ajal sünkroniseerimise peatada. + Ma ei soovi kasutada regulaarset sünkroniseerimisvälpa. * + %s ühilduvus + Nutiseadme tootja poolt lisatud püsivara võib blokeerida sünkroniseerimist. Kui see sinu tegevust mõjutab, siis saad olukorra lahendada käsitsi. + Ma juba kasutan nõutavaid seadistusi. Ära enam tuleta seda mulle meelde.* + * Kui soovid hilisemat meeldetuletust, jäta see märkimata. Lisaks saad seada muuta rakenduse seadistustest / %s. + Lisateave + jtx Board + + Ülesannete tugi + Kui sinu kasutatav server toetab ülesannete haldust, siis nende sünkroniseerimine on võimalik toetatud ülesannete rakendusega: + OpenTasks + Tundub, et arendus on lõppenud ja seega pole kasutamine enam mõistlik. + Tasks.org + pole toetatud.]]> + Rakendustepoodi pole saadaval + Ma ei vaja ülesannete tuge.* + Avatud lähtekoodiga tarkvara + Me oleme rõõmsad, et kasutad avatud lähtekoodil põhinevat rakendust %s. Selle arendus, hooldus ja kasutajatugi nõuavad märgatavat tööd. Palun kaalu erinevaid võimalusi osalemiseks või rahalist toetamist. Me hindaksime seda väga! + Võimalused kaastööks või rahaliseks toetamiseks + Ära näita seda uuesti + + %d kuu jooksul + %d kuu jooksul + + Järgmine + + Õigused + %s vajab korralikuks toimimiseks õigusi. + Kõik alljärgnev + Kasuta seda valikut kõikide funktsionaalsuste sisselülitamiseks (soovitatav) + Rakenduse õigused on olemas + Kontaktide õigused + Kontaktide sünkroniseerimine puudub (pole soovitatud) + Kontaktide sünkroniseerimine on võimalik + Kalendri õigused + Kalendri sünkroniseerimine puudub (pole soovitatud) + Kalendri sünkroniseerimine on võimalik + Teavituste õigused + Teavitused pole kasutusel (pole soovitatav) + Teavitused on kasutusel + Õigused - jtx Board + Õigused - OpenTasks + Ülesannete õigused + Ülesannete sünkroniseerimine puudub + Ülesannete sünkroniseerimine on võimalik + Säilita õigused + Õigusi võib muuta automaatselt (pole soovitatud) + Õigused ei saa olema automaatselt muudetud + Klõpsi Õigused ja eemalda valik „Eemalda load, kui rakendust ei kasutata“ + Kui muutmine ei toimi, siis kasuta rakenduse õiguste seadistusi. + Rakenduse seadistused + + WiFi SSID õigused + Selleks, et toimiks ligipääs hetkel kasutatavale WiFi võrgunimele (SSID), peavad olema täidetud järgnevad tingimused: + Õigused täpse asukoha tuvastamiseks + Õigused asukoha tuvastamiseks on olemas + Õigused asukoha tuvastamiseks on keelatud + Õigused asukoha tuvastamiseks taustal + Luba alati + Asukohaõigused on: %s + Asukohaõiguseid pole: %s + %s kasutab asukohaandmeid (vaid WiFi SSID võrgutunnust) vaid sünkroniseerimise tagamiseks konkreetse WiFi-võrgu piires. See kehtib ka siis, kui sünkroniseerimine on seadistatud töötama taustal. + Kõik asukohaandmed (vaid WiFi SSId võrgutunnus) on kasutusel kohalikus nutiseadmes ega saadeta mitte kuhugile mujale. + Asukohateenus on alati kasutusel + Asukohateenus on lubatud + Asukohateenus pole lubatud + + Tõlked + Teegid + Versioon %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) ja kaasautorid + Selle rakenduse kasutamisega EI KAASNE MITTE ÜHTEGI GARANTIID. Tegemist on vaba ja avatud tarkvaraga ning sa võid seda levitada kindlate tingimuste alusel. + + Logifaili loomine ei õnnestunud + Nüüd logime kõiki %s rakenduse tegevusi + Vaata/jaga + Lülita välja + + CalDAV/CardDAV sünkroniseerimise sobitaja + Teave / litsents + Beetaversiooni tagasiside + Palun paigalda veebibrauser + Seadistused + Uudised ja uuendused + Tarvikud + Välised lingid + Veebisait + Käsiraamat + KKK + Organisatsioonide jaoks + Kogukond + Toeta projekti + Osalemise viisid + Privaatsusreeglid + Tere tulemast kasutama rakendust DAVx⁵! + Loo ühendus oma serveriga ja hoia kalendrid ning kontaktid sünkroniseerituna. + Sünkroniseeri kõik kasutajakontod + + Teavitused on välja lülitatud ja seega sünkroniseerimisvigade infot sa ei näe. + Automaatne sünkroniseerimine pole aktiivne (kontrollitud internetiühendus puudub) + Halda ühendusi + Andmemahu piiraja on kasutusel. Taustal sünkroniseerimine võib toimida piirangutega. + Halda andmemahu piirajat + Akukasutuse piiraja on kasutusel. Taustal sünkroniseerimine võib toimida piirangutega. + Halda akukasutuse piirajat + Vaba andmeruumi napib. Android ei sünkroniseeri kohalikke muudatusi kohe, vaid järgmise regulaarse sünkroniseerimise ajal. + Halda andmeruumi + Kalendri teenusepakkuja puudub. + Kas sa oled lülitanud välja süsteemse kalendri salvestusruumi rakenduse „Calendar storage“ välja? + Kontaktide teenusepakkuja puudub. + Kas sa oled lülitanud välja süsteemse kontaktide salvestusruumi rakenduse „Contacts storage“ välja? + Halda rakendusi + + Teenuse tuvastamine ei õnnestunud + Kogumike loendi uuendamine ei õnnestunud + + Töötame esiplaanil + See eelistus on vajalik sünkroniseerimiseks mõnedes seadmetes. + + Seadistused + Silumine ja veaotsing + Näita silumisteavet + Vaata/jaga seadistuse üksikasju ja logisid + Väga üksikasjalik logimine + Logimine on kasutusel. Silumisteabe osana saad vaadata logisid. + Logimine pole kasutusel + Akukasutuse optimeerimine + See rakendus ei allu akukasutuse optimeerimisele (soovitatav valik) + Akukasutuse optimeerimise piirangud on kasutusel (mittesoovitatav valik) + Ühendus + Proksiserveri tüüp + + Süsteemi proksiserver + Proksiserver puudub + HTTP + SOCKS (Orboti jaoks) + + Proksiserveri hostinimi + Proksiserveri port + Turvalisus + Rakenduse õigused + Täpsusta sünkroniseerimiseks vajalike õigusi + Ära usalda nutiseadme süsteemseid sertifikaate + Süsteemsed ja kasutaja lisatud sertifitseerimiskeskused ei ole usaldatud + Süsteemsed ja kasutaja lisatud sertifitseerimiskeskused on usaldatud (soovitatav valik) + Kui see seadistus on aktiivne, siis operatsioonisüsteemis leiduvad sertifikaate ei loeta usaldusväärseteks. See tähendab, et iga kord pead sertifikaadiga käsitsi nõustuma (seda ka siis, kui server uuendab oma sertifikaate), vastasel juhul kasutajakonto seadistamine ja sünkroniseerimine ei toimi. + Lähtesta (mitte)usaldatud sertifikaatide loend + Selle valikuga eemaldatakse kõik sinu lisatud sertifikaatide usaldusmärked + Kõik sinu lisatud sertifikaatide usaldusmärked on eemaldatud + Kasutajaliides + Teavituste seadistused + Halda teavituskanaleid ja nende seadistusi + Vali kujundus + + Süsteemi kujundus + Hele kujundus + Tume kujundus + + Lähtesta vihjed + Lülitab varem väljalülitatud vihtjete kuvamise uuesti sisse + Näitame jälle kõiki vihjeid + Lõimimine + Ülesannete rakendus + Ühilduvat ülesannete rakendust ei leidu + UnifiedPush (katseline) + Puudub (tõuketeenuseid pole) + Vali levitaja + Ühtegi tõukesõnumite levitajat pole paigaldatud + Otspunkt on seadistamata + Valmis tõuketeadete vastuvõtmiseks %s vahendusel + FCM (Google Play) + Tõuketeavituste sõnumid on alati krüptitud. + + Kasutajakonto on eemaldatud + CardDAV + CalDAV + Webcal + Nende kogumike sünkroniseerimiseks on vajalikud täiendavad õigused. + Halda õigusi + Sünkroniseeri nüüd + Kasutajakonto seadistused + Muuda kasutajakonto nime + Salvestamata kohalik teave võib vahele jääda. Peale nime muutmist palun sünkroniseeri uuesti. + Kasutajakonto uus nimi + Muuda nime + Selline nimi on juba kasutusel + Kasutajakonto nime muutmine ei õnnestunud + Kustuta kasutajakonto + Kas tõesti kustutame kasutajakonto? + Sellega kustutame ka kõik aadresside, kalendrite ja ülesannete kohalikud koopiad. + sünkroniseeri see kogumik + ainult lugemisõigus + kalender + kontaktid + päevik + ülesanded + Näita vaid isiklikke + Uuenda loendit + Webcali tellimusi on võimalik sünkroniseerida väliste rakendustega. + Webcaliga ühilduvaid rakendusi ei leidu + Paigalda ICSx⁵ + + Lisa kasutajakonto + meie Privaatsusreeglitest.]]> + Üldine sisselogimine + Teenusepakkujakohane sisselogimine + Jätka + Logi sisse + Logi sisse e-posti aadressiga + E-posti aadress + Nõutav on korrektne e-posti aadress + Teenused tuvastame nimeserveri kirjete ning „.well-known“ tunnusaadresside abil.]]> + Salasõna + Peida salasõna + Näita salasõna + Salasõna (kui on vaja) + Logi sisse võrguaadressi ja kasutajanimega + Kasutajanimi + Kasutajanimi (kui on vaja) + Alustuseks mõeldud võrguaadress + tuvastame teenuseid nimeserveri kirjete ning „.well-known“ tunnusaadresside abil.]]> + Vali sertifikaat + Lisa kasutajakonto + Kasutajakonto nimi + Ülakomade (\') kasutamine tundub mõnedes seadmetes tekitama probleeme. + Kuna Android pruugib kasutajakonto nime sinu loodavate ürituste Korraldaja ehk ORGANIZER välja väärtustamiseks, siis soovitame, et sinu kasutajakonto nimi on sinu e-posti aadress. Palun arvesta, et sul ei saa olla kahte samanimelist kasutajakontot. + Kontaktgrupi meetod: + Kasutajakonto nimi on nõutav + Selline nimi on juba kasutusel + Kasutajakonto lisamine ei õnnestunud + Lõpeta + Täiendavad sisselogimise seadistused + Kliendisertifikaat puudub (kui on vaja) + Kliendi sertifikaat: %s + Kliendisertifikaati ei leidunud + Paigalda sertifikaat + Fastmail + Fastmaili kasutajakonto + Logi sisse Fastmaili kasutajakontoga + Google\'i Kontaktid / Kalender + Google\'i kasutajakonto + Logi sisse Google\'i kasutajakontoga + Klienditunnus (kui soovid lisada) + Privaatsusreeglitest.]]> + Google\'i API teenuste kasutajaandmete poliitikat, sealhulgas piiratud kasutuse nõudeid.]]> + Autoriseerimiskoodi saamine polnud võimalik + Nextcloud + Logi sisse Nextcloudi kontoga + Selle eelistusega käivitad Nextcloudi sisselogimise veebibrauseris. + Nextcloudi serveri aadress + Logi sisse + Sisselogimise võrguaadressi tuvastamine polnud võimalik + Sisselogimisandmete tuvastamine polnud võimalik + Seadistuste tuvastamine + Palun oota, pärime andmeid serverist… + Ei õnnestunud leida CalDAV või CardDAV teenust. + Antud võrguaadress ei tundu olema ligipääsetav CalDAVi/CardDAVi võrguaadress ja teenuse tuvastamine ei õnnestunud. + meie poolt testitud teenuste loendist koos toimivate võrguaadressidega.]]> + Palun samuti topeltkontrolli autentimist (tavaliselt kasutajanimi ja salasõna) + Täiendav tehniline teade leidub logides. + Vaata logisid + + Sünkroniseerimine + Kontaktide sünkroniseerimise välp + Vaid käsitsi + Iga %d minuti järel + kohalikud muudatused koheselt + Kalendrite sünkroniseerimise välp + Ülesannete sünkroniseerimise välp + + Vaid käsitsi + Iga 15 minuti järel + Iga 30 minuti järel + Kord tunnis + Iga 2 tunni järel + Iga 4 tunni järel + Kord päevas + + Sünkroniseeri vaid WiFi ühendusega + Sünkroniseerimine on lubatud vaid WiFi ühendusega + Ühenduse liik pole oluline + WiFi SSID piirangud + Sünkroniseeri vaid %s võrgus + Kasuta kõiki WiFi ühendusi + Lubatud WiFi võrgunimede (SSID) komadega eraldatud loend (kui jätad tühjaks on kõik lubatud) + WiFi SSID piirang vajab täiendavat saedistamist + Halda + VPNi kasutamine eeldab, et võrguühendus toimib + VPN ilma toimiva ja kontrollitud internetiühenduseta pole piisav sünkroniseerimiseks (soovitatud) + VPN ilma toimiva ja kontrollitud internetiühenduseta on sünkroniseerimiseks piisav + Autentimine + Kasutajanimi + Salasõna või rakenduse salasõna + Rakenduse salasõna kasutamine peaks olema esimene eelistus.]]> + Uus salasõna + Uuenda salasõna vastavalt oma serveri juhendile. + Autoriseeri uuesti (OAuth) + Kasuta olukorras, kus ligipääs on tühistatud + Autoriseerimine õnnestus + Kliendi sertifikaat + Sertifikaati pole saadaval või paigaldatud + Paigalda sertifikaat + CalDAV + Möödunud sündmuste ajapiir + Kõik sündmused kuuluvad sünkroniseerimisele + + Eira enam kui üks päev vanu sündmuseid + Eira enam kui %d päeva vanu sündmuseid + + Sündmused, mis on vanemad, kui siin märgitud päevade arv, jäävad sünkroniseerimata (võib olla ka 0). Kõikide sündmuste sünkroniseerimiseks jäta tühjaks. + Vaikimisi meeldetuletus + + Vaikimisi meeldetuletus üks minutit enne sündmust + Vaikimisi meeldetuletus %d minutit enne sündmust + + Vaikimisi meeldetuletused puuduvad + Eelistus määrab, kas kasutame vaikimisi meeldetuletust sündmuste puhul, kus eraldi meeldetuletus on seadistamata. Aktiveerimiseks sisesta vaikimisi meeldetuletuse aeg minutites. Väljalülitamiseks jäta tühjaks. + Halda kalendrivärve + Kalendri värvid lähtestatakse igal sünkroniseerimisel + Muud rakendused võivad kalendrivärve seadistada + Sündmuste värvide tugi + Sündmuste värvid kuuluvad sünkroniseerimisele + Sündmuste värvid ei kuulu sünkroniseerimisele + CardDAV + Kontaktgrupi meetod + + Grupid on eraldi vCard-kirjed + Grupid on kontaktikohased kategooriad + + + Loo aadressiraamat + See server ei pruugi toetada aadressiraamatu loomist CardDAVi ühenduse abil. + Loo kalender + Vaikimisi ajavöönd (kui on vaja) + + Võimalikud kalendrikirjed + Sündmused + Ülesanded + Märkmed / päevik + See server ei pruugi toetada kalendri loomist CalDAVi ühenduse abil. + Värv + Pealkiri + Andmeruumi asukoht + Kirjeldus (kui on vaja) + Loo + + kontaktid + sündmust + ülesanded + Kustuta kogumik + See kogumik (%s) koos oma kõikide andmetega kustutatakse nüüd jäädavalt nii serverist, kui kohalikust nutiseadmest. + Sünkroniseerimine + Sünkroniseerimine on kasutusel + Sünkroniseerimine pole kasutusel + Ainult lugemisõigus + Ainult lugemisõigus (serveri poolt) + Ainult lugemisõigus (reeglite alusel) + Ainult lugemisõigus (ainult kohalikus nutiseadmes) + Lugemis- ja kirjutamisõigus + Pealkiri + Kirjeldus + Omanik + Tõuketeenuse tugi + Server teavitab tõuketeenuse toe olemasolust + Tellitud %1$s, aegub %2$s + Viimane sünkroniseerimine (%s) + Aadress (võrguaadress) + + Silumisteave + ZIP-arhiivifail + Sisaldab silumisteavet ja logisid + Tõsta arhiiv uurimiseks arvutisse, saada huvilisele e-postiga või lisa veateatele meie veahalduses. + Jaga arhiivi + Sõnumile lisatud silumisteave (eeldab, et vastuvõttev rakendus oskab manuseid käsitleda). + HTTP-viga + Serveri viga + WebDAVi viga + Sisend-/väljundviga + Server keeldus päringule vastamast. + Päritud andmeressurssi ei leidu (enam). + Server ei võimalda antud päringu tüüpi kasutada või soovitud tegevust teha. + Tekkis serveripoolne viga. Palun võta ühendust serveri haldajaga. + Tekkis ootamatu viga. Lisainfot leiad silumisteabest. + Vaata üksikasju + Silumisteave on kogutud + Seotud teenused ja tarvikud + Probleemi või veaga seotud teave + Serveris asuvad teenused ja tarvikud: + Kohalikus nutiseadmes teenused ja tarvikud: + Logid + Saadaval on üksikasjalikud logid + Vaata logisid + Privaatsusteade + Logid ja veaotsingu teave võivad sisaldada privaatset teavet. Nende andmete avalikul jagamisel palun arvesta sellega. + + Tekkis viga. + Tekkis http-viga. + Tekkis sisend-väljundviga. + Näita üksikasju + + WebDAVi haakepunktid + Kasutatud mahukvoot: %1$s / saadaval: %2$s + Jaga sisu + Eemalda haakimine + Lisa WebDAVi haakepunkt + Otseligipääs sinu failidele WebDAVi haakepunktist! + kuidas WebDAVi haakepunktid toimivad.]]> + Kuvatav nimi + WebDAVi võrguaadress + Vigane võrguaadress + Haakepunkt ja kuvatav nimi + Autentimine + Kasutajanimi + Salasõna + Kasutajanimi (kui on vaja) + Salasõna (kui on vaja) + Lisa haakepunkt + Sellel võrguaadressil ei leidu WebDAVi teenust + Eemalda haakepunkt + Ühenduse andmed lähevad kaotsi, aga ühtegi faili ei kustutata. + Ligipääs WebDAVi failile + Laadime WebDAVi faili alla + Laadime WebDAVi faili üles + WebDAVi haakepunkt + + DAVx⁵ õigused + Vajalikud on täiendavad õigused + %s on liiga vana + Väikseim nõutav versioon: %1$s + Autentimine ei õnnestunud (kontrolli, et kasutajanimi/salasõna oleksid õiged) + Võrgu- või sisend/väljundviga – %s + HTTP serveri viga – %s + Kohaliku salvestusruumi viga – %s + Pehme viga (korduspäringute arvu ülempiir on käes) + Saime serverist vigase kontaktikirje + Saime serverist vigase sündmusekirje + Saime serverist vigase ülesandekirje + Eirame ühte või enamat teenust või tarvikut + Sünkroniseerimine on ootel + Serveris olevad andmed on muutunud + + Sünkroniseeri kõik + Sünkroniseeri kõik kasutajakontod + Sildiga sünkroniseerimisnupp + Ikooniga sünkroniseerimisnupp + Klõpsi sünkroniseerimise käsitsi käivitamiseks. + + diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml new file mode 100644 index 0000000..542f519 --- /dev/null +++ b/app/src/main/res/values-eu/strings.xml @@ -0,0 +1,477 @@ + + + + Kontua ez da existitzen (dagoeneko) + DAVx⁵ Helbide-liburua + Ez aldatu kontua hemen! Erabili aplikazioa zuzenean kontuak kudeatzeko. + Ezabatu + Kendu + Utzi + Gaitu + Eremu hau beharrezkoa da + Laguntza + Nabigatu gora + Aukeren menua + Partekatu + Sinkronizazioa hasi/ilaran jarri da + Datu-basea hondatua + Kontu lokal guztiak ezabatu dira. + Arazten + Beste mezu garrantzitsu batzuk + Prioritate baxuko egoera mezuak + Sinkronizazioa + Sinkronizazio erroreak + Sinkronizazioa gelditzen duten errore garrantzitsuak, esaterako ustekabeko zerbitzariaren erantzunak + Sinkronizazio abisuak + Fitxategi baliogabe moduko sinkronizazio arazo ez-kritikoak + Sare eta S/I erroreak + Denbora-mugak, konexio arazoak, etab. (normalean behin-behinekoak) + + Zure datuak. Zure aukera. + Har ezazu kontrola. + Sinkronizazio tarte erregularrak + Sinkronizazioa tarte erregularretan ahalbidetzeko, %s atzeko planoan exekutatzen utzi behar da. Bestela, Androidek sinkronizazioa gelditu dezake edozein unean. + Ez ditut sinkronizazio tarte erregularrak behar.* + %s bateragarritasuna + Salatzailearen firmwareak sinkronizazioa blokeatu dezake. Kaltetua bazara, eskuz bakarrik konpon dezakezu. + Beharrezko ezarpenak bukatu ditut. Ez gogorarazi berriro.* + * Utzi aktibatu gabe gero gogorarazteko. Aplikazioaren ezarpenetan berrezarri daiteke / %s + Informazio gehiago + jtx taula + + Tasks bateragarritasuna + Zereginak zure zerbitzarian onartuta badaude, onartutako zeregin aplikazio batekin sinkronizatu daitezke: + OpenTasks + Badirudi ez dela garatzen – ez da gomendatzen. + Tasks.org + ez dira onartzen.]]> + Ez dago denda aplikaziorik eskuragarri + Ez dut zereginen funtzionalitatea behar.* + Kode irekiko softwarea + %s erabiltzen duzula pozik gaude, software irekia delako. Garapen, mantentze eta laguntza lan gogorrak dira. Mesedez pentsatu kolaboratzen (modu asko daude) edo dohaintza bat. Asko eskertuko genuke! + Nola lagundu/dirua eman + Ez gogorarazi honetarako + + Hilabete %d + %d hilabete + + Hurrengoa + + Baimenak + %s baimenak behar ditu ondo funtzionatzeko. + Azpiko guztiak + Erabili hau ezaugarri guztiak gaitzeko (gomendatuta) + Baimen guztiak eman dira + Kontaktuen baimenak + Kontaktu sinkronizaziorik ez (ez gomendatuta) + Kontaktuen sinkronizazioa posible + Egutegiaren baimenak + Egutegi sinkronizaziorik ez (ez gomendatuta) + Egutegiaren sinkronizazioa posible + Jakinarazpen baimena + Jakinarazpenak desgaituta (ez gomendatuta) + Jakinarazpenak gaituta + jtx taularen baimenak + OpenTasks baimenak + Tasks baimenak + Zeregin sink. ez + Zereginen sinkronizazioa posible + Mantendu baimenak + Baimenak automatikoki berezarri daitezke (ez gomendatuta) + Baimenak ez dira automatikoki berezarriko + Egin klik Baimenak atalean > kendu marka \"Kendu baimenak aplikazioa ez bada erabiltzen\" aukeratik + Interruptore batek funtzionatzen ez badu, erabili aplikazioaren ezarpenak / Baimenak. + Aplikazioaren ezarpenak + + WiFi SSID baimenak + Uneko WiFi izena (SSID) atzitzeko, baldintza hauek bete behar dira: + Kokapen zehatz baimena + Kokapen baimena emanda + Kokapen baimena ukatuta + Atzeko planoko kokapen baimena + Baimendu beti + Kokapen-baimena honela ezarri da: %s + Kokapen-baimena ez da honela ezarri: +%s + %s kokapen-datuak (WiFi SSID soilik) erabiltzen ditu sinkronizazioa WiFi SSID zehatz batera mugatzeko soilik. Hori sinkronizazioa atzeko planoan exekutatzen denean ere gertatuko da. + Kokapen-datu guztiak (WiFi SSID soilik) lokalean bakarrik erabiltzen dira eta ez dira inora bidaltzen. + Kokapena beti gaituta + Kokapen zerbitzua gaituta dago + Kokapen zerbitzua desgaituta dago + + Itzulpenak + Liburutegiak + %1$s (%2$d) bertsioa + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) eta kolaboratzaileak + Programa hau INOLAKO BERMERIK GABE dator. Software librea da, eta birbanatzeko baimena duzu baldintza batzuk kontuan hartuz. + + Ezin izan da egunkari fitxategia sortu + %s jarduera guztiak erregistratzen + Ikusi/partekatu + Desgaitu + + CalDAV/CardDAV sinkronizazio moldagailua + Honi buruz / Lizentzia + Beta iritzia + Instalatu web nabigatzaile bat + Ezarpenak + Berriak eta eguneraketak + Tresnak + Kanpo loturak + Webgunea + Manuala + FAQ + Erakundeentzat + Komunitatea + Lagundu proiektuari + Nola lagundu + Pribatutasun gidalerroak + Ongi etorri DAVx⁵-ra! + Konektatu zure zerbitzarira eta mantendu zure egutegi eta kontaktuak sinkronizatuta. + Sinkronizatu kontu guztiak + + Jakinarazpenak desgaituta. Ez zaitugu sinkronizazio-erroreei buruz jakinaraziko. + Sinkronizazio automatikoa ez dago gaituta (ezin da Internet konexioa egiaztatu) + Kudeatu konexioak + Datu-aurrezpena gaituta. Atzeko planoko sinkronizazioa murriztuko da. + Kudeatu datu-aurrezpena + Bateria-aurrezlea aktibatuta dago. Sinkronizazioa mugatuta egon daiteke. + Kudeatu bateria-arrezpena + Biltegiratze lekua baxua. Android-ek ez ditu tokiko aldaketak berehala sinkronizatuko, hurrengo sinkronizazio arruntean baizik. + Kudeatu biltegia + Egutegiaren hornitzailea falta da + \"Egutegiaren biltegia\" sistemaren aplikazioa desgaitu al duzu? + Kontaktuen hornitzailea falta da + \"Kontaktuen biltegia\" sistemaren aplikazioa desgaitu al duzu? + Kudeatu aplikazioak + + Zerbitzuaren detekzioak huts egin du + Ezin izan da bilduma zerrenda freskatu + + Aurrealdean exekutatzen + Gailu batzuetan, hau beharrezkoa da sinkronizazio automatikorako. + + Ezarpenak + Arazketa + Erakutsi arazte informazioa + Ikusi/partekatu konfigurazio xehetasun eta egunkariak + Erregistro xehatuak + Erregistratzea aktibo dago. Erregistroak arazketa-informazioaren zati gisa ikus ditzakezu. + Erregistratzea desgaituta dago + Bateria optimizazioa + Aplikazioa salbuetsita dago (gomendatua) + Bateriaren murrizketak aplikatzen dira (ez da gomendagarria) + Konexioa + Proxy mota + + Sistemaren lehenetsia + Proxyrik ez + HTTP + SOCKS (Orbot-erako) + + Proxy ostalariaren izena + Proxy ataka + Segurtasuna + Aplikazioaren baimenak + Berrikusi sinkronizaziorako beharrezkoak diren baimenak + Mesfidatu sistemaren ziurtagiritaz + Sistemako eta erabiltzaileak gehitutako CAk ez dira fidagarritzat hartuko + Sistemako eta erabiltzaileak gehitutako CAk fidagarritzat hartuko dira (gomendatuta) + Ezarpen hau aktibo badago, sistemaren ziurtagiriak ez dira fidagarritzat hartzen. Horrek esan nahi du ziurtagiri guztiak eskuz onartu beharko dituzula (zerbitzariak ziurtagiria berritzen duenean ere bai) baita kontuaren konfigurazioa eta sinkronizazioak ez duela funtzionatuko. + Berezarri (mes)fidatutako ziurtagiriak + Ziurtagiri pertsonalizatu guztien fidagarritasuna berezartzen du + Ziurtagiri pertsonalizatu guztiak garbitu dira + Erabiltzaile interfazea + Jakinarazpen-ezarpenak + Kudeatu jakinarazpen kanalak eta haien ezarpenak + Hautatu gaia + + Sistemaren lehenetsia + Argia + Iluna + + Berezarri aholkuak + Lehen baztertu diren aholkuak berriro gaitzen ditu + Aholku guztiak erakutsiko dira berriro + Integrazioa + Tasks aplikazioa + Ez da zeregin aplikazio bategarririk aurkitu + UnifiedPush (esperimentala) + Bat ere es (desgaitu push) + Aukeratu banatzaile bat + Ez dago push banatzailerik instalatuta + Ez da amaiera punturik konfiguratu + Push jakinarazpenak %s(r)en bidez jasotzeko prest + FCM (Google Play) + Bultzatutako mezuak beti zifratzen dira. + + Kontua ezabatu da + CardDAV + CalDAV + Webcal + Bilduma hauek sinkronizatzeko baimen gehigarriak behar dira. + Kudeatu baimenak + Sinkronizatu orain + Kontuaren ezarpenak + Berrizendatu kontua + Gorde gabeko datu lokalak baztertu daitezke. Berriro sinkronizatu behar da izena aldatu ostean. + Kontuaren izen berria + Berrizendatu + Kontuaren izena hartuta dago + Ezin izan da kontua berrizendatu + Ezabatu kontua + Ezabatu kontua? + Helbide liburu, egutegi eta zeregin zerrenden kopia lokal guztiak ezabatuko dira. + sinkronizatu kolekzio hau + irakurri-soilik + egutegia + kontaktuak + egunkaria + zereginak + Erakutsi pertsonala soilik + Freskatu zerrenda + Webcal harpidetzak kanpoko aplikazioen bidez sinkroniza daitezke. + Ez da Webcal-ekin bateragarria den aplikaziorik aurkitu + Instalatu ICSx⁵ + + Gehitu kontua + pribatutasun politika.]]> + Saio-hasiera orokorra + Hornitzailearen berariazko saio-hasiera + Jarraitu + Saioa hasi + Saioa hasi helbide elektronikoarekin + Helbide elektornikoa + Baliozko eposta beharrezkoa da + Zerbitzuak aurkitzen dira DNS erregistroak eta URL ezagunak erabilita.]]> + Pasahitza + Ezkutatu pasahitza + Erakutsi pasahitza + Pasahitza (aukerakoa) + Saioa hasi URL eta erabiltzaile izenarekin + Erabiltzaile izena + Erabiltzaile izena (aukerakoa) + Oinarri URL + zerbitzuak ere aurkitzen dira DNS erregistroak eta URL ezagunak erabilita.]]> + Aukeratu ziurtagiria + Gehitu kontua + Kontuaren izena + Apostrofoak (\') erabiltzeak gailu batzuetan.arazoak sortzen dituela dirudi. + Erabili zure eposta helbidea kontu izen bezala Androidek kontuaren izena ANTOLATZAILE eremuan ezarriko duelako sortzen dituzun gertaerentzako. Ezin dituzu bi kontu izen berdinarekin eduki. + Kontaktuen taldekatze metodoa: + Kontuaren izena beharrezkoa + Kontuaren izena hartuta dago + Ezin izan da kontua gehitu + Bukatu + Saio-hasiera aurreratua + Bezero-ziurtagiririk gabe (aukerakoa) + Bezeroaren ziurtagiria: %s + Ez da ziurtagiririk aurkitu + Instalatu ziurtagiria + Fastmail + Fastmail kontua + Ireki saioa Fastmail-ekin + Google Kontaktuak / Egutegia + Google kontua + Hasi saioa Google-rekin + Bezeroaren ID (aukerazkoa) + Pribatutasun politika xehetasunetarako.]]> + <![CDATA[%1$s -k <a href="%2$s">Google API Zerbitzuen Erabiltzaileen Datuen Gidalerroak</a>, erabilera mugatuko eskakizunak barne.]]. + Ezin izan da baimen-kodea lortu + Nextcloud + Hasi saioa Nextcloud-ekin + Honek Nextcloud Flow saio-hasiera abiaraziko du web-nabigatzaile batean. + Nextcloud zerbitzariaren helbidea + Hasi saioa + Ezin izan da saio-hasieraren URLa lortu + Ezin izan dira saio-hasierako datuak lortu + Konfigurazio detekzioa + Mesedez itxaron, zerbitzaria kontsultatzen... + Ezin izan da CalDAV edo CardDAV zerbitzua aurkitu. + Oinarrizko URLa ez dirudi CalDAV/CardDAV URL eskuragarria denik eta zerbitzuaren detekzioa ez da gauzatu. + gure probatutako zerbitzuen zerrenda eta haien oinarrizko URLak.]]> + Mesedez, egiaztatu autentifikazioa ere (normalean erabiltzaile-izena eta pasahitza). + Informazio tekniko gehiago eskuragarri dago erregistroetan. + Ikusi egunkariak + + Sinkronizazioa + Kontaktuen sink. tartea + Eskuz soilik + %d minuturo + berehala aldaketa lokaletan + Egutegien sink. tartea + Zereginen sink. tartea + + Eskuz soilik + 15 minuturo + 30 minuturo + Ordu batero + 2 orduro + 4 orduro + Egunero + + Sinkronizatu soilik WiFi bidez + Sinkronizazioa WiFi konexioetara murriztuta dago + Konexio mota ez da kontuan hartzen + WiFi SSID murriztapena + %s(r)en bidez soilik sinkronizatuko du + WiFi konexio guztiak erabiliko dira + Komaz banatutako izenak (SSIDak) baimendutako WiFi sareentzako (utzi hutsik denentzako) + WiFi SSID murriztapenak ezarpen gehiago behar ditu + Kudeatu + VPN-k azpiko Internet behar du + Interneterako konexio balioztatu gabeko VPN ez da nahikoa sinkronizazioa exekutatzeko (gomendatua) + Interneterako konexio balioztatu gabeko VPN nahikoa da sinkronizazioa exekutatzeko + Autentifikazioa + Erabiltzaile izena + Pasahitza edo aplikazioaren pasahitza + aplikazio pasahitzaerabili .]]> + Pasahitz berria + Eguneratu pasahitza zure zerbitzariaren arabera + Baimendu berriro (OAuth) + Erabili sarbidea ukatu denean + Baimena eskuratu da + Bezeroaren ziurtagiria + Ez dago ziurtagiririk eskuragarri edo hautaturik + Instalatu ziurtagiria + CalDAV + Aurreko gertaeren denbora muga + Ekintza guztiak sinkronizatuko dira + + Egun bat baino gehiago dituzten gertaerak ezikusiko dira + %d egun baino gehiago dituzten gertaerak ezikusiko dira + + Orain dela egun hauek gertatu ziren gertaerak ezikusiko dira (0 izan daiteke). Utzi hutsik gertaera guztiak sinkronizatzeko. + Abisu lehentsia + + Abisu lehenetsia gertaera baino minutu bat lehenago + Abisu lehenetsia gertaera baino %d minutu lehenago + + Ez dago abisu lehenetsirik sortuta + Abisu lehenetsiak sortu behar badira abisurik gabeko gertaerentzat: nahi den minutu kopurua gertaera baino lehen. Utzi hutsik abisu lehenetsiak desgaitzeko. + Kudeatu egutegi koloreak + Egutegiaren koloreak sinkronizazio bakoitzean berrezarten dira + Egutegiaren koloreak beste aplikazio batzuetatik ezarri daitezke + Gertaera kolore bateragarritasuna + Gertaera koloreak sinkronizatuta daude + Gertaera koloreak ez daude sinkronizatuta + CardDAV + Kontaktu taldekatze metodoa + + Taldeak vCard banatuak dira + Taldeak kontaktu bakoitzeko kategoriak dira + + + Sortu helbide liburua + Baliteke zerbitzariak ez onartzea CardDAV bidez sortutako helbide-liburua. + Sortu egutegia + Lehenetsitako ordu-zona (aukerakoa) + + Egutegi sarrera posibleak + Gertaerak + Zereginak + Notak / egunkaria + Baliteke zerbitzariak ez onartzea CalDAV bidez sortutako egutegia. + Kolorea + Izenburua + Biltegiratze kokapena + Deskribapena (aukerakoa) + Sortu + + kontaktuak + gertakizunak + zereginak + Ezabatu kolekzioa + Bilduma hau (%s) eta bere datu guztiak behin betiko kenduko dira, bai lokaletik bai zerbitzaritik. + Sinkronizazioa + Sinkronizazioa gaitua dago + Sinkronizazioa desgaitua dago + Irakurtzeko soilik + Irakurtzeko soilik (zerbitzarian) + Irakurtzeko soilik (politikengatik) + Irakurtzeko soilik (lokala soilik) + Irakurri/idatzi + Izenburua + Deskribapena + Jabea + Bultzaden onarpena + Zerbitzariak bultzaden laguntza iragartzen du + %1$s-(e)n harpidetuta, %2$s iraungitzen da + Azken sinkronizazioa (%s) + Helbidea (URL) + + Arazketa informazioa + ZIP artxiboa + Arazte informazioa eta erregistroak ditu + Partekatu artxiboa ordenagailu batera transferitzeko, e-posta edo ticket baten bidez bidaltzeko. + Partekatu artxiboa + Arazketa informazioa mezuan erantsiko da (hartuko dituen aplikazioak eranskinak onartu behar ditu). + HTTP errorea + Zerbitzari errorea + WebDAV errorea + S/I errorea + Ikusi xehetasunak + Arazketa informazioa lortu da + Parte hartzen duten baliabideak + Arazoarekin erlazionatuta + Kanpoko baliabidea: + Baliabide lokala: + Egunkariak + Erregistro xehetuak eskuragarri daude + Ikusi egunkariak + Kopiatu URL + Pribatutasun oharra + Erregistroek eta arazketa-informazioak informazio pribatua izan dezakete. Kontuan izan hau publikoki partekatzerakoan. + + Errore bat gertatu da + HTTP errore bat gertatu da. + S/I errore bat gertatu da. + Erakutsi xehetasunak + + WebDAV muntaiak + Erabilitako kuota: %1$s / eskuragarri: %2$s + Partekatu edukia + Desmuntatu + Gehitu WebDAV muntaia + Atzitu zure cloud fitxategiak zuzenean WebDAV muntaia bat gehitzen! + nola funtzionatzen du WebDAVek.]]> + Bistaratze-izena + WebDAV URL + URL baliogabea + Muntatu puntua eta bistaratu izena + Autentifikazioa + Erabiltzaile izena + Pasahitza + Erabiltzaile izena (aukerakoa) + Pasahitza (aukerakoa) + Gehitu muntaia + Ez dago WebDAV zerbitzurik URL honetan + Kendu muntaia-puntua + Konexio xehetasunak galduko dira, baina ez da fitxategirik ezabatuko. + WebDAV fitxategia atzitzen + WebDAV fitxategia deskargatzen + WebDAV fitxategia kargatzen + WebDAV muntaia + + DAVx⁵ baimenak + Baimen gehigarriak beharrezkoak + %s zaharregia + Beharrezko bertsio minimoa: %1$s + Autentifikazioak huts egin du (egiaztatu saio-hasiera kredentzialak) + Sare edo S/I errorea – %s + HTTP zerbitzari-errorea – %s + Biltegiratze lokal errorea – %s + Errore leuna (saiakera maximora heldu da) + Kontaktu baliogabea jaso da zerbitzaritik + Gertaera baliogabea jaso da zerbitzaritik + Zeregin baliogabea jaso da zerbitzaritik + Baliabide baliogabe bat edo gehiago ezikusten + Sinkronizazioa zain + Urruneko datuak aldatu egin dria + + Sinkronizatu guztiak + Sinkronizatu kontu guztiak + Etiketatutako sinkronizazio botoia + Ikonotu Sinkronizatu botoia + Sakatu sinkronizazioa eskuz exekutatzeko. + + diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml new file mode 100644 index 0000000..142422d --- /dev/null +++ b/app/src/main/res/values-fa/strings.xml @@ -0,0 +1,329 @@ + + + + حساب کاربری موجود نیست (بیشتر از این) + کتاب آدرس DAVx⁵ + حذف + لغو + فعال + این قسمت الزامیست + راهنما + اشتراک گذاری + پایگاه داده، دارای مشکل است + تمام حساب های کاربری حذف شدند. + خطایابی + پیام های مهم دیگر + پیامهای وضعیت با اولویت پایین + همگام‌سازی + خطای همگام‌سازی + خطاهای مهمی که همگام سازی را مانند پاسخ های غیر منتظره سرور متوقف می کند + هشدارهای همگام‌سازی + مشکلات همگام سازی مانند برخی از پرونده های نامعتبر + شبکه و خطاهای ورودی خروجی + مهلت زمانی ، مشکلات اتصال و غیره (اغلب موقت) + + داده های شما، انتخاب شما + کنترل را بدست بگیرید + فواصل همگام سازی منظم + برای همگام سازی در فواصل منظم ، باید %s در پس زمینه اجرا شود. در غیر این صورت ، اندروید ممکن است هماهنگ سازی را متوقف کند. + من به فواصل همگام سازی منظم نیاز ندارم. * + سازگاری %s + من تنظیمات مورد نیاز را انجام داده ام. دیگر به من یادآوری نکن. * + * علامت را بردارید تا بعداً یادآوری شود. در تنظیمات برنامه %s قابل تنظیم مجدد است. + اطلاعات بیشتر + jtx Board + + پشتیبانی فعالیت ها + اگر وظایف توسط سرور شما پشتیبانی شود، می توان آنها را همگام سازی کرد: + وظایف را باز کنید + عدم توسعه توسط برنامه نویسان - توصیه نمیشود. + Tasks.org + فروشگاه در دسترس نیست + من به پشتیبانی وظایف نیاز ندارم. * + برنامه‌های متن باز + ما خوشحالیم که شما از %sکه یک نرم افزار منبع باز است استفاده می‌کنید. توسعه، نگهداری و پشتیبانی کار سختی است. لطفاً با یک کمک مالی (راه های زیادی وجود دارد) کمک کنید. بسیار ممنون می‌شویم! + نحوه کمک / اهدا + + مجوزها + %s برای کارکرد صحیح به این مجوزها نیاز دارد. + همه موارد زیر + برای فعال کردن همه ویژگی ها از این (توصیه می شود) استفاده کنید + همه مجوزها اعطا شده است + مجوزهای مخاطبین + بدون همگام سازی تماس (توصیه نمی شود) + همگام سازی تماس امکان پذیر است + مجوزهای تقویم + همگام سازی تقویم انجام نشود (توصیه نمی شود) + همگام سازی تقویم امکان پذیر است + مجوز اعلان + اعلان ها غیرفعال اند (توصیه نمی شود). + اعلان ها فعال اند. + مجوز برنامه jtx Board + مجوزهای OpenTasks + مجوز فعالیت ها + عدم همگام سازی وظایف + امکان همگام سازی وظایف وجود دارد + نگاه داری مجوزها + مجوزها ممکن است به طور خودکار تنظیم مجدد شوند (توصیه نمی شود) + مجوز به طور خودکار تنظیم مجدد نخواهد شد + روی مجوزها کلیک کنید> علامت \"حذف مجوزها در صورت استفاده نشدن برنامه\" را بردارید + اگر سوییچ کار نمی کند ، از تنظیمات / مجوزهای برنامه استفاده کنید. + تنظیمات برنامه + + مجوزهای SSID WiFi + برای دسترسی به نام WiFi فعلی (SSID) ، باید این شرایط را داشته باشید: + مجوز موقعیت دقیق + مجوز مکان اعطا شده است + مجوز مکان رد شد + مجوز مکان پس زمینه + همیشه اجازه دهید + مکان همیشه فعال است + سرویس مکان فعال است + سرویس مکان غیرفعال است + + ترجمه ها + کتابخانه ها + ورژن %1$s (%2$d) + این برنامه کاملاً بدون ضمانت است. این یک نرم افزار رایگان است ، و شما می توانید تحت شرایط خاص توزیع مجدد آن را انجام دهید. + + پرونده ثبت ایجاد نشد + اکنون همه %s فعالیت ها را ثبت می کنید + مشاهده / اشتراک گذاری + غیر فعال + + همگام سازی CalDAV / CardDAV + درباره / مجوز + بازخورد بتا + لطفاً یک مرورگر وب نصب کنید + تنظیمات + اخبار و amp؛ به روز رسانی + ابزارها + لینک های خارجی + سایت اینترنتی + دستی + FAQ + انجمن + سیاست حفظ حریم خصوصی + همگام سازی همه حساب‌ها + + اعلان ها غیر فعال اند. از همگام سازی با خبر نخواهید شد. + مدیریت ارتباطات + محافظ داده فعال است. همگام سازی در پس زمینه محدود می شود. + مدیریت محافظ داده + مدیریت حافظه + + تشخیص سرویس ناموفق بود + لیست مجموعه به روز نشد + + در حال اجرا در پیش زمینه + در برخی از دستگاه ها ، این مورد برای همگام سازی خودکار لازم است. + + تنظیمات + اشکال زدایی + نمایش اطلاعات اشکال زدایی + ورود به سیستم + ورود به سیستم غیرفعال است + بهینه ساز باتری + ارتباط + نوع پروکسی + + پیش فرض سیستم + بدون پروکسی + HTTP + ساکس (برای Orbot) + + هاست پروکسی + پورت پروکسی + امنیت + مجوزهای برنامه + مجوزهای لازم برای همگام سازی را مرور کنید + به گواهینامه های سیستم بی اعتماد باشید + سیستم و CA های اضافه شده توسط کاربر قابل اعتماد نخواهند بود + سیستم و CA های اضافه شده توسط کاربر قابل اعتماد خواهند بود (توصیه می شود) + تنظیم مجدد گواهی های مورد اعتماد و یا غیر قابل اعتماد + اعتماد همه گواهینامه های سفارشی را تنظیم مجدد می‌کند + همه گواهینامه های سفارشی پاک شده اند + رابط کاربر + تنظیمات اعلان + کانال های اعلان و تنظیمات آنها را مدیریت کنید + انتخاب تم + + پیش فرض سیستم + روشن + تیره + + تنظیم مجدد نکات + نکاتی را که قبلاً رد شده اند دوباره فعال می کند + همه نکات دوباره نشان داده خواهد شد + ادغام + برنامه مدیریت فعالیت ها + برنامه سازگار یافت نشد + + CardDAV + CalDAV + Webcal + اکنون همگام سازی کنید + تنظیمات حساب + تغییر نام حساب + تغییر نام + نام حساب قبلاً گرفته شده است + نمی‌توانید نام حساب را تغییر دهید + حذف حساب + واقعاً حساب حذف شود؟ + همه نسخه های محلی کتاب آدرس ، تقویم ها و لیست کارها حذف می شوند. + همگام سازی این مجموعه + فقط خواندنی + تقویم + وقایع + فقط شخصی ها را نمایش بده + هیچ برنامه ای با قابلیت Webcal پیدا نشد + ICSx⁵ را نصب کنید + + افزودن حساب + وارد شدن + با آدرس ایمیل وارد شوید + آدرس پست الکترونیکی + آدرس ایمیل معتبر لازم است + گذرواژه + با URL و نام کاربری وارد شوید + نام کاربری + آدرس پایه + گواهی را انتخاب کنید + افزودن حساب + عنوان حساب + از آدرس ایمیل خود به عنوان نام حساب استفاده کنید زیرا Android از نام حساب به عنوان قسمت ORGANIZER برای رویدادهایی که ایجاد می کنید استفاده خواهد کرد. نمی توانید دو حساب با یک نام داشته باشید. + روش گروه تماس: + نام حساب لازم است + نام حساب قبلاً گرفته شده است + گواهی یافت نشد + نصب گواهی + تشخیص پیکربندی + لطفا صبر کنید، پرس و جو سرور ... + سرویس CalDAV یا CardDAV پیدا نشد. + دیدن رویدادها + + همگام سازی + همگام سازی مخاطبین + فقط دستی + هر %d دقیقه + بلافاصله با تغییرات محلی + همگام سازی تقویم ها + همگام سازی فعالیت ها + + فقط به صورت دستی + هر ۱۵ دقیقه + هر ۳۰ دقیقه + هر ساعت + هر ۲ ساعت + هر ۴ ساعت + یک‌بار در روز + + همگام سازی فقط از طریق WiFi + همگام سازی محدود به اتصالات WiFi است + نوع اتصال در نظر گرفته نمی شود + محدودیت SSID WiFi + فقط بیش از %s همگام سازی می شود + از تمام اتصالات WiFi استفاده خواهد شد + نام های جدا شده با کاما (SSID) شبکه های WiFi مجاز (برای همه خالی بگذارید) + محدودیت WiFi SSID به تنظیمات بیشتری نیاز دارد + مدیریت + احراز هویت + نام کاربری + رمز عبور را با توجه به سرور خود به روز کنید. + نصب گواهی + CalDAV + محدودیت زمانی رویداد گذشته + همه رویدادها همگام سازی می شوند + + رخدادهایی که بیش از یک روز از گذشتنشان می‌گذرد نادیده گرفته می‌شوند + رویدادهای بیش از٪ d روز گذشته نادیده گرفته خواهد شد + + رویدادهایی که بیش از این تعداد روز در گذشته باشد نادیده گرفته می شوند (ممکن است 0 باشد). برای همگام سازی همه رویدادها خالی بگذارید. + یادآوری پیش فرض + + یادآوری پیش‌فرض یک دقیقه پیش از رویداد + یادآوری پیش‌فرض %d دقیقه پیش از رویداد + + هیچ یادآوری پیش فرض ایجاد نمی شود + اگر یادآوری های پیش فرض برای رویدادهای بدون یادآوری ایجاد شود: به میزان دلخواه دقیقه قبل از رویداد. برای غیرفعال کردن یادآوری های پیش فرض ، خالی بگذارید. + رنگ های تقویم را مدیریت کنید + رنگ تقویم در هر همگام سازی تنظیم مجدد می شود + رنگ های تقویم را می توان توسط برنامه های دیگر تنظیم کرد + پشتیبانی از رنگ رویداد + رنگهای رویداد همگام سازی می شوند + رنگهای رویداد همگام سازی نمی شوند + CardDAV + روش گروه تماس + + گروه‌ها کارت‌های مجازی جداگانه هستند + گروه ها دسته های هر مخاطب هستند + + + ایجاد دفترچه آدرس + تقویم ایجاد کنید + ورودی های احتمالی تقویم + رویدادها + فعالیت ها + یادداشت ها / ژورنال + رنگ + عنوان + محل ذخیره سازی + ایجاد + + حذف مجموعه + همگام سازی + عنوان + شرح + + اطلاعات اشکال زدایی + آرشیوسازی ZIP + حاوی اطلاعات و پیغام های دیباگ + اشتراک گذاری فایل آرشیو شده، جهت انتقال به کامپیوتر، ارسال از طریق ایمیل و افزودن به تیکت پشتیبانی + اشتراک گذاری آرشیو + اطلاعات اشکال زدایی پیوست شده به این پیام (نیاز به پشتیبانی پیوست از برنامه دریافت کننده دارد). + خطای HTTP + خطای سرور + خطای WebDAV + خطای ورودی خروجی + نمایش جزئیات + اطلاعات اشکال زدایی جمع آوری شده است + منابع درگیر + مربوط به مشکل است + منبع از راه دور: + منبع محلی: + رویدادها + رویدادهای مربوط به گفتار موجود است + دیدن رویدادها + URL را کپی کنید + + خطایی رخ داده است. + خطای HTTP رخ داده است. + خطای ورودی خروجی رخ داده است. + نمایش جزئیات + + اشتراک گذاری محتوا + نام نمایشی + URL نامعتبر + احراز هویت + نام کاربری + گذرواژه + اتصال قطع میگردد، اما هیچ فایلی حذف نخواهد شد. + دسترسی به فایل WebDAB + بارگیری فایل WebDAV + بارگذاری فایل WebDAV + + مجوزهای همگام‌ساز DAVx⁵ + مجوزهای اضافی لازم است + %s خیلی قدیمی است + حداقل نسخه مورد نیاز: %1$s + احراز هویت ناموفق بود (اعتبار ورود به سیستم را بررسی کنید) + شبکه یا خطای ورودی / خروجی – %s + خطای سرور HTTP – %s + خطای ذخیره سازی محلی – %s + مخاطب نامعتبر از سرور دریافت شد + رویداد نامعتبر از سرور دریافت شد + کار نامعتبر از سرور دریافت شد + نادیده گرفتن یک یا چند منبع نامعتبر + + همگام سازی همه حساب‌ها + + diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml new file mode 100644 index 0000000..08afe72 --- /dev/null +++ b/app/src/main/res/values-fi/strings.xml @@ -0,0 +1,42 @@ + + + + DAVx⁵ + DAVx⁵ Osoitekirja + Osoitekirjat + Apua + Hallitse tilejä + Debuggaus + Muut tärkeät viestit + Synkronointi + Synkronoinnin virheet + Huomattavat virheet jotka estävät synkronoinnin kuten palvelimen odottamattomat vastaukset + Synkronoinnin varoitukset + Ei-kohtalokkaat synkronoinnin ongelmat kuten tietyt virheelliset tiedostot + Verkko ja I/O virheet + Aikakatkaisut, yhteysvirheet, yms. (usein väliaikaisia) + + + + + + + + + + + + Kirjaudu sähköpostilla + Sähköpostiosoite + Salasana + Kirjaudu verkko-osoitteella ja käyttäjänimellä + Käyttäjänimi + + Käyttäjänimi + Salasana + + + + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000..024f0fa --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,437 @@ + + + + Le compte n’existe plus (supprimé) + Carnet d\'adresses DAVx⁵ + Ne modifiez pas votre compte ici ! Utilisez plutôt l\'application pour configurer directement votre compte. + Supprimer + Retirer + Annuler + Activer + Ce champ est requis + Aide + Revenir en haut + Menu d\'options + Partager + Synchronisation démarrée/en file d\'attente + Base de données corrompue + Tous les comptes ont été supprimés localement. + Débogage + Autres messages importants + Messages d’état de faible priorité + Synchronisation + Erreurs de synchronisation + Erreurs importantes qui bloquent la synchronisation, telles que des réponses inattendues du serveur + Avertissements de synchronisation + Problèmes de synchronisation non fatals tels que certains fichiers non valides + Erreurs de réseau et d\'entrée/sortie + Délais d\'attente, problèmes de connexion, etc. (souvent temporaires) + + Vos données, votre choix. + Prenez le contrôle. + Intervalles de synchronisation régulières + Pour une synchronisation à intervalles réguliers, %s doit être autorisé à fonctionner en arrière-plan. Sinon, Android peut interrompre la synchronisation à tout moment. + Je n\'ai pas besoin d\'intervalles de synchronisation réguliers.* + %s compatibilité + J\'ai fait les réglages nécessaires. Ne me le rappelez plus.* + * Laisser non coché pour un rappel ultérieur. Peut être réinitialisé dans les paramètres de l\'application / %s. + Plus d\'informations + jtx Board + + Gestion des taches + Si les tâches sont prises en charge par votre serveur, elles peuvent être synchronisées avec une application de tâches externe : + OpenTasks + Ne semble plus être développé - non recommandé. + Tasks.org + Pas de magasin d\'application disponible + Je n\'ai pas besoin de support des tâches.* + Logiciels open-source + Nous sommes heureux que vous utilisiez %s, qui est un logiciel open-source. Le développement, la maintenance et l\'assistance sont un travail difficile. Veuillez envisager de contribuer (il y a plusieurs façons de le faire) ou de faire un don. Ce serait très apprécié ! + Comment contribuer / donner + Ne me rappelle pas pour + Suivant + + Autorisations + %s nécessite des autorisations pour fonctionner correctement. + Tout autoriser + A utiliser pour activer toutes les fonctionnalités (recommandé) + Toutes les autorisations accordées + Autorisations d\'accès aux contacts + Pas de synchronisation du carnet d\'adresses (non recommandé) + Synchronisation du carnet d\'adresses possible + Autorisations du calendrier + Pas de synchronisation du calendrier (non recommandé) + Synchronisation du calendrier possible + Autorisations des notifications + Notifications désactivées (non recommandé) + Notifications activées + Autorisations jtx Board + Autorisations d\'OpenTasks + Autorisations de Tasks + Aucune tâche de synchro + Synchronisation des tâches possible + Conserver les autorisations + Les autorisations peuvent être réinitialisées automatiquement (non recommandé) + Les autorisations ne seront pas réinitialisées automatiquement + Cliquez sur Autorisations > décochez \"Supprimer les autorisations si l\'application n\'est pas utilisée\". + Si un commutateur ne fonctionne pas, utiliser les paramètres de l\'application / Autorisations + Paramètres de l\'application + + WiFi SSID autorisations + Pour pouvoir accéder au nom du WiFi actuel (SSID), ces conditions doivent être remplies : + Permission de localisation précise + Autorisation de localisation accordée + Autorisation de localisation refusée + Autorisation de localisation en arrière-plan + Autorisez tout le temps + Autorisation de localisation réglée sur : %s + Autorisation de localisation non réglée sur : %s + %sutilise vos données de localisation (seulement concernant les SSID de WiFi) uniquement pour restreindre la synchronisation à un SSID WiFi spécifique. Cela se produira même lorsque la synchronisation se fera en tâche de fond. + Les données de localisation (seulement les SSID WiFi) ne sont utilisées que localement et ne sont envoyées nulle part. + Localisation toujours activée + Le service de localisation est activé + Le service de localisation est désactivé + + Traductions + Librairies + Version %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) et les contributeurs + Ce programme est fourni sans AUCUNE GARANTIE. C\'est un logiciel libre, et vous êtes en droit de le redistribuer sous certaines conditions. + + Impossible de créer le fichier journal + Maintenant toutes les activités de %s seront enregistrées + Voir/partager + Désactiver + + Adaptateur de synchronisation CalDAV/CardDAV + À propos / Licence + Commentaire pour la version Beta + Veuillez installer un navigateur + Paramètres + Actualités & mises à jour + Outils + Liens externes + Site Web + Manuel + Foire aux questions + Communauté + Soutenir le projet + Comment contribuer + Politique de confidentialité + Bienvenue sur DAVx⁵! + Connectez-vous à votre serveur pour garder vos calendriers et contacts synchronisés. + Synchroniser tous les comptes + + Notifications désactivées. Vous ne serez pas averti des erreurs de synchronisation. + La synchronisation automatique n\'est pas active (aucune connexion Internet vérifiée) + Gérer les connexions + L\'économiseur de données est activé. La synchronisation en arrière-plan est limitée. + Gérer l\'économiseur de données + L\'économiseur de batterie est activé. La synchronisation risque d\'être limitée. + Gérer l\'économiseur de batterie + Espace de stockage faible. Android ne synchronisera pas les changements locaux immédiatement mais pendant la prochaine synchronisation. + Gérer le stockage + Fournisseur de calendrier manquant + Avez-vous désactivé l\'application système \'Stockage du calendrier\' ? + + La détection du service a échoué + Impossible d\'actualiser la liste de collection + + Fonctionne au premier plan + Sur certains appareils, cela est nécessaire pour la synchronisation automatique. + + Paramètres + Débogage + Afficher les infos de débogage + Voir/partager les détails de configuration et les logs + Journalisation verbeuse + Les journaux sont activés. Vous pouvez voir les journaux parmi les informations de débogage. + La journalisation est désactivée + Optimisation de la batterie + Pas de restriction (recommandé) + Restriction d\'utilisation de la batterie activée (non recommandé) + Connexion + Type de proxy + + Par défaut (système) + Pas de proxy + HTTP + SOCKS (pour Orbot) + + Nom de l\'hôte du proxy + Port du proxy + Sécurité + Autorisations de l\'application + Consulter les autorisations requises pour la synchronisation + Révoquer les certificats du système + Les certificats du système et ceux ajoutés par l\'utilisateur ne seront pas dignes de confiance + Les certificats du système et ceux ajoutés par l\'utilisateur seront dignes de confiance (recommandé) + Réinitialiser les certificats de (non)confiance + Réinitialiser la confiance de tous les certificats personnalisés + Tous les certificats personnalisés ont été effacés + Interface utilisateur + Paramètres de notification + Gérer les canaux de notification et leurs paramètres + Sélectionner un thème + + Par défaut + Clair + Sombre + + Réinitialiser les astuces + Réactiver les astuces qui ont été vues précédemment + Toutes les astuces seront affichés à nouveau + Intégration + Applications de gestion de tâches + Aucune application de tâches compatibles trouvée + UnifiedPush (expérimental) + Aucun point de terminaison (endpoint) configuré + + Carnets d\'adresses (CardDAV) + Agendas (CalDAV) + WebCal + Des autorisations supplémentaires sont nécessaires pour synchroniser ces collections. + Gérer les autorisations + Synchroniser maintenant + Paramètres du compte + Renommer le compte + Les données locales non sauvegardées risquent d\'être perdues. Une resynchronisation est nécessaire après le renommage. + Nouveau nom de compte + Renommer + Le nom du compte est déjà pris + Impossible de renommer le compte + Supprimer le compte + Voulez-vous vraiment supprimer le compte ? + Toutes les copies locales des carnets d\'adresses, des calendriers et des listes de tâches seront supprimées. + Synchroniser cette collection + En lecture seulement + calendrier + contacts + journal + tâches + N\'afficher que les comptes personnels + Rafraîchir la liste + Les abonnements Webcal peuvent être synchronisés avec des applications tierces. + Aucune application compatible WebCal + Installer ICSx⁵ + + Ajouter un compte + la politique de confidentialité.]]> + Connexion standard + Connexion par fournisseur spécifique + Suivant + Se connecter + Connexion avec une adresse de courriel + Adresse de courriel + Une adresse de courriel valide est requise + Les services sont découverts en utilisant les enregistrements DNS et les URL connues.]]> + Mot de passe + Cacher le mot de passe + Montrer le mot de passe + Connexion avec une URL et un nom d\'utilisateur + Nom d\'utilisateur + URL de base + les services sont aussi découverts en utilisant les enregistrements DNS et les URL connues.]]> + Choisir le certificat + Ajouter un compte + Nom du compte + L\'utilisation d\'apostrophe (\') semble poser problème sur certains appareils. + Utilisez votre adresse de courriel comme nom de compte car Android utilisera ce nom en tant que champ ORGANISATEUR pour les événements que vous créerez. Vous ne pouvez pas avoir deux comptes avec le même nom. + Méthode pour les contacts de type groupe : + Nom du compte requis + Le nom du compte est déjà pris + Le compte n\'a pas pu être ajouté + Finis + Connexion avancée + Certificat client : %s + Aucun certificat trouvé + Installer un certificat + Google Contacts / Agenda + Compte Google + Se connecter avec Google + ID client (optionnel) + Politique de confidentialité.]]> + politique des données utilisateur des services API Google, incluant les exigences de limitation d\'utilisation.]]> + N\'a pas pu obtenir le code d\'autorisation + Nextcloud + Se connecter avec Nextcloud + La page de connexion Nextcloud va se lancer dans le navigateur + Adresse du serveur Nextcloud + Se connecter + Impossible d\'obtenir l\'URL de connexion + Impossible d\'obtenir les données de connexion + Détection de la configuration + Veuillez patienter, nous interrogeons le serveur … + Aucun accès possible au service CalDAV ou CardDAV. + L\'URL de base ne semble pas être une URL CalDAV/CardDAV et la détection du service a échoué. + notre liste de services testés ainsi que leurs URL de base.]]> + Merci de bien vérifier l\'authentification (souvent le nom d\'utilisateur et le mot de passe). + Plus d\'information technique est disponible dans les journaux. + Voir les journaux + + Synchronisation + Intervalle de synchronisation des carnets d\'adresses + Manuellement + Toutes les %d minutes et immédiatement après un changement local + Intervalle de synchronisation des agendas + Intervalle de synchronisation des tâches + + Manuellement + Tous les quarts d\'heure + Toutes les demi-heures + Toutes les heures + Toutes les deux heures + Toutes les quatre heures + Une fois par jour + + Synchronisation en Wifi seulement + La synchronisation est limitée aux connexions WiFi + Le type de connexion n\'est pas pris en charge + Restriction WiFi SSID + Synchronisation possible seulement en %s + Toutes les connexions WiFi seront utilisées + Liste des points d\'accès WiFi (SSID) autorisés, séparés par des virgules. (Laissez vide pour tous) + La restriction du SSID WiFi nécessite des réglages supplémentaires + Gérer + Le VPN nécessite un accès Internet fonctionnel + Un VPN sans connexion Internet fonctionnelle ne permet pas de lancer une synchronisation (recommandé) + Un VPN sans connexion Internet fonctionnelle peut lancer une synchronisation + Authentification + Nom d\'utilisateur + Nouveau mot de passe + Mettre à jour le mot de passe + Certificat client + Aucun certificat disponible ou sélectionné + Installer un certificat + CalDAV + Limite des événements passés + Tous les événements seront synchronisés + + Les événements de plus d’un jour passé seront ignorés + Les événements de plus de %d jours passés seront ignorés + Les événements de plus de %d jours passés seront ignorés + + Les événements antérieurs à ce nombre de jours seront ignorés (peut être 0). Laissez vide pour synchroniser tous les événements. + Rappel par défaut + + Rappel par défaut une minute avant l\'événement + Rappel par défaut %d minutes avant les événements + Rappel par défaut %d minutes avant les événements + + Aucun rappel par défaut + Si des rappels par défaut doivent être créé pour des événements sans rappel: le nombre de minutes avant l\'événement. Laisser blanc pour désactiver les rappels par défaut. + Choisir la couleur du calendrier + Les couleurs du calendrier sont réinitialisées à chaque synchronisation + Les couleurs du calendrier peuvent être définies par d\'autres applications + Couleur associée aux événements + Les couleurs des événements sont synchronisées + Les couleurs des événements sont pas synchronisées + CardDAV + Méthode pour les contacts de type groupe + + Les groupes sont des VCards indépendantes + Les groupes sont des catégories pour chacun des contacts + + + Créer un carnet d\'adresses + La création de carnet d\'adresses via CardDAV n\'est peut-être pas supportée par ce serveur. + Créer un calendrier + + Entrées possibles de calendrier + Événements + Tâches + Notes / journal + La création de calendrier via CalDAV n\'est peut-être pas supportée par ce serveur. + Couleur + Titre + Emplacement de stockage + Description (facultative) + Créer + + contacts + tâches + Supprimer la collection + Cette collection (%s) et toutes ses données vont être définitivement supprimées, sur cet appareil et sur le serveur. + Synchronisation + Synchronisation activée + Synchronisation désactivée + Lecture seule + Lecture seule (côté serveur) + Lecture seule (selon les politiques) + Lecture seule (sur cet appareil) + Lecture / écriture + Titre + Description + Responsable + Support du \'Push\' + Le serveur annonce supporter \'Push\' + Dernière synchro (%s) + Adresses (URL) + + Infos de débogage + Archive ZIP + Contient des informations de débogage et des journaux + Partagez l’archive pour la transférer sur un ordinateur, pour l’envoyer par courriel ou pour la joindre à un ticket de support. + Partager l\'archive + Informations de débogage jointes à ce message (nécessite une application compatible). + Erreur HTTP + Erreur Serveur + Erreur WebDAV + Erreur d\'entrée/sortie + Voir les détails + Les informations de débogage ont été collectées + Ressources impliquées + En rapport avec le problème + Ressource à distance : + Ressource locale : + Journaux + Des journaux verbeux sont disponibles + Voir les journaux + Copier l\'URL + + Une erreur est survenue. + Une erreur HTTP est survenue. + Une erreur d\'entrée/sortie est survenue. + Voir détails + + Points de montage WebDAV + Quota utilisé : %1$s / disponible : %2$s + Partager le contenu + Démonter + Ajouter un point de montage WebDAV + Accédez directement à vos fichiers du cloud en ajoutant un point de montage WebDAV ! + Nom affiché + URL WebDAV + URL incorrecte + Authentification + Nom d\'utilisateur + Mot de passe + Ajouter un point de montage + Aucun service WebDAV à cette URL + Retirer le point de montage + Les détails de la connexion seront perdus, mais aucun fichier ne sera supprimé. + Accès au fichier WebDAV + Téléchargement du fichier WebDAV + Téléversement du fichier WebDAV + Point de montage WebDAV + + Autorisations DAVx⁵ + Autorisations supplémentaires demandées + %s trop ancien + Version minimale requise : %1$s + Echec de connexion (vérifier vos identifiants de connexion) + Erreur de réseau ou d\'entrée/sortie - %s + Erreur de serveur HTTP - %s + Erreur de stockage local - %s + Erreur logicielle (nombre maximum de tentatives atteint) + Reçu un contact invalide du serveur + Reçu un événement invalide du serveur + Reçu une tâche invalide du serveur + Ignorer une ou plusieurs ressources non valides + Synchronisation en attente + Les informations à distance ont changé + + Tout synchroniser + Synchroniser tous les comptes + + diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml new file mode 100644 index 0000000..83b1013 --- /dev/null +++ b/app/src/main/res/values-gl/strings.xml @@ -0,0 +1,450 @@ + + + + A conta non existe (definitivamente) + Libreta de enderezos DAVx⁵ + Non cambies de conta aquí! Usa directamente a aplicación para xestonar as contas. + Eliminar + Eliminar + Desbotar + Activar + Este campo é requerido + Axuda + Ir arriba + Menú de opcións + Compartir + Sincronización iniciada/en agarda + Base de datos estragada + Elimináronse todas as contas de xeito local + Depurando + Outras mensaxes importantes + Mensaxes de estado de baixa prioridade + Sincronización + Fallos na sincronización + Erros importantes como respostas non agardadas do servidor que deteñen a sincronización + Avisos sobre a sincronización + Problemas non-fatais de sincronización como certos ficheiros non válidos + Fallos de rede e I/O + Caducidades, problemas de conexión, etc. (soen ser temporais) + + Os teus datos. Ti elixes. + Toma o control. + Intervalos regulares de sincronización + Para sincronizar a intervalos regulares, %s debe ter permiso para executarse en segundo plano. Se non, Android podería deter a sincronización. + Non preciso sincr. con regularidade.* + Compatibilidade de %s + Xa fixen o que me pediades. Non mo lembres máis.* + * Deixar sen marcar para lembrar máis tarde. Pode restablecerse nos axustes da app / %s. + Máis información + jtx Board + + Soporte para Tasks + Se as tarefas están soportadas polo teu servidor, poden ser sincronizadas cunha app que soporte tarefas: + OpenTasks + Semella que xa non está en desenvolvemento – non recomendado. + Task.org + Non hai compatibilidade para algunhas características]]> + Sen tenda de apps dispoñible + Non necesito soporte para tarefas.* + Software de código aberto + Encántanos que uses %s, que é software de código aberto. O desenvolvemento, mantemento e soporte son un traballo difícil. Considera colaborar (hai moitos xeitos) ou facer unha doazón. Sería de agradecer! + Como contribuír/doar + Non mo lembres durante + + %d mes + %d meses + + Seguinte + + Permisos + %s require permisos para funcionar axeitadamente. + Todos os de abaixo + Usa isto para habilitar tódalas características (recomendado) + Todos os permisos concedidos + Permisos de contactos + Sen sincronización de contactos (non recomendado) + É posible sincronizar os contactos + Permisos de calendario + Sen sincronización de calendario (non se recomenda) + É posible sincronizar o calendario + Permiso de notificacións + Notificacións desactivadas (non recomendado) + Notificacións activadas + permisos para jtx Board + Permisos para OpenTasks + Permisos para Tasks + Tarefas non sincr. + É posible sincronizar as tarefas + Manter permisos + Os permisos poden restablecerse automáticamente (non recomendado) + Os permisos non se restablecerán automáticamente + Preme en Permisos > desmarca \"Eliminar permisos se a app non se usa\" + Se unha opción non funciona, usa axustes da aplicación / Permisos. + Permisos da aplicación + + Permisos WiFi SSID + Para poder acceder á WiFi (SSID) actual, deben darse estas condicións: + Permiso para localización precisa + Permiso de localización outorgado + Permiso de localización denegado + Permiso de localización en segundo plano + Permitir en todo momento + Permiso de localización establecido como: %s + O permiso de localización non é: %s + %s usa datos de localización (só WiFi SSID) có ánimo de limitar a sincronización a unha rede WifFi concreta. Isto acontece incluso cando a sincronización se realiza en segundo plano. + Todos os datos de localización (WiFi SSID) só se usan de xeito local e non se envían a ningures. + Localización sempre activada + Servizo de localización activado + Servizo de localización desactivado + + Traducións + Bibliotecas + Versión %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) e colaboradoras + Este programa non proporciona NINGUNHA GARANTÍA. É software libre, e convidámoste a redistribuílo baixo certas condicións. + + Non se creou ficheiro de rexistro + Rexistrando todas as %s actividades + Ver/compartir + Desactivar + + Adaptador de sincr. CalDAV/CardDAV + Acerca de / Licenza + Comenta sobre a Beta + Instala un navegador web + Axustes + Novas & actualizacións + Ferramentas + Ligazóns externas + Sitio web + Manual + PMF + Comunidade + Apoia ao proxecto + Como colaborar + Política de Privacidade + Benvida a DAVx⁵! + Conecta co teu servidor e mantén os contactos e calendarios sincronizados. + Sincroniza todas as contas + + Notificacións desactivadas. Non verás os avisos de erro na sincr. + A sincronización automática non está activa (sen conexión a internet verificada). + Xestionar conexións + O aforro de datos está activado. A sincronización en segundo plano está restrinxida. + Xestionar aforro de datos + Activado o aforrador de batería. A sincronización podería estar limitada. + Xestionar aforro da batería + Queda pouco espazo de almacenaxe. Android non vai sincronizar os cambios locais inmediatamente, farao na próxima sincronización regular. + Xestionar almacenaxe + Falta o provedor do calendario + Desactivouse a app do sistema \"Almacenaxe do calendario\"? + Falta o provedor de contactos + Desactivouse a app do sistema \"Almacenaxe de contactos\"? + Xestionar apps + + Fallou a detección do servizo + Non se actualizou a lista da colección + + Executándose en primeiro plano + En algúns dispositivos esto é necesario para a sincronización automática. + + Axustes + Depurando + Mostrar info de depuración + Ver/compartir os detalles da configuración e rexistros + Rexistro polo miúdo + O rexistro está activo. Podes ver os rexistros como parte da info de depuración. + O rexistro está desactivado + Optimización da batería + A app está excluída (recomendado) + Aplícanse restricións da batería (non recomendado) + Conexión + Tipo de Proxy + + Por defecto no sistema + Sen proxy + HTTP + SOCKS (para Orbot) + + Servidor do proxy + Porto do proxy + Seguridade + Permisos da App + Revisa os permisos requeridos para a sincronización + Non confiar en certificados do sistema + Non se confiará nos CAs do sistema ou engadidos por usuaria + Confiarase nos CAs do sistema e engadidos por usuaria (recomendado) + Se este axuste está activo, os certificados do sistema non se consideran de confianza. Isto significa que haberá que aceptar manualmente cada certificado (tamén cando o servidor o renove) ou a configuración e a sincronización non funcionarán. + Restablecer certificados repudiados + Restablece a confianza en todos os certificados personalizados + Todos os certificados personalizados foron admitidos + Interface de usuaria + Axustes das notificacións + Xestiona as canles de notificación e os seus axustes + Elixe decorado + + Por defecto no sistema + Claro + Escuro + + Restablecer consellos + Restablece todos os consellos que foran desbotados anteriormente + Mostraranse todos os consellos de novo + Integración + App Tarefas + Non se atopan app de tarefas compatible + UnifiedPush (experimental) + Ningún (push desactivado) + Elixe un distribuidor + Non hai un distribuidor instalado + Sen punto de acceso configurado + Preparado para recibir mensaxes push desde %s + + CardDAV + CalDAV + Webcal + Requírense permisos adicionais para poder sincronizar estas coleccións. + Xestionar permisos + Sincronizar agora + Axustes da conta + Renomear conta + Poden perderse os datos locais non gardados. Requírese a sincronización após o cambio de nome. + Novo nome da conta + Renomear + O nome de conta xa está a ser utilizado + Non cambiou o nome da conta + Eliminar conta + Desexas eliminar a conta? + Todas as copias locais de libretas de enderezos, calendarios e tarefas eliminaranse. + sincronizar esta colección + só lectura + calendario + contactos + diario + tarefas + Mostrar só personal + Actualizar lista + As subscricións Webcal poden sincronizarse con apps externas. + Non se atopou app para Webcal + Instalar ICSx⁵ + + Engadir conta + política de privacidade.]]> + Acceso xenérico + Acceso específico do provedor + Continuar + Conectar + Conectar con enderezo de correo-e + Enderezo de correo-e + Requíre un enderezo de correo válido + Servizos + usando rexistros DNS de URLs tipo well-known.]]> + Contrasinal + Agochar contrasinal + Mostrar contrasinal + Conectar con URL e nome de usuaria + Nome de usuaria + URL base + servizos tamén se atopan usando rexistros DNS e URLs tipo well-known.]]> + Escoller certificado + Engadir conta + Nome da conta + O uso de apóstrofes (\') semella que causa problemas nalgúns dispositivos. + Utiliza o teu enderezo de correo electrónico como nome de conta xa que Android utilizará o nome da conta como campo ORGANIZADOR para os eventos que cree. Non poderás ter dúas contas co mesmo nome. + Método para agrupar contacto: + Nome de conta requerido + O nome de conta xa está a ser utilizado + Non se puido engadir a conta + Rematar + Acceso avanzado + Certificado cliente: %s + Non se atopa certificado + Instalar certificado + Contactos / Calendario de Google + Conta de Google + Inicia sesión con Google + ID Cliente (optativo) + política de Privacidade para saber máis.]]> + Google API Services User Data Policy, incluíndo os requerimentos de Limited Use.]]> + Non se puido obter o código de autorización + Nextcloud + Acceder con Nextcloud + Accederás usando Nextcloud nun navegador Web. + Enderezo do servidor Nextcloud + Acceder + Non se obtivo o URL de acceso + Non se obtiveron os datos de acceso + Detección da configuración + Agarda por favor, consultando o servidor… + Non se atopou servizo CalDAV ou CardDAV. + O URL base non parece ser accesible. Non se puido detectar o URL do servizo CalDAV/CardDAV. + a nosa lista de servizos comprobados e o seu URL base.]]> + Ademais comproba ben a autenticación (normalmente identificador e contrasinal). + Tes dispoñible máis información técnica nos rexistros. + Ver rexistros + + Sincronización + Intervalo de sincr. contactos + Só manual + Cada %d minutos + tras cambios locais + Intervalo de sincr. calendarios + Intervalo sincr. de tarefas + + Só manual + Cada 15 minutos + Cada 30 minutos + Cada hora + Cada 2 horas + Cada 4 horas + Unha vez ao día + + Sincronizar só con WiFi + Sincronización restrinxida a conexións WiFi + Non se terá en conta o tipo de conexión + Restrición SSID WiFi + Sincr. só en %s + Utilizaranse todas as conexións WiFi + Nome das rede WiFi permitidas (SSIDs) separados por vírgulas (en branco para todas) + A restrición WiFi SSID precisa máis axustes + Xestionar + A VPN require acceso a Internet + VPN, sen ter a conexión a Internet verificada, non é suficiente para lanzar a sincronización (recomendado) + VPN, sen ter a conexión a Internet verificada, é suficiente para lanzar a sincronización + Autenticación + Nome de usuaria + Novo contrasinal + Actualizar o contrasinal de acordo ao teu servidor. + Certificado do cliente + Sen certificado dispoñible ou seleccionado + Instalar certificado + CalDAV + Límite temporal para eventos pasados + Sincronizaranse todos os eventos + + Eventos anteriores a un día serán ignorados + Eventos anteriores a %d días serán ignorados + + Eventos anteriores a máis dos días indicados serán ignorados (podería ser 0). Deixar en branco para sincronizar todos os eventos. + Alarma por omisión + + Alarma por omisión un minuto antes do evento + Alarma por omisión %d minutos antes do evento + + Non se crearon alarmas por omisión + Se deben ser creadas as alarmas por omisión para eventos sen alarma: o número de minutos desexado antes do evento. Deixar baleiro para desactivar alarmas por omisión. + Xestionar cores dos calendarios + As cores do calendario restablécense tras cada sincr. + Outras apps poden establecer as cores do calendario + Soporte para cor de eventos + As cores dos eventos están sincronizadas + As cores dos eventos non están sincronizadas + CardDAV + Método para agrupar contacto + + Grupos son vCards separados + Grupos son categorías por contacto + + + Crear libreta de enderezos + O servidor podería non ter soporte para crear libretas de enderezos usando CardDAV. + Crear calendario + + Entradas posibles no calendario + Eventos + Tarefas + Notas / diario + O servidor podería non ter soporte para crear caledarios usando CalDAV. + Cor + Título + Localización do almacenamento + Descrición (optativo) + Crear + + contactos + tarefas + Eliminar colección + Esta colección (%s) e todos os seus datos serán borrados permanentemente, tanto localmente como no servidor. + Sincronización + Sincronización activada + Sincronización desactivada + Só lectura + Só lectura (polo servidor) + Só-lectura (polas normas) + Só lectura (só localmente) + Lectura/escritura + Título + Descrición + Dona + Soporte Push + O servidor anuncia soporte para Push + Subscrición desde %1$s, caduca o %2$s + Última sincr (%s) + Enderezo (URL) + + Info depuración + Arquivo ZIP + Contén info de depuración e rexistros + Comparte o arquivo para pasalo á computadora, envialo por email ou anexalo a un tícket de axuda + Compartir arquivo + Info de depuración anexa a esta mensaxe (require soporte de anexos na app receptora). + Erro HTTP + Fallo no servidor + Fallo WebDAV + Fallo I/O + Ver detalles + Recolleuse a info de depuración + Recursos implicados + Relacionado co problema + Recurso remoto: + Recurso local: + Rexistros + Están dispoñibles rexistros explicativos + Ver rexistros + Copiar URL + + Algo fallou. + Houbo un fallo HTTP. + Houbo un fallo I/O. + Mostrar detalles + + Montaxes WebDAV + Cota utilizada: %1$s / dispoñible: %2$s + Compartir contido + Desmontar + Engadir montaxe WebDAV + Accede aos teus ficheiros na nube engadindo unha montaxe WebDAV! + Nome mostrado + URL WebDAV + URL inválido + Autenticación + Nome de usuaria + Contrasinal + Engadir montaxe + Neste URL non hai ningún servizo WebDAV + Eliminar punto de montaxe + Perderanse os detalles da conexión, mais non se eliminarán ficheiros. + Accedendo ao ficheiro WebDAV + Descargando ficheiro WebDAV + Subindo ficheiro WebDAV + Montaxe WebDAV + + Permisos DAVx⁵ + Precísanse permisos adicionais + %s demasiado antigo + Versión mínima requerida: %1$s + Fallo na autenticación (verifique credenciais) + Fallo de Rede ou I/O – %s + Fallo servidor HTTP – %s + Fallo almacenamento local – %s + Erro (acadouse o máx. de reintentos) + Recibido contacto non válido desde o servidor + Recibido evento non válido desde o servidor + Recibida tarefa non válida desde o servidor + Ignorando un ou varios recursos non válidos + Sincr pendente + Os datos remotos cambiaron + + Sincr todo + Sincroniza todas as contas + + diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml new file mode 100644 index 0000000..638b745 --- /dev/null +++ b/app/src/main/res/values-hr/strings.xml @@ -0,0 +1,281 @@ + + + + Račun (više) ne postoji + DAVx⁵ Adresar + Omogući + Pomoć + Dijeli + Debugging + Ostale važne poruke + Statusne poruke niskog prioriteta + Sinkronizacija + Sinkronizacijske greške + Važne greške koje zaustavljaju sinkronizaciju poput neočekivanih odgovora poslužitelja + Sinkronizacijska upozorenja + Sinkronizacijski problemi koji nisu fatalni poput određenih nevaljanih datoteka + Mrežne i I/O greške + Vremensko ograničenje, greške povezivanja, itd. (obično privremeno) + + Vaši podaci. Vaš izbor. + Preuzmite kontrolu. + Redoviti intervali sinkronizacije + Za sinkronizaciju u redovitim intervalima, %s mora imati omogućen rad u pozadini. Inače, Android može zaustaviti sinkronizaciju u bilo kojem trenutku. + Ne trebam redovita sinkroniziranja. + %s kompatibilnost + Tražene izmjene su napravljene. Ne podsjećaj me više. + * Ostavi neoznačeno za podsjetnik kasnije. Moguće je resetirati u aplikacijskim postavkama / %s. + Više informacija + Podrška za zadatke + Ukoliko su zadatci podržani od strane vašeg poslužitelja, moguće ih je sinkronizirati sa podržanom aplikacijom za zadatke: + OpenTasks + Trgovina aplikacijama nije dostupna + Ne trebam podršku za zadatke.* + Softver otvorenog koda + Sretni smo što upotrebljavate %s, softver otvorenog koda. Razvoj, održavanje i podrška težak su posao. Molimo razmislite o doprinosu (mnogo je načina) ili o donaciji. Biti ćemo vam vrlo zahvalni! + Kako doprinijeti/donirati + + Dopuštenja + %s treba dopuštenja kako bi radio ispravno. + Sve od ispod + Koristi ovo za aktiviranje svih mogućnosti (preporučeno) + Sva dopuštenja odobrena + Dopuštenja kontakata + Bez sinkronizacije kontakata (nije preporučeno) + Moguća sinkronizacija kontakata + Dopuštenja kalendara + Bez sinkronizacije kalendara (nije preporučeno) + Moguća sinkronizacija kalendara + OpenTasks dopuštenja + Dopuštenja zadataka + Moguća sinkronizacija zadataka + Zadrži dopuštenja + Dopuštenja mogu biti automatski resetirana (nije preporučeno) + Dopuštenja neće biti automatski resetirana + Ukloni dopuštenja ukoliko se aplikacija ne upotrebljava + Ukoliko prekidač ne radi, koristi postavke aplikacije / Dopuštenja. + Postavke aplikacije + + WiFi SSID dopuštenja + Kako bi se pristupilo trenutnom WiFi imenu (SSID), ovi uvjeti trebaju biti zadovoljeni: + Dopuštenja lokacije odobrena + Dopuštenja lokacije odbijena + Pozadinska dopuštenja lokacije + Dopusti cijelo vrijeme + Lokacija uvijek omogućena + Lokacijske usluge su omogućene + Lokacijske usluge su onemogućene + + Prijevodi + Bibiloteke + Verzija %1$s(%2$d) + Ovaj program dolazi BEZ APSOLUTNO BILO KAKVOG JAMSTVA. To je besplatni softver i možete ga distribuirati pod određenim uvjetima. + + Couldn\'t create log file + Now logging all %s activities + Pregledaj/podijeli + Onemogući + + CalDAV/CardDAV Sync Adapter + About / License + Beta feedback + Molimo instalirajte web preglednik + Postavke + Novosti & aktualnosti + Vanjske poveznice + Web stranica + Upute + FAQ + Pravila o zaštiti privatnosti + Sinkroniziraj sve račune + + + Detekcija servisa nije uspjela + Nije moguće osvježiti popis zbirki + + Pokrenuto u prednjem planu + Na nekim uređajima, ovo je neophodno za automatsku sinkronizaciju. + + Postavke + Debugging + Prikaži debug informacije + Verbose logging + Logging je onemogućen + Veza + Sigurnost + Dopuštenja aplikacije + Pregledajte dopuštenja potrebna za sinkronizaciju + Ukidanje povjerenja sistemskim certifikatima + Sistemski i korisnički dodani CA-i neće biti od povjerenja + Sistemski i korisnički dodani CA-i su od povjerenja (preporučeno) + Resetiraj certifikate od (ne)povjerenja + Reseira povjerenje svih prilagođenih certifikata + Svi prilagođeni certifikati su obrisani + Korisničko sučelje + Postavke obavijesti + Uredi izvore obavještavanja i njihove postavke + Resetiraj naznake + Ponovno omogućuje naznake koje su prethodno bile odbačene + Sve naznake će ponovno biti prikazane + Integracija + Tasks aplikacija + Kompatibilna aplikacija za zadatke nije pronađena + + CardDAV + CalDAV + Webcal + Sinkroniziraj sada + Postavke računa + Preimenuj račun + Preimenuj + Naziv računa se već koristi + Račun nije moguće preimenovati + Obriši račun + Stvarno izbrisati račun? + Sve lokalne kopije adresara, kalendara i popisa zadataka biti će izbrisane. + sinkroniziraj ovu zbirku + read-only + kalendar + Prikaži samo osobno + Aplikacija sa Webcal mogućnostima nije pronađena + Instaliraj ICSx⁵ + + Dodaj račun + Prijava + Prijavi se sa adresom e-pošte + Adresa e-pošte + Potrebna je valjana adresa e-pošte + Lozinka + Prijava sa URL-om i korisničkim imenom + Korisničko ime + Osnovni URL + Odaberi certifikat + Dodaj račun + Naziv računa + Koristite svoju adresu e-pošte kao naziv računa jer Android će koristiti naziv računa kao ORGANIZER polje za događaje koje kreirate. Nije moguće imati dva računa sa istim imenom. + Metoda kontaktnih grupa: + Potreban je naziv računa + Naziv računa se već koristi + Certifikat nije pronađen + Instaliraj certifikat + Detektiranje konfiguracije + Pričekajte, postavljanje upita poslužitelju... + Nije moguće pronaći CalDAV ili CardDAV uslugu. + Pregledaj logove + + Sinkronizacija + Interval sinkr. kontakata + Samo ručno + Svakih %d minuta + odmah nakon lokalnih izmjena + Interval sinkr. kalendara + Interval sinkr. zadataka + + Samo ručno + Svakih 15 minuta + Svakih 30 minuta + Svaki sat + Svaka 2 sata + Svaka 4 sata + Jednom dnevno + + Sinkroniziraj preko WiFi + Sinkronizacija je ograničena na WiFi veze + Vrsta veze se ne uzima u obzir + WiFi SSID ograničenje + Sinhronizirat će se samo preko %s + Koristit će se sve WiFi veze + Zarezom odijeljeni nazivi (SSIDa) dozvoljenih WiFi mreža (ostaviti prazno za sve mreže) + WiFi SSID ograničenje zahtijeva daljnje postavke + Upravljaj + Autentifkacija + Korisničko ime + Ažurirajte lozinku vezanu uz vaš poslužitelj. + Instaliraj certifikat + CalDAV + Vremensko ograničenje za prošli događaj + Svi događaju biti će sinkronizirani + + Događaji duži od jednog dana u prošlosti bit će zanemareni + Događaji duži od %d dana u prošlosti bit će zanemareni + Događaji duži od %d dana u prošlosti bit će zanemareni + + Događaji koji su prošli više od ovog broja dana bit će zanemareni (može biti 0). Ostavite prazno za sinkronizaciju svih događaja. + Zadani podsjetnik + + Zadani podsjetnik jednu minuta prije događaja + Zadani podsjetnik %d minuta prije događaja + Zadani podsjetnik %d minuta prije događaja + + Zadani podsjetnici nisu kreirani + Ako će se stvoriti zadani podsjetnici za događaje bez podsjetnika: željeni broj minuta prije događaja. Ostavite prazno da biste onemogućili zadane podsjetnike. + Upravljaj bojama kalendara + Boje kalendara se resetiraju pri svakoj sinkronizaciji + Boje kalendara mogu biti postavljene od strane drugih aplikacija + Podrška za boju događaja + Boje događaja su sinkronizirane + Boje događaja nisu sinkronizirane + CardDAV + Metoda kontaktnih grupa + + Grupe su odvojene vCards + Grupe su kategorije po kontaktima + + + Kreiraj adresar + Kreiraj kalendar + Mogući unosi u kalendar + Događaji + Zadatci + Bilješke / dnevnik + Boja + Naslov + Mjesto pohrane + Kreiraj + + Obriši zbirku + Sinkronizacija + Naslov + Opis + + Debug info + Debug info priložen je uz ovu poruku (zahtijeva podršku za privitke aplikacije koja će primiti poruku). + HTTP greška + Greška na poslužitelju + WebDAV greška + I/O greška + Pogledaj pojedinosti + Debug info je prikupljen + Uključeni resursi + Povezano s problemom + Udaljeni resurs: + Lokali resurs: + Logovi + Opsežniji logovi su dostupni + Pregledaj logove + Kopiraj URL + + Dogodila se greška. + Dogodila se HTTP greška. + Dogodila se I/O greška. + Prikaži detalje + + Autentifkacija + Korisničko ime + Lozinka + + DAVx⁵ dopuštenja + Dodatna dopuštenja su potrebna + %s prestar + Najmanja potrebno verzija: %1$s + Autentifikacija nije uspjela (provjerite podatke za prijavu) + Mrežna ili I/O greška – %s + HTTP poslužiteljska greška – %s + Greška lokalne pohrane – %s + Primljen je nevažeći kontakt sa poslužitelja + Primljen je nevažeći dogđaj sa poslužitelja + Primljen je nevažeći zadatak sa poslužitelja + Zanemarivanje jednog ili više nevaljanih resursa + + Sinkroniziraj sve račune + + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml new file mode 100644 index 0000000..bb2ef91 --- /dev/null +++ b/app/src/main/res/values-hu/strings.xml @@ -0,0 +1,450 @@ + + + + A felhasználói fiók (már) nem létezik + DAVx⁵ címjegyzék + Itt ne változtassa meg a fiókot! Használja közvetlenül az alkalmazást a fiókok kezeléséhez. + Törlés + Eltávolítás + Mégse + Bekapcsolás + A mező megadása kötelező + Súgó + Navigáció felfelé + Beállítások menü + Megosztás + A szinkronizáció elkezdődött/sorba lett állítva + Az adatbázis megsérült + A fiókok törölve lettek az eszközön. + Hibakeresés + Egyéb fontos üzenetek + Alacsony prioritású státuszüzenetek + Szinkronizáció + Szinkronizációs hibák + Fontos hibák, amelyek leállítják a szinkronizációt, például a váratlan kiszolgálóválaszok + Szinkronizációs figyelmeztetések + Nem kritikus szinkronizációs hibák, például bizonyos fajta hibás fájlok + Hálózati és I/O hibák + Időtúllépésék, kapcsolódási problémák, stb. (gyakran átmeneti problémák) + + Az Ön adatai. Az Ön döntése. + Vegye kézbe az irányítást. + Rendszeres ütemezett szinkronizálás + A rendszeres ütemezett szinkronizáláshoz a %s számára engedélyezni kell a háttérben futást, különben az Android rendszer a szinkronizálást bármikor leállíthatja. + Nincs szükségem rendszeres ütemezett szinkronizálásra.* + %s kompatibilitás + Elvégeztem a szükséges beállításokat, nincs szükségem további figyelmeztetésre.* + * Hagyja üresen, ha szeretné, ha legközelebb is kapjon emlékeztetést. Később felülírható az alkalmazásbeállításoknál (%s). + További információk + jtx Board + + Feladatok támogatása + Ha a kiszolgáló támogatja a feladatokat, akkor a következő feladatalkalmazásokkal lehet szinkronizálni őket: + OpenTasks + A fejlesztése leállt – nem ajánlott. + Tasks.org + nem támogatottak.]]> + Nincs elérhető alkalmazás-áruház + Nincs szükségem a feladatok támogatására.* + Nyílt forráskódú szoftver + Nagyon örülünk, hogy a %s felhasználói közé tartozik, amely nyílt forráskódú szoftver. A fejlesztés, karbantartás és támogatás ugyanakkor kemény munkát igényel. Fontolja meg, hogy ezt pénzzel, vagy valamilyen más módon támogassa (több lehetőség közül is választhat). Nagyon megköszönnénk! + A hozzájárulás lehetőségei + Ne emlékeztessen + + %d hónapig + %d hónapig + + Tovább + + Engedélyek + A %s megfelelő működése bizonyos engedélyeket igényel. + Az összes alább felsorolt + Ezt választva valamennyi funkció bekapcsolható (javasolt) + Az összes engedély megadva + Névjegyengedélyek + A címtárak szinkronizálásának mellőzése (nem javasolt) + A névjegy-szinkronizálás lehetséges + Naptárengedélyek + A naptárak szinkronizálásának mellőzése (nem javasolt) + A naptárak szinkronizálása lehetséges + Értesítési engedély + Az értesítések tiltása (nem javasolt) + Értesítések engedélyezése + jtx Board engedélyek + OpenTasks engedélyek + Feladatok engedélyek + Nincs feladatszinkronizálás + A feladatlisták szinkronizálása lehetséges + Engedélyek megtartása + Az engedélyek automatikusan visszaállhatnak az alapértelmezettre (nem javasolt) + Az engedélyek nem fognak visszaállni az alapértelmezettre automatikusan + Kattintson az Engedélyekre > vegy ki a pipát az „Engedélyek eltávolítása, ha nem használja az alkalmazást” mellől + Ha a kapcsolók nem működnek, használja az alkalmazásbeállításokat. + Alkalmazásbeállítások + + WiFi SSID engedélyek + A jelenlegi WiFi nevének (SSID) az eléréséhez a következő feltételeknek kell teljesülniük: + Pontos helyadatok használatának engedélyezése + Helyadatok engedélye megadva + Helyadatok engedélye megtagadva + Helyadatok engedélyezése a háttérben + Engedélyezés mindig + A Helyadatok engedély erre van állítva: %s + A Helyadatok engedély nincs erre állítva: %s + A %s csak arra használ helyadatokat (a WiFi SSID-t), hogy a szinkronizációt egy konkrét WiFi SSID-ra korlátozza. Ez akkor is megtörténik, ha a szinkronizáció a háttérben fut. + Az összes helyadat (csak a WiFi SSID) csak helyben lesz használva, és nem lesz sehová sem elküldve + A helyadatok mindig engedélyezve vannak + A helyadat-szolgáltatás bekapcsolva + A helyadat-szolgáltatás kikapcsolva + + Fordítások + Programkönyvtárak + Verziószám:%1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) és a közreműködők + Ehhez a program SEMMIFÉLE GARANCIA NEM JÁR. Szabad szoftver, amely bizonyos feltételek mellett szabadon terjeszthető. + + A naplófájl létrehozása nem sikerült + Mostantól a %s minden művelete naplózásra kerül + Megtekintés/megosztás + Kikapcsolás + + CalDAV/CardDAV szinkronizációs adapter + Névjegy/licenc + Tesztelői visszajelzés + Telepítsen egy böngészőt + Beállítások + Hírek és frissítések + Eszközök + Weblapok + Honlap + Kézikönyv + GYIK + Közösség + A projekt támogatása + A közreműködés lehetőségei + Adatvédelmi irányelvek + Üdvzöli a DAVx⁵! + Kapcsolódjon a kiszolgálójához, és tartsa szinkronban a naptárait és névjegyeit. + Az összes fiók szinkronizálása + + Az értesítések tiltva vannak. A szinkronizálási hibákról nem fog értesítést kapni. + Az automatikus szinkronizálás nem aktív (nincs megerősített internetkapcsolat) + Kapcsolatok kezelése + Az adatcsökkentés bekapcsolva. A háttérben futó szinkronizálás korlátozva van. + Adatcsökkentés kezelése + Az akkumulátorkímélés bekapcsolva. A háttérben futó szinkronizálás korlátozva van. + Akkumulátorkímélés kezelése + A rendelkezésre álló tárhely kevés. Az Android nem fogja a helyi erőforrásokat azonnal szinkronizálni, csak a következő rendes alkalommal. + Tárhely kezelése + A naptárszolgáltató hiányzik + Letiltotta a „Naptártároló” rendszeralkalmazást? + A névjegyszolgáltató hiányzik + Letiltotta a „Névjegytároló” rendszeralkalmazást? + Alkalmazások kezelése + + A szolgáltatások felderítése nem sikerült + A gyűjteménylista frissítése nem sikerült + + Futás előtérben + Egyes eszközökön szükséges az automatikus szinkronizáció működéséhez + + Beállítások + Hibakeresés + Hibakeresési információ megtekintése + A beállítások részleteinek és a naplók megtekintése/megosztása + Részletes naplózás + A naplózás aktív. A naplókat a hibakeresési információk részeként tekintheti meg. + Naplózás kikapcsolva + Akkumulátorhasználat optimalizálása + Az alkalmazás kivétel alóla (ajánlott) + Akkumulátorkorlátozások érvényesek (nem ajánlott) + Kapcsolat + A proxy típusa + + Alapértelmezett + Kikapcsolva + HTTP + SOCKS (Orbot számára) + + A proxy kiszolgálóneve + A proxy által használt port + Biztonság + Alkalmazásengedélyek + Tekintse át a szinkronizáláshoz szükséges engedélyeket + A rendszertanúsítványok elfogadása + A rendszer által kezelt, előre vagy felhasználó által telepített tanúsítványok figyelmen kívül lesznek hagyva + A rendszer által kezelt, előre vagy felhasználó által telepített tanúsítványok megbízhatóak (javasolt) + Ha ez a beállítás aktív, akkor a rendszertanúsítványok nem lesznek megbízhatónak tekintve. Ez azt jelenti, hogy minden tanúsítványt kézileg kell elfogadnia (akkor is, ha a kiszolgáló megújítja a tanúsítványát), különben a fiókbeállítások és a szinkronizáció nem fog működni. + A tanúsítványok megbízhatóságának törlésére + A tanúsítványok megbízhatóságával kapcsolatos beállítások törlésére + A tanúsítványok megbízhatóságával kapcsolatos beállítások törölve + Felhasználói felület + Értesítési beállíások + Az értesítési csatornák és azok beállításának kezelése + Stílus kiválasztása + + Rendszerbeállítás szerinti + Világos + Sötét + + Tippek visszaállítása + Újra jelenjen meg az összes tipp + Az összes tipp újra meg fog jelenni + Integráció + Feladatok alkalmazás + Nem található kompatibilis feladatalkalmazás + UnifiedPush (kísérleti) + Nincs (leküldés letiltása) + Válasszon egy disztribútort + Nincs leküldésdisztribútor telepítve + Nincs végpont beállítva + Készen áll a leküldéses üzenetek fogadására innen: %s + + CardDAV + CalDAV + Webcal + További engedélyek szükségesek ezen gyűjtemények szinkronizálásához. + Engedélyek kezelése + Szinkronizálás most + Fiókbeállítások + Fiók átnevezése + A mentetlen helyben tárolt adatok elvesznek. Az átnevezés után szinkronizálásra lesz szükség. Új fióknév: + Új fióknév + Átnevezés + A fióknév már használatban van + A fiók átnevezése nem sikerült. + Fiók törlése + Valóban törölni akarja a fiókot? + Az összes címjegyzék, naptár és feladatlista helyi példányai törölve lesznek. + a gyűjtemény szinkronizálása + csak olvasható + naptár + névjegyek + napló + feladatok + Csak a személyesek megjelenítése + Lista frissítése + A Webcal feliratkozások külső alkalmazásokkal szinkronizálhatók. + Nem található Webcal-képes alkalmazás + ICSx⁵ telepítése + + Fiók hozzáadása + adatvédelmi nyilatkozatot.]]> + Általános bejelentkezés + Szolgáltatófüggő bejelentkezés + Folytatás + Bejelentkezés + Bejelentkezés e-mail-cím segítségével + E-mail-cím: + Érvényes e-mail-cím szükséges + szolgáltatások felfedezése DNS rekordok és jól ismert webcímek alapján történik.]]> + Jelszó + Jelszó elrejtése + Jelszó megjelenítése + Bejelentkezés webcím és felhasználónév segítségével + Felhasználónév + Alapwebcím + szolgáltatások felfedezése DNS rekordok és jól ismert webcímek alapján is történik.]]> + Tanúsítvány kiválasztása + Fiók hozzáadása + A fiók neve + Az aposztrófok (\') használata a visszajelzések szerinte egyes eszközökön problémát okoz. + Használja az e-mail-címét fióknévként, mert később a létrehozandó események szervezőjeként (ORGANIZER mező) az Android ezt fogja használni. Két fiókot nem lehet azonos néven létrehozni. + A csoportok kezelésének módja: + A fióknév kötelező + A fióknév már használatban van + A fiók hozzáadása nem sikerült + Befejezés + Speciális bejelentkezés + Klienstanúsítvány: %s + Nem található tanúsítvány + Tanúsítvány telepítése + Google Névjegyek / Naptár + Google-fiók + Bejelenetezés a Google használatával + Kliensazonosító (nem kötelező) + Adatvédelmi irányelveket.]]> + Google API szolgáltatások felhasználói adatokra vonatkozó irányelvének, ide értve a korlátozott felhasználásra vonatkozó előírásokat is.]]> + A hitelesítőkódot nem sikerült megszerezni + Nextcloud + Bejelentkezés Nextcloud használatával + Ez elindítja a bejelentkezési folyamatot egy webböngészőben. + Nextcloud-kiszolgáló címe + Bejelentkezés + A bejelentkezési webcím megszerzése nem sikerült + A bejelentkezési adatok megszerzése nem sikerült + A konfiguráció felderítése + Várjon, a kiszolgáló lekérdezése… + Nem található CalDAV vagy CardDAV szolgáltatás. + Ez az alapwebcím nem tűnik elérhető CalDAV/CardDAV webcímnek, és a szolgáltatásfelderítés nem sikerült. + általunk tesztelt szolgáltatások listáját, és az alapwebcímeiket.]]> + Ellenőrizze a hitelesítő adatok is (általában a felhasználónevet és a jelszót). + További műszaki információk érhetők el a naplókban. + Naplóbejegyzések megtekintése + + Szinkronizálás + Névjegyszinkronizálás sűrűsége + Csak kézileg + Minden %d percben + az eszközön történt módosítás után + Naptárszinkronizálás sűrűsége + Feladatlisták szinkronizálásának sűrűsége + + Csak kézi + 15 percenként + 30 percenként + Óránként + Kétóránként + Négyóránként + Naponta + + Szinkronizálás csak WIFI-n + Szinkronizálás csak WIFI kapcsolaton keresztül + Szinkronizálás a kapcsolat típusától függetlenül + WiFi SSID-ra korlátozása + Az alábbi hálózatok használhatók: %s + Minden hálózat használható + A használható WiFi hálózatok nevei (SSID), vesszővel elválasztva (hagyja üresen, ha nem akar szűrést beállítani) + A WiFi SSID-ra szűréséhez további beállítások szükségesek + Beállítások + A VPN-hez mögöttes internetkapcsolat szükséges + A VPN ellenőrzött internetkapcsolat nélkül nem elég a szinkronizáláshoz (ajánlott) + A VPN ellenőrzött internetkapcsolat nélkül is elég a szinkronizáláshoz + Hitelesítés + Felhasználónév + Új jelszó + Adja meg a kiszolgálón érvényes új jelszót. + Klienstanúsítvány + Nem érhető el vagy nincs kiválasztva tanúsítvány + Tanúsítvány telepítése + CalDAV + Múltbéli események időkorlátja + Minden esemény szinkronizálása + + Az egy napnál régebbi események figyelmen kívül hagyása + A(z) %d napnál régebbi események figyelmen kívül hagyása + + Az ennyi napnál (lehet 0) régebbi események figyelmen kívül lesznek hagyva. Hagyja üresen, ha minden múltbéli eseményt szinkronizálni akar. + Alapértelmezett emlékeztető + + Az alapértelmezett emlékeztető az esemény kezdete előtt egy perccel van + Az alapértelmezett emlékeztető az esemény kezdete előtt %d perccel van + + Nem lesznek alapértelmezett emlékeztetők beállítva + Ha szeretné, hogy az emlékeztető nélküli eseményekhez egy alapértelmezett emlékeztető legyen beállítva, akkor adja meg, hogy az hány perccel az esemény előtt legyen. Ha nem akar ilyet, hagyja üresen. + Naptárszínek kezelése + A naptárszínek minden szinkronizáláskor visszaállnak az alapértelmezettre + A naptárszíneket más alkalmazásokban lehet beállítani + Eseményszínek támogatása + Az eseményszínek szinkronizálva vannak + Az eseményszínek nincsenek szinkronizálva + CardDAV + A csoportok kezelésének módja + + A csoportok különálló vCard objektumok + A csoportok névjegy-kategóriák + + + Címjegyzék létrehozása + A címjegyzék CardDAV-on keresztüli létrehozását nem biztos, hogy támogatja a kiszolgáló. + Naptár létrehozása + + Lehetséges naptárbejegyzések + Események + Feladatok + Jegyzetek/napló + A naptár CalDAV-on keresztüli létrehozását nem biztos, hogy támogatja a kiszolgáló. + Szín + Cím + Tárhely + Leírás (nem kötelező) + Létrehozás + + névjegyek + feladatok + Gyűjtemény törlése + A gyűjtemény (%s) és a hozzá tartozó adatok véglegesen törölve lesznek, helyben és a kiszolgálóról is. + Szinkronizálás + Szinkronizálás bekapcsolva + Szinkronizálás kikapcsolva + Csak olvasható + Csak olvasható (kiszolgálón) + Csak olvasható (házirend szerint) + Csak olvasható (csak helyben) + Olvasás/írás + Cím + Leírás + Tulajdonos + Leküldés támogatása + A kiszolgáló a leküldés támogatását hirdeti + Feliratkozva: %1$s, lejár: %2$s + Utolsó szinkronizálás (%s) + Webcím + + Hibakeresési információk + ZIP archívum + Hibakeresési információkat tartalmaz + Az archívum megosztásával lehetőség van egy másik számítógépre áthelyezni, e-mail formájában elküldeni vagy egy hibabejelentéshez mellékelni. + Archívum megosztása + Hibakeresési információ csatolása ehhez az üzenethez (ha a fogadó alkalmazás támogatja a mellékleteket). + HTTP hiba + Kiszolgálóhiba + WebDAV hiba + Ki-/bemeneti hiba + Részletek megtekintése + A hibakeresési információ összegyűjtése befejeződött + Érintett erőforrások + A probléma kapcsán érintett erőforrások + Távoli erőforrás: + Helyi erőforrás: + Naplók + Rendelkezésre állnak részletes naplóbejegyzések + Naplóbejegyzések megtekintése + URL másolása + + Hiba történt. + HTTP hiba történt. + Ki-/bemeneti hiba történt. + Részletek megjelenítése + + WebDAV kötetek + Felhasznált kvóta: %1$s / keret: %2$s + Tartalom megosztása + Leválasztás + WebDAV kötet hozzáadása + Közvetlenül hozzáférhet a felhőben tárolt fájlokhoz egy WebDAV kötet hozzáadásával! + a WebDAV kötetek hogyan működnek.]]> + Megjelenítendő név + WebDAV webcím + Érvénytelen webcím + Hitelesítés + Felhasználónév + Jelszó + WebDAV kötet hozzáadása + Ezen a webcímen nincs WebDAV szolgáltatás + WebDAV kötet leválasztása + A kapcsolat beállításai elvesznek, de maguk a fájlok nem. + Hozzáférés a WebDAV fájlhoz + WebDAV fájl letöltése + WebDAV fájl feltöltése + WebDAV kötetek + + DAVx⁵ engedélyek + További engedélyek szükségesek + %s túl régi + Legalacsonyabb szükséges verzió: %1$s + A hitelesítés nem sikerült (ellenőrizze a hitelesítési adatokat) + Hálózati vagy ki-/bemeneti hiba – %s + HTTP kiszolgálóhiba – %s + Helyi tárhelyhiba –%s + Nem végzetes hiba (az újrapróbálkozások száma elérte a maximumot) + A kiszolgáló érvénytelen névjegyet küldött + A kiszolgáló érvénytelen eseményt küldött + A kiszolgáló érvénytelen feladatot küldött + Egy vagy több érvénytelen erőforrás kihagyva + Szinkronizálás függőben + A távoli adatok megváltoztak + + Az összes szinkronizálása + Az összes fiók szinkronizálása + + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml new file mode 100644 index 0000000..83f488c --- /dev/null +++ b/app/src/main/res/values-it/strings.xml @@ -0,0 +1,406 @@ + + + + Account inesistente (o cancellato) + Rubrica DAVx⁵ + Cancella + Elimina + Annulla + Attiva + Questo campo è necessario + Aiuto + Menu opzioni + Condividi + Sincronizzazione avviata + Database danneggiato + Tutti gli account sono stati rimossi localmente. + Debugging + Altri messaggi importanti + Messaggi di stato a bassa priorità + Sincronizzazione + Errori di sincronizzazione + Errori importanti che bloccano la sincronizzazione, come risposte inattese del server + Avvisi di sincronizzazione + Problemi di sincronizzazione non gravi come alcuni file non validi + Errori di Rete e di I/O + Timeouts, problemi di connessione, ecc. (spesso temporanei) + + Tuoi i dati. Tua la scelta. + Riprendi il controllo. + Intervalli di sincronizzazione regolari. + Per sincronizzare i dati a intervalli regolari, %s deve essere autorizzato a girare in background. Altrimenti Android può mettere in pausa gli aggiornamenti in qualunque momento. + Non ho bisogno di sincronizzare a intervalli di tempo regolari.* + %s compatibilità + Ho settato le impostazioni richieste. Non ricordarmelo più. + * Lascia smarcato per fartelo ricordare dopo. Può essere reimpostato nelle impostazione dell\'app %s. + Maggiori informazioni + + Supporto per le attività + Se le attività sono supportate dal tuo server, possono essere sincronizzate con una app per attività supportata: + OpenTasks + Non sembra essere più sviluppato - non raccomandato. + Tasks.org + Nessun app store disponibile + Non ho bisogno del supporto alle attività.* + Software open-source + Siamo felici che tu usi %s, che è un software open source. Lo sviluppo, la manutenzione e il supporto sono compiti duri. Per piacere prendi in considerazione di dare una mano (puoi farlo in molti modi) o una donazione. Sarebbe davvero apprezzato! + Come aiutare/donare + + Autorizzazioni + %s richiede autorizzazioni per funzionare correttamente. + Tutti i seguenti + Usare questo per abilitare tutte le funzioni (consigliato) + Concedi tutte le autorizzazioni + Autorizzazioni per i contatti + Non sincronizzare i contatti (sconsigliato) + Possibilità di sincronizzare i contatti + Autorizzazioni per il calendario + Non sincronizzare il calendario (sconsigliato) + Permette di sincronizzare il calendario + Autorizza notifiche + Notifiche disabilitate (non consigliato) + Notifiche attive + Autorizzazioni di OpenTasks + Autorizzazioni delle attività + Permette di sincronizzare le attività + Mantieni autorizzazioni + Le autorizzazioni possono essere reimpostate automaticamente (sconsigliato) + Le autorizzazioni non si reimposteranno automaticamente + Fai click su Autorizzazioni > deseleziona \"Rimuovi autorizzazioni se l\'app non è in uso\" + Se uno slider non funziona, vai a impostazioni app/ autorizzazioni. + Impostazioni app + + Autorizzazioni per WiFi SSID + Per poter accedere al nome dell\'attuale nome del WIFI (SSID), devono essere soddfsfatte queste condizioni: + Autorizzazione precisa della localizzazione + Garantire l\'autorizzazione della posizione + Negare l\'autorizzazione della posizione + Autorizzazione della posizione in background + Permettere sempre + Permessi di localizzazione impostati a: %s + Permessi di localizzazione non impostati a: %s + Posizione sempre disabilitata + Servizio di posizione abiltato + Servizio di posizione disabilitato + + Traduzioni + Librerie + Versione %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) e contibutori + Il programma è distribuito SENZA ALCUNA GARANZIA. È software libero e può essere redistribuito sotto alcune condizioni. + + Impossibile creare il file di log + Adesso l\'accesso all\' %s delle attività + Visualizza/condividi + Disabilita + + CalDAV/CardDAV adattatore di sincronizzazione + Informazioni / Licenza + Feedback sulla beta + Installare un browser Web + Impostazioni + Notizie & aggiornamenti + Strumenti + Link esterni + Sito web + Manuale + Domande Frequenti + Comunità + Supporta il progetto + Come contribuire + Politica sulla riservatezza + Sincronizzazione di tutti gli account + + Notifiche non attive. Non sarai avvisato di eventuali errori di sincronizzazione + Gestione connessioni + Risparmio dati attivo. La sincronizzazione in background è limitata, + Risparmio energetico attivo. La sincronizzazione in background è limitata, + Gestisci risparmio energetico + Spazio di memorizzazione scarso. Androin non salverà immediatamente i cambiamente, ma alla prossima sincronizzazione programmata. + Gestisci spazio di memorizzazione + + Impossibile trovare il servizio + Impossibile aggiornare la lista delle raccolte + + Esecuzione in primo piano + Su alcuni dispositivi, questo è necessario per la sincronizzazione automatica. + + Impostazioni + Debug + Mostra informazioni di debug + Log completo + Log disabilitato + Ottimizzazione batteria + Connessione + Tipo di proxy + + Predefinito di sistema + Nessun proxy + HTTP + SOCKS (per Orbot) + + Nome host proxy + Porta proxy + Sicurezza + Autorizzazioni app + Controlla le autorizzazioni per la sincronizzazione + Non ti fidare dei certificati di sistema + Le CA di sistema e quelle aggiunte dall\'utente non sono affidabili + Le CA di sistema e quelle aggiunte dall\'utente sono affidabili (raccomandato) + Reimposta la fiducia in tutti i certificati + Reimposta la fiducia nei certificati aggiunti + Sono stati cancellati tutti i certificati aggiunti + Interfaccia utente + Impostazioni di notifica + Gestisci i canali di notifica e le loro impostazioni + Seleziona il tema + + Sistema predefinito + Luce + Buio + + Reimposta i suggerimenti + Riabilita i suggerimenti precedentemente disabilitati + I suggerimenti verranno mostrati + Integrazione + Funzioni dell\'applicazione + Nessuna applicazione compatibile con e funzionalità trovata + + CardDAV + CalDAV + Webcal + Per sincronizzare questi dati sono richiesti permessi aggiuntivi. + Gestisci permessi + Sincronizza adesso + Impostazioni account + Rinomina account + Dati locali non salvati potrebbero venir persi. Dopo il cambio nome è necessaria la ri-sincronizzazione. + Nuovo nome account + Rinomina + Nome account già usato + Impossibile rinominare l\'account + Elimina account + Cancellare l\'account? + Tutte le copie locali delle rubriche, dei calendari e degli elenchi attività verranno eliminate. + Sincronizza questa raccolta + sola lettura + calendario + contatti + diario + attività + Mostra solo personale + Aggiorna lista + Sottoscrizioni al Webcal possono essere sincronizzate con applicazioni esterne. + Non ho trovato nessuna applicazione abilitata per Webcal + Installa ICSx⁵ + + Aggiungi account + Login generico + Login del Provider + Continua + Login + Accedi con indirizzo email + Indirizzo email + È necessario un indirizzo email valido + I servizi sono individuati usando record DNS e le URL well-known.]]> + Password + Nascondi password + Mostra password + Accedi con URL e nome utente + Nome utente + Base URL + i servizi sono individuati anche usando record DNS records e le URL well-known.]]> + Seleziona certificato + Aggiungi account + Nome account + L\'uso degli apostrofi (\') potrebbe causare problemi su alcuni dispositivi. + Inserisci il tuo indirizzo email come nome dell\'account in quanto Android userà il nome dell\'account nel campo ORGANIZER degli eventi creati. Non è possibile avere due account con nome uguale. + Metodo del contact group: + Richiesto il nome dell\'account + Nome account già usato + Login avanzato + Certificato client: %s + Nessun certificato trovato + Installa il certificato + Contatti Google / Calendario + Account Google + Accedi con Google + ID Client (facoltativo) + Google API Services User Data Policy, incluso il Limited Use requirements.]]> + Non posso ottenere il codice di autorizzazione + Nextcloud + Accedi con Nextcloud + Questo aprirà la pagina di login di Nextcloud nel browser. + Indirizzo del server Nextcloud + Iscriviti + Non posso ottenere l\'URL di login + Non posso ottenere i dati di login + Rilevazione configurazione + Attendere, invio richiesta al server… + Impossibile trovare servizi CalDAV o CardDAV. + L\'URL base non sembra essere un URL CalDAV/CardDAV accessibile e i servizi di individuazione hanno fallito. + Controlla attentamente i dati di autenticazione (normalmente username e password). + Informazioni tecniche aggiuntive sono reperibili nei log. + Vedi i registri + + Sincronizzazione + Intervallo sincr. Contatti + Solo manualmente + Ogni %d minuti e a seguito di ogni cambiamento locale + Intervallo sincr. calendari + Intervallo sincr. attività + + Solo manualmente + Ogni 15 minuti + Ogni 30 minuti + Ogni ora + Ogni 2 ore + Ogni 4 ore + Una volta al giorno + + Sincr. solo tramite WiFi + La sincronizzazione è limitata alle connessioni WiFi + Il tipo di connessione non è preso in considerazione + Restrizione SSID WiFi + Sincronizzeremo solo oltre %s + Verranno utilizzate tutte le connessioni WIFI + Nomi (SSID) delle reti WiFi autorizzate separati da virgola (lascia vuoto per autorizzarle tutte) + Le restrizioni del SSID WIFI richiedono ulteriori impostazioni + Riuscire + La VPN richiede connessione internet + La VPN senza una connessione internet validata non è sufficiente per lanciare la sincronizzazione (raccomandato) + La VPN senza una connessione internet validata è sufficiente per lanciare la sincronizzazione + Autenticazione + Nome utente + Nuova password + Aggiorna la password come sul tuo server. + Certificato client + Nessun certificato disponibile o selezionato + Installa il certificato + CalDAV + Limite di tempo per gli eventi trascorsi + Verranno sincronizzati tutti gli eventi + + Eventi più vecchi di un giorno saranno ignorati + Eventi più vecchi di %d giorni saranno ignorati + Eventi più vecchi di %d giorni saranno ignorati + + Eventi più vecchi di questo numero di giorni verranno ignorati(può anche essere 0). Lasciare in bianco per sincronizzare tutti gli eventi. + Promemoria predefinito + + Promemoria predefinito un minuto prima dell\'evento + Promemoria predefinito %d minuti prima dell\'evento + Promemoria predefinito %d minuti prima dell\'evento + + Nessun promemoria di default creato + Indicare il numero di minuti che si desidera per il promemoria predefinito. +Lasciare vuoto per non creare un promemoria predefinito. + Cambia il colore del calendario + I colori del calendario sono resettati ad ogni sincronizzazione + I colori del calendario possono essere scelti da altre applicazioni + Supporto colore dell\'evento + I colori degli eventi sono sincronizzati + I colori degli eventi non sono sicnronizzati + CardDAV + Organizzazione dei gruppi di contatto + + I gruppi sono vCards separate + I gruppi sono categorie per ogni contatto + + + Crea rubrica + La creazione di rubriche tramitte CardDAV potrebbe non essere supportata dal server. + Crea calendario + + Possibili voci del calendario + Eventi + Attività + Note / diario + La creazione do calendari tramite CalDAV potrebbe non essere supportata dal server. + Colore + Titolo + Percorso di archiviazione + Crea + + contatti + attività + Elimina raccolta + Questa raccolta (%s) e tutti i suoi dati saranno rimossi definitivamente, sia localmente che sul server. + Sincronizzazione + Sincronizzazione attivata + Sincronizzazione disattivata + Sola lettura + Sola lettura (dal server) + Sola lettura (locale) + Lettura/scrittura + Titolo + Descrizione + Proprietario + Supporto push + Ultima sincronizzazione %s + Indirizzo (URL) + + Informazioni di debug + Archivio ZIP + Contiene informazioni sui debug e sugli accessi + Condividi l\'archivio per trasferirlo ad un computer, per inviarlo tramite email o per fissarlo ad un ticket di supporto. + Condividi l\'archivio + Informazioni sul debug fissate a questo messaggio (richiede un supporto di fissaggio dell\'applicazione di supporto). + Errore HTTP + Errore del Server + Errore WebDAV + Errore I/O + Vedi dettagli + Sono state raccolte informazioni di debug + Fonti coinvolte + Collegate con il problema + Fonti remote: + Fonti locali: + Registri + Sono disponibili registri verbali + Vedi i registri + Copia URL + + Si è verificato un errore. + Si è verificato un errore HTTP. + Si è verificato un errore di I/O. + Mostra dettagli + + Installazioni WebDAV + Quantità utilizzata: %1$s / disponibile: %2$s + Condividi i contenuti + Disinstallazioni + Aggiungi installazioni WedDAV + Accedi direttamente ai tuoi file nel cloud aggiungendo un supporto WebDAV! + Nome del display + URL WebDVA + URL non valido + Autenticazione + Nome utente + Password + Aggiungi installazioni + Nessun servizio WebDAV a questo URL + Rimuovi punto di mont + I dettagli della connessione saranno perduti, ma nessun file verrà cancellato. + File di accesso WebDAV + File di download WebDAV + Caricare file WebDAV + Installazione WebDAV + + Autorizzazioni DAVx⁵ + Autorizzazioni addizionali richieste + %s troppo vecchio + Versione minima richiesta %1$s + Autenticazione fallita (controlla credenziali login) + Errore di rete o di I/O – %s + Errore server HTTP – %s + Errore di archiviazione locale – %s + Contatto non valido ricevuto dal server + Evento non valido ricevuto dal server + Attività non valida ricevuta dal server + Una o più risorse non valide ignorate + + Sincronizza tutto + Sincronizzazione di tutti gli account + + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000..48fcdbe --- /dev/null +++ b/app/src/main/res/values-ja/strings.xml @@ -0,0 +1,478 @@ + + + + アカウントがありません + DAVx⁵ アドレス帳 + ここでアカウントを変更しないでください! アカウントの管理は、アプリから直接行ってください。 + 削除 + 削除 + キャンセル + 有効 + このフィールドは入力必須です + ヘルプ + 上に移動 + オプションメニュー + 共有 + 同期中/待機中 + データベースが破損しています + すべてのアカウントがローカルから削除されました + デバッグ中 + 他の重要なメッセージ + 優先度の低いステータスメッセージ + 同期 + 同期エラー + 予期しないサーバーの応答のような、同期を停止させる重要なエラー + 同期の警告 + 特定の無効なファイルのような、致命的ではない同期の問題 + ネットワークおよび I/O エラー + タイムアウト、接続の問題など (多くの場合、一時的なもの) + + あなたのデータはあなたの手に + コントロールを始める + 一定の間隔で同期する + 一定の間隔で同期するには、%s のバックグラウンドでの動作を許可する必要があります。許可しない場合、Android によって同期が停止されることがあります。 + 一定間隔での同期は不要です* + %s 適合性 + 端末の製造元のファームウェアによって同期がブロックされている可能性があります。この問題に遭遇した場合、手動で問題を解決していただく必要があります。 + 必要な設定は完了したのでリマインダーは不要です* + * 未チェックのままにすると後でリマインドします。アプリの設定 / %s でリセットできます + 追加情報 + jtx Board + + ToDo リストの同期に対応 + お使いのサーバーが ToDo リストに対応している場合、対応するアプリで同期できます: + OpenTasks + 開発を停止している可能性があります – 非推奨 + Tasks.org + 対応していません]]> + アプリストアが利用できません + ToDo リスト対応は不要です* + オープンソースソフトウェア + オープンソースソフトウェアとして %s をお届けできることをとても嬉しく思っています。開発・維持・サポートは簡単ではありません。貢献 (さまざまな方法があります) や寄付をご検討ください。プロジェクトはそれらに支えられています。 + 貢献/寄付の方法 + 次の期間は通知しない: + + %d か月 + + 次へ + + 許可 + %s を正しく動作させるには、権限を許可してください。 + 以下のすべて + これを有効にすると、すべての機能が使用できるようになります (推奨) + すべての権限が許可されました + 連絡先へのアクセス + 無効のため、連絡先を同期しません (非推奨) + 連絡先を同期できます + カレンダーへのアクセス + 無効のため、カレンダーを同期しません (非推奨) + カレンダーを同期できます + 通知の権限 + 通知が無効になっています (非推奨) + 通知が有効です + jtx Board へのアクセス + OpenTasks へのアクセス + Tasks へのアクセス + 無効のため、ToDo リストを同期しません + ToDo リストを同期できます + 権限を維持する + 無効のため、権限が自動的にリセットされることがあります (非推奨) + 権限は自動でリセットされることはありません + 許可から「アプリが使用されていない場合に権限を削除」を無効化してください + スイッチが機能しない場合、アプリ情報 / 許可 にアクセスしてください + アプリ設定 + + WiFi SSID へのアクセス + 現在の WiFi 名 (SSID) にアクセスするため、これらの条件を満たす必要があります: + 正確な位置情報へのアクセス + 位置情報へのアクセスが許可されています + 位置情報の権限が拒否されています + バックグラウンドでの位置情報へのアクセス + 常に許可 + 位置情報の権限は %s に設定されています + 位置情報の権限は %s に設定されていません + %s は位置情報 (WiFi の SSID のみ) を、特定の WiFi SSID で同期する目的に限定して使用します。同期がバックグラウンドで実行中の場合にも発生します。 + すべての位置情報 (WiFi SSID のみ) はローカルのみで使用され、送信されることはありません。 + 位置情報は常に有効です + 位置情報サービスは有効です + 位置情報サービスは無効です + + 翻訳 + ライブラリー + バージョン %1$s (%2$d) + © Ricki Hirner、Bernhard Stockmann (bitfire web engineering GmbH) と貢献者 + このプログラムは完全に無保証で提供されます。これはフリーソフトウェアで、特定の条件下での再頒布を歓迎します。 + + ログファイルを作成できませんでした + %s のすべてのアクティビティのログを記録しています + 表示/共有 + 無効にする + + CalDAV/CardDAV 同期アダプター + アプリについて / ライセンス + ベータフィードバック + ウェブブラウザをインストールしてください + 設定 + ニュース & アップデート + ツール + 外部リンク + ウェブサイト + マニュアル + FAQ + 組織向け + コミュニティ + プロジェクトを支援 + 貢献する方法 + プライバシーポリシー + DAVx⁵ にようこそ! + サーバーに接続して、カレンダーと連絡先を同期しましょう。 + すべてのアカウントを同期 + + 通知が無効です。同期エラーが発生しても通知されません。 + 自動同期が無効です (有効なインターネット接続がありあせん)。 + 接続を管理 + データサーバーが有効です。バックグラウンド同期が制限されています + データサーバーを管理 + バッテリー最適化機能が有効です。同期が制限される可能性があります。 + バッテリー最適化機能を管理 + ストレージの容量が残りわずかです。Android はローカルの変更を即座に同期しませんが、次の通常サイクルで同期します。 + ストレージを管理 + カレンダープロバイダーが見つかりません + システムアプリ「Calendar storage (カレンダーの保存などと表示)」を無効にしていませんか? + 連絡先プロバイダーが見つかりません + システムアプリ「Contacts storage (連絡帳などと表示)」を無効にしていませんか? + アプリを管理 + + サービスの検出に失敗しました + コレクションリストを再読み込みできませんでした + + フォアグラウンドで実行 + 一部のデバイスでは自動同期にこの設定が必要になります。 + + 設定 + デバッグ + デバッグ情報を表示 + 設定の詳細とログを表示/共有します + 詳細ログ + ログを取得します。デバッグ情報の一部としてログを確認できます。 + ログを取得しません + バッテリー最適化 + アプリは除外されています (推奨) + バッテリー最適化が適用されています (非推奨) + 接続 + プロキシーの種類 + + システムのデフォルト + プロキシーなし + HTTP + SOCKS (Orbot 向け) + + プロキシーのホスト名 + プロキシーのポート番号 + セキュリティ + アプリの権限 + 同期に必要な権限を確認する + システム証明書を無視する + システムとユーザーが追加した CA を信頼しません + システムとユーザーが追加した CA を信頼します (推奨) + この設定が有効な場合、システム証明書は信頼されません。(サーバーが証明書を更新した場合を含めて) すべての証明書を手動で許可する必要があります。手動で許可しない場合、アカウントセットアップと同期は機能しません。 + (未) 信頼証明書をリセット + すべてのカスタム証明書の信頼をリセットします + すべてのカスタム証明書をクリアしました + ユーザーインターフェイス + 通知設定 + 通知チャネルとその設定を管理します + テーマを選択 + + システムのデフォルト + ライト + ダーク + + ヒントをリセット + 以前非表示にしたヒントを再表示します + すべてのヒントを再表示します + 統合 + ToDo リストアプリ + 連携できるアプリがありません + UnifiedPush (試験的) + なし (プッシュを無効にする) + ディストリビューターを選択 + プッシュのディストリビューターがインストールされていません + エンドポイントが設定されていません + %s 経由でプッシュメッセージを受信できます + FCM (Google Play) + プッシュメッセージは常に暗号化されます。 + + アカウントが削除されました + CardDAV + CalDAV + Webcal + これらのコレクションを同期するには、追加の権限が必要です。 + 権限を管理 + 今すぐ同期 + アカウント設定 + アカウントの名前を変更 + 保存していないローカルデータが破棄されることがあります。名前を変更した後にもう一度同期してください。 + 新しいアカウント名 + 名前を変更 + アカウント名はすでに取得されています + アカウントの名前を変更できません + アカウントを削除 + 本当にアカウントを削除しますか? + アドレス帳、カレンダー、ToDo リストのすべてのローカルコピーが削除されます。 + このコレクションを同期 + 読み取り専用 + カレンダー + 連絡先 + ジャーナル + ToDo リスト + プライベートのみ表示する + リストを再読み込み + Webcal の購読は外部アプリで同期できます。 + Webcal に対応するアプリが見つかりませんでした + ICSx⁵ をインストール + + アカウントを追加 + プライバシーポリシー をご確認ください。]]> + 共通のログイン方法 + プロバイダー固有のログイン方法 + 続行 + ログイン + メールアドレスでログイン + メールアドレス + 有効なメールアドレスが必要です + サービスを検出します。]]> + パスワード + パスワードを非表示 + パスワードを表示 + パスワード (オプション) + URL とユーザー名でログイン + ユーザー名 + ユーザー名 (オプション) + ベース URL + サービスの検出には DNS レコードと well-known URL も使用します。]]> + 証明書を選択 + アカウントを追加 + アカウント名 + アポストロフィー「\'」を使用すると一部のデバイスで問題が発生します。 + Android はあなたが作成した予定の ORGANIZER フィールドにアカウント名を使用するので、アカウント名としてメールアドレスを使用してください。同じ名前のアカウントを 2 つ保持することはできません。 + 連絡先グループ方法: + アカウント名が必要です + アカウント名はすでに取得されています + アカウントを追加できませんでした + 完了 + 高度なログイン + クライアント証明書が選択されていません (オプション) + クライアント証明書: %s + 証明書が見つかりませんでした + 証明書をインストール + Fastmail + Fastmail アカウント + Fastmail でログイン + Google コンタクト / カレンダー + Google アカウント + Google でログイン + クライアント ID (オプション) + プライバシーポリシー をご確認ください。]]> + Google API サービスのユーザーデータに関するポリシー に準拠しています。]]> + 認可コードを取得できませんでした + Nextcloud + Nextcloud でログイン + ウェブブラウザーでのログインフローが開始されます。 + Nextcloud サーバーアドレス + サインイン + ログイン URL を入手できませんでした + ログイン情報を入手できませんでした + 設定の検出 + しばらくお待ちください。サーバーに問い合わせ中… + CalDAV または CardDAV サービスが見つかりませんでした。 + ベース URL から CalDAV/CardDAV URL に到達できなかったため、サービス検出に失敗しました。 + 私たちがテストしたサービス一覧 を確認してください。]]> + 認証情報 (ほとんどの場合はユーザー名とパスワード) もご確認ください。 + さらに詳細な技術情報はログで確認できます。 + ログを表示 + + 同期 + 連絡先の同期間隔 + 手動のみ + %d 分ごと + ローカルの変更時はすぐに + カレンダーの同期間隔 + ToDo リストの同期間隔 + + 手動のみ + 15 分ごと + 30 分ごと + 1 時間ごと + 2 時間ごと + 4 時間ごと + 毎日 1 回 + + WiFi のみで同期 + WiFi 接続のみで同期します + 接続の種類は考慮されません + WiFi SSID 制限 + %s のみで同期します + すべての WiFi 接続が使用されます + 利用可能な WiFi ネットワークのカンマ区切りの名前 (SSID) (空白にするとすべて) + WiFi SSID 制限にはさらに設定が必要です + 管理 + インターネット接続のない VPN + 検証されたインターネット接続のない VPN では同期を実行しません (推奨) + 検証されたインターネット接続のない VPN でも同期を実行します + 認証 + ユーザー名 + パスワードまたはアプリパスワード + アプリパスワードを使用しているかもしれません。]]> + 新しいパスワード + ご利用のサーバーに従ってパスワードを更新します。 + もう一度認可する (OAuth) + アクセス権が失効した場合に使用してください + 正常に認可しました + クライアント証明書 + クライアント証明書が使用できないか、選択されていません + 証明書をインストール + CalDAV + 過去の予定の読み込み制限 + すべての予定が同期されます + + %d 日より前の予定は無視されます + + この日数より過去の予定は無視されます (0 も可)。すべての予定を同期するには、空白のままにしてください。 + デフォルトのリマインダー + + デフォルトのリマインダーは予定の %d 分前です + + デフォルトのリマインダーはありません + デフォルトのリマインダーは、リマインダーのない予定に適用されます。希望する分数を入力してください。デフォルトのリマインダーを無効にするには、空白のままにしてください。 + カレンダーの色を管理する + カレンダーの色は同期ごとにリセットされます + カレンダーの色は他のアプリで設定できます + 予定の色に対応する + 予定の色が同期されます + 予定の色は同期されません + CardDAV + 連絡先のグループ方法 + + グループで個別の vCard に分割する + 各連絡先にグループをカテゴリーとして記録する + + + アドレス帳を作成 + サーバーが CardDAV 経由のカレンダー作成に対応していない可能性があります。 + カレンダーを作成 + 既定のタイムゾーン (オプション) + + 可能なカレンダーエントリー + 予定 + ToDo リスト + メモ / ジャーナル + サーバーが CalDAV 経由のカレンダー作成に対応していない可能性があります。 + + タイトル + ストレージの場所 + 説明 (オプション) + 作成 + + 連絡先 + 件の予定 + ToDo リスト + コレクションを削除 + このコレクション (%s) とコレクション内のすべてのデータが、ローカルとサーバーから永久に削除されます。 + 同期 + 同期が有効です + 同期が無効です + 読み取り専用 + 読み取り専用 (サーバー設定) + 読み取り専用 (ポリシーによる) + 読み取り専用 (ローカル設定) + 読み取り/書き込み + タイトル + 説明 + 所有者 + プッシュ対応 + サーバーがプッシュへの対応を通知しています + 購読開始 %1$s、期限 %2$s + 最終同期 (%s) + アドレス (URL) + + デバッグ情報 + ZIP アーカイブ + デバッグ情報とログを含みます + アーカイブをコンピューターに転送、メールで送信、サポートチケットに添付するには共有してください + アーカイブを共有 + デバッグ情報がこのメッセージに添付されています (受信したアプリが添付ファイルに対応している必要があります) + HTTP エラー + サーバーエラー + WebDAV エラー + I/O エラー + リクエストはサーバーにより拒否されました。 + リクエストされたリソースが存在しません。 + サーバーがリクエストされた形式の操作を許可していません。 + サーバー側で問題が発生しました。あなたのサーバーのサポートに連絡してください。 + 予期せぬエラーが発生しました。詳細はデバッグ情報を確認してください。 + 詳細を表示 + 収集されたデバッグ情報 + 関連するリソース + 関係する問題 + リモートリソース: + ローカルリソース: + ログ + 詳細なログが利用できます + ログを表示 + URL をコピー + プライバシー通知 + ログやデバッグ情報はプライベートな情報を含むことがあります。共有する場合には、注意して取り扱ってください。 + + エラーが発生しました + HTTP エラーが発生しました + I/O エラーが発生しました + 詳細を表示 + + WebDAV マウント + 割り当て 使用中: %1$s / 利用可能: %2$s + コンテンツを共有 + マウント解除 + WebDAV マウントを追加 + WebDAV マウントを追加してクラウドファイルに直接アクセスしましょう! + WebDAV マウントの動作についてを確認してください。]]> + 表示名 + WebDAV URL + 無効な URL + マウントポイントと表示名 + 認証 + ユーザー名 + パスワード + ユーザー名 (オプション) + パスワード (オプション) + マウントを追加 + この URL では WebDAV サービスがありません + マウントポイントを削除 + 接続の詳細が失われました。ファイルは削除されていません。 + WebDAV ファイルにアクセスしています + WebDAV ファイルをダウンロードしています + WebDAV ファイルをアップロードしています + WebDAV マウント + + DAVx⁵ の権限 + アクセス権限の許可が必要です + %s が古すぎます + 最小要求バージョン: %1$s + 認証に失敗しました (ログイン情報を確認してください) + ネットワークまたは I/O エラー – %s + HTTP サーバーエラー – %s + ローカルストレージエラー – %s + ソフトエラー (再試行回数の上限に到達) + サーバーから無効な連絡先を受信しました + サーバーから無効な予定を受信しました + サーバーから無効な ToDo リストを受信しました + 1 件または複数の無効なリソースを無視します + 同期が中断されました + リモートの情報が変更されました + + すべて同期 + すべてのアカウントを同期 + 同期ボタン (ラベル) + 同期ボタン (アイコン) + 手動で同期したいときにタップしてください。 + + diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml new file mode 100644 index 0000000..39bf4d7 --- /dev/null +++ b/app/src/main/res/values-ka/strings.xml @@ -0,0 +1,413 @@ + + + + ანგარიში (აღარ) არსებობს + DAVx⁵ მისამართთა წიგნაკი + წაშლა + ამოშლა + გაუქმება + ეს ველი სავალდებულოა + დახმარება + ზემოთ გადასვლა + ოპციების მენიუ + გაზიარება + სინქრონიზაცია დაიწყა/დადგა რიგში + მონაცემთა ბაზა კორუმპირებულია + ყველა ანგარიში წაშლილ იქნა ადგილობრივად. + დებაგი + სხვა მნიშვნელოვანი შეტყობინებები + დაბალი პრიორიტეტის სტატუსის შეტყობინებები + სინქრონიზაცია + სინქრონიზაციის შეცდომები + მნიშვნელოვანი შეცდომები, რომლებიც აჩერებს სინქრონიზაციას, მაგ., მოულოდნელი სერვერის პასუხები + სინქრონიზაციის გაფრთხილებები + არა-ლეტალური სინქრონიზაციის პრობლემები, როგორც ზოგი არასწორი ფაილი + ქსელის ან ჩაწერა/წაკითხვის შეცდომები + ვადის გასვლა, კავშირის პრობლემები, სხვა (ხშირად დროებითი) + + თქვენი მონაცემები. თქვენი არჩევანი. + აიღეთ კონტროლი. + რეგულარული სინქრონიზაციის ინტერვალები + რეგულარული ინტერვალი სინქრონიზაციისთვის, %s-ს უნდა ჰქონდეს უფლება გაეშვას ფონურ რეჟიმში. სხვაგვარად, Android-მა შეიძლება ნებისმიერ მომენტში შეაჩეროს სინქრონიზაცია. + მე არ მჭირდება რეგულარული სინქრონიზაციის ინტერვალები.* + %s თავსებადობა + მე შევცვალე საჭირო პარამეტრები. აღარ შემახსენოთ.* + * დატოვეთ მოუნიშნელად მოგვიანებით შესახსენებლად. შეიძლება ჩამოგდებულ იქნას აპის პარამეტრებში /%s. + მეტი ინფორმაცია + jtx Board + + დავალებების მხარდაჭერა + თუ დავალებები მხარდაჭერილია თქვენი სერვერის მიერ, მათი სინქრონიზირება შეიძლება მხარდაჭერილი დავალებათა აპით: + OpenTasks + აღარ მიმდინარეობს განვითარება - არ არის რეკომენდებული. + Tasks.org + აპების მაღაზია ხელმიუწვდომია + მე არ მჭირდება დავალებების მხარდაჭერა.* + ღია კოდის პროგრამული უზრუნველყოფა + კმაყოფილები ვართ, რომ იყენებთ %s-ს, რომელიც ღია კოდის პროგრამული უზრუნველყოფაა. განვითარება და მხარდაჭერა რთული სამუშაო. გთხოვთ, გაითვალისწინოთ წილის შეტანა (მრავალი გზა არსებობს) ან ფულის ჩუქბეა. ძალიან მადლობელი ვიქნებით! + როგორ შევიტანო წვლილი/დაგეხმაროთ + + უფლებები + %s-ს სჭირდება უფლებები სწორად სამუშაოდ. + ყველა ქვემოთ მოცემული + გამოიყენეთ ეს ყველა ფუნქციის ჩასართავად (რეკომენდებული) + ყველა უფლება დართულია + კონტაქტების უფლებები + კონტაქტის სინქრონიზაციის გარეშე (არა რეკომენდებული) + კონტაქტის სინქრონიზაცია შესაძლებელია + კალენდარის უფლებები + კალენდარის სინქრონიზაციის გარეშე (არა რეკომენდებული) + კალენდარის სინქრონიზაცია შესაძლებელია + შეტყობინებების უფლება + შეტყობინებები გათიშულია (არა რეკომენდებული) + შეტყობინებები ჩართლია + jtx Board-ის უფლებები + OpenTasks-ის უფლებები + დავალებების უფლებები + დავალებების სინქრონიზაციის გარეშე + დავალებების სინქრონიზაცია შესაძლებელია + Keep-ის უფლებები + უფლებები შეიძლება ავტომატურად ჩამოიყაროს (არა რეკომენდებული) + უფლებები ავტომატურად არ ჩამოიყრება + შეამოწმეთ უფლებები > მოხსენით \"უფლებების ამოშლა, თუ აპი არ გამოიყენება\"-ს მონიშვნა + თუ გადამრთველი არ მუშაობს, გამოიყენეთ აპის პარამეტრები / უფლებები. + აპის პარამეტრები + + WiFi SSID-ს უფლებები + რათა მიწვდეთ მიმდინარე WiFi-ს სახელს (SSID), ეს პირობები უნდა შესრულდეს: + ზუსტი ადგილმდებარეობის უფლება + ადგილმდებარეობის უფლება დართულია + ადგილმდებარეობის უფლება უარყოფილია + ფონური ადგილმდებარეობის უფლება + ყოველთვის დაშვება + ადგილმდებარეობის უფლების მნიშვნელობა: %s + ადგილმდებარეობის უფლება არ არის შემდეგი: %s + ადგილმდებარეობა ყოველთვის ჩართულია + ადგილმდებარეობის სერვისი ჩართულია + ადგილმდებარეობის სერვისი გათიშულია + + თარგმანი + ბიბლიოთეკები + ვერსია %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) და მონაწილეები + ამ პროგრამას არ აქვს არანაირი გარანტია. იგი არის უფასო პროგრამული უზრუნველყოფა, ხოლო თქვენ შეგეძლეიათ იგი გაავრცელოთ გარკვეული პირობების გათვალისწინებით. + + ჟურნალის ფაილი ვერ შეიქმნა + აწი მიმდინარეობს ყველა %s აქტივობის ჟურნალში ჩაწერა + ნახვა/გაზიარება + გათიშვა + + CalDAV/CardDAV სინქრონიზაციის ადაპტერი + შესახებ / ლიცენზია + ბეტას უკუკავშირი + გთხოვთ, დააყენოთ ვებ ბრაუზერი + პარამეტრები + ახალი ამბები & განახლებები + ხელსაწყოები + გარე ბმულები + ვებ საიტი + ინსტრუქცია + ხდკ + საზოგადოება + პროექტის მხარდაჭერა + როგორ შევიტანო ღვაწლი + პირადულობის პოლიტიკა + ყველა ანგარიშის სინქრონიზაცია + + შეტყობინებები გათიშული. თქვენ არ მიიღებთ შეტყობინებებს სიქნრონიზაციის შეცდომების შესახებ. + კავშირების მართვა + გააქტიურებულია მონაცემთა შემნახველი. ფონური სინქრონიზაცია შეზღუდულია. + მონაცემთა შემნახველის მართვა + გფააქტიურებულია კვების ელემენტის შემნახველი. სინქრონიზაცია შეიძლება შეზღუდულ იქნას. + კვების ელემენტის შემნახველის მართვა + მეხსიერება ცოტა დარჩა. Android არ დაასინქრონიზირებს ადგილობრივ ცვლილებებს დაუყონებლივ, ხოლო დაასინქრონიზირებს შემდეგი რეგულარული სინქრონიზაციის დროს. + მეხსიერების მართვა + + სერვისის აღმოჩენა ჩაიშალა + კოლექციათა სიის განახლება ვერ მოხერხდა + + მუშაობს ფონში + ზოგ მოწყობილობაზე, ეს საჭიროა ავტომატური სინქრონიზაციისთვის. + + პარამეტრები + დებაგი + დებაგის ინფორმაციის ჩვენება + დეტალური ჟურნალში ჩაწერა + ჟურნალში ჩაწერა გათიშულია + კვების ელემენტის ოპტიმიზაცია + აპი გამორიცხულია (რეკომენდებულია) + გამოიყენება კვების ელემენტის შეზღუდვები (არა რეკომენდებულია) + კავშირი + პროქსის ტიპი + + ნაგულისხმევი სისტემის მიერ + პროქსის გარეშე + HTTP3 + SOCKS (Orbot-სთვის) + + პროქსის ჰოსტის სახელი + პროქსის პორტი + უსაფრთხოება + აპის ეფლებები + გადახედეთ სინქრონიზაციისთვის საჭირო ეფლებებს + სისტემური სერთიფიკატების ნდობის გაუქმება + სისტემური და მომხმარებლის მიერ დამატებული სერთიფიცირების ავტორიტეტების ნდობა არ იქნება + სისტემური და მომხმარებლის მიერ დამატებული სერთიფიცირების ავტორიტეტების ნდობა იქნება (რეკომენდებული) + (არა) ნდობითი სერთიფიკატების ჩამოყრა + ნდობის ჩამოყრა ყველა კერძო სერთიფიკატზე + ყველა კერძო სერთიფიკატი გასუფთავდა + მომხმარებლის ინტერფეისი + შეტყობინებების პარამეტრები + შეტყობინებების არხების და პარამეტრების მართვა + აირჩიეთ თემა + + სისტემის მიერ ნაგულისხმევი + ღია + მუქი + + მითითებების ჩამოყრა + თავიდან ააქტიურებს მითითებებს, რომლებიც დამალულ იქნა წარსულში + ყველა მითითება თავიდან იქნება ნაჩვენები + ინტეგრაცია + დავალებათა აპი + თავსებადი დავალებათა აპი ვერ მოიძებნა + + CardDAV + CalDAV + Webcal + საჭიროა დამატებითი უფლებები ამ კოლექციების სინქრონიზაციისთვის. + უფლებების მართვა + ახლავე სინქრონიზირება + ანგარიშის პარამეტრები + ანგარიშის სახელის შეცვლა + შეუნახავი ადგილობრივი მონაცემები შეიძლება გაუქმებულ იქნას. საჭიროა თავიდან სინქრონიზირება სახელის შეცვლის შემდეგ. + ახალი ანგარიშის სახელი + სახელის შეცვლა + ანგარიშის სახელი უკვე დაკავებულია + ანგარიშის სახელის შეცვლა ვერ მოხერხდა + ანგარიშის წაშლა + მართლა წაიშალოს ანგარიში? + წაიშლება მისამართთა წიგნაკების, კალენდრების და დავალებათა სიების ყველა ადგილობრივი ასლი. + ამ კოლექციის სინქრონიზირება + მხოლოდ წაკითხვადი + კალენდარი + კონტაქტები + ჟურნალი + დავალებები + მხოლოდ პირადის ჩვენება + სიის განახლება + Webcal გამოწერები შეიძ₾ება სინქრონიზირებულ იქნას გარე აპებთან. + Webcal-თან თავსებადი აპი ვერ მოიძებნა + ICSx⁵-ს დაყენება + + ანგარიშის დამატება + ზოგადი შესვლა + პროვაიდერის შესვლა + გაგრძელება + შესვლა + ელ. ფოსტის მისამართით შესვლა + ელ. ფოსტის მისამართი + საჭიროა სწორი ელ. ფოსტის მისამართი + აღმოჩენილია სერვისები DNS ჩანაწერების და კარგად ცნობილი URL-ების მეშვეობით.]]> + პაროლი + პაროლის დამალვა + პაროლის ჩვენება + URL-ით და მომხმარებლის სახელით შესვლა + მომხმარებლის სახელი + საბაზო URL + ასევე აღმოჩენილია სერვისები DNS ჩანაწერების და კარგად ცნობილი URL-ების მეშვეობით.]]> + სერტიფიკატის არჩევა + ანგარიშის დამატება + ანგარიშის სახელი + აპოსტროფების (\') გამოყენება იწვევს პრობლემებს ზოგ მოწყობილობაზე. + გამოიყენეთ თქვენი ელ. ფოსტის მსიამართი ანგარიშის სახელად, რადგან Android გამოიყენებს ანგარიშის სახელს ორგანიზატორის ველში თქვენს მიერ შექმნილ ღონისძიებებისთვის. თქვენ არ შეიძლება გქონდეთ ორი ანგარიში იგივე სახელით. + კონტაქტების დაჯგუფების მეთოდი: + საჭიროა ანგარიშის სახელი + ანგარიშის სახელი უკვე დაკავებულია + გაფართოებული შესვლა + კლიენტის სერტიფიკატი: %s + სერტიფიკატი ვერ მოიძებნა + სერტიფიკატის დაყენება + Google კონტაქტები / კალენდარი + Google ანგარიში + Google-ით შესვლა + კლიენტის ID (aრასავალდებულო) + პირადულობის პოლიტიკა დეტალებისთვის.]]> + Google API სერვისების მომხმარებელთა მონაცემების პოლიტიკას, მათ შორის, შეზღუდული გამოყენების მოთხოვნებს.]]> + ავტორიზაციის კოდის მიღება ვერ მოხერხდა + Nextcloud + შესვლა Nextcloud-ისთ + ეს დაიწყებს Nextcloud-ის შესვლის პროცესს ვებ ბრაუზერში. + Nextcloud-ის სერვერის მისამართი + შესვლა + შესვლის URL-ის მიღება ვერ მოხერხდა + შესვლის მონაცემების მიღება ვერ მოხერხდა + კონფიგურაციის აღმოჩენა + გთხოვთ, დაელოდოთ, მიმდინარეობს სერვერის გამოკითხვა... + CalDAV-ის ან CardDAV-ის სერვისის მოძებნა ვერ მოხერხდა. + საბაზო URL არ არის წვდომადი CalDAV/CardDAV URL და სერვერისის აღმოჩენა არ იყო წარმატებული. + ჩვენს მიერ ტესტირებული სერვისების სია და მათი საბაზო URL.]]> + გთხოვთ, ასევე გადაამოწმოთ აუთენტიფიკაცია (ზოგადად, მომხმარებლის სახელი დაპაროლი). + დამატებითი ტექნიკური ინფორმაცია ხელმისაწვდომია ჟურნალებში. + ჟურნალების ნახვა + + სინქრონიზაცია + კონტაქტების სინქრონიზაციის ინტერვალი + მხოლოდ ხელით + ყოველ %d წუთში + დაუყონებლივ ადგილობრივი ცვლილებებისას + კალენდრების სინქრონიზაციის ინტერვალი + დავალებვათა სინქრონიზაციის ინტერვალი + + მხოლოდ ხელით + ყოველ 15 წუთში + ყოველ 30 წუთში + ყოველ 1 საათში + ყოველ 2 საათში + ყოველ 4 საათში + ყოველდღე + + მხოლოდ WiFi-ით სინქრონიზაცია + სინქრონიზაცია შეზღუდულია WiFi კავშირზე + კავშირის ტიპი არ გაითვალისწინება + WiFi SSID-ს შეზღუდვა + დასინქრონიზირდება მხოლო %s-ით + გამოიყენება ყველა WiFi კავშირი + დაშვებული WiFi ქსელების მძიმეთი დაყოფილი სახელები (SSID) (დატოვეთ ცარიელად ყველასთვის) + WiFi SSID-ს შეზღუდვას სჭირდება დამატებითი პარამეტრები + მართვა + VPN-ს სჭირდება არსებული ინტერნეტ-კავშირი + VPN არსებული დადასტურებული ინტერნეტ-კავშირის გარეშე არ არის საკმარისი სინქრონიზაციის გასაშვებად (რეკომენდებული) + VPN არსებული დადასტურებული ინტერნეტ-კავშირის გარეშე არ არის საკმარისი სინქრონიზაციის გასაშვებად + აუთენტიფიკაცია + მომხმარებლის სახელი + ახალი პაროლი + პაროლის განახლება თქვენი სერვერის მიხედვით + კლიენტის სერთიფიკატი + სერთიფიკატი ხელმიუწვდომია ან არ არის არჩეული + სერტიფიკატის დაყენება + CalDAV + გასული ღონისძიების დროის შეზღუდვა + დასინქრონიზირდება ყველა ღონისძიება + + ერთ დღეზე უფრო ძველი ღონისძიებები იქნება იგნორირებული + %d დღეზე უფრო ძველი ღონისძიებები იქნება იგნორირებული + + ღონისძიებები, რომლებიც უფრო ძველია, ვიდრე დღეთა მითითებული რაოდენობა, იქნება იგნორირებული (შეიძლება იყოს 0). დატოვეთ ცარიელად ყველას სინქრონიზებისთვის. + ნაგულისხმევა შეხსენება + + ნაგულისხმევი შეხსენება ღონისძიებამდე ერთი წუთით ადრე + ნაგულისხმევი შეხსენება ღონისძიებამდე %d წუთით ადრე + + ნაგულისხმევი შეხსენება არ არის შექმნილი + თუ ნაგულისხმევი შეხსენება უნდა შეიქმნას შეხსენების გარეშე ღონისძიებებისთვის: ღონისძიებამდე წუთების სასურველი რიცხვი. დატოვეთ ცარიელად ნაგულისხმევი შეხსენებების გასათიშად. + კალენდარის ფერების მართვა + კალენდარის ფერები ჩამოიყრება ყოველ სინქრონიზაციაზე + კალენდარის ფერები შეიძლება დაყენებულ იქნას სხვა აპების მიერ + ღონისძიების ფერის მხარდაჭერა + ღონისძიების ფერები არის სინქრონიზირებული + ღონისძიების ფერები არ არის სინქრონიზირებული + CardDAV + კონტაქტების დაჯგუფების მეთოდი + + ჯგუფები ცალკე vCard-ებია + ჯგუფები არის კონტაქტთა კატეგორია + + + მისამართთა წიგნაკის შექმნა + მისამართთა წიგნაკის შექმნა CardDAV-ით შეიძლება არ იყოს მხარდაჭერილი სერვერის მიერ. + კალენდარის შექმნა + + დაშვებული კალენდარის ჩანაწერები + ღონისძიებები + დავალებები + შენიშვნები / ჟურნალი + კალენდრის შექმნა CalDAV-ით შეიძლება არ იყოს მხარდაჭერილი სერვერის მიერ. + ფერი + სათაური + მეხსიერების ადგილმდებარეობა + აღწერა (არასავალდებულო) + შექმნა + + კონტაქტები + დავალებები + კოლექციის წაშლა + ეს კოლექცია (%s) და მისი ყველა მონაცემი სამუდამოდ წაიშლება, როგორც ადგილობრივად, ისე სერვერზეც. + სინქრონიზაცია + სინქრონიზაცია ჩართულია + სინქრონიზაცია გამორთულია + მხოლოდ წაკითხვადი + მხოლოდ წაკითხვადი (სერვერის მიერ) + მხოლოდ წაკითხვადი (მხოლოდ ადგილობრივად) + წაკითხვა/ჩაწერა + სათაური + აღწერა + მფლობელი + Push-ის მხარდაჭერა + სერვერი გადმოსცემს Push-ის მხარდაჭერას + ბოლო სინქრონიზაცია (%s) + მისამართი (URL) + + დებაგის ინფო + ZIP არქივი + შეიცავს დებაგის ინფოს და ჟურნალებს + გააზიარეთ არქივი მისი კომპიუტერზე გადასაგზავნად, ელ. ფოსტით გასაგზავნად ან მისი მხარდაჭერის ბილეთზე მისაბმელად. + არქივის გაზიარება + დებაგის ინფო მიბმულია ამ შეტყობინებაზე (სჭირდება მიბმის მხარდაჭერა მიმღებ აპში). + HTTP შეცდომა + სერვერის შეცდომა + WebDAV შეცდომა + წაკითხვა/ჩაწერის შეცდომა + დეტალების ნახვა + დებაგის ინფო შეგროვდა + შესაბამისი რესურსები + დაკავშირებული პრობლემასთან + დაშორებული რესურსი: + ადგილობრივი რესურსი: + ჟურნალები + ხელმისაწვდომია დეტალური ჟურნალები + ჟურნალების ნახვა + + მოხდა შეცდომა. + მოხდა HTTP შეცდომა. + მოხდა წაკითხვა/ჩაწერის შეცდომა. + დეტალების ჩვენება. + + WebDAV-ის მიბმები + გამოყენებული კვოტა: %1$s / ხელმისაწვდომი: %2$s + შიგთავსის გაზიარება + მიბმის გათიშვა + WebDAV-ის მიბმის დამატება + პირდაპირ იქონიეთ წვდომა თქვენი ღრუბლის ფაილებზე WebDAV-ის მიბმის დამატებით! + ნაჩვენები სახელი + WebDAV URL + არასწორი URL + აუთენტიფიკაცია + მომხმარებლის სახელი + პაროლი + მიბმის დამატება + WebDAV სერვისი ამ URL-ზე არ არის + მიბმის წერტილის ამოშლა + კავშირის დეტალები დაიკარგება, მაგრამ ფაილები არ წაიშლება. + მიმდინარეობს WebDAV ფაილზე წვდომა + მიმდინარეობს WebDAV ფაილის გადმოტვირთვა + მიმდინარეობს WebDAV ფაილის ატვირთვა + WebDAV-iს მიბმა + + DAVx⁵-ის უფლებები + საჭიროა დამატებითი უფლებები + %s ნამეტანი ძველია + მინიმალური საჭირო ვერსია: %1$s + აუთენტიფიკაცია ჩაიშალა (შეამოწმეთ შევლის იდენტიფიკატორები) + ქსელური ან ჩაწერა/წაკითხვის შეცდომა - %s + HTTP სერვერის შეცდომა - %s + ადგილობრივი მეხსიერების შეცდომა - %s + რბილის შეცდომა (მიღწეულია თავიდან ცდის მაწსიმუმი) + მიღებულია არასწორი კონტაქტი სერვერიდან + მიღებულია არასწორი ღონისძიება სერვერიდან + მიღებული არასწორი დავალება სერვერიდან + ერთი ან მეტი არასწორი რესურსის იგნორირება + + ყველაფრის სინქრონიზირება + ყველა ანგარიშის სინქრონიზაცია + + diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml new file mode 100644 index 0000000..5817875 --- /dev/null +++ b/app/src/main/res/values-ko/strings.xml @@ -0,0 +1,462 @@ + + + + 계정이 (더이상) 존재하지 않음 + DAVx⁵ 주소록 + 여기에서 계정을 변경하지 마세요. 앱에서 직접 계정을 관리해 주세요. + 삭제 + 제거 + 취소 + 활성 + 이 항목은 필수입니다 + 도움말 + 상위 항목으로 이동 + 옵션 메뉴 + 공유 + 동기화 시작/대기열에 추가 + 데이터베이스 손상 + 모든 계정이 로컬에서 제거되었습니다. + 디버깅 + 기타 중요한 메시지 + 우선순위가 낮은 메시지 + 동기화 + 동기화 오류 + 예기치 않은 서버 응답과 같이 동기화를 중지시키는 중요한 오류 + 동기화 경고 + 일부 잘못된 파일과 같이 심각하지 않은 동기화 문제 + 네트워크 및 I/O 에러 + 시간 초과, 연결 문제 등 (주로 일시적인 문제) + + 당신의 데이터. 당신의 선택. + 관리. + 정기적 동기화 주기 + 정기적으로 동기화하려면 백그라운드에서 %s이 실행되도록 허용해야 합니다. 그렇지 않으면 Android는 언제든지 동기화를 일시 중지할 수 있습니다. + 나는 정기적인 동기화 주기가 필요하지 않다.* + %s 호환성 + 필요한 설정을 완료했습니다. 더 이상 다시 수행하지 마세요.* + * 나중에 알림이 표시되도록 선택 해제된 상태로 둡니다. / 앱 설정에서 재설정할 수 있습니다. / %s + 상세 정보 + jtx Board + + 작업 지원 + 서버에서 작업을 지원하는 경우 지원되는 작업 앱과 동기화할 수 있습니다. + OpenTasks + 더 이상 개발되지 않는 것 같으니 추천하지 않습니다. + Tasks.org + 일부 기능이 지원되지 않습니다.]]> + 사용 가능한 앱 스토어 없음 + 업무 지원은 필요 없습니다.* + 오픈 소스 소프트웨어 + 오픈 소스 소프트웨어인 %s를 사용해 주셔서 기쁩니다. 개발, 유지보수 및 지원은 힘든 작업입니다. (여러 방식의) 기여나 기부를 고려해 보세요. 정말 고마울 것 같습니다. + 기여/기부 방법 + 한동안 알리지 않기 + + %d 개월 + + 다음 + + 권한 + %s이 제대로 작동하려면 사용 권한이 필요합니다 + 아래의 모든 것 + 이를 통해 모든 기능을 사용할 수 있습니다(권장). + 모든 권한 부여 + 연락처 권한 + 연락처 동기화 하지않음 (권장하지 않음) + 연락처 동기화 가능 + 캘린더 권한 + 캘린더 동기화 없음(권장하지 않음) + 캘린더를 동기화 할 수 있음 + 알림 권한 + 알림 사용 안함 (권장하지 않음) + 알림 활성화 + jtx Board 권한 + OpenTasks 권한 + 작업 권한 + 할일 목록 동기화 하지않음 + 할일 목록 동기화 가능 + 권한 유지 + 권한이 자동으로 재설정됨(권장하지 않음) + 권한이 자동으로 재설정 되지않음 + 권한을 선택하세요 > \"앱을 사용하지 않을 경우 사용 권한 제거\" + 스위치가 작동하지 않으면 앱 설정 / 사용 권한을 사용하십시오. + 앱 설정 + + WiFi SSID 권한 + 현재 WiFi 이름(SSID)에 액세스할 수 있으려면 다음 조건을 충족해야 합니다. + 상세 위치 권한 + 위치 권한 부여 + 위치 권한 거부 + 백그라운드 위치 권한 + 항상 허용 + 위치 권한 설정: %s + 위치 권한 미설정: %s + %s 은(는) 특정 WiFi SSID로 동기화를 제한하기 위한 목적으로 위치 데이터(WiFi SSID만 사용)를 활용합니다. 이 동작은 백그라운드에서 동기화할 때도 동일하게 적용됩니다. + 모든 위치 데이터(와이파이 SSID만 해당)는 오직 기기 내에서만 사용되며, 어디에도 전송되지 않습니다. + 위치 정보 항상 사용 + 위치 서비스를 사용할 수 있습니다. + 위치 서비스가 거부되었습니다. + + 번역 + 라이브러리 + 버전 %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) and contributors + 이 프로그램은 보증 없이 제공됩니다. 그것은 무료 소프트웨어이며, 특정한 조건 하에서 재배포하는 것을 환영합니다. + + log file을 만들 수 없습니다. + 이제 모든 %s 활동을 로깅합니다. + 보기/공유 + 사용 안함 + + CalDAV/CardDAV 동기화 어댑터 + 관련 / 라이센스 + 베타 피드백 + 웹 브라우저를 설치해 주십시오. + 설정 + 뉴스 & 업데이트 + + 외부 링크 + 웹 사이트 + 메뉴얼 + FAQ + 커뮤니티 + 프로젝트 지원 + 기여 방법 + 개인 정보 보호 정책 + DAVx⁵에 오신 것을 환영합니다! + 서버에 연결하여 캘린더와 주소록을 항상 동기화된 상태로 유지하세요. + 모든 계정 동기화 + + 알림 비활성화되어 있습니다. 동기화 에러 알림을 받을 수 없습니다. + 자동 동기화 비활성화 (검증된 인터넷 연결 없음) + 연결 관리 + 데이터 절약 모드가 활성화되어 있습니다. 백그라운드 동기화가 제한됩니다. + 배터리 절약 모드 관리 + 배터리 세이버가 활성화되어 있습니다. 동기화가 제한될 수 있습니다. + 배터리 세이버 관리 + 저장 공간이 부족합니다. 기기에 저장된 변경 사항은 즉시 동기화되지 않으며, 다음 정기 동기화 시에 반영됩니다. + 저장 공간 관리 + 캘린더 공급자를 찾을 수 없음 + \"캘린더 저장소\" 시스템 앱을 비활성화하셨나요? + 연락처 공급자를 찾을 수 없음 + \"연락처 저장소\" 시스템 앱을 비활성화하셨나요? + 관리 앱 + + 서비스 검색 실패 + 컬렉션 목록을 새로 고칠 수 없습니다. + + foreground에서 실행중 + 일부 장치에서는 자동 동기화를 위해 이 작업이 필요합니다. + + 설정 + Debugging + 디버그 정보 보기 + 구성 세부 정보 및 로그 보기/공유 + 상세 로그 + 로깅이 활성화되었습니다. 로그는 디버그 정보의 일부로 확인할 수 있습니다. + 로깅이 비활성화되었습니다. + 배터리 최적화 + 앱이 배터리 최적화에서 제외됨 (권장됨) + 배터리 제한이 적용됨 (권장되지 않음) + 연결 + 프록시 타입 + + 기본값 + No proxy + HTTP + SOCKS (for Orbot) + + 프록시 호스트 이름 + 프록시 포트 + 보안 + 앱 권한 + 동기화에 필요한 사용 권한 검토 + 신뢰할 수 없는 시스템 인증 + 시스템 및 사용자 추가한 CA를 신뢰할 수 없음 + 시스템 및 사용자 추가한 CA를 신뢰할 수 있음(권장) + 이 설정이 활성화되면 시스템 인증서는 신뢰되지 않습니다. 따라서 (서버가 인증서를 갱신할 때도) 모든 인증서를 직접 수동으로 허용해야 합니다. 그렇지 않으면 계정 설정이나 동기화가 작동하지 않습니다. + 신뢰할 수 있는(없는) 인증서 재설정 + 모든 사용자 지정 인증서를 재설정합니다. + 모든 사용자 지정 인증서가 확인되었습니다. + 사용자 인터페이스 + 알림 설정 + 알림 채널 및 해당 설정 관리 + 테마 선택 + + 시스템 디자인 + 밝은 테마 + 어두운 테마 + + 힌트 재설정 + 이전에 해제된 힌트를 다시 사용 + 모든 힌트가 다시 표기 + 통합 + 테스크 앱 + 호환되는 작업 앱을 찾을 수 없습니다. + UnifiedPush (실험적 기능) + 없음 (푸시 비활성화) + 배포자(distributor) 선택 + 푸시 배포자(distributor)가 없음 + 엔드포인트(Endpoint)가 설정되지 않았습니다. + %s 을(를) 통해 푸시 메시지를 수신할 준비가 되었습니다. + FCM (Google Play) + 푸시 메시지는 항상 암호화됩니다. + + CardDAV + CalDAV + Webcal + 이 컬렉션을 동기화하려면 추가적인 권한이 필요합니다. + 권한 관리 + 동기화 + 계정 설정 + 계정 이름 바꾸기 + 저장하지 않은 로컬 데이터는 삭제될 수 있습니다. 이름 변경 후 재동기화가 필요합니다. + 새로운 계정 이름 + 이름 바꾸기 + 계정 이름이 이미 사용되었습니다. + 계정 이름을 바꿀 수 없습니다. + 계정 삭제 + 정말 계정을 삭제하시겠습니까? + 주소록, 캘린더 및 업무 목록의 모든 로컬 복사본이 삭제됩니다. + 이 컬렉션을 동기화합니다. + 읽기전용 + 캘린더 + 연락처 + 일지 + 작업 + 개인만 표시 + 목록 새로고침 + Webcal 구독은 외부 앱과 동기화할 수 있습니다. + Webcal 지원 앱을 찾을 수 없습니다. + ICSx⁵를 설치합니다. + + 계정 추가 + 개인정보처리방침을 참조하십시오.]]> + 일반 로그인 + 제공자별 로그인 + 계속 + 로그인 + 이메일 주소로 로그인 + 이메일 주소 + 올바른 이메일 주소가 필요합니다. + 이 서비스 는 DNS 레코드와 잘 알려진 URL을 통해 탐색됩니다.]]> + 비밀번호 + 비밀번호 숨기기 + 비밀번호 표시 + 로그인 URL 과 사용자 이름 + 사용자 이름 + 기본 URL + 이 서비스 는 DNS 레코드와 잘 알려진 URL을 통해서도 탐색합니다.]]> + 인증서 선택 + 계정 추가 + 계정 이름 + 일부 기기에서는 아포스트로피(\')를 사용할 경우 문제가 발생할 수 있습니다. + Android는 사용자가 만든 이벤트에 대해 계정 이름을 ORGANGER 필드로 사용하므로 전자 메일 주소를 계정 이름으로 사용합니다. 이름이 같은 두 개의 계정을 가질 수 없습니다. + 연락처 분류 방법: + 계정 이름 필요 + 계정 이름이 이미 사용되었습니다. + 계정을 추가할 수 없습니다. + 종료 + 고급 로그인 + 클라이언트 인증서: %s + 인증서를 찾을 수 없음 + 인증서 설치 + Fastmail + Fastmail 계정 + Fastmail로 로그인 + 구글 주소록 / 캘린더 + 구글 계정 + 구글로 로그인 + 클라이언트 ID (선택) + 개인정보처리방침 을 참조하십시오.]]> + Google API 서비스 사용자 데이터 정책을 준수합니다.]]> + 인증 코드를 가져올 수 없습니다. + Nextcloud + Nextcloud로 로그인 + 웹 브라우저에서 Nextcloud 로그인 절차가 시작됩니다. + Nextcloud 서버 주소 + 로그인 + 로그인 URL을 가져올 수 없습니다. + 로그인 데이터를 가져올 수 없습니다. + 구성 탐색 + 잠시 기다려 주십시오. 서버를 쿼리하고 있습니다... + CalDAV 또는 CardDAV 서비스를 찾을 수 없습니다. + 기본 URL이 접근 가능한 CalDAV/CardDAV 주소가 아닌 것 같으며, 서비스 탐색에 실패했습니다. + 저희가 테스트한 서비스 목록 그리고 기본 URL을 참조하시기 바랍니다.]]> + 인증 정보(일반적으로 사용자 이름 및 비밀번호)를 다시 한번 확인해 주십시오. + 더 자세한 기술 정보는 로그에서 확인하실 수 있습니다. + logs 보기 + + 동기화 + 주기적 연락처 동기화 + 직접 선택 + 매 %d 분 + 로컬 변경시 즉시 + 주기적 캘린더 동기화 + 주기적으로 작업 동기화 + + 직접 선택 + 매 15분마다 + 매 30분마다 + 매 1시간마다 + 매 2시간마다 + 매 4시간마다 + 매일 한번 + + WiFi로만 동기화 + 동기화는 WiFi 연결로 제한됩니다. + 연결 유형은 고려되지 않습니다. + WiFi SSID 제한 + 다음에 대해서만 동기화됨 %s + 모든 WiFi 연결이 사용됩니다. + 쉼표로 구분된 허용되는 WIFI 네트워크의 이름(모두 빈칸으로 두세요) + WiFi SSID 제한에 추가 설정이 필요함 + 관리 + VPN은 인터넷 연결을 기반으로 동작 + 검증된 인터넷 연결 없이 VPN만으로는 동기화를 실행하기에 충분하지 않습니다. (권장) + 검증된 인터넷 연결이 없어도 VPN만으로도 동기화를 실행할 수 있습니다. + 인증 + 사용자 이름 + 비밀번호 또는 앱 비밀번호 + 앱 비밀번호를 사용했을 수 있습니다.]]> + 새 비밀번호 + 귀하의 서버에 비밀번호 업데이트. + OAuth로 다시 인증해주세요. + 접근이 차단되었을 때 사용하십시오. + 인증에 성공했습니다. + 클라이언트 인증서 + 인증서를 사용할 수 없거나 선택되지 않았음 + 인증서 설치 + CalDAV + 지난 이벤트 시간 제한 + 모든 이벤트가 동기화 됩니다. + + 지난 이벤트 중 %d일은 무시됩니다. + + 지난 일 수보다 많은 이벤트는 무시됩니다(0일). 모든 이벤트를 동기화하려면 비워 두십시오. + 기본 리마인더 + + 이벤트 %d분 전 기본 리마인더 + + 기본 리마인더이 생성되지 않았습니다 + 리마인더없이 이벤트에 대해 default 리마인더가 생성되어야 하는 경우: 이벤트 시작 전 원하는 시간. default 리마인더을 사용하지 않으려면 비워 두십시오. + 캘린더 색상 관리 + 캘린더 색상은 동기화할 때마다 재설정됩니다. + 다른 앱에서 캘린더 색상을 설정할 수 있습니다. + 이벤트 색상 지원 + 이벤트 색상이 동기화 되었습니다. + 이벤트 색상이 동기화되지 않았습니다. + CardDAV + 연락처 분류 방법 + + 별도의 전자 명함으로 분류 + 연락처 별 항목으로 분류 + + + 주소록 생성 + 서버에서 CardDAV를 통한 주소록 생성을 지원하지 않을 수 있습니다. + 캘린더 생성 + + 가능한 캘린더 항목 + 이벤트 + 작업 + 메모 및 저널 + 서버에서 CalDAV를 통한 캘린더 생성을 지원하지 않을 수 있습니다. + 색상 + 제목 + 저장 위치 + 생성 + + 주소록 + 일정 + 작업 + 컬렉션 삭제 + 이 컬렉션 (%s) 과 모든 데이터는 로컬과 서버에서 영구적으로 삭제됩니다. + 동기화 + 동기화 활성화됨 + 동기화 비활성화됨 + 읽기 전용 + (서버 설정에 의해) 읽기 전용 + (정책상) 읽기 전용 + (로컬에서만) 읽기 전용 + 읽기/쓰기 모두 가능 + 제목 + 설명 + 소유자 + 푸시 지원 + 서버가 푸시 기능을 지원 + 구독 시작: %1$s, 만료일: %2$s + 마지막 동기화: %s + 주소 (URL) + + 디버그 정보 + ZIP 아카이브 + 디버그 정보과 로그를 포함 + 아카이브를 공유하여 컴퓨터로 전송하거나 email로 보내거나 support ticket에 첨부합니다. + 아카이브를 공유 + 이 메시지에 디버그 정보가 첨부되어 있습니다. (수신 앱이 첨부 파일을 지원해야 함) + HTTP 에러 + 서버 에러 + WebDAV 에러 + I/O 에러 + 상세 설명보기 + 디버그 정보가 수집되었습니다. + 관련 리소스 + 관련된 문제 + 원격 리소스: + 로컬 리소스: + Logs + 상세 logs를 사용할 수 있습니다. + logs 보기 + URL 복사 + 개인정보 보호 고지 + 로그 및 디버그 정보에는 개인 정보가 포함될 수 있습니다. 이를 공개적으로 공유할 때 유의하시기 바랍니다. + + 에러가 발생 하였습니다. + HTTP 에러가 발생 하였습니다. + I/O 에러가 발생 하였습니다. + 자세히 + + WebDAV mounts + 사용된 할당량: %1$s / 사용가능한 할당량: %2$s + 공유 콘텐츠 + Unmount + Add WebDAV mount + WebDAV 마운트를 추가하여 클라우드 파일에 직접 액세스하십시오! + WebDAV 마운트 작동 방식은 매뉴얼을 참고해주세요.]]> + 이름 표기 + WebDAV URL + 잘못된 URL + 인증 + 사용자 이름 + 비밀번호 + Add mount + 이 URL에 WebDAV 서비스가 없습니다. + 마운트 지점 제거 + 연결 세부 정보는 사라지지만, 파일은 삭제되지 않습니다. + Accessing WebDAV file + Downloading WebDAV file + Uploading WebDAV file + WebDAV mount + + DAVx⁵ 권한 + 추가 권한 필요 + %s는 너무 오래되었습니다. + 최소 필요 버전: %1$s + 인증에 실패했습니다. (로그인 정보 확인) + 네트워크 혹은 I/O 에러 – %s + HTTP 서버 오류 – %s + Local storage 오류 – %s + 일시적 오류 (최대 재시도 횟수 도달) + 서버로부터 잘못된 연락처를 받았습니다. + 서버에서 잘못된 이벤트를 수신했습니다. + 서버에서 잘못된 작업을 수신했습니다. + 하나 이상의 잘못된 리소스 무시 + 동기화 대기 중 + 원격 데이터가 변경되었습니다 + + 전체 동기화 + 모든 계정 동기화 + 텍스트 동기화 버튼 + 아이콘 동기화 버튼 + 수동으로 동기화하려면 탭하세요. + + diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml new file mode 100644 index 0000000..fe6865e --- /dev/null +++ b/app/src/main/res/values-nb/strings.xml @@ -0,0 +1,171 @@ + + + + DAVx⁵-adressebok + Skru på + Hjelp + Feilsøking + Andre viktige beskjeder + Synkronisering + Synkroniseringsfeil + Viktige feil som avbryter synkronisering, som uventede svar fra serveren + Ikke-kritiske synkroniseringsproblem, som enkelte ugyldige filer + Nettverk- og I/O-feil + + Mer informasjon + + Kontakt-tilgang + Kalender-tilgang + OpenTasks-tilganger + + + Bibliotek + Versjon %1$s(%2$d) + Dette programmet kommer uten NOEN FORM FOR GARANTI. Det er fri programvare, og du er velkommen til å redistribuere det under gitte forhold. + + Kan ikke opprette loggfil + + CalDAV/CardDAV -synkroniseringsadapter + Om / Lisens + Tilbakemelding om beta-en + Innstillinger + Nyheter og oppdateringer + Eksterne lenker + Nettside + Manuell + O-S-S + + + Tjenesteoppdagelse mislyktes + Kunne ikke gjenoppfriske innsamlingsliste + + + Innstillinger + Feilretting + Vis feilrettingsinfo + Grundig logging + Logging er skrudd av + Tilkobling + Sikkerhet + Fjern tiltro til systemsertifikater + System og brukertillagte sertifikatsmyntigheter vil ikke bli tiltrodd + System- og bruker -tillagte sertifikatsmyndigheter vil bli tiltrodd (anbefalt) + Tilbakestill (ikke)tiltrodde sertifikater + Tilbakestiller tillit til alle egendefinerte sertifikater + Alle egendefinerte sertifikater har blitt fjernet + Brukergrensesnitt + Varselsinnstillinger + Tilbakestill hint + Skrur på hint som har blitt avslått tidligere + Alle hint vil bli vist igjen + + CardDAV + CalDAV + Webcal + Synkroniser nå + Kontoinnstillinger + Gi konto nytt navn + Gi nytt navn + Brukernavnet er allerede i bruk + Slett konto + Vil du virkeling slette kontoen? + Alle lokale kopier av adressebøker, kalendere og gjøremålslister vil bli slettet. + synkroniser denne samlingen + kun lesbar + kalender + Fant ingen programmer med støtte for Webcal + Installer ICSx⁵ + + Legg til konto + Logg inn + Innlogging med e-postadresse + E-postadresse + Gyldig e-postadresse påkrevd + Passord + Logg inn med nettadresse og brukernavn + Brukernavn + Landings-nettadresse + Legg til konto + Kontonavn + Bruk din e-postadresse som kontonavn fordi Android vil bruke kontonavnet som ORGANISATOR-felt for hendelser du oppretter. Du kan ikke ha to kontoer med samme navn. + Kontaktgruppemetode: + Kontonavn påkrevd + Brukernavnet er allerede i bruk + Oppdagelse av oppsett + Vent, spør tjener… + Fant ikke CalDAV eller CardDAV-tjeneste. + + Synkronisering + Intervall for kontaktsynkronisering + Åpne manuelt + Hvert %d minutt + umiddelbart ved lokale endringer + Kalendersynkroniseringsintervall + Gjøremålssynkroniseringsintervall + + Bare manuelt + Hvert kvarter + Hver halvtime + Hver time + Hver andre time + Hver fjerde time + Én gang om dagen + + Bare synk. over Wi-Fi + Synkronisering er begrenset til Wi-Fi -tilkoblinger + Tilkoblingstypen blir ikke tatt i betraktning + Wi-Fi SSID -begrensning + Vil kun synkronisere over %s + Alle Wi-Fi -tilkoblinger vil bli brukt + Kommainndelte navn (SSID-er) på tillatte Wi-Fi -nettverk (la stå tomt for alle) + Identitetsbekreftelse + Brukernavn + Oppdater passordet i henhold til din tjener. + CalDAV + Tidsgrense for tidligere hendelser + Alle gjøremål vil bli synkronisert + + Gjøremål for mer enn én dag siden vil bli sett bort fra + Gjøremål for mer enn %d dager siden vil bli sett bort fra + + Hendelser som er mer enn dette antallet dager i fortid vil bli ignorert (kan være 0). La stå tomt for å synkronisere alle hendelser. + Velg kalenderfarger + Støtte for fargelegging av hendelser + CardDAV + Kontaktgruppemetode + + Opprett adressebok + Opprett kalender + Mulige kalenderhendelser + Hendelser + Oppgaver + Farge + Tittel + Lagringslokasjon + Opprett + + Slett samling + Synkronisering + Tittel + Beskrivelse + + Feilrettingsinfo + Kopier nettadresse + + En feil har inntruffet + En HTTP-feil har inntruffet. + En I/O-feil har inntruffet. + Vis detaljer + + Identitetsbekreftelse + Brukernavn + Passord + + DAVx⁵-tilganger + Ytterligere tilganger kreves + Nettverk- og I/O-feil - %s + HTTP-tjenerfeil - %s + Feil med lokallagring - %s + Fikk ugyldig kontakt fra server + + + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml new file mode 100644 index 0000000..ba19da6 --- /dev/null +++ b/app/src/main/res/values-nl/strings.xml @@ -0,0 +1,483 @@ + + + + Account bestaat niet (of niet meer) + DAVx⁵ Adresboek + Verander hier niet van account! Gebruik in plaats daarvan direct de app om accounts te beheren. + Verwijderen + Verwijderen + Annuleren + Inschakelen + Dit veld is verplicht + Hulp + Navigeer omhoog + Opties menu + Delen + Synchronisatie begonnen/in wachtrij geplaatst + Database beschadigd + Alle accounts zijn lokaal verwijderd. + Debuggen + Andere belangrijke berichten + Statusberichten met lage prioriteit + Synchroniseren + Synchronisatiefouten + Belangrijke fouten die het synchroniseren stoppen, zoals onverwachte server antwoorden + Synchronisatie waarschuwingen + Niet-fatale problemen bij het synchroniseren zoals bepaalde ongeldige bestanden + Netwerk en I/O fouten + Timeouts, connectie problemen, etc. (vaak tijdelijk). + + Jouw gegevens. Jouw keuze. + Houd zelf de controle + regelmatige sync-intervallen + Om op gezette tijden te synchroniseren moet %s zonder beperking op de achtergrond kunnen draaien. Anders kan Android het synchroniseren op elk moment onderbreken. + Synchroniseren op gezette tijden is niet nodig.* + %s compatibiliteit + Leverancierspecifieke firmware kan de synchronisatie blokkeren. Als je hier last van hebt, kan dit alleen handmatig worden opgelost. + De vereiste instellingen zijn verricht. Er aan herinneren is niet meer nodig.* + * Niet aanvinken om later herinnerd te worden. Kan teruggezet in app instellingen / %s. + Meer informatie + jtx Board + + Ondersteunt taken + Als de server taken ondersteunt, synchroniseert een geschikte taken-app ze: + OpenTasks + Schijnt niet meer ontwikkeld te worden - niet aanbevolen. + Tasks.org + worden niet ondersteund.]]> + Geen app-store beschikbaar + Ik hoef geen ondersteuning van taken.* + Open-source software + We zijn blij dat de keuze valt op open source software %s. Ontwikkelen, onderhouden en ondersteunen is veel werk. Overweeg daarom bij te dragen (kan op vele manieren) of een donatie. Wij waarderen het zeer! + Hoe bijdragen/doneren + Herinner me er niet aan voor + + %d maand + %d maanden + + Volgende + + Rechten toestaan + %s heeft rechten nodig om goed te werken. + Alle onderstaande + Gebruik dit om alle functies in te schakelen (aanbevolen) + Alle rechten toegekend + Contacten toestaan + Geen contacten synchroniseren (niet aanbevolen) + Contacten synchroniseren mogelijk + Kalender machtigingen + Geen kalenders synchroniseren (niet aanbevolen) + Kalenders synchroniseren mogelijk + Toestemming voor meldingen + Meldingen uitgeschakeld (niet aanbevolen) + Meldingen ingeschakeld + jtx Board-rechten + OpenTasks rechten + Rechten voor taken + Geen taak-sync + Taak-sync mogelijk + Rechten behouden + Rechten kunnen automatisch worden teruggezet (niet aanbevolen) + Rechten worden niet automatisch teruggezet + Klik op App Rechten > vinkje uit bij \"Rechten intrekken\" + Als een schakeloptie niet werkt, gebruik dan App-info / Rechten. + App instellingen + + WiFi SSID rechten + Voor toegang tot de huidige WiFi-naam (SSID), moet aan deze voorwaarden worden voldaan: + Recht van toegang tot exacte locatie + Toegang tot locatie verleend + Toegang tot locatie geweigerd + Toegang tot locatie op de achtergrond + Onbeperkt toestaan + Locatietoestemming ingesteld op: %s + Locatietoestemming niet ingesteld op: %s + %s gebruikt locatiegegevens (alleen WiFi SSID) uitsluitend om de synchronisatie te beperken tot een specifieke WiFi SSID. Dit gebeurt zelfs als de synchronisatie op de achtergrond wordt uitgevoerd. + Alle locatiegegevens (alleen WiFi SSID) worden alleen lokaal gebruikt en worden nergens naartoe verzonden. + Toegang tot locatie altijd ingeschakeld + Toegang tot locatie is ingeschakeld + Toegang tot locatie is uitgeschakeld + + Vertalingen + Bibliotheken + Versie%1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) en bijdragers + Dit programma wordt geleverd met ABSOLUUT GEEN GARANTIE. Het is gratis software, en mag opnieuw worden verspreid onder bepaalde voorwaarden. + + Kon geen logbestand aanmaken + Logt nu alle %s activiteiten + Bekijken/delen + Uitschakelen + + CalDAV/CardDAV Sync adapter + Over / Licentie + Beta terugkoppeling + Webbrowser is vereist + Instellingen + Nieuws & updates + Gereedschap + Externe links + Website + Handleiding + FAQ + Voor organisaties + Community + Ondersteun het project + Hoe bijdragen + Privacybeleid + Welkom bij DAVx⁵! + Maak verbinding met je server en houd je agenda\'s en contactpersonen gesynchroniseerd. + Alle accounts synchroniseren + + Meldingen uitgeschakeld. U krijgt geen meldingen over synchronisatiefouten. + Automatische synchronisatie niet actief (geen geverifieerde internetverbinding). + Verbindingen beheren + Gegevensbesparing ingeschakeld. Synchronisatie op de achtergrond is beperkt. + Beheer van gegevensbesparing + Batterijbesparing ingeschakeld. Synchronisatie kan beperkt zijn. + Batterijbesparing beheren + Weinig opslagruimte. Android zal lokale wijzigingen niet onmiddellijk synchroniseren, maar tijdens de volgende reguliere synchronisatie. + Opslag beheren + Aanbieder voor Kalender ontbreekt + Heb je de systeemapp \"Kalenderopslag\" uitgeschakeld? + Aanbieder voor Contactpersonen ontbreekt + Heb je de systeemapp \"Contactenopslag\" uitgeschakeld? + Apps beheren + + Service herkenning is mislukt + De collectielijst is niet bijgewerkt + + Draait op de voorgrond + Op sommige toestellen is dit nodig voor automatische synchronisatie. + + Instellingen + Debuggen + Debug-info + Configuratiedetails en logbestanden bekijken/delen + Uitgebreid loggen + Loggen is actief. Je kunt de logs bekijken als onderdeel van de debug-info. + Loggen is niet actief + Batterijoptimalisatie + App is vrijgesteld (aanbevolen) + Batterijbeperkingen van toepassing (niet aanbevolen) + Verbinding + Proxy-type + + Systeem standaard + Geen proxy + HTTP + SOCKS (voor Orbot) + + Proxy hostnaam + Proxy poort + Beveiliging + App rechten + De vereiste rechten om te synchroniseren controleren + Wantrouw systeemcertificaten + Door systeem en gebruiker toegevoegde CA certificaten niet vertrouwen + Door systeem en gebruiker toegevoegde CA certificaten vertrouwen (aanbevolen) + Als deze instelling actief is, worden systeemcertificaten niet als betrouwbaar beschouwd. Dit betekent dat je elk certificaat handmatig moet accepteren (ook wanneer de server zijn certificaat vernieuwt) anders werken accountinstelling en synchronisatie niet. + (Niet-)vertrouwde certificaten terugzetten + Herstelt het vertrouwen van alle aangepaste certificaten + Alle aangepaste certificaten zijn gewist + Gebruikersinterface + App-meldingen + Meldingskanalen en hun instellingen beheren + Thema selecteren + + Systeem standaard + Licht + Donker + + Hints opnieuw instellen + Hints die al gezien zijn opnieuw weergeven + Alle hints opnieuw weergeven + Integratie + Taken app + Geen compatibele taken app gevonden + UnifiedPush (experimenteel) + Geen (push uitschakelen) + Kies een distributeur + Geen push distributeur geïnstalleerd + Geen eindpunt geconfigureerd + Klaar om pushberichten te ontvangen via %s + FCM (Google Play) + Pushberichten zijn altijd versleuteld. + + Account is verwijderd + CardDAV + CalDAV + Webcal + Er zijn extra rechten nodig om deze collecties te synchroniseren. + Machtigingen beheren + Nu synchroniseren + Account-instellingen + Accountnaam wijzigen + Niet opgeslagen lokale gegevens kunnen worden verwijderd. Na het hernoemen is opnieuw synchroniseren vereist. + Nieuwe accountnaam + Naam wijzigen + Accountnaam is al in gebruik + Accountnaam is niet gewijzigd + Account verwijderen + Account echt verwijderen? + Alle lokale kopieën van adresboeken, kalenders en takenlijsten worden verwijderd. + deze collectie synchroniseren + alleen-lezen + kalender + contacten + logboek + taken + Alleen persoonlijk tonen + Lijst verversen + Webcal abonnementen kunnen worden gesynchroniseerd met externe apps. + Geen Webcal-app gevonden + ICSx⁵ installeren + + Account toevoegen + privacybeleid.]]> + Algemeen inloggen + Aanbieder-specifieke login + Ga verder + Login + Inloggen met e-mailadres + E-mailadres + Geldig e-mailadres vereist + Diensten worden ontdekt met behulp van DNS-records en bekende URL\'s.]]> + Wachtwoord + Wachtwoord verbergen + Wachtwoord tonen + Wachtwoord (optioneel) + Inloggen met URL en gebruikersnaam + Gebruikersnaam + Gebruikersnaam (optioneel) + Basis-URL + services worden ook ontdekt met behulp van DNS records en bekende URL\'s.]]> + Certificaat selecteren + Account toevoegen + Accountnaam + Het gebruik van apostrofs (\') lijkt problemen te veroorzaken op sommige apparaten. + Gebruik het eigen e-mailadres als accountnaam, want Android gebruikt het als ORGANIZER veld voor gebeurtenissen. Twee accounts met hetzelfde adres kan niet. + Methode voor contact-groepen: + Accountnaam verplicht + Accountnaam is al in gebruik + Account kon niet worden toegevoegd + Afwerken + Geavanceerd inloggen + Geen cliëntcertificaat (optioneel) + Cliëntcertificaat: %s + Geen certificaat gevonden + Certificaat installeren + Fastmail + Fastmail-account + Inloggen met Fastmail + Google Contacten / Kalender + Google account + Inloggen met Google + Client ID (optioneel) + Privacybeleid voor meer informatie.]]> + beleid voor gebruikersgegevens van Google API Services, met inbegrip van de vereisten voor beperkt gebruik.]]> + Kon geen autorisatiecode verkrijgen + Nextcloud + Inloggen met Nextcloud + Hiermee wordt de Nextcloud Flow-aanmelding in een webbrowser gestart. + Nextcloud serveradres + Aanmelden + Kan inlog-URL niet verkrijgen + Kan inlog-URL niet verkrijgen + Configuratie detecteren + Even geduld, verzoek naar server… + Geen CalDAV- of CardDAV-service gevonden. + De basis URL lijkt geen toegankelijke CalDAV/CardDAV URL te zijn en de detectie van de service was niet succesvol. + onze lijst met geteste services en hun basis URL\'s.]]> + Controleer ook de authenticatie (meestal gebruikersnaam en wachtwoord). + Meer technische informatie is beschikbaar in de logboeken. + Details bekijken + + Synchronisatie + Contacten synchronisatie interval + Alleen handmatig + Elke %d minuten + direct bij lokale veranderingen + Kalenders synchronisatie-interval + Taken synchronisatie-interval + + Handmatig + Elke 15 minuten + Elke 30 minuten + Elk uur + Elke 2 uur + Elke 4 uur + Eenmaal daags + + Synchronisatie beperken tot WiFi + Alleen verbinden via WiFi + Type verbinding is niet relevant + Tot bepaalde WiFi-SSID beperken + Synchronisatie alleen via %s + Elke WiFI-SSID toestaan + Door komma\'s gescheiden namen (SSID\'s) van toegestane WiFi-netwerken (laat leeg voor alle) + Beperking WiFi-SSID vereist verdere instellingen + Beheren + VPN vereist onderliggend internet + VPN zonder onderliggende gevalideerde internetverbinding is niet voldoende om synchronisatie uit te voeren (aanbevolen) + VPN zonder onderliggende gevalideerde internetverbinding is voldoende om synchronisatie uit te voeren + Authenticatie + Gebruikersnaam + Wachtwoord of app-wachtwoord + app-wachtwoord.]]> + Nieuw wachtwoord + Gebruik het zelfde wachtwoord als op de server. + Opnieuw autoriseren (OAuth) + Gebruiken wanneer de toegang is ingetrokken + Autorisatie geslaagd + Cliëntcertificaat + Geen certificaat beschikbaar of geselecteerd + Certificaat installeren + CalDAV + Gebeurtenissen in verleden tijd + Worden alle gesynchroniseerd + + Afspraken ouder dan een dag worden genegeerd + Ouder dan %d dagen worden genegeerd + + Gebeurtenissen ouder dan ingevuld aantal dagen worden genegeerd (mag 0 zijn). Veld leeg laten om alle te synchroniseren. + Standaardherinnering + + Standaardherinnering één minut voor het evenement + %d minuten voor aanvang gebeurtenis + + Wordt niet aangemaakt + Vul het gewenste aantal minuten in. Leeg laten om herinneringen uit te schakelen. + Kalender kleuren beheren + Worden bij elke sync teruggezet + Kunnen door andere apps worden ingesteld + Gebeurtenis kleuren ondersteunen + Worden gesynchroniseerd + Worden niet gesynchroniseerd + CardDAV + Methode voor contact-groepen: + + Groepen zijn afzonderlijke vCards + Groepen zijn categorieën per contact + + + Adresboek aanmaken + Het aanmaken van een adresboek via CardDAV wordt mogelijk niet ondersteund door de server. + Kalender aanmaken + Standaard tijdzone (optioneel) + + Mogelijke kalender-items + Gebeurtenissen + Taken + Notities / Dagboek + Het aanmaken van een kalender via CalDAV wordt mogelijk niet ondersteund door de server. + Kleur + Titel + Opslaglocatie + Beschrijving (optioneel) + Aanmaken + + contacten + gebeurtenissen + taken + Collectie verwijderen + Deze collectie (%s) en alle gegevens worden permanent verwijderd, zowel lokaal als op de server. + Synchroniseren + Synchronisatie ingeschakeld + Synchronisatie uitgeschakeld + Alleen-lezen + Alleen-lezen (door server) + Alleen-lezen (volgens beleid) + Alleen-lezen (alleen lokaal) + Lezen/schrijven + Titel + Beschrijving + Eigenaar + Push-ondersteuning + Server adverteert Push-ondersteuning + Ingeschreven op %1$s, vervalt op %2$s + Laatste synchronisatie (%s) + Adres (URL) + + Debug informatie + ZIP archief + Bevat debuginformatie en logbestanden + Deel het archief om over te zetten naar een computer, per e-mail te verzenden of als bijlage bij een supportticket te voegen.. + Archief delen + Debug info als bijlage bij dit bericht (vereist ondersteuning voor bijlagen van de ontvangende app). + HTTP-fout + Serverfout + WebDAV fout + I/O-fout + Het verzoek is door de server afgewezen. + De gevraagde bron bestaat niet (meer). + De server staat het gevraagde type bewerking niet toe. + Er deed zich een probleem aan de serverzijde voor. Neem contact op met uw serverondersteuning. + Er is een onverwachte fout opgetreden. Bekijk foutopsporingsinformatie voor details. + Details bekijken + Debug-info is verzameld + Betrokken bronnen + Gerelateerd aan het probleem + Externe bron: + Lokale bron: + Logboeken + Uitgebreide logboeken zijn beschikbaar + Details bekijken + URL kopiëren + Bron inspecteren + Privacyverklaring + Logboeken en foutopsporingsgegevens kunnen privé-informatie bevatten. Houd hier rekening mee als u ze openbaar deelt. + Kan bron niet bekijken + + Er is een fout opgetreden. + Een HTTP-fout is opgetreden. + Een I/O fout is opgetreden. + Details weergeven + + WebDAV-koppelingen + Quotum gebruikt: %1$s / Beschikbaar: %2$s + Inhoud delen + Ontkoppelen + WebDAV-koppeling toevoegen + Verkrijg directe toegang tot cloudbestanden met een WebDAV-koppeling! + hoe WebDAV-mounts werken.]]> + Weergavenaam + WebDAV-URL + Ongeldige URL + Koppelpunt en weergavenaam + Authenticatie + Gebruikersnaam + Wachtwoord + Gebruikersnaam (optioneel) + Wachtwoord (optioneel) + Koppeling toevoegen + Geen WebDAV-service op deze URL + Verwijder het koppelpunt + Verbindingsgegevens gaan verloren, maar er worden geen bestanden gewist. + WebDAV-bestand openen + WebDAV-bestand downloaden + WebDAV-bestand uploaden + WebDAV-koppeling + + DAVx⁵ rechten + Aanvullende rechten vereist + %ste oud + Minimaal vereiste versie: %1$s + Verificatie mislukt (controleer aanmeldingsgegevens) + Netwerk of I/O error - %s + HTTP-server fout - %s + Lokale opslag fout - %s + Soft error (max. aantal pogingen bereikt) + Ongeldig contact ontvangen van server + Ongeldige gebeurtenis ontvangen van server + Ongeldige taak ontvangen van server + Een of meer ongeldige bronnen negeren + Synchronisatie in afwachting + De gegevens op afstand zijn veranderd + + Alles synchroniseren + Alle accounts synchroniseren + Gelabelde synchronisatieknop + Pictogram synchronisatieknop + Tik om de synchronisatie handmatig uit te voeren. + + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml new file mode 100644 index 0000000..d9cf246 --- /dev/null +++ b/app/src/main/res/values-pl/strings.xml @@ -0,0 +1,489 @@ + + + + Konto (już) nie istnieje + Książka adresowa DAVx⁵ + Nie zmieniaj konta tutaj! Zamiast tego zarządzaj kontami bezpośrednio za pomocą aplikacji. + Usuń + Usuń + Anuluj + Włącz + To pole jest wymagane + Pomoc + Nawiguj w górę + Menu opcji + Udostępnij + Synchronizacja rozpoczęta/zakolejkowana + Uszkodzona baza danych + Wszystkie konta zostały usunięte lokalnie. + Debugowanie + Inne ważne wiadomości + Komunikaty statusu o niskim priorytecie + Synchronizacja + Błędy synchronizacji + Ważne błędy, które zatrzymują synchronizację, takie jak nieoczekiwane odpowiedzi serwera + Ostrzeżenia synchronizacji + Niekrytyczne problemy z synchronizacją, takie jak niektóre nieprawidłowe pliki + Błędy sieci oraz we/wy + Przekroczenia czasu, problemy z połączeniem itp. (często tymczasowe) + + Twoje dane. Twój wybór. + Przejmij kontrolę. + Regularne przedziały synchronizacji + Dla synchronizacji w regularnych przedziałach, %s musi mieć pozwolenie na pracę w tle. W przeciwnym razie, Android może wstrzymać synchronizację w dowolnym momencie. + Nie potrzebuję regularnych przedziałów synchronizacji.* + %s kompatybilność + Sterowniki sprzętowe, specyficzne dla wybranych dostawców, mogą blokowac synchronziację. Jeśli dotyczy to również Ciebie, możesz rozwiązać ten problem tylko ręcznie. + Wprowadziłem potrzebne ustawienia. Nie przypominaj mi ponownie.* + * Pozostaw nie zaznaczone, aby otrzymać przypomnienie później. Można zresetować w ustawieniach aplikacji wybierając / %s. + Więcej informacji + jtx Board + + Obsługa zadań + Jeśli zadania są wspierane przez Twój serwer, mogą być synchronizowane ze wspieraną aplikacją zadań: + OpenTasks + Wydaje się, że nie jest już rozwijany – nie jest zalecany. + Tasks.org + nie są wspierane.]]> + Sklep aplikacji nie jest dostępny + Nie potrzebuję obsługi zadań.* + Oprogramowanie open-source + Cieszymy się, że używasz %s, czyli oprogramowania typu open-source. Rozwój, utrzymanie i wsparcie to ciężka praca. Prosimy o rozważenie wniesienia swojego wkładu (jest wiele sposobów) lub darowizny. Byłoby to bardzo cenne! + Jak wspomóc/wesprzeć + Nie przypominaj mi przez + + %dmiesiąc + %dmiesiące + %dmiesięcy + %d miesiące/miesięcy + + Dalej + + Uprawnienia + %s wymaga uprawnień do prawidłowego działania + Wszystkie poniższe + Użyj tego aby odblokować wszystkie funkcje (zalecane) + Wszystkie uprawnienia nadane + Uprawnienia kontaktów + Bez synchronizacji kontaktów (nie zalecane) + Synchronizacja kontaktów możliwa + Uprawnienia kalendarza + Bez synchronizacji kalendarza (nie zalecane) + Synchronizacja kalendarza możliwa + Uprawnienie do powiadomień + Powiadomienia wyłączone (niezalecane) + Powiadomienia włączone + Uprawnienia jtx Board + Uprawnienia OpenTasks + Uprawnienia zadań + Brak synchronizacji zadań + Synchronizacja zadań możliwa + Utrzymaj uprawnienia + Uprawnienia mogą zostać automatycznie zresetowane (nie zalecane) + Uprawnienia nie będą automatycznie resetowane + Kliknij Uprawnienia > odznacz \"Usuń uprawnienia jeśli aplikacja nie jest używana\" + Jeśli przełącznik nie działa użyj ustawień dla aplikacji / Uprawnienia. + Ustawienia aplikacji + + Uprawnienia WiFi SSID + Aby móc uzyskać dostęp do obecnej nazwy WiFi (SSID), następujące warunki muszą być spełnione: + Uprawnienie dokładnej lokalizacji + Uprawnienie lokalizacji nadane + Uprawnienie lokalizacji odebrane + Uprawnienie lokalizacji w tle + Zezwól przez cały czas + Uprawnienia lokalizacji ustawione na: %s + Uprawnienia lokalizacji nie ustawione na: %s + %s używa danych o lokalizacji (tylko SSID WiFi ) wyłącznie do ograniczania synchronizacji tylko do wybranych SSID-ów WiFi. Będzie się to działo również gdy synchronizacja przebiega w tle. + Wszystkie dane o lokalizacji (tylko SSID WiFi) są używane tylko lokalnie i nie są nigdzie wysyłane. + Lokalizacja zawsze włączona + Usługa lokalizacji jest włączona + Usługa lokalizacji jest wyłączona + + Tłumaczenia + Biblioteki + Wersja %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) i współpracownicy + Ten program jest ABSOLUTNIE BEZ GWARANCJI. To jest wolne oprogramowanie i mile widziane jest dalsze rozpowszechnianie go zgodnie z warunkami licencji. + + Nie udało się utworzyć pliku logu + Obecnie zapisywane są wszystkie %s aktywności + Zobacz/udostępnij + Wyłącz + + Adapter synchronizacji CalDAV/CardDAV + Informacje/Licencja + Przekaż opinię + Proszę zainstalować przeglądarkę internetową + Ustawienia + Nowości i aktualizacje + Narzędzia + Zewnętrzne odnośniki + Strona WWW + Podręcznik + Często zadawane pytania + Dla organizacji + Społeczność + Wesprzyj ten projekt + Jak wnieść wkład + Polityka prywatności + Witaj w DAVx⁵! + Połącz się ze swoim serwerem i utrzymuj swój kalendarz i kontakty zsynchronizowane. + Synchronizuj wszystkie konta + + Powiadomienia wyłączone. Nie będziesz otrzymywać powiadomień o błędach synchronizacji. + Automatyczna synchronizacja nie jest aktywna (brak zweryfikowanego połączenia z Internetem). + Zarządzaj połączeniami + Włączono oszczędzanie danych. Synchronizacja w tle jest ograniczona. + Zarządzaj oszczędzaniem danych + Oszczędzanie baterii jest włączone. Synchronizacja może być ograniczona. + Zarządzaj oszczędzaniem baterii + Mało miejsca do przechowywania. Android nie zsynchronizuje lokalnych zmian od razu, ale podczas następnej regularnej synchronizacji. + Zarządzaj pamięcią + Brak dostawcy kalendarza + Czy zablokowałeś aplikację systemową \"Przechowywanie kalendarza\" ? + Brak dostawcy kontaktów + Czy zablokowałeś aplikację systemową \"Przechowywanie kontaktów\" ? + Zarządzaj aplikacjami + + Wykrycie serwisu nie powiodło się + Nie można odświeżyć listy kolekcji + + Działa na pierwszym planie + Na niektórych urządzeniach jest to konieczne do automatycznej synchronizacji. + + Ustawienia + Debugowanie + Pokaż informacje do debugowania + Wyświetl/udostępnij szczegóły konfiguracji i logi + Rozszerzone logowanie + Zbieranie logów jest włączone. Możesz zobaczyć logi jako część informacji debugowania. + Logowanie jest wyłączone + Optymalizacja baterii + Aplikacja dodana do wyjątków (zalecane) + Zastosowano ograniczenia baterii (nie zalecane) + Łączność + Typ proxy + + Domyślne ustawienie systemowe + Brak proxy + HTTP + SOCKS (dla Orbota) + + Nazwa hosta proxy + Port proxy + Bezpieczeństwo + Uprawnienia aplikacji + Przejrzyj uprawnienia konieczne do synchronizacji + Nie ufaj certyfikatom systemowym + Certyfikaty systemowe i użytkownika nie są zaufane + Certyfikaty systemowe i użytkownika są zaufane (zalecane) + Jeśli to ustawienie jest aktywne to certyfikaty systemowe nie są uznawane za zaufane. Oznacza to, że będziesz musiał(a) ręcznie zakceptować każdy certyfikat (również gdy serwer odświeży swój certyfikat) lub konfiguracja konta i synchronizacja nie będą działały. + Zresetuj (nie)zaufane certyfikaty + Zresetuj wszystkie niestandardowe certyfikaty + Wszystkie niestandardowe certyfikaty zostały wyczyszczone + Interfejs użytkownika + Ustawienia powiadomień + Zarządzaj kanałami powiadomień i ich ustawieniami + Wybierz motyw + + Domyślne ustawienie systemowe + Jasny + Ciemny + + Zresetuj podpowiedzi + Ponownie włącz wskazówki, które zostały usunięte wcześniej + Wszystkie wskazówki pojawią się ponownie + Integracja + Aplikacja zadań + Nie znaleziono kompatybilnej aplikacji zadań + UnifiedPush (eksperymentalny) + Żaden (wyłącz Push) + Wybierz kolportera + Nie zainstalowano kolportera wiadomości Push + Brak konfiguracji punktu końcowego + Gotowy aby otrzymywać wiadomości Push poprzez %s + FCM (Google Play) + Wiadomości Push są zawsze szyfrowane. + + Konto zostało usunięte + CardDAV + CalDAV + Webcal + Dodatkowe uprawnienia są wymagane aby zsynchronizować te kolekcje. + Zarządzaj uprawnieniami + Synchronizuj teraz + Ustawienia konta + Zmień nazwę konta + Niezapisane dane lokalne mogą zostać usunięte. Po zmianie nazwy wymagana jest powtórna synchronizacja. + Nowa nazwa konta + Zmień nazwę + Nazwa konta jest już zajęta + Nie udało się zmienić nazwy konta + Usuń konto + Naprawdę chcesz usunąć konto? + Wszystkie lokalne kopie książek adresowych, kalendarzy i list zadań zostaną usunięte. + synchronizuj tę kolekcję + tylko do odczytu + kalendarz + kontakty + dziennik + zadania + Pokaż tylko osobiste + Odśwież listę + Subskrypcje Webcal mogą być synchronizowane z zewnętrznymi aplikacjami. + Nie znaleziono aplikacji obsługującej Webcal + Zainstaluj ICSx⁵ + + Dodaj konto + polityką prywatności.]]> + Logowanie ogólne + Logowanie zależne od dostawcy + Kontynuj + Zaloguj + Logowanie za pomocą adresu e‑mail + Adres e‑mail + Wymagany poprawny adres e‑mail + Usługi są wykrywane używając rekordów DNS oraz znanych adresów URL.]]> + Hasło + Ukryj hasło + Pokaż hasło + Hasło (opcjonalnie) + Logowanie za pomocą adresu URL i nazwy użytkownika + Nazwa użytkownika + Nazwa użytkownika (opcjonalnie) + Podstawowy adres URL + usługi są również wykrywane używając rekordów DNS oraz znanych adresów URL.]]> + Wybierz certyfikat + Dodaj konto + Nazwa konta + Użycie znaku apostrofu (\') wydaje się powodować problemy na niektórych urządzeniach. + Użyj swojego adresu e‑mail jako nazwy konta, ponieważ Android będzie używał nazwy konta jako pola ORGANIZATOR dla wydarzeń, które stworzysz. Nie możesz posiadać dwóch kont o takiej samej nazwie. + Metoda grupowania kontaktów: + Wymagana nazwa konta + Nazwa konta jest już zajęta + Konto nie mogło być dodane + Zakończ + Logowanie zaawansowane + Brak certyfikatu klienta (opcjonalnie) + Certyfikat klienta: %s + Nie znaleziono certyfikatu + Zainstaluj certyfikat + Fastmail + Konto Fastmail + Zaloguj się poprzez Fastmail + Kontakty Google / Kalendarz + Konto Google + Zaloguj się za pomocą Google + ID klienta (opcjonalnie) + Polityce prywatności.]]> + Zasadami dotyczącymi danych użytkownika usług interfejsu API Google, w tym wymaganiami dotyczącymi Ograniczonego użytkowania.]]> + Nie udało się uzyskać kodu autoryzacyjnego + Nextcloud + Zaloguj się za pomocą Nextcloud + Spowoduje to rozpoczęcie procesu logowania do Nextcloud w przeglądarce internetowej. + Adres serwera Nextcloud + Zaloguj się + Nie można uzyskać adresu URL logowania + Nie udało się uzyskać danych logowania + Wykrywanie konfiguracji + Proszę czekać, odpytywanie serwera… + Nie można znaleźć usługi CalDAV lub CardDAV. + Podstawowy adres URL prawdopodobnie nie jest dostępnym adresem URL CalDAV/CardDAV, a wykrycie usługi nie powiodło się. + naszą listę przetestowanych usług i ich podstawowych adresów URL.]]> + Sprawdź również dokładnie uwierzytelnianie (zazwyczaj nazwę użytkownika i hasło). + Dalsze informacje techniczne są dostępne w logach. + Otwórz logi + + Synchronizacja + Częstotliwość synchronizacji kontaktów + Tylko ręcznie + Co %d minut oraz natychmiast przy zmianach lokalnych + Częstotliwość synchronizacji kalendarzy + Częstotliwość synchronizacji list zadań + + Tylko ręcznie + Co 15 minut + Co 30 minut + Co godzinę + Co 2 godziny + Co 4 godziny + Raz dziennie + + Synchronizuj tylko przez Wi‑Fi + Synchronizacja jest ograniczona do połączeń Wi‑Fi + Rodzaj połączenia nie jest brany pod uwagę + Ograniczenia SSID Wi‑Fi + Będzie synchronizować tylko w %s + Wszystkie połączenia Wi‑Fi będą używane + Oddzielone przecinkami nazwy (SSID) dozwolonych sieci Wi‑Fi (pozostaw puste dla wszystkich) + Ograniczenie WiFi SSID wymaga dalszych ustawień + Zarządzaj + VPN wymaga podstawowego Internetu + VPN bez sprawdzonego połączenia internetowego nie wystarczy do przeprowadzenia synchronizacji (zalecane) + Do przeprowadzenia synchronizacji wystarczy VPN bez sprawdzonego połączenia internetowego + Uwierzytelnianie + Nazwa użytkownika + Hasło lub hasło aplikacji + hasła aplikacji.]]> + Nowe hasło + Zaktualizuj hasło zgodnie z serwerem + Autoryzuj ponownie (OAuth) + Użyj jeśli dostęp został cofnięty + Poprawna autoryzacja + Certyfikat klienta + Brak dostępnego lub wybranego certyfikatu + Zainstaluj certyfikat + CalDAV + Limit czasowy przeszłych wydarzeń + Wszystkie wydarzenia zostaną zsynchronizowane + + Wydarzenia starsze niż jeden dzień zostaną zignorowane. + Wydarzenia starsze niż %d dni zostaną zignorowane. + Wydarzenia starsze niż %d dni zostaną zignorowane. + Wydarzenia starsze niż %d dni zostaną zignorowane. + + Wydarzenia, które są starsze niż podana liczba dni zostaną zignorowane (może być 0). Zostaw puste, aby synchronizować wszystkie wydarzenia. + Przypomnienie domyślne + + Przypomnienie domyślne na jedną minutę przed wydarzeniem + Przypomnienie domyślne %d minut(y) przed wydarzeniem + Przypomnienie domyślne %d minut(y) przed wydarzeniem + Przypomnienie domyślne %d minut(y) przed wydarzeniem + + Nie utworzono przypomnień domyślnych + Jeżeli domyślne przypomnienia mają być utworzone dla zdarzeń bez przypomnienia: pożądana liczba minut przed zdarzeniem. Pozostaw puste aby wyłączyć domyślne przypomnienia. + Zarządzaj kolorami kalendarza + Kolory kalendarza są resetowane przy każdej synchronizacji + Kolory kalendarza mogą być ustawiane przez inne aplikacje + Obsługa kolorów wydarzeń + Kolory wydarzeń są zsynchronizowane + Kolory wydarzeń nie są zsynchronizowane + CardDAV + Metoda grupowania kontaktów + + Grupy są odrębnymi vCards + Grupy są kategoriami dla pojedynczego kontaktu + + + Stwórz książkę adresową + Tworzenie książki adresowej poprzez CardDAV może nie być wspierane przez ten server. + Utwórz kalendarz + Domyslna strefa czasowa (opcjonalnie) + + Możliwe wpisy kalendarza + Wydarzenia + Zadania + Notatki/dziennik + Tworzenie kalendarza poprzez CalDAV może nie być wspierane przez ten server. + Kolor + Tytuł + Miejsce zapisu + Opis (opjonalnie) + Stwórz + + kontakty + wydarzenia + zadania + Usuń kolekcję + Ta kolekcja (%s) i wszystkie jej dane zostaną bezpowrotnie usunięte, zarówno lokalnie jak i na serwerze. + Synchronizacja + Synchronizacja włączona + Synchronizacja wyłączona + Tylko do odczytu + Tylko do odczytu (przez serwer) + Tylko do odczytu (przez politykę) + Tylko do odczytu (tylko lokalnie) + Odczyt/zapis + Tytuł + Opis + Właściciel + Wsparcie protokołu Push + Serwer zgłasza wsparcie protokołu Push + Zasubskrybowano %1$s, wygasa %2$s + Ostatnia synchronizacja (%s) + Adres (URL) + + Informacje debugowania + Archiwum ZIP + Zawiera informacje o debugowaniu i dzienniki + Udostępnij archiwum, aby przenieść je na komputer, wysłać e-mailem lub dołączyć do zgłoszenia do pomocy technicznej. + Udostępnij archiwum + Informacja debugowania załączona do tej wiadomości (wymaga wsparcia dla załączników w odbierającej aplikacji). + Błąd HTTP + Błąd serwera + Błąd WebDAV + Błąd we/wy + Żądanie zostało odrzucone przez serwer. + Żądany zasób (już) nie istnieje. + Serwer nie zezwala na żądany typ operacji. + Wystąpił problem po stronie serwera. Skontaktuj się proszę ze wsparciem serwera. + Wystapił nieoczekiwany błąd. Przejrzyj informacje debugowania dla dodatkowych szczegółów. + Obejrzyj szczegóły + Informacje debuggowania zostały zebrane + Zaangażowane zasoby + Powiązane z problemem + Zasób zdalny: + Zasób lokalny: + Logi + Szczegółowe logi są dostępne + Otwórz logi + Kopiuj adres URL + Sprawdź zasób + Notatka o prywatności + Logi i informacje debugowania mogą zawierać prywatne dane. Proszę być tego świadomymi w przypadku publicznego udostępniania. + Nie można wyświetlić zasobu + + Wystąpił błąd. + Wystąpił błąd HTTP. + Wystąpił błąd we/wy. + Pokaż szczegóły + + Punkty linkowania WebDAV + Limit użyty: %1$s / dostępny: %2$s + Udostępnij zawartość + Odlinkuj + Dodaj punkt linkowania WebDAV + Uzyskaj bezpośredni dostęp do plików w chmurze, dodając montowanie WebDAV! + jak działają punkty montowania WebDAV .]]> + Nazwa wyświetlana + WebDAV URL + Błędny URL + Punkt montowania i wyświetlana nazwa + Uwierzytelnianie + Nazwa użytkownika + Hasło + Nazwa użytkownika (opcjonalnie) + Hasło (opcjonalnie) + Dodaj link + Brak usługi WebDAV pod tym URL + Usuń punkt montowania + Szczegóły połączenia zostaną utracone, ale żadne pliki nie będą usunięte. + Uzyskiwanie dostępu do pliku WebDAV + Pobieranie pliku WebDAV + Wgrywanie pliku WebDAV + Punkt linkowania WebDAV + + Uprawnienia DAVx⁵ + Wymagane dodatkowe uprawnienia + %s zbyt stary/a + Minimalna wymagana wersja: %1$s + Nieudane uwierzytelnienie (sprawdź dane logowania) + Błąd sieci lub we/wy – %s + Błąd serwera HTTP — %s + Błąd lokalnego storage’u — %s + Błąd programowy (osiągnięto maksymalną liczbę ponownych prób) + Otrzymano błędny kontakt z serwera + Otrzymano błędne wydarzenie z serwera + Otrzymano błędne zadanie z serwera + Zignorowano jeden lub więcej nieważnych zasobów + Oczekująca synchronizacja + Zdalne dane uległy zmianie + + Synchronizuj wszystko + Synchronizuj wszystkie konta + Przycisk Etykietowanej Synchronizacji + Przycisk Synchronizacji Ikon + Naciśnij aby zsynchronizować ręcznie. + + diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 0000000..5a4c4e3 --- /dev/null +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,482 @@ + + + + A conta não existe (mais) + Lista de contatos do DAVx⁵ + Não mude a conta por aqui! Em vez disso, use o app diretamente para gerenciar as contas. + Apagar + Remover + Cancelar + Ativar + Este campo é necessário + Ajuda + Navegar para cima + Menu de opções + Compartilhar + Sincronização foi iniciada/enfileirada + O banco de dados está corrompido + Todas as contas foram removidas localmente. + Depuração + Outras mensagens importantes + Mensagens de estado de baixa prioridade + Sincronização + Erros de sincronização + Erros importantes que interrompem a sincronização, como respostas inesperadas do servidor + Alertas de sincronização + Problemas não fatais de sincronização, como certos arquivos inválidos + Erros de E/S e de rede + Tempos limite atingidos, problemas de conexão, etc. (geralmente temporários) + + Seus dados. Sua escolha. + Assuma o controle. + Intervalos periódicos de sincronização + Para sincronizar em intervalos periódicos, o %s deve ter permissão para executar-se em segundo plano. Caso contrário, o Android pode pausar a sincronização a qualquer momento. + Eu não preciso de sincronização periódica.* + Compatibilidade com o %s + Firmware de fabricantes específicas podem bloquear a sincronização. Se for atingido, você pode resolver isso manualmente. + Fiz as configurações necessárias. Não me lembre novamente.* + * Deixe desmarcado para ser lembrado depois. Pode ser reconfigurado nas configurações do app / %s + Mais informações + jtx Board + + Suporte a tarefas + Se seu servidor ter suporte a tarefas, elas podem ser sincronizadas com um app de tarefas compatível: + OpenTasks + Parece não ser mais desenvolvido – não é recomendado. + Tasks.org + alguns recursos.]]> + Nenhuma loja de apps disponível + Não preciso de suporte a tarefas.* + Software de código aberto + Estamos felizes que você usa o %s, que é software de código aberto. O desenvolvimento, a manutenção, e o suporte são um trabalho díficil. Considere contribuir (há varias formas) ou uma doação. Seria muito apreciado! + Como contribuir/doar + Não me lembre por + + %dmês + %d de meses + %d meses + + Avançar + + Permissões + O %s requer permissões para funcionar corretamente. + Todas as abaixo + Use isso para ativar todos os recursos (recomendado) + Todas as permissões foram concedidas + Permissões de contatos + Sem sincronização dos contatos (não é recomendado) + A sincronização dos contatos é possível + Permissões de calendário + Sem sincronização do calendário (não é recomendado) + A sincronização do calendário é possível + Permissão de notificação + Notificações desativadas (não é recomendado) + Notificações ativadas + Permissões do jtx Board + Permissões do OpenTasks + Permissões do Tasks + Sem sincronização de tarefas + A sincronização de tarefas é possível + Manter permissões + As permissões podem ser reconfiguradas automaticamente (não é recomendado) + As permissões não serão reconfiguradas automaticamente + Clique em Permissões > desmarque \"Gerenciar o app fora do uso\" + Se uma opção não funciona, use as configurações do app / Permissões. + Configurações do app + + Permissões de SSID do Wi-Fi + Para poder acessar o nome da rede Wi-Fi atual (o SSID), essas condições devem ser cumpridas: + Permissão de localização precisa + A permissão de localização foi concedida + A permissão de localização foi negada + Permissão de localização em segundo plano + Permitir o tempo todo + A permissão de localização está configurada para: %s + A permissão de localização não está configurada para: %s + O %s usa dados de localização (somente o SSID do Wi-Fi) para restringir a sincronização para somente um SSID de Wi-Fi. Isso pode acontecer até mesmo quando a sincronização está sendo executada em segundo plano. + Todos os dados de localização (que são somente o SSID do Wi-Fi) são usados apenas localmente e não são enviados para quaisquer lugares. + Localização sempre ativada + O serviço de localização está ativado + O serviço de localização está desativado + + Traduções + Bibliotecas + Versão %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) e contribuidores + Este programa é distribuído SEM QUALQUER GARANTIA. É software livre e pode ser redistribuído sob algumas condições. + + Não foi possível criar o arquivo de registros + Agora registrando todas as atividades do %s + Visualizar/compartilhar + Desativar + + Adaptador de sincronização do CalDAV/CardDAV + Sobre / Licença + Retorno da beta + Instale um navegador da web + Configurações + Novidades e atualizações + Ferramentas + Links externos + Site + Manual + Perguntas frequentes + Para organizações + Comunidade + Apoie o projeto + Como contribuir + Política de privacidade + Boas-vindas ao DAVx⁵! + Conecte-se ao seu servidor e mantenha seus calendários e contatos sincronizados. + Sincronizar todas as contas + + As notificações estão desativadas. Você não será notificado sobre erros de sincronização. + A sincronização automática não está ativa (sem conexão verificada à internet). + Gerenciar conexões + A economia de dados está ativada. A sincronização em segundo plano está restrita. + Gerenciar economia de dados + A economia de bateria está ativada. A sincronização pode ser restrita. + Gerenciar economia de bateria + Há pouco espaço de armazenamento. O Android não sincronizará alterações locais imediatamente, mas sim na próxima sincronização periódica. + Gerenciar armazenamento + O provedor de calendários está ausente + Você desativou o app do sistema chamado \"Armazenamento de calendários\"? + O provedor de contatos está ausente + Você desativou o app do sistema chamado \"Armazenamento de contatos\"? + Gerenciar apps + + A detecção de serviço falhou + Não foi possível recarregar a lista de coleções + + Executando em primeiro plano + Em alguns dispositivos, isto é necessário para a sincronização automática. + + Configurações + Depuração + Mostrar informações de depuração + Visualizar/compartilhar registros e detalhes da configuração + Registro verboso + A coleta de registro está ativa. Você pode visualizar os registros nas informações de depuração. + A coleta de registros está desativada + Otimização de bateria + O app está isento (recomendado) + O app não está isento (não recomendado) + Conexão + Tipo da proxy + + Padrão do sistema + Sem proxy + HTTP + SOCKS (pro Orbot) + + Nome do servidor da proxy + Porta da proxy + Segurança + Permissões do app + Revise as permissões necessárias para a sincronização + Desconfiar dos certificados do sistema + ACs do sistema e adicionadas pelo usuário não serão confiadas + ACs do sistema e adicionadas pelo usuário serão confiadas (recomendado) + Se essa configuração está ativa, os certificados do sistema não são tratados como confiáveis. Isso significa que você terá que manualmente aceitar cada certificado (e também quando o servidor renova o seu certificado) ou a configuração da conta e a sincronização não funcionarão. + Reconfigurar certificados + Reconfigura a confiança de todos os certificados personalizados + Todos os certificados personalizados foram limpos + Interface do usuário + Configurações de notificações + Gerencie canais de notificação e suas configurações + Escolher tema + + Padrão do sistema + Claro + Escuro + + Reconfigurar dicas + Reativa as dicas que foram ignoradas anteriormente + Todas as dicas serão mostradas novamente + Integração + App de tarefas + Nenhum app compatível de tarefas encontrado + UnifiedPush (experimental) + Nenhum (desativar push) + Escolha um distribuidor + Nenhum distribuidor de push instalado + Nenhum servidor configurado + Pronto para receber mensagens push pelo %s + FCM (Google Play) + As mensagens push são sempre criptografadas. + + A conta foi removida + CardDAV + CalDAV + Webcal + São necessárias permissões adicionais para sincronizar essas coleções. + Gerenciar permissões + Sincronizar agora + Configurações da conta + Renomear conta + Dados locais que não foram salvos podem ser ignorados. Uma nova sincronização é necessária após uma renomeação. + Nome novo da conta + Renomear + O nome da conta já foi utilizado + Não foi possível renomear a conta + Apagar conta + Realmente apagar a conta? + Todas as cópias locais das listas de contatos, calendários e listas de tarefas serão apagadas. + sincronizar esta coleção + somente leitura + calendário + contatos + diário + tarefas + Mostrar somente pessoais + Recarregar lista + Inscrições de Webcal podem ser sincronizadas com apps externos. + Nenhum app compatível com Webcal encontrado + Instalar ICSx⁵ + + Adicionar conta + política de privacidade.]]> + Autenticação genérica + Autenticação específica ao provedor + Continuar + Entrar + Entrar com endereço de e-mail + Endereço de e-mail + Um endereço de e-mail válido é necessário + Os serviços são descobertos usando registros de DNS e URLs well-known.]]> + Senha + Ocultar senha + Mostrar senha + Senha (opcional) + Entrar com URL e nome de usuário + Nome do usuário + Nome do usuário (opcional) + URL base + serviços também são descobertosusando registros de DNS e URLs well-known.]]> + Selecionar certificado + Adicionar conta + Nome da conta + O uso de apóstrofos (\') pode causar problemas em alguns dispositivos. + Use o seu endereço de e-mail como o nome da conta pois o Android usará o nome como o campo ORGANIZER pata os eventos que cria. Você não pode ter duas contas com o mesmo nome. + Método de agrupamento de contatos: + O nome da conta é necessário + O nome da conta já foi utilizado + A conta não pôde ser adicionada + Concluir + Autenticação avançada + Sem certificado de cliente (opcional) + Certificado de cliente: %s + Nenhum certificado encontrado + Instalar certificado + Fastmail + Conta do Fastmail + Entrar com Fastmail + Google Contatos / Agenda + Conta do Google + Entrar com Google + ID do cliente (opcional) + política de privacidade para detalhes.]]> + Política de Dados de Usuário dos Google API Services, incluindo os requisitos de Uso Limitado.]]> + Não foi possível obter o código de autorização + Nextcloud + Entrar com Nextcloud + Isso iniciará o processo de autenticação do Nextcloud num navegador da web. + Endereço do servidor do Nextcloud + Entrar + Não foi possível obter a URL de autenticação + Não foi possível obter os dados de autenticação + Detecção de configuração + Aguarde, consultando o servidor… + Não foi possível encontrar o serviço de CalDAV ou CardDAV. + O URL base não parece ser um URL acessível de CalDAV/CardDAV e a detecção de serviço não foi bem-sucedida. + nossa lista de serviços testados e seus URLs base.]]> + Certifique-se da autenticação (normalmente nome de usuário e senha). + Mais informações técnicas estão disponíveis nos registros. + Visualizar registros + + Sincronização + Intervalo de sincronização dos contatos + Apenas manualmente + A cada %d minutos e imediatamente em alterações locais + Intervalo de sincronização dos calendários + Intervalo de sincronização das tarefas + + Apenas manualmente + A cada 15 minutos + A cada 30 minutos + A cada hora + A cada 2 horas + A cada 4 horas + Todo dia + + Sincronizar apenas por Wi-Fi + A sincronização está restrita a apenas conexões de Wi-Fi + O tipo de conexão não está sendo considerado + Restrição de SSID do Wi-Fi + Sincronizará apenas em %s + Todas as conexões Wi-Fi serão utilizadas + Nomes das redes Wi-Fi permitidas (SSIDs) separados por vírgulas (deixe em branco para todas) + A restrição de SSID de Wi-Fi requer configuração adicional + Gerenciar + Exigir conexão base verificada para VPNs + Uma VPN sem conexão base verificada não é suficiente para executar a sincronização (recomendado) + Uma VPN sem conexão base verificada é suficiente para executar a sincronização + Autenticação + Nome do usuário + Senha ou senha de app + senha de app.]]> + Senha nova + Atualize a senha de acordo com o seu servidor. + Autorizar novamente (OAuth) + Use caso o acesso for revogado + A autorização foi bem-sucedida + Certificado de cliente + Nenhum certificado disponível ou selecionado + Instalar certificado + CalDAV + Limite de tempo para eventos passados + Todos os eventos serão sincronizados + + Eventos que ocorreram a mais de um dia atrás serão ignorados + Eventos que ocorreram a mais de %d de dias atrás serão ignorados + Eventos que ocorreram a mais de %d dias atrás serão ignorados + + Os eventos que ocorreram antes desse número de dias serão ignorados (pode ser 0). Deixe em branco para sincronizar todos os eventos. + Lembrete padrão + + Lembrete padrão um minuto antes do evento + Lembrete padrão %d de minutos antes do evento + Lembrete padrão %d minutos antes do evento + + Nenhum lembrete padrão será criado + Se lembretes padrão devem ser criados para eventos sem um: o número de minutos desejado antes do evento. Deixe em branco para desativar os lembretes padrão. + Gerenciar cores do calendários + As cores dos calendários serão reconfiguradas a cada sincronização + As cores dos calendários podem ser configuradas por outros apps + Suporte a cores de eventos + As cores de eventos serão sincronizadas + As cores dos eventos não serão sincronizadas + CardDAV + Método de agrupamento dos contatos + + Os grupos são vCards separados + Os grupos são categorias por contato + + + Criar lista de contatos + A criação de listas de contatos pelo CardDAV pode não ser suportada pelo servidor. + Criar calendário + Fuso horário padrão (opcional) + + Possíveis itens do calendário + Eventos + Tarefas + Anotações / diário + A criação de calendários pelo CalDAV pode não ser suportada pelo servidor. + Cor + Título + Localização de armazenamento + Descrição (opcional) + Criar + + contatos + eventos + tarefas + Apagar coleção + Esta coleção (%s) e todos os seus dados serão removidos para sempre, tanto localmente como no servidor. + Sincronização + A sincronização está ativada + A sincronização está desativada + Somente leitura + Somente leitura (pelo servidor) + Somente leitura (pela política) + Somente leitura (apenas localmente) + Ler/gravar + Título + Descrição + Proprietário + Suporte a push + O servidor anuncia suporte a push + Inscrito em %1$s, vence às %2$s + Última sincronização (%s) + Endereço (URL) + + Informações de depuração + Arquivo ZIP + Contém informações de depuração e registros + Compartilhe o arquivo para transferi-lo para um computador, ou envie-o por e-mail, anexando ele a um ticket de suporte. + Compartilhar arquivo + Informações de depuração anexadas à mensagem (requer suporte a anexos no app destinatário) + Erro de HTTP + Erro do servidor + Erro de WebDAV + Erro de E/S + A solicitação foi negada pelo servidor. + O recurso solicitado não existe (mais). + O servidor não permite o tipo de operação solicitada. + Ocorreu um problema no lado do servidor. Contate o suporte do seu servidor. + Ocorreu um erro inesperado. Visualize as informações de depuração para detalhes. + Visualizar detalhes + As informações de depuração foram coletadas + Recursos envolvidos + Relacionados ao problema + Recurso remoto: + Recurso local: + Registros + Registros verbosos estão disponíveis + Visualizar registros + Copiar URL + Comunicado de privacidade + Os registros e as informações de depuração podem conter informações privadas. Tenha isso em mente ao compartilhá-os publicamente. + + Ocorreu um erro. + Ocorreu um erro de HTTP. + Ocorreu um erro de E/S. + Mostrar detalhes + + Montagens WebDAV + Cota utilizada: %1$s / disponível: %2$s + Compartilhar conteúdo + Desmontar + Adicionar montagem WebDAV + Acesse diretamente seus arquivos da nuvem adicionando uma montagem WebDAV! + como as montagens WebDAV funcionam.]]> + Nome de exibição + URL do WebDAV + URL inválido + Ponto de montagem e nome de exibição + Autenticação + Nome do usuário + Senha + Nome do usuário (opcional) + Senha (opcional) + Adicionar montagem + Nenhum serviço de WebDAV neste URL + Remover ponto de montagem + Os detalhes da conexão serão perdidos, mas nenhum arquivo será apagado. + Acessando arquivo do WebDAV + Baixando arquivo do WebDAV + Enviando arquivo do WebDAV + Montagem WebDAV + + Permissões do DAVx⁵ + São necessárias permissões adicionais + %s é muito antigo + Versão mínima necessária: %1$s + Falha na autenticação (certifique-se das credenciais) + Erro de rede ou E/S – %s + Erro do servidor de HTTP – %s + Erro do armazenamento local – %s + Erro suave (número máximo de tentativas atingido) + Contato inválido foi recebido do servidor + Evento inválido foi recebido do servidor + Tarefa inválida foi recebida do servidor + Ignorando um ou mais recursos inválidos + Sincronização pendente + Os dados remotos mudaram + + Sincronizar tudo + Sincronizar todas as contas + Toque para executar a sincronização manualmente. + + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml new file mode 100644 index 0000000..03766a8 --- /dev/null +++ b/app/src/main/res/values-pt/strings.xml @@ -0,0 +1,452 @@ + + + + A conta não existe (mais) + Livro de endereços do DAVx⁵ + Não mude a conta por aqui! Em vez disso, use o app diretamente para gerenciar as contas. + Excluir + Remover + Cancelar + Ativar + Este campo é necessário + Ajuda + Navegar para cima + Menu de opções + Partilhar + Sincronização começou/enfileirada + Base de dados corrompida + Todas as contas foram removidas localmente. + Depuração + Outras mensagens importantes + Mensagens de status de baixa prioridade + Sincronização + Erros de sincronização + Erros importantes que interrompem a sincronização, como respostas inesperadas do servidor + Avisos de sincronização + Problemas de sincronização não graves, como determinados arquivos inválidos + Erros de rede e E/S + Tempos de espera, problemas de conexão, etc. (geralmente temporários) + + Seus dados. Sua escolha. + Assuma o controle. + Intervalos de sincronização regulares + Para sincronização em intervalos regulares, o %s deve ter permissão para executar em segundo plano. Caso contrário, o Android poderá pausar a sincronização a qualquer momento. + Eu não preciso de intervalos de sincronização regulares.* + Compatibilidade %s + Fiz as configurações necessárias. Não me lembre novamente.* + * Deixe desmarcado para ser lembrado mais tarde. Pode ser redefinido nas configurações do aplicativo / %s. + Mais informações + jtx Board + + Suporte a tarefas + Se tarefas são suportadas por seu servidor, elas podem ser sincronizadas com um app suportado de tarefas: + OpenTasks + Parece não ser mais desenvolvido -- não recomendado. + Tasks.org + não são suportados.]]> + Nenhuma loja de aplicativos disponível + Não preciso de suporte a tarefas.* + Software Livre + Estamos felizes por usar o %s, que é um software de código aberto. Desenvolvimento, manutenção e suporte são um trabalho árduo. Considere contribuir (existem várias maneiras) ou fazer uma doação. Seria muito apreciado! + Como contribuir/doar + Não me lembrar por + + %d mês + %d de meses + %d meses + + Próximo + + Permissões + %s requer permissões para trabalhar corretamente. + Todos os abaixo + Use isto para ativar todas as funcionalidades (recomendado) + Todas as permissões concedidas + Permissões de contatos + Nenhum contato sincronizado (não recomendado) + Possível sincronização de contatos + Permissões de calendário + Nenhum calendário sincronizado (não recomendado) + Possível sincronização de calendário + Permissão de notificação + Notificações desativadas (não recomendado) + Notificações ativadas + Permissões do jtx Board + Permissões do OpenTasks + Permissões das tarefas + Sem sincronização de tarefas + Sincronização de tarefas possível + Manter as permissões + As permissões podem ser redefinidas automaticamente (não recomendado) + As permissões não serão redefinidas automaticamente + Toque em Permissões > desmarque \"Pausar atividade no app quando não usado\" + Se uma opção não funcionar, use as configurações / permissões do aplicativo. + Configurações do aplicativo + + Permissões WiFi SSID + Para que seja possível acessar o nome do WiFi atual (SSID), essas condições tem que ser compridas: + Permissão de localização precisa + Permissão de localização concedida + Permissão de localização negada + Permissão de acesso à localização em segundo plano + Permitir o tempo todo + Permissão de localização está definida como:%s + Permissão de localização não está definida como:%s + O %susa dados de localização (somente os SSIDs de Wi-Fi) para restringir a sincronização em uma rede Wi-Fi específica. Isso pode acontecer mesmo quando a sincronização é executada em segundo plano. + Todos os dados de localização (que são somente SSIDs de Wi-Fi) são usados somente localmente e não são enviados para qualquer servidor. + Localização sempre ativa + Serviço de localização está ativado + Serviço de localização está desativado + + Traduções + Bibliotecas + Versão %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) e contribuidores + Este programa é distribuído SEM NENHUMA GARANTIA. Ele é software livre e pode ser redistribuído sob algumas condições. + + Não foi possível criar o arquivo de log + Registrando todas as atividades de %s + Ver/partilhar + Desativar + + Sincronização de CalDAV/CardDAV + Sobre / Licença + Comentários sobre a versão beta + Instale um navegador Web + Configurações + Novidades e atualizações + Ferramentas + Links externos + Site na Web + Manual + Perguntas fequentes + Comunidade + Apoie o projeto + Como contribuir + Política de privacidade + Bem-vindo ao DAVx⁵! + Conecte-se ao seu servidor e mantenha seus calendários e contatos sincronizados. + Sincronizar todas as contas + + As notificações estão desativadas. Você não será notificado sobre erros de sincronização. + Sincronização automática inativa (sem conexão à internet verificada). + Gerenciar conexões + A economia de dados está ativada. A sincronização em segundo plano está restrita. + Gerenciar a economia de dados + A economia de bateria está ativada. A sincronização pode estar restrita. + Gerenciar a economia de bateria + Pouco espaço de armazenamento. O Android não sincronizará as mudanças locais imediatamente, mas sim na próxima sincronização regular. + Gerenciar armazenamento + Provedor de calendários ausente + Você desativou o app do sistema chamado \"Armazenamento de calendários\"? + Provedor de contatos ausente + Você desativou o app do sistema chamado \"Armazenamento de contatos\"? + Gerenciar apps + + Falha na detecção do serviço + Não foi possível atualizar a lista da coleção + + Executando em primeiro plano + Em alguns dispositivos, isto é necessário para a sincronização automática. + + Configurações + Depuração + Mostrar informações de depuração + Ver/compartilhar configurações de configuração e registros + Registro de atividades detalhado + O registro está ativo. Você pode ver os registros como parte das informações de depuração. + Registro de atividades desativado + Otimização da bateria + O app está isento (recomendado) + As restrições de bateria se aplicam (não recomendado) + Conexão + Tipo de proxy + + Padrão do sistema + Sem proxy + HTTP + SOCKS (para Orbot) + + Nome do host da proxy + Porta do proxy + Segurança + Permissões do aplicativo + Revise as permissões necessárias para sincronização + Desconfiar dos certificados de sistema + ACs adicionadas pelo usuário e pelo sistema não serão confiáveis + ACs adicionadas pelo usuário e pelo sistema serão confiáveis (recomendado) + Se essa configuração está ativa, os certificados do sistema não são tratados como confiáveis. Isso significa que você terá que manualmente aceitar cada certificado (e quando o servidor renova o seu certificado) ou a configuração da conta e a sincronização não funcionarão. + Redefinir certificados não-confiáveis + Restaura a confiança de todos os certificados personalizados + Todos os certificados personalizados foram restaurados + Interface de usuário + Configurações das notificações + Gerenciar os canais de notificação e suas configurações + Escolha um tema + + Padrão do sistema + Claro + Escuro + + Restaurar sugestões + Restaura as sugestões que foram descartadas anteriormente + Todas as sugestões serão exibidas novamente + Integração + App de tarefas + Nenhum app de tarefas compatível encontrado + UnifiedPush (experimental) + Nenhum (desativar push) + Escolha um distribuidor + Nenhum distribuidor push instalado + Nenhum servidor configurado + Pronto para receber mensagems push pelo %s + + CardDAV + CalDAV + WebCal + Permissões adicionais são necessárias para sincronizar essas coleções. + Gerenciar permissões + Sincronizar agora + Configurações da conta + Renomear conta + Dados locais que não foram salvos podem ser ignorados. Uma nova sincronização é necessária após uma renomeação. + Nome novo da conta + Renomear + O nome da conta já foi utilizado + Não foi possível renomear a conta + Excluir conta + Deseja excluir a conta? + Todas as cópias locais dos livros de endereços, calendários e listas de tarefas serão excluídas. + sincronizar esta coleção + Somente leitura + calendário + contatos + jornal + tarefas + Mostrar somente pessoal + Recarregar lista + Inscrições WebCAL podem ser sincronizadas com apps externos. + Não foi encontrado um aplicativo capaz de lidar com Webcal + Instalar ICSx⁵ + + Adicionar conta + política de privacidade.]]> + Login genérico + Login de provedor específico + Continuar + Autenticar + Autenticação com endereço de e-mail + Endereço de e-mail + É necessário um e-mail válido + Serviços são descobertos através do DNS e URLs conhecidas.]]> + Senha + Ocultar senha + Mostrar senha + Autenticação com usuário e URL + Usuário + URL base + serviços são também descobertos usando DNS e URLs conhecidas.]]> + Selecionar certificado + Adicionar conta + Nome da conta + O uso de apóstrofos (\') pode causar problemas em certos dispositivos. + Use seu endereço de e-mail como nome da conta porque o Android irá usar esse nome como campo AGENDA nos eventos que você criar. Não é possível ter duas contas com o mesmo nome. + Método do grupo Contato: + É necessário um nome de conta + O nome da conta já foi utilizado + A conta não pôde ser adicionada + Concluir + Login avançado + Certificado do cliente: %s + Nenhum certificado encontrado + Instalar certificado + Google Contatos / Agenda + Conta Google + Fazer login com o Google + ID do cliente (opcional) + Política de Privacidade para detalhes.]]> + Google API Services User Data Policy, incluindo com os requisitos de Uso Limitado.]]> + Não foi possível obter o código de autorização + Nextcloud + Fazer login com Nextcloud + Isto iniciará o processo de login do Nextcloud em um navegador web. + Endereço do servidor Nextcloud + Fazer login + Não foi possível obter a URL de login + Não foi possível obter os dados de login + Detecção de configuração + Aguarde, procurando servidor… + Não foi possível encontrar o serviço CalDAV ou CardDAV. + A URL base não parece ser uma URL de CalDAV/CardDAV accesível e a detecção de serviço não foi sucedida. + nossa lista de serviços testados e suas URLs base.]]> + Tenha certeza dos dados de autenticação (normalmente nome de usuário e senha) + Mais informações técnicas estão disponíveis nos logs. + Visualizar logs + + Sincronização + Intervalo sinc. de contatos + Apenas manualmente + A cada %d minutos + imediatamente nas alterações locais + Intervalo sinc. de calendários + Intervalo sinc. de tarefas + + Apenas manualmente + A cada 15 minutos + A cada 30 minutos + A cada hora + A cada 2 horas + A cada 4 horas + Uma vez por dia + + Sincronizar apenas por Wi-Fi + Sincronização restrita a conexões Wi-Fi + O tipo de conexão não é considerado + Restrição de WiFi SSID + Sincronizar apenas com %s + Todas as conexões WiFi serão usadas + Nomes separados por vírgula (SSIDs) das redes WiFi (deixe em branco para todas) + A restrição de SSID de WiFi requer configuração adicional + Gerenciar + VPN requer conexão à internet + VPN sem uma conexão à internet validada não é suficiente para executar uma sincronização (recomendado) + VPN sem uma conexão à internet validada é suficiente para executar uma sincronização + Autenticação + Nome do usuário + Senha nova + Atualize a senha de acordo com seu servidor + Certificado do cliente + Nenhum certificado disponível ou selecionado + Instalar certificado + CalDAV + Limite de tempo para eventos passados + Todos os eventos serão sincronizados + + Os eventos que ocorreram a mais de um dia serão ignorados + Eventos que ocorreram a mais de %d dias serão ignorados + Eventos que ocorreram a mais de %d dias serão ignorados + + Os eventos que ocorreram antes desse número de dias serão ignorados (pode ser 0). Deixe em branco para sincronizar todos os eventos. + Lembrete padrão + + Lembrete padrão um minuto antes do evento + Lembrete padrão %d minutos antes do evento + Lembrete padrão %d minutos antes do evento + + Nenhum lembrete padrão está criado + Se lembretes padrão devem ser criados para eventos sem lembrete: o número desejado de minutos antes do evento. Deixe em branco para desativar os lembretes padrão. + Gerenciar cores dos calendários + Cores dos calendários são redefinidas quando uma sincronização é feita + Cores dos calendários podem ser definidas por outros apps + Suporte para cor de evento + Cores de eventos são sincronizadas + Cores de eventos não são sincronizadas + CardDAV + Método do grupo Contato + + Grupos são vCards separados + Grupos são categorias por contato + + + Criar livro de endereços + A criação de livro de endereços por CardDAV pode não ser suportada pelo servidor. + Criar calendário + + Possíveis itens de calendário + Eventos + Tarefas + Notas / Diário + A criação de calendários por CalDAV pode não ser suportada pelo servidor. + Cor + Título + Local de armazenamento + Descrição (opcional) + Criar + + contatos + tarefas + Excluir coleção + Esta coleção (%s) e todos os seus dados serão removidos permanentemente, tanto localmente como no servidor. + Sincronização + Sincronização ativada + Sincronização desativada + Somente leitura + Somente leitura (pelo servidor) + Somente-leitura (pela política) + Somente leitura (somente localmente) + Leitura/escrita + Título + Descrição + Proprietário + Suporte à Push + Servidor anuncia suporte à Push + Inscrito em %1$s, expira às %2$s + Última sincronização (%s) + Endereço (URL) + + Informações de depuração + Arquivo ZIP + Contém informações de debug e logs + Compartilhe o arquivo para transferir ele para um computador, para enviar por e-mail ou para anexar ele à um ticket de suporte. + Partilhar arquivo + Informações de depuração anexadas a esta mensagem (requer suporte a anexo pelo aplicativo receptor). + Erro HTTP + Erro do servidor + Erro do WebDAV + Erro de E/S + Veja detalhes + Informações sobre depuração foram coletadas + Recursos envolvidos + Relacionado ao problema + Recurso remoto: + Recurso local: + Registros + Registros descritivos disponíveis + Visualizar logs + Copiar URL + + Ocorreu um erro. + Ocorreu um erro de HTTP. + Ocorreu um erro de leitura/gravação. + Mostrar detalhes + + Pastas WebDAV + Quota utilizada: %1$s / disponível: %2$s + Partilhar conteúdo + Desmontar + Adicionar uma pasta WebDAV + Acesse diretamente seus arquivos da nuvem adicionando uma pasta WebDAV! + Nome de exibição + URL do WebDAV + URL inválida + Autenticação + Nome do usuário + Palavra passe + Adicionar pasta + Nenhum serviço WebDAV nesta URL + Remover pasta + Detalhes de conexão serão perdidos, mas nenhum arquivo será excluído. + Acessando arquivo do WebDAV + Baixando arquivo do WebDAV + Enviando arquivo do WebDAV + Pasta WebDAV + + Permissões do DAVx⁵ + É necessário permissões adicionais + %s muito antigo + Versão mínima exigida: %1$s + Falha de autenticação (verifique as credenciais) + Erro de rede ou E/S – %s + Erro no servidor HTTP – %s + Erro de armazenamento local – %s + Erro simples (número de tentativas máximo atingido) + Contato inválido recebido do servidor + Evento inválido recebido do servidor + Tarefa inválida recebida do servidor + Ignorando um ou mais recursos inválidos + Sincronização em espera + Dados remotos podem ter mudado + + Sincronizar tudo + Sincronizar todas as contas + + diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml new file mode 100644 index 0000000..15c8610 --- /dev/null +++ b/app/src/main/res/values-ro/strings.xml @@ -0,0 +1,478 @@ + + + + Contul nu (mai) există + Agenda DAVx⁵ + Nu schimba contul aici! Utilizează direct aplicația pentru a gestiona conturile în schimb. + Șterge + Elimină + Anulează + Activează + Acest câmp este obligatoriu + Ajutor + Navigare în sus + Meniul Opțiuni + Distribuie + Sincronizare începută/pusă în coadă + Bază de date deteriorată + Toate conturile au fost eliminate local. + Depanare + Alte mesaje importante + Mesaje de stare cu prioritate redusă + Sincronizare + Erori de sincronizare + Erori importante care opresc sincronizarea, cum ar fi răspunsurile neașteptate ale serverului + Avertismente de sincronizare + Probleme de sincronizare non-fatale, cum ar fi anumite fișiere nevalide + Erori de rețea și I/O + Expirare, probleme de conexiune etc. (adesea temporare) + + Datele tale. Alegerea ta. + Preia controlul. + Intervale regulate de sincronizare + Pentru sincronizare la intervale regulate, %s trebuie să aibă voie să ruleze în fundal. În caz contrar, Android poate întrerupe sincronizarea în orice moment. + Nu am nevoie de intervale regulate de sincronizare.* + Compatibilitate %s + Firmware-ul specific vendorului poate bloca sincronizarea. Dacă ești afectat, poți rezolva acest lucru manual. + Am făcut setările necesare. Nu-mi mai aminti.* + * Lasă nebifat pentru a fi reamintit mai târziu. Poate fi resetat în setările aplicației / %s. + Mai multe informații + Placă de bază jtx + + Suport pentru sarcini + Dacă sarcinile sunt acceptate de server, acestea pot fi sincronizate cu o aplicație de sarcini acceptată: + OpenTasks + Nu pare a mai fi dezvoltat – nu este recomandat. + Tasks.org + nu sunt acceptate.]]> + Nu există un magazin de aplicații disponibil + Nu am nevoie de suport pentru sarcini.* + Software cu sursă deschisă + Ne bucurăm că utilizezi %s, care este un software open-source. Dezvoltarea, întreținerea și suportul sunt o muncă grea. Ia în considerare contribuția (există mai multe moduri) sau o donație. Ar fi foarte apreciat! + Cum să contribui/donezi + Nu-mi aminti + + %d lună + %d luni + %d luni + + Înainte + + Permisiuni + %s necesită permisiuni pentru a funcționa corect. + Toate cele de mai jos + Utilizează aceasta pentru a activa toate funcțiile (recomandat) + Toate permisiunile sunt acordate + Permisiuni Contacte + Fără sincronizare de contacte (nu este recomandat) + Este posibilă sincronizarea contactelor + Permisiuni pentru calendar + Fără sincronizare calendar (nu este recomandat) + Sincronizarea calendarului este posibilă + Permisiune de notificare + Notificări dezactivate (nu este recomandat) + Notificări activate + Permisiuni pentru jtx Board + Permisiuni OpenTasks + Permisiuni pentru sarcini + Nicio sincronizare a sarcinilor + Este posibilă sincronizarea sarcinilor + Păstrează permisiunile + Permisiunile pot fi resetate automat (nu este recomandat) + Permisiunile nu vor fi resetate automat + Clic pe Permisiuni > debifează „Elimină permisiunile dacă aplicația nu este utilizată” + Dacă un comutator nu funcționează, utilizează setările/permisiunile aplicației. + Setările aplicației + + Permisiuni SSID WiFi + Pentru a putea accesa numele actual WiFi (SSID), trebuie îndeplinite următoarele condiții: + Permisiune de locație precisă + Permisiunea de locație acordată + Permisiunea de locație refuzată + Permisiunea de locație în fundal + Permite tot timpul + Permisiunea locației setată la: %s + Permisiunea de locație nu este setată la: %s + %s folosește datele locației (doar WiFi SSID) numai pentru a restricționa sincronizarea la un anumit SSID WiFi. Acest lucru se va întâmpla chiar și atunci când sincronizarea rulează în fundal. + Toate datele locației (doar WiFi SSID) sunt folosite doar local și nu sunt trimise nicăieri. + Locația este întotdeauna activată + Serviciul de localizare este activat + Serviciul de localizare este dezactivat + + Traduceri + Biblioteci + Versiune %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (inginerie web bitfire GmbH) și contribuitori + Acest program vine cu ABSOLUT NICIO GARANȚIE. Este software gratuit și ești binevenit să îl redistribui în anumite condiții. + + Nu s-a putut crea fișierul jurnal + Acum se înregistrează toate activitățile %s + Vizualizare/distribuire + Dezactivează + + Adaptor de sincronizare CalDAV/CardDAV + Despre / Licență + Feedback beta + Instalează un browser web + Setări + Știri și actualizări + Instrumente + Link-uri externe + Pagină web + Manual + Întrebări frecvente + Pentru organizații + Comunitate + Susține proiectul + Cum să contribui + Politica de confidențialitate + Bun venit la DAVx⁵! + Conectează-te la server și păstrează calendarele și contactele sincronizate. + Sincronizează toate conturile + + Notificări dezactivate. Nu vei fi notificat despre erorile de sincronizare. + Sincronizarea automată nu este activă (fără conexiune la internet verificată). + Gestionează conexiunile + Economizorul de date este activat. Sincronizarea în fundal este restricționată. + Gestionează economizorul de date + Economisirea bateriei este activată. Sincronizarea poate fi restricționată. + Gestionează economisirea bateriei + Spațiu de depozitare redus. Android nu va sincroniza modificările locale imediat, ci în timpul următoarei sincronizări obișnuite. + Gestionează stocarea + Furnizorul de calendar lipsește + Ai dezactivat aplicația de sistem „Stocare Calendar”? + Furnizorul de contacte lipsește + Ai dezactivat aplicația de sistem „Stocare Contacte”? + Gestionează aplicațiile + + Detectarea serviciului a eșuat + Lista de colecții nu a putut fi actualizată + + Rulează în prim-plan + Pe unele dispozitive, acest lucru este necesar pentru sincronizarea automată. + + Setări + Depanare + Afișează informațiile de depanare + Vizualizează/partajează detaliile de configurare și jurnalele + Jurnalizare detaliată + Înregistrarea este activă. Poți vizualiza jurnalele ca parte a informațiilor de depanare. + Înregistrarea este dezactivată + Optimizarea bateriei + Aplicația este exclusă (recomandat) + Se aplică restricții pentru baterie (nu este recomandat) + Conexiune + Tip proxy + + Implicit + Fără proxy + HTTP + SOCKS (pentru Orbot) + + Nume gazdă proxy + Port proxy + Securitate + Permisiunile aplicației + Examinează permisiunile necesare pentru sincronizare + Nu avea încredere în certificatele de sistem + CA de sistem și de utilizator nu vor fi de încredere + CA de sistem și de utilizator vor fi de încredere (recomandat) + Dacă această setare este activă, certificatele de sistem nu sunt considerate ca fiind de încredere. Aceasta înseamnă că va trebui să accepți manual fiecare certificat (de asemenea, atunci când serverul își reînnoiește certificatul) sau configurarea contului și sincronizarea nu va funcționa. + Resetează certificatele de (ne)încredere + Resetează încrederea tuturor certificatelor personalizate + Toate certificatele personalizate au fost șterse + Interfață de utilizator + Setări de notificare + Gestionează canalele de notificare și setările acestora + Selectează tema + + Ca în sistem + Luminoasă + Întunecată + + Resetează sugestiile + Reactivează sugestiile care au fost respinse anterior + Toate sugestiile vor fi afișate din nou + Integrare + Aplicația de sarcini + Nu a fost găsită nicio aplicație de sarcini compatibilă + UnifiedPush (experimental) + Nimic (dezactivare Push) + Alege un distribuitor + Nu este instalat un distribuitor push + Niciun punct final configurat + Gata să primească mesaje push peste %s + FCM (Google Play) + Mesajele push sunt întotdeauna criptate. + + Contul a fost eliminat + CardDAV + CalDAV + Webcal + Sunt necesare permisiuni suplimentare pentru a sincroniza aceste colecții. + Gestionează permisiunile + Sincronizează acum + Setările contului + Redenumește contul + Datele locale nesalvate pot fi respinse. Resincronizarea este necesară după redenumire. + Nume cont nou + Redenumește + Numele contului este deja luat + Nu s-a putut redenumi contul + Șterge contul + Chiar ștergi contul? + Toate copiile locale ale agendelor, calendarelor și listelor de sarcini vor fi șterse. + sincronizează această colecție + numai pentru citire + calendar + contacte + jurnal + sarcini + Afișează numai personal + Actualizează lista + Abonamentele Webcal pot fi sincronizate cu aplicații externe. + Nu a fost găsită nicio aplicație compatibilă cu Webcal + Instalează ICSx⁵ + + Adaugă contul + Politica de confidențialitate.]]> + Autentificare generică + Autentificare specifică furnizorului + Continuă + Autentificare + Conectează-te cu adresa de e-mail + Adresa de e-mail + Este necesară o adresă de e-mail validă + Serviciile sunt descoperite folosind înregistrări DNS și adrese URL bine-cunoscute.]]> + Parolă + Ascunde parola + Afișează parola + Parolă (opțional) + Conecteează-te cu adresa URL și numele de utilizator + Nume de utilizator + Nume de utilizator (opțional) + Adresa URL de bază + serviciile sunt de asemenea descoperite folosind înregistrări DNS și adrese URL bine-cunoscute.]]> + Selectează certificatul + Adaugă contul + Nume de cont + Utilizarea apostrofelor (\') pare să cauzeze probleme pe unele dispozitive. + Utilizează adresa de e-mail ca nume de cont, deoarece Android va folosi numele contului ca câmp ORGANIZATOR pentru evenimentele pe care le creezi. Nu poți avea două conturi cu același nume. + Metoda de grupare a contactelor: + Numele contului este necesar + Numele contului este deja luat + Contul nu a putut fi adăugat + Finalizează + Autentificare avansată + Fără certificat de client (opțional) + Certificat de client: %s + Nu a fost găsit niciun certificat + Instalare certificat + Fastmail + Cont Fastmail + Conectează-te cu Fastmail + Contacte Google / Calendar + Cont Google + Conectează-te cu Google + ID client (opțional) + Politica de confidențialitate pentru detalii.]]> + Politica privind datele utilizatorilor serviciilor API Google, inclusiv cerințele de utilizare limitată.]]> + Nu s-a putut obține codul de autorizare + Nextcloud + Conectare cu Nextcloud + Aceasta va porni fluxul de conectare Nextcloud într-un browser web. + Adresa serverului Nextcloud + Conectare + Nu s-a putut obține adresa URL de conectare + Nu s-au putut obține datele de conectare + Detectarea configurației + Se interoghează serverul… + Nu s-a putut găsi serviciul CalDAV sau CardDAV. + Adresa URL de bază nu pare să fie o adresă URL CalDAV/CardDAV accesibilă, iar detectarea serviciului nu a avut succes. + lista de servicii testate și adresele lor URL de bază.]]> + Verifică, de asemenea, și autentificarea (de obicei, numele de utilizator și parola). + Informații tehnice suplimentare sunt disponibile în jurnale. + Vezi jurnalele + + Sincronizare + Interval de sincronizare a contactelor + Doar manual + La fiecare %d minute + imediat la modificări locale + Interval de sincronizare a calendarelor + Interval de sincronizare a sarcinilor + + Doar manual + La fiecare 15 minute + La fiecare 30 de minute + La fiecare oră + La fiecare 2 ore + La fiecare 4 ore + O dată pe zi + + Sincronizare numai prin WiFi + Sincronizarea este limitată la conexiunile WiFi + Tipul de conexiune nu este luat în considerare + Restricție SSID WiFi + Se va sincroniza numai prin %s + Toate conexiunile WiFi vor fi utilizate + Nume separate prin virgulă (SSID) ale rețelelor WiFi permise (lasă necompletat pentru toate) + Restricția SSID WiFi necesită setări suplimentare + Gestionează + VPN necesită internetul de bază + VPN fără conexiune validată la Internet nu este suficient pentru a rula sincronizarea (recomandat) + VPN fără conexiune validată la Internet este suficient pentru a rula sincronizarea + Autentificare + Nume de utilizator + Parolă sau parola aplicației + parola aplicației.]]> + Parolă nouă + Actualizează parola în funcție de server. + Autorizează din nou (OAuth) + Utilizează atunci când accesul a fost revocat + Autorizare cu succes + Certificat de client + Niciun certificat disponibil sau selectat + Instalare certificat + CalDAV + Limită de timp pentru evenimentele din trecut + Toate evenimentele vor fi sincronizate + + Evenimentele cu mai mult de o zi în trecut vor fi ignorate + Evenimentele cu peste %d zile în trecut vor fi ignorate + Evenimentele cu peste %d zile în trecut vor fi ignorate + + Evenimentele care depășesc acest număr de zile în trecut vor fi ignorate (poate fi 0). Lasă necompletat pentru a sincroniza toate evenimentele. + Memento implicit + + Memento implicit cu un minut înainte de eveniment + Memento implicit cu %d minute înainte de eveniment + Memento implicit cu %d minute înainte de eveniment + + Nu sunt create mementouri implicite + Dacă vor fi create memento-uri implicite pentru evenimente fără memento: numărul dorit de minute înainte de eveniment. Lasă necompletat pentru a dezactiva memento-urile implicite. + Gestionează culorile calendarului + Culorile calendarului sunt resetate la fiecare sincronizare + Culorile calendarului pot fi setate de alte aplicații + Suport pentru culoarea evenimentului + Culorile evenimentelor sunt sincronizate + Culorile evenimentelor nu sunt sincronizate + CardDAV + Metoda de grupare a contactelor + + Grupurile sunt vCard-uri separate + Grupurile sunt categorii per-contact + + + Creează agendă de adrese + Crearea agendei prin CardDAV poate să nu fie acceptată de server. + Creează un calendar + Fus orar implicit (opțional) + + Posibile intrări din calendar + Evenimente + Sarcini + Note/jurnal + Crearea calendarului prin CalDAV poate să nu fie acceptată de server. + Culoare + Titlu + Locația de stocare + Descriere (opțional) + Crează + + contacte + evenimente + sarcini + Șterge colecția + Această colecție (%s) și toate datele sale vor fi șterse definitiv, atât local, cât și de pe server. + Sincronizare + Sincronizarea este activată + Sincronizarea este dezactivată + Numai citire + Numai citire (de pe server) + Numai citire (după politică) + Numai citire (doar local) + Citire/scriere + Titlu + Descriere + Proprietar + Suport Push + Serverul informează despre suportul Push + Abonat la %1$s, expiră la %2$s + Ultima sincronizare (%s) + Adresă (URL) + + Informații de depanare + Arhivă ZIP + Conține informații de depanare și jurnale + Partajează arhiva pentru a o transfera pe un computer, pentru a o trimite prin e-mail sau pentru a o atașa la un bilet de asistență. + Partajează arhiva + Informații de depanare atașate la acest mesaj (necesită suport pentru atașamentele aplicației care primește). + Eroare HTTP + Eroare de server + Eroare WebDAV + Eroare I/O + Vezi detaliile + Au fost colectate informații de depanare + Resurse implicate + Legat de problema + Resursa de la distanță: + Resursa locală: + Jurnale + Jurnalele detaliate sunt disponibile + Vezi jurnalele + Notificare de confidențialitate + Jurnalele și informațiile de depanare pot conține informații private. Fii conștient de acest lucru atunci când îl publici. + + A avut loc o eroare. + A apărut o eroare HTTP. + A apărut o eroare I/O. + Afișează detaliile + + Montări WebDAV + Cotă utilizată: %1$s / disponibilă: %2$s + Partajează conținutul + Demontează + Adaugă o montare WebDAV + Accesează direct fișierele din cloud adăugând o montare WebDAV! + cum funcționează montările WebDAV.]]> + Numele afișat + URL WebDAV + URL greșit + Punctul de montare și numele de afișare + Autentificare + Nume de utilizator + Parolă + Nume de utilizator (opțional) + Parolă (opțional) + Adaugă montare + Niciun serviciu WebDAV la această adresă URL + Elimină punctul de montare + Detaliile conexiunii se vor pierde, dar niciun fișier nu va fi șters. + Se accesează fișierul WebDAV + Se descarcă fișierul WebDAV + Se actualizează fișierul WebDAV + Montare WebDAV + + Permisiuni DAVx⁵ + Sunt necesare permisiuni suplimentare + %s prea vechi + Versiunea minimă necesară: %1$s + Autentificare eșuată (verifică datele de conectare) + Eroare de rețea sau I/O – %s + Eroare de server HTTP – %s + Eroare de stocare locală – %s + Eroare soft (încercări maxime atinse) + S-a primit contact nevalid de la server + S-a primit eveniment nevalid de la server + S-a primit sarcină nevalidă de la server + Ignorarea uneia sau mai multor resurse nevalide + Sincronizare în așteptare + Datele de la distanță s-au schimbat + + Sincronizează tot + Sincronizează toate conturile + Eticheta butonului de sincronizare + Pictograma butonului de sincronizare + Atinge pentru a rula sincronizarea manual. + + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml new file mode 100644 index 0000000..92e124c --- /dev/null +++ b/app/src/main/res/values-ru/strings.xml @@ -0,0 +1,489 @@ + + + + Аккаунт (больше) не существует + Адресная книга DAVx⁵ + Не меняйте аккаунт здесь! Вместо этого используйте приложение для управления учетными записями. + Удалить + Удалить + Отмена + Включить + Это поле является обязательным + Помощь + Перейти наверх + Меню параметров + Поделиться + Синхронизация начата/завершена + База данных повреждена + Все учетные записи были удалены локально. + Отладка + Другие важные сообщения + Низкоприоритетные сообщения о состоянии + Синхронизация + Ошибки синхронизации + Важные ошибки, которые останавливают синхронизацию, например, неожиданные ответы сервера. + Предупреждения синхронизации + Некритичные проблемы с синхронизацией, такие как некоторые неверные файлы + Ошибки сети и ввода/вывода + Таймауты, проблемы с подключением и т. д. (часто временные) + + Ваши данные. Ваш выбор. + Возьмите под контроль. + Регулярные интервалы синхронизации + Для обеспечения синхронизации с регулярными интервалами необходимо разрешить работу %s в фоновом режиме. В противном случае, Android может приостановить синхронизацию в любое время. + Мне не нужна синхронизация с регулярными интервалами.* + Совместимость %s + Прошивка конкретного производителя может блокировать синхронизацию. Если вы столкнулись с этой проблемой, ее можно решить только самостоятельно. + Я выполнил необходимые настройки. Больше не напоминать.* + * Чтобы получить предупреждение позже, снимите флажок. Можно сбросить в настройках приложения / %s. + Дополнительная информация + jtx Board + + Поддержка задач + Если задачи поддерживаются вашим сервером, их можно синхронизировать при помощи соответствующего приложения: + OpenTasks + По всей видимости, больше не разрабатывается (не рекомендуется) + Tasks.org + не поддерживаются.]]> + Магазин приложений недоступен + Мне не нужна поддержка задач.* + ПО с открытым исходным кодом + Мы рады, что вы используете %s, программное обеспечение с открытым исходным кодом. Разработка, сопровождение и поддержка - это тяжелая работа. Пожалуйста, подумайте о том, чтобы внести свой вклад (существует множество способов) или сделать пожертвование. Будем очень признательны! + Как внести свой вклад/пожертвовать + Не напоминайте мне об этом в течение + + %d месяц + %d месяца + %d месяцев + %d месяца + + Далее + + Разрешения + Для правильной работы %s требуются разрешения. + Все нижеперечисленное + Используйте для включения всех опций (рекомендуется) + Все разрешения предоставлены + Разрешения для контактов + Контакты не синхронизируются (не рекомендуется) + Синхронизация контактов возможна + Разрешения для календаря + Календарь не синхронизируется (не рекомендуется) + Синхронизация календаря возможна + Разрешение на уведомление + Уведомления отключены (не рекомендуется) + Уведомления включены + Разрешения jtx Board + Разрешения OpenTasks + Разрешения Tasks + Не синхронизируются задачи + Синхронизация задач возможна + Сохранять разрешения + Разрешения могут быть сброшены автоматически (не рекомендуется) + Разрешения не будут сброшены автоматически + Выберите Разрешения > снимите флажок \"Удалить разрешения, если приложение не используется\". + Если переключатель не работает, используйте Настройки приложения / Разрешения. + Настройки приложения + + Разрешения WiFi SSID + Для получения доступа к названию текущей сети WiFi (SSID), должны быть выполнены следующие условия: + Разрешение на точное местоположение + Разрешение на определение местоположения предоставлено + Разрешение на определение местоположения не предоставлено + Разрешение на фоновое определение местоположения + Разрешать всегда + Геолокация задана: %s + Геолокация не задана: %s + %s использует данные о местоположении (только WiFi SSID) только для того, чтобы ограничить синхронизацию только определенным WiFi SSID. Это будет происходить, даже если синхронизация выполняется в фоновом режиме. + Все данные о местоположении (только WiFi SSID) используются только локально и никуда не передаются. + Определение местоположения всегда включено + Служба определения местоположения включена + Служба определения местоположения отключена + + Переводы + Библиотеки + Версия %1$s (%2$d) + © Рикки Хирнер (Ricki Hirner), Бернхард Штокманн (Bernhard Stockmann) (bitfire web engineering GmbH) и контрибьюторы + Эта программа поставляется БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ. Это свободное программное обеспечение и вы можете распространять его при соблюдении определенных условий. + + Не удалось создать файл лога + Сейчас логируется вся активность %s + Просмотр/обмен + Отключить + + Адаптер синхронизации CalDAV/CardDAV + О программе / Лицензия + Отзыв о бета-тестировании + Пожалуйста, установите браузер + Настройки + Новости и обновления + Инструменты + Внешние ссылки + Сайт + Руководство + FAQ + Для организаций + Сообщество + Поддержать проект + Как внести свой вклад + Политика конфиденциальности + Добро пожаловать в DAVx⁵! + Подключитесь к вашему серверу и синхронизируйте календари и контакты. + Синхронизировать все аккаунты + + Уведомления отключены. Вы не будете получать уведомления об ошибках синхронизации. + Автоматическая синхронизация недоступна (нет подключения к интернету). + Управление подключениями + Экономия трафика включена. Фоновая синхронизация ограничена. + Экономия трафика + Включена экономия заряда батареи. Синхронизация может быть ограничена. + Управлять экономией заряда батареи + Недостаточно памяти для хранения данных. Android будет синхронизировать локальные изменения не сразу, а во время следующей регулярной синхронизации. + Управление хранилищем + Отсутствует провайдер календаря + Вы отключили системное приложение \"Хранилище календаря\"? + Отсутствует провайдер контактов + Вы отключили системное приложение \"Хранилище контактов\"? + Управление приложениями + + Не удалось обнаружить службу + Не удалось обновить список коллекций + + Запущен в приоритетном режиме + Для автосинхронизации на ряде устройств. + + Настройки + Отладка + Показать отладочную информацию + Просмотр/обмен информацией о конфигурации и логами + Подробное логирование + Логирование активно. Вы можете просматривать журналы как часть отладочной информации. + Логирование отключено + Оптимизация батареи + Приложение исключено (рекомендуется) + Применяются ограничения по использованию батареи (не рекомендуется) + Соединение + Тип прокси + + Определен системой + Без прокси + HTTP + SOCKS (для Orbot) + + Имя хоста прокси + Порт прокси + Безопасность + Разрешения приложения + Проверка разрешений, необходимых для синхронизации + Сертификаты системы + Не доверять системным и пользовательским CA + Доверять системным и пользовательским CA (рекомендуется) + Если этот параметр активен, системные сертификаты считаются не надежными. Это означает, что вам придется вручную принимать каждый сертификат (в том числе когда сервер обновляет свой сертификат), иначе настройка и синхронизация учетных записей работать не будут. + Сброс сертификатов + Отменяет доверие ко всем пользовательским сертификатам + Все пользовательские сертификаты были удалены + Интерфейс пользователя + Настройки уведомлений + Управление каналами уведомлений и их настройками + Выбор темы + + Определена системой + Светлая + Темная + + Включить подсказки + Включить подсказки, которые были отключены ранее + Все подсказки будут показаны снова + Интеграция + Приложение Tasks + Не найдено совместимое приложение для задач + UnifiedPush (экспериментально) + Нет (отключить push) + Выберите дистрибьютора + Push-дистрибьютор не установлен + Конечная точка не сконфигурирована + Готов получать push-сообщения %s + FCM (Google Play) + Push-сообщения всегда зашифрованы. + + Аккаунт удален + CardDAV + CalDAV + WebСal + Для синхронизации этих коллекций требуются дополнительные разрешения. + Управление разрешениями + Синхронизировать + Настройки аккаунта + Переименовать аккаунт + Несохраненные локальные данные могут быть потеряны. После переименования необходима повторная синхронизация. + Новое название аккаунта + Переименовать + Название аккаунта уже используется + Не удалось переименовать аккаунт + Удалить аккаунт + Вы действительно хотите удалить аккаунт? + Все локальные копии адресных книг, календарей и задач будут удалены. + синхронизировать эту коллекцию + только для чтения + календарь + контакты + журнал + задачи + Показать только личные + Обновить список + Подписки Webcal можно синхронизировать с внешними приложениями. + Не найдено приложение, поддерживающее WebCal + Установить ICSx⁵ + + Добавить аккаунт + Политику конфиденциальности.]]> + Общий логин + Авторизация для специфического поставщика + Продолжить + Войти + Войти c адресом email + Адрес email + Требуется действительный адрес email + Сервисы обнаруживаются по записям DNS и известным URL-адресам.]]> + Пароль + Скрыть пароль + Показать пароль + Пароль (необязательно) + Войти с URL и именем пользователя + Имя пользователя + Имя пользователя (необязательно) + Базовый URL + сервисы также обнаруживаются по записям DNS и известным URL.]]> + Выберите сертификат + Добавить аккаунт + Название аккаунта + Использование апострофов (\'), как оказалось, вызывает проблемы на некоторых устройствах. + Используйте свой адрес email в качестве названия аккаунта, поскольку Android будет использовать его в качестве поля ОРГАНИЗАТОР для создаваемых вами событий. Не допускается наличие двух аккаунтов с одинаковыми названиями. + Метод группировки контактов: + Название аккаунта обязательно + Название аккаунта уже используется + Не удалось добавить аккаунт + Завершить + Расширенный вход + Без клиентского сертификата (необязательно) + Сертификат клиента: %s + Сертификат не найден + Установить сертификат + Fastmail + Аккаунт Fastmail + Войти с Fastmail + Google Контакты / Календарь + Google аккаунт + Войти с Google + ID клиента (необязательно) + Политику конфиденциальности.]]> + Политику в отношении пользовательских данных Google API Services, включая требования Ограниченного использования.]]> + Не удалось получить код авторизации + Nextcloud + Войти с Nextcloud + Это запустит процесс авторизации в Nextcloud в браузере. + Адрес сервера Nextcloud + Войти + Не удалось получить URL для авторизации + Не удалось получить данные для авторизации + Обнаружение конфигурации + Ожидайте, выполняется запрос к серверу… + Не удалось найти службу CalDAV или CardDAV. + Судя по всему, базовый URL не является допустимым URL CalDAV/CardDAV, и обнаружение службы не увенчалось успехом. + нашему списку проверенных сервисов и их базовых URL.]]> + Кроме того, проверьте правильность авторизации (обычно это имя пользователя и пароль). + Дополнительную техническую информацию можно найти в логах. + Просмотр логов + + Синхронизация + Интервал синхронизации контактов + Вручную + Каждые %d минут и немедленно при локальных изменениях + Интервал синхронизации календарей + Интервал синхронизации задач + + Только вручную + Каждые 15 минут + Каждые 30 минут + Каждый час + Каждые 2 часа + Каждые 4 часа + Раз в день + + Синхронизировать только через WiFi + Разрешить синхронизацию только через WiFi + Не учитывать тип соединения + Ограничение WiFi SSID + Будет синхронизироваться только %s + Будут использоваться все WiFi-подключения + Имена (SSID) разрешенных сетей WiFi, разделенные запятыми (оставьте пустым для всех) + Ограничение WiFi SSID требует дополнительных настроек + Управлять + VPN требует наличия основного интернета + VPN без основного интернета недостаточно для выполнения синхронизации (рекомендуется) + VPN без основного интернета достаточно для выполнения синхронизации + Аутентификация + Имя пользователя + Пароль или пароль приложения + пароль приложения.]]> + Новый пароль + Обновить пароль + Авторизовать снова (OAuth) + Использовать, когда доступ был отозван + Авторизация успешна + Сертификат клиента + Сертификат отсутствует или не выбран + Установить сертификат + CalDAV + Ограничение по времени для прошедших событий + Будут синхронизироваться все события + + События старше одного дня будут игнорироваться + События старше %d дней будут игнорироваться + События старше %d дней будут игнорироваться + События старше %d дней будут игнорироваться + + События, произошедшие ранее указанного количества дней, будут игнорироваться (может быть 0). Оставьте пустым, если хотите синхронизировать все события. + Напоминание по умолчанию + + Напоминание по умолчанию за 1 минуту до события + Напоминание по умолчанию за %d минуты до события + Напоминание по умолчанию за %d минут до события + Напоминание по умолчанию за %d минут до события + + Напоминания по умолчанию не создаются + Для событий без напоминания введите желаемое количество минут для получения уведомления. Оставьте пустым, чтобы не использовать напоминания по умолчанию. + Управление цветами календаря + Цвета календаря сбрасываются после каждой синхронизации + Цвета календаря могут быть установлены другими приложениями. + Поддержка цвета событий + Цвета событий синхронизируются + Цвета событий не синхронизируются + CardDAV + Метод группировки контактов + + Группы являются отдельными vCards + Группы являются категориями контактов + + + Создать адресную книгу + Создание адресной книги через CardDAV может не поддерживаться сервером. + Создать календарь + Часовой пояс по умолчанию (необязательно) + + Возможные записи календаря + События + Задачи + Заметки / журнал + Создание календаря через CalDAV может не поддерживаться сервером. + Цвет + Название + Место хранения + Описание (необязательно) + Создать + + контакты + события + задачи + Удалить коллекцию + Эта коллекция (%s) и все ее данные будут удалены навсегда, как локально, так и на сервере. + Синхронизация + Синхронизация включена + Синхронизация отключена + Только для чтения + Только для чтения (со стороны сервера) + Только для чтения (в соответствии с политикой) + Только для чтения (локально) + Чтение/запись + Название + Описание + Владелец + Поддержка push + Сервер анонсирует поддержку push + Подписан на %1$s, истекает %2$s + Последняя синхронизация (%s) + Адрес (URL) + + Отладочная информация + ZIP-архив + Содержит отладочную информацию и логи + Поделитесь архивом, чтобы перенести его на компьютер, отправить по электронной почте или прикрепить к запросу в службу поддержки. + Поделиться архивом + Отладочная информация, прикреплена к данному сообщению (требует поддержки вложений со стороны принимающего приложения). + Ошибка HTTP + Ошибка сервера + Ошибка WebDAV + Ошибка ввода/вывода + Запрос был отклонен сервером. + Запрошенный ресурс (больше) не существует. + Сервер не разрешает запрошенный тип операции. + Возникла проблема на стороне сервера. Пожалуйста, свяжитесь со службой поддержки вашего сервера. + Произошла неожиданная ошибка. Просмотрите отладочную информацию, чтобы узнать подробности. + Просмотр + Отладочная информация собрана + Задействованные ресурсы + Связанная с этим проблема + Удаленный ресурс: + Локальный ресурс: + Логи + Доступны подробные логи + Просмотр логов + Скопировать URL + Проверить ресурс + Предупреждение о конфиденциальности + Журналы и отладочная информация могут содержать конфиденциальную информацию. Пожалуйста, помните об этом, когда делитесь ими. + Невозможно просмотреть ресурс + + Произошла ошибка. + Произошла ошибка HTTP + Произошла ошибка ввода/вывода. + Показать детали + + Точки монтирования WebDAV + Использованная квота: %1$s / доступно: %2$s + Поделиться контентом + Отмонтировать + Добавление точки монтирования WebDAV + Прямой доступ к вашим облачным файлам с помощью точек монтирования WebDAV! + как работают точки монтирования WebDAV.]]> + Отображаемое имя + WebDAV URL + Некорректный URL + Точка монтирования и отображаемое имя + Аутентификация + Имя пользователя + Пароль + Имя пользователя (необязательно) + Пароль (необязательно) + Добавить точку монтирования + Служба WebDAV отсутствует на данном URL + Удалить точку монтирования + Информация о подключении будет потеряна, но никакие файлы не будут удалены. + Доступ к файлу WebDAV + Загрузка файла WebDAV + Выгрузка файла WebDAV + Точка монтирования WebDAV + + Разрешения DAVx⁵ + Требуются дополнительные разрешения + Приложение %s устарело + Минимально необходимая версия: %1$s + Ошибка аутентификации (проверьте учетные данные) + Ошибка сети или ввода/вывода – %s + Ошибка HTTP-сервера – %s + Ошибка локального хранилища – %s + Ошибка (достигнуто максимальное количество повторных попыток) + Получен неверный контакт с сервера + Получено недействительное событие от сервера + Получена недействительная задача от сервера + Игнорирование одного или нескольких недействительных ресурсов + Ожидается синхронизация + Удаленные данные изменились + + Синхронизировать все + Синхронизировать все аккаунты + Ярлык кнопки синхронизации + Значок кнопки синхронизации + Нажмите для запуска синхронизации вручную. + + diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml new file mode 100644 index 0000000..bb69bf0 --- /dev/null +++ b/app/src/main/res/values-sk/strings.xml @@ -0,0 +1,194 @@ + + + + DAVx⁵ adresár + Povoliť + Pomoc + Ladenie + Ďalšie dôležité správy + Synchronizácia + Chyby synchronizácie + Významné chyby ktoré spôsobia koniec synchronizácie ako napríklad neočakávané odozvy servera + Varovania synchronizácie + Nefatálne synchronizačné chyby ako napríklad neplatné súbory + Sieťové a V/V chyby + Vypršanie času, problémy spojenia, atď. (často dočasné) + + Viac informácii + + + + Knižnice + Verzia %1$s (%2$d) + Tento program sa poskytuje BEZ AKEJKOĽVEK ZÁRUKY. Je to slobodný softvér a môžete ho ďalej šíriť pri splnení určitých podmienok. + + Nie je možné vytvoriť súbor protokolu + + CalDAV/CardDAV Sync Adaptér + O programe / Licencia + Odozva na beta-verziu + Prosím, nainštalujte webový prehliadač + Nastavenia + Novinky & aktualizácie + Externé odkazy + Webové sídlo + Manuál + FAQ + Zásady bezpečnosti + + + Zisťovanie služby zlyhalo + Nie je možné obnoviť zoznam kolekcií + + + Nastavenia + Ladenie + Zobraziť ladiace informácie + Zvýšené protokolovanie + Protokolovanie je zakázané + Spojenie + Zabezpečenie + Nedôverovať systémovým certifikátom + Nebude sa dôverovať systémovým a požívateľom pridaným certifikátom + Bude sa dôverovať systémovým a používateľom pridaným certifikátom (doporučuje sa) + Vynulovanie ne(dôveryhodných) certifikátov + Vynuluje dôveru pre všetky užívateľské certifikáty + Všetky užívateľské certifikáty boli vyčistené + Používateľské rozhranie + Nastavenia notifikácií + Spravovať notifikačné kanály a ich nastavenia + Vynulovať náznaky + Znovu povolí náznaky odstránené skôr + Všetky náznaky budú zobrazené znovu + + CardDAV + CalDAV + Webcal + Teraz synchronizovať + Nastavenia používateľského účtu + Premenovať používateľský účet + Premenovať + Meno účtu sa už používa + Odstrániť účet + Skutočne si želáte odstrániť účet? + Všetky miestne kópie adresárov, kalendárov a zoznamov úloh budú vymazané. + synchronizovať túto zbierku + len na čítanie + kalendár + Nenašla sa žiadna aplikácia schopná používať Webcal + Nainštalovať ICSx⁵ + + Pridať účet + Prihlásiť sa + Prihlásiť sa e-mailovou adresou + E-mailová adresa + Vyžaduje sa platná e-mailová adresa + Heslo + Prihlásiť sa s použitím URL a používateľského mena + Používateľské meno + Základné URL + Zvoliť certifikát + Pridať účet + Meno používateľského účtu + Použite vašu e-mailovú adresu ako meno používateľského účtu pretože Android používa meno účtu v poli ORGANIZÁTOR pre udalosti ktoré vytvoríte. Nie je možné mať dva používateľské účty s rovnakým menom. + Spôsob práce so skupinami + Vyžaduje sa meno používateľského účtu + Meno účtu sa už používa + Zisťuje sa konfigurácia + Čakajte, prosím, zasiela sa dopyt na server... + Nie je možné nájsť služby CalDAV ani CardDAV. + + Synchronizácia + Synchr. interval pre kontakty + Iba manuálne + Každú %d minút + okamžite pri miestnych zmenách + Synchr. interval pre kalendáre + Synchr. interval pre úlohy + + Iba manuálne + Každých 15 minút + Každých 30 minút + Každú hodinu + Každé 2 hodiny + Každé 4 hodiny + Raz za deň + + Synchronizovať iba cez WiFi + Synchronizácia je obmedzená na WiFi pripojenie + Typ pripojenia sa neberie do úvahy + Obmedzenie na WiFi SSID + Synchronizuje sa iba cez %s + Použije sa akékoľvek WiFi pripojenie + Čiarkou oddelený zoznam mien (SSID) povolených WiFi sietí (ponechať prázdne pre všetky) + Overenie + Meno používateľa + Aktualizujte heslo podľa vášho servera. + CalDAV + Uplynul časový limit pre udalosť + Budú sa synchronizovať všetky udalosti + + Udalosti staršie ako jeden deň budú ignorované + Udalosti staršie ako %d dni budú ignorované + Udalosti staršie ako %d dní budú ignorované + Udalosti staršie ako %d dní budú ignorované + + Udalosti ktoré sú staršie ako tento počet dní budú ignorované (0 je povolená). Ponechajte prázdne aby ste synchronizovali všetky udalosti. + Prednastavená pripomienka + + Prednastavená pripomienka 1 minútu pred udalosťou + Prednastavená pripomienka minút pred udalosťou: %d + Prednastavená pripomienka minút pred udalosťou: %d + Prednastavená pripomienka minút pred udalosťou: %d + + Neboli vytvorené prednastavené pripomienky + Ak majú byť vytvorené prednastavené pripomienky pre udalosti bez pripomienok: počet minút pred udalosťou. Ponechajte prázdne na zrušenie prednastavených pripomienok. + Spravovať farby kalendára + Podpora farieb pre udalosť + CardDAV + Spôsob práce so skupinami kontaktov + + Skupiny sú osobitné vKarty + Skupiny sú kategórie na kontakt + + + Vytvoriť adresár + Vytvoriť kalendár + Možné kalendárové položky + Udalosti + Úlohy + Poznámky / denník + Farba + Názov + Umiestnenie úložiska + Vytvoriť + + Zmazať kolekciu + Synchronizácia + Názov + Popis + + Ladiace informácie + Kopírovať URL + + Vyskytla sa chyba. + Vyskytla sa HTTP chyba. + Vyskytla sa V/V chyba. + Zobraziť detaily + + Overenie + Meno používateľa + Heslo + + Oprávnenia DAVx⁵ + Vyžadujú sa dodatočné oprávnenia + Overenie zlyhalo (skontroluje prihlasovacie údaje) + Sieťová alebo V/V chyba – %s + Chyba HTTP servera – %s + Chyba miestneho úložiska – %s + Kontakt prijatý zo servera je neplatný + Udalosť prijatá zo servera nie je platná + Úloha prijatá zo servera nie je platná + Ignoruje sa jeden alebo viac neplatných zdrojov + + + diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml new file mode 100644 index 0000000..6067be7 --- /dev/null +++ b/app/src/main/res/values-sl/strings.xml @@ -0,0 +1,211 @@ + + + + Račun ne obstaja (več) + DAVx⁵ imenik + Odstrani + Preklic + Omogoči + To polje je obvezno + Pomoč + Deli + Okvarjena baza podatkov + Vsi računi so bili lokalno odstranjeni. + Razhroščevalnik + Ostale pomembne nastavitve + Statusno sporočilo nizke prioritete + Sinhronizacija + Napake v sinhronizaciji + Pomembne napake, ki zaustavijo sinhronizacijo (npr. nepričakovani odgovori strežnika) + Opozorila med sinhronizacijo + Neusodni problemi v sinhronizaciji npr. določene neveljavne datoteke + Omrežje in I/O napake + Pavze, povezave v povezavi z omrežjem, itd. (ponavadi začasno) + + Tvoji podatki. Tvoja izbira. + Prevzemi nadzor + Ne potrebujem enakomernih intervalov sinhroniziranja + %szdružljivost + Več informacij + Tasks.org + Nobene trgovine z aplikacijami ni na voljo + Odprtokodna programska oprema + Kako prispevati ali donirati + + Dovoljenja + Vse spodaj + Uporabi to da omogočiš vse funkcije (priporočeno) + Vsa dovoljenja odobrena + Dovoljenje za dostop do imenika + Ne sinhroniziraj imenika (ni priporočeno) + Možnost sinhronizacije imenika + Dovoljenje za dostop do koledarja + Ne sihroniziraj koledarja (ni priporočeno) + Možnost sinhronizacije koledarja + Dovoljenje za prikaz obvestil + Obvestila onemogočena (ni priporočeno) + Obvestila omogočena + jtx Board dovoljenja + + + Knjižnice + Verzija %1$s (%2$d) + Ta program ne vsebuje NIČ garancije. To je brezplačna programska oprema in jo lahko pod določenimi pogoji delite naprej. + + Ni bilo mogoče ustvariti zapisnika + + CalDAV/CardDAV sinhronizacijski adapter + O aplikaciji / licenca + Beta povratne aplikacije + Nastavitve + Novice & posodobitve + Zunanje povezave + Spletna stran + Priročnik + Pogosta vprašanja + + + Zaznava storitve ni uspela + Zbirke ni bilo mogoče osvežiti + + + Nastavitve + Razhroščevalnik + Prikaži informacije razhroščevalnika + Podrobno zapisovanje procesov + Zapisovanje je onemogočeno + Povezava + Varnost + Nezaupaj sistemskim cerfitikatom + Sistemski in od uporabnika dodani certifikati ne bodo zaupani + Sistemski in od uporabnika dodani certifikati bodo zaupani(priporočeno) + Ponastavi (ne)zaupane certifikate + Ponastavi zaupanje vse lastnih certifikatov + Vsi lastni certifikati so bili odstranjeni + Uporabniški vmesnik + Nastavitve opozoril + Uredi kanale opozoril in njihove nastavitve + Ponastavi namige + Ponovno omogoči namige, ki si bilo predhodno izključeni + Vsi namigi bodo ponovno prikazani + + CardDAV + CalDAV + Webcal + Sinhroniziraj zdaj + Nastavitve računa + Preimenuj račun + Preimenuj + Ima računa že obstaja + Izbriši račun + Ali res želite izbrisati račun? + Vse lokalne kopije imenika, koledarjev in seznamov opravil bodo izbrisane. + sinhroniziraj to zbirko + samo za branje + koledar + dnevnik + Pokaži samo osebno + Nobena Webcal sposobna aplikacija ni bila najdena + Namesti ICSx⁵ + + Dodaj račun + Prijava + Prijava z email naslovom + Email naslov + Potreben je veljaven email naslov + Geslo + Prijava z URL in uporabniškim imenom + Uporabniško ime + URL osnova + Izberi certifikat + Dodaj račun + Ime računa + Uporabi email naslov kot ime računa, ker bo Android uporabil to ime računa kot organizacijsko povelj za dogodke, ki jih ustvariš. Dveh računov z istim imenom ni mogoče imeti. + Metoda skupine kontaktov: + Zahtevano je ime računa + Ima računa že obstaja + Zaznava konfiguracije + Prosim počakajte, povezava s strežnikom je v teku... + CalDAV ali CardDAV storitve ni bilo mogoče najti. + + Sinhronizacija + Kontakti interval sinhronizacije + Samo ročno + Vsakih %d minut + takoj po lokalnih spremembah + Koladar interval sinhronizacije + Naloge interval sinhronizacij + + Samo ročno + Vsakih 15 minut + Vsakih 30 minut + Vsako uro + Vsaki 2 uri + Vsake 4 ure + Enkrat na dan + + Sinhronizacija samo preko Wifi + Sinhronizacije je omejena na Wifi omrežja + Tip povezave ni upoštevan + WiFI SSIF omejitev + Bo sinhroniziralo samo preko %s + Vse WiFi povezave bodo uporabljene + Z vejico ločena imena (SSID) dovoljenih WiFi omrežij (pusti prazno za vse) + Avtentikacija + Uporabniško ime + Posodobi geslo ustrezajoč strežniku. + CalDAV + Pretekli dogodek časovna omejitev + Vsi dogodki bodo sinhronizirani + + Dogodki starejši od enega dne v preteklosti bodo prezrti + Dogodki starejši od %d dni v preteklosti bodo prezrti + Dogodki starejši od %d dni v preteklosti bodo prezrti + Dogodki starejši od %d dni v preteklosti bodo prezrti + + Dogodki, ki so v preteklosti več kot ta številka dni bodo prezrti (lahko je 0). Pusti prazno za sinhronizacijo vseh dogodkov. + Uredi barve koledarjev + Podpora barva dogodka + CardDAV + Metoda skupine kontaktov + + Ustvari imenik + Ustvari koledar + Mogoči koledarski vnosi + Dogodki + Naloge + Beležnice / dnevnik + Barva + Naslov + Lokacija shrambe + Ustvari + + Izbriši zbirko + Sinhronizacija + Naslov + Opis + + Informacije razhroščevalnika + Kopiraj URL + + Zgodila se je napaka + Zgodila se je HTTP napaka. + I/O napaka se je zgodila. + Pokaži podrobnosti + + Avtentikacija + Uporabniško ime + Geslo + + DAVx⁵ dovoljenja + Dodatna dovoljenja so zahtevana + Avtentikacija ni uspela (preverite podatke prijave) + Omrežna ali I/O napaka -- %s + HTTP strežniška napaka -- %s + Napaka lokalne shrambe -- %s + S strežnika so bili prejeti neveljavni kontakti + S strežnika so bili prejeti neveljavni dogodki + S strežnika so bili prejeti neveljavni dogodki + Eden ali več neveljavnih virov bo ignoriranih + + + diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml new file mode 100644 index 0000000..9a8d510 --- /dev/null +++ b/app/src/main/res/values-sr/strings.xml @@ -0,0 +1,282 @@ + + + + Налог не постоји (више) + ДАВдроид адресар + Уклони + Поништи + Укључи + Ово поље је обавезно + Помоћ + Подели + База података је корумпирана + Сви налози су уклоњени локално. + Тражење грешака + Остале важне поруке + Статусне поруке ниског приоритета + Синхронизација + Грешке синхронизације + Важне грешке које заустављају синхронизацију попут неочекиваних одговора сервера + Упозорења синхронизације + Не критични проблеми са синхронизацијом попут одређених неисправних датотека + Мрежне и У/И грешке + Истекла времена, проблеми са повезивањем, итд. (често привремено) + + Ваши подаци. Ваш избор. + Преузмите контролу. + Регуларни интервали синхронизације + За синхронизацију у регуларним интервалима, %s мора бити дозвољено да се извршава у позадини. У супротном, Андроид може зауставити синхронизацију у било ком тренутку. + Не требају ми регуларни интервали синхронизације.* + %s компатибилност + Изменио сам потребна подешавања. Не подсећај ме више.* + * Остави непотврђено да би био подсетнут касније. Може бити ресетовано у подешавањима / %s. + Још информација + + Подршка за задатке + Ако су задаци подржани од стране вашег сервера, они могу бити синхронизовани са подржаном апликацијом: + OpenTasks + Изгледа да се више не развија - не препоручује се. + Tasks.org + Ниједна продавница апликација није доступна + Не треба ми подршка за задатке.* + Софтвер отвореног кода + Како допринети/донирати + Следеће + + Дозволе + %s захтева дозволе да би исправно радила. + Све испод + Користите ово да би сте омогућили све функционалности (пропоручено) + Све дозволе су дате + Дозволе за контакте + Без синхронизације контаката (није препоручено) + Могућа је синхронизација контакта + Дозволе за календар + Без синхронизације календара (није препоручено) + Могућа је синхронизација календара + Дозволе за обавештења + Обавештења су онемогућена (није препоручено) + Обавештења су омогућена + Дозволе за OpenTasks + Дозволе за задатке + Без синхронизације задатака + Могућа је синхронизација задатака + Задржи дозволе + Дозволе могу бити аутоматски поништене (није препоручено) + Дозволе неће бити аутоматски поништене + Изаберите Дозволе > искључите \"Уклони дозволе ако се апликација не користи\" + Ако опција не функционише, користите подешавања апликације / Дозволе. + Подешавања апликације + + WiFi SSID дозволе + Дозвола прецизне локације + Дата је дозвола за локацију + Одбијена је дозвола за локацију + Дозволе за локацију у позадини + Дозволи сво време + Локација је увек омогућена + Услуга локације је омогућена + Услуга локације је онемогућена + + Преводи + Библиотеке + Издање %1$s (%2$d) + Овај програм НЕМА НИКАКВЕ ГАРАНЦИЈЕ. Бесплатан је софтвер којег можете слободно да делите под одређеним условима. + + Није се могла направити датотека записа + Сада се записују све %s активности + Прегледај/подели + Онемогући + + КалДАВ/КардДАВ адаптер синхронизације + О програму/лиценца + Повратне информације бета издања + Молим вас инсталирајте прегледач интернета + Поставке + Новости и ажурирања + Алати + Вањске везе + Веб-сајт + Приручник + ЧПП + Заједница + Политика приватности + Синхронизуј све налоге + + Обавештења су онемогућена. Нећете бити обавештени о проблемима са синхронизацијом. + Управљајте складиштем + + Откривање услуге није успело + Не могох да освежим списак збирки + + На неким уређајима је ово неопходно за аутоматску синхронизацију. + + Поставке + Тражење грешака + Прикажи податке за исправљање грешака + Исцрпна евиденција + Оптимизација батерије + Повезивање + Врста проксија + + Системски предефинисан + Без проксија + HTTP + SOCKS (за Orbot) + + Назив прокси домаћина + Порт проксија + Безбедност + Дозволе апликације + Прегледај дозволе неопходне за синхронизацију + Посумњај у системске сертификате + Системски и кориснички додати сертификати неће бити поуздани + Системски и кориснички додати сертификати ће бити поуздани (препоручљиво) + Ресетуј (не)поуздане сертификате + Ресетуј поуздање свих прилагођених сертификата + Сви прилагођени сертификати су уклоњени + Корисничко сучеље + Подешавања обавештења + Управљај каналима обавештења и њиховим подешавањима + Изабери тему + + Према систему + Светла + Тамна + + Ресетуј савете + Поновно приказивање претходно одбачених савета + Сви савети ће поново бити приказани + Интеграција + Апликација за задатке + + КардДАВ + КалДАВ + Вебкал + Синхронизуј одмах + Поставке налога + Преименуј налог + Преименуј + Назив налога је већ заузет + Није било могуће преименовати налог + Обриши налог + Заиста обрисати налог? + Све локалне копије адресара, календара и листи задатака ће бити обрисане. + синхронизуј ову збирку + само-за-читање + календар + журнал + Прикажи само личне + Нема апликације за Вебкал + Инсталирај ICSx⁵ + + Додај налог + Пријава + Пријавите се адресом е-поште + Адреса е-поште + Исправна адреса е-поште је обавезна + Лозинка + Пријавите се УРЛ-ом и корисничким именом + Корисничко име + Корени УРЛ + Изабери сертификат + Додај налог + Назив налога + Користите вашу е-адресу за назив налога јер Андроид користи назив налога за поље ОРГАНИЗАТОР за догађаје које направите. Не можете имати два налога истог назива. + Режим група контаката: + Назив налога је обавезан + Назив налога је већ заузет + Сертификат није пронађен + Инсталирај сертификат + Гугл контакти / календар + Гугл налог + ИД клијента (опционо) + Откривање конфигурације + Сачекајте, шаљем упит серверу… + Не могох да нађем КалДАВ или КардДАВ услугу. + Прикажи записе + + Синхронизација + Интервал синх. контаката + Само ручно + Сваких %d минута + одмах по локалним изменама + Интервал синх. календара + Интервал синх. задатака + + Само ручно + Сваких 15 минута + Сваких 30 минута + Сваког сата + Свака 2 сата + Свака 4 сата + Једном дневно + + Само преко бежичног + Синхронизовање само преко бежичних мрежа + Тип везе није узет у обзир + Ограничења ССИД-а бежичних + Синхронизовање само преко %s + Коришћење свих бежичних мрежа + Имена (ССИД) дозвољених мрежа. одвојена зарезом (оставите празно за све мреже) + Управљај + Аутентификација + Корисничко име + Ажурирајте лозинку за ваш сервер. + Инсталирај сертификат + КалДАВ + Ограничење догађаја у прошлости + Сви догађаји се синхронизују + + Догађаји старији од једног дана ће бити занемарени + Догађаји старији од %d дана ће бити занемарени + Догађаји старији од %d дана ће бити занемарени + + Догађаји старији од овог броја дана ће бити занемарени (може бити 0). Оставите празно за синхронизацију свих догађаја. + Предефинисани подсетник + Управљај бојама календара + Подршка за боју догађаја + Боје догађаја су синхронизоване + Боје догађаја нису синхронизоване + КардДАВ + Режим група контаката + + Направи адресар + Направи календар + Догађаји + Задаци + Боја + Наслов + Локација складишта + Направи + + Обриши збирку + Синхронизација + Наслов + Опис + + Подаци за исправљање грешака + Прикажи детаље + Записи + Прикажи записе + Копирај УРЛ + + Десила се грешка. + Десила се ХТТП грешка. + Десила се У/И грешка. + Прикажи детаље + + Име за приказ + Аутентификација + Корисничко име + Лозинка + + ДАВдроид дозволе + Потребне су додатне доволе + Аутентификација није успела (проверите акредитиве за пријаву) + Мрежна или У/И грешка – %s + Грешка ХТТП сервера – %s + Грешка локалног складишта – %s + + Синхронизуј све налоге + + diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml new file mode 100644 index 0000000..d8d2dc7 --- /dev/null +++ b/app/src/main/res/values-sv/strings.xml @@ -0,0 +1,465 @@ + + + + Kontot finns inte (längre) + DAVx⁵ Adressbok + Ändra inte kontot här! Använd appen direkt för att hantera konton istället + Radera + Ta bort + Avbryt + Aktivera + Detta fält är obligatoriskt + Hjälp + Navigera upp + Inställningsmeny + Dela + Synkronisering startad/köad + Databasen är korrupt + Alla konton har blivit borttagna lokalt. + Felsökning + Andra viktiga meddelanden + Statusmeddelanden med låg prioritet + Synkronisering + Synkroniseringsfel + Viktiga fel som stoppar synkronisering, såsom oväntade serversvar + Synkroniseringsvarningar + Icke allvarliga synkroniseringsfel såsom vissa felaktiga filer + Nätverk och I/O fel + Tidsgräns eller anslutningsproblem etc. (ofta temporärt) + + Din data. Ditt val. + Ta kontroll + Regelbundna synkroniseringsintervall + För att programmet skall kunna köra regelbunden synkronisering %s måste det tillåtas att köra i bakgrunden. Annars kan Android pausa synkroniseringen när som helst. + Jag behöver inte regelbunden synkronisering.* + %s kompatibilitet + Jag har gjort de nödvändiga inställningarna. Påminn mig inte igen.* + * Lämna omarkerat för att bli påmind senare.. Kan återställas i appens inställningar / %s. + Mer information + jtx Bord + + Tasks stöd + Om ärenden stöds av din server kan de synkroniseras med en ärendeapp som stöds: + OpenTasks + Verkar inte utvecklas längre - rekommenderas ej. + Tasks.org + stöds inte.]]> + Ingen appbutik tillgänglig + Jag behöver inte stöd för tasks.* + Öppen källkod mjukvara + Vi är glada att du använder %s, som är mjukvara med öppen källkod. Utveckling, underhåll och support är hårt arbete. Överväg att bidra (det finns många sätt) eller en donation. Det skulle vara mycket uppskattat! + Hur man kan bidra/donera + Påminn mig inte på + + %d månad + %d månader + + Nästa + + Behörigheter + %s behöver behörighet för att kunna fungera + Allt nedanstående + Använd detta för att aktivera alla funktioner (rekommenderat) + Alla behörigheter godkända + kontaktbehörigheter + Synkronisera inte kontakter (ej rekommenderat) + Synkronisering av kontakter möjlig + Kalenderbehörigheter + Synkronisera inte kalendern (ej rekommenderat) + Synkronisering av kalender möjlig + Notifieringsbehörigheter + Notifieringar avaktiverade (ej rekommenderat) + Notifieringar aktiva + jtx Board-behörigheter + OpenTasks-behörigheter + Tasks behörighet + Ingen synkronisering av uppgifter + Uppgiftssynkronisering möjlig + Behåll behörigheter + Behörigheter kan återställas automatiskt (rekommenderas inte) + Behörigheterna återställs inte automatiskt + Klicka på Behörigheter > avmarkera \"Ta bort behörigheter om appen inte används\" + Om en brytare inte fungerar, använd app-inställningar->behörigheter + App-inställningar + + WiFi SSID-behörigheter + För att kunna komma åt det aktuella WiFi-namnet (SSID) måste dessa villkor vara uppfyllda: + Behörighet för exakt plats + Behörighet för platsdata beviljad + Behörighet för platsdata nekad + Behörighet för platsdata i bakgrunden + Tillåt hela tiden + Platsbehörighet satt till: %s + Platsbehörighet inte satt till: %s + %s använder platsdata (bara WiFi SSID) endast för att hindra synkronisering mot ett specifikt WiFi SSID. Detta händer även när synkroniseringen körs i bakgrunden. + All platsdata (bara WiFi SSID) används bara lokalt och skickas ingenstans. + Plats alltid påslagen + Platstjänster är påslagna + Platstjänster är avstängda + + Översättningar + Bibliotek + Version %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) och bidragsgivare + Detta program levereras med ABSOLUT INGEN GARANTI. Det är fri programvara, och du kan vidaredistribuera den under vissa förutsättningar. + + Kunde inte skapa loggfil + Loggar nu alla %s aktiviteter + Visa/dela + Inaktivera + + CalDAV/CardDAV Synk Adapter + Om / Licens + Betafeedback + Vänligen installera en webbläsare + Inställningar + Nyheter & uppdateringar + Verktyg + Externa länkar + Websida + Manual + FAQ + Gemenskap + Stöd projektet + Hur man kan bidra + Integritetspolicy + Välkommen till DAVx⁵! + Anslut till din server och håll dina kalendrar och kontakter synkroniserade + Synkronisera alla konton + + Notifieringar avstängda. Du kommer inte bli informerad om synkroniseringsfel. + Automatisk synkronisering ej aktiv (ingen verifierad internetanslutning) + Hantera anslutningar + Begränsad data-synkronisering aktiverad. Begränsad bakgrundssynkronisering. + Hantera begränsad data-synkronisering + Batterisparning aktiverad. Det kan hända att synkronisering begränsas. + Hantera batterisparare + Lagringsutrymmet är lågt. Android kommer inte synka lokala ändringar direkt, utan vänta till nästa regelbundna synkronisering. + Hantera lagring + Kalenderdistributör saknas + Har du stängt av \"kalenderlagrings\"-systemappen? + Kontaktdistributör saknas + Har du stängt av \"kontaktlagrings\"-systemappen + Hantera appar + + Servicedetektering misslyckades + Det gick inte att uppdatera samlingslistan + + Arbetar i förgrunden + På vissa enheter är detta nödvändigt för automatisk synkronisering. + + Inställningar + Felsökning + Visa felsökningsinformation + Visa/dela konfigurations detaljer och loggar + Omfattande loggning + Loggning är aktiv. Du kan visa loggarna som en del av felsökningsinfon. + Loggning är avstängd + Batterioptimering + Appen är undantagen (rekommenderas) + Batteribegränsningar tillämpas (rekommenderas inte) + Anslutning + Proxy typ + + Systemstandard + Ingen proxy + HTTP + SOCKS (för Orbot) + + Proxy hostnamn + Proxy port + Säkerhet + App behörigheter + Granska behörigheter som krävs för synkronisering + Misstro systemcertifikat + System- och användartillagda certifikatutfärdare kommer inte att vara betrodda + System- och användartillagda certifikatutfärdare kommer att vara betrodda (rekommenderat) + Om den här inställningen är aktiv anses inte system certifikaten vara tillförlitliga. Det betyder att du måste acceptera alla certifikat manuellt (även när servern förnyar sitt certifikat) annars fungerar inte konto inställningar och synkronisering. + Återställ (o)betrodda certifikat + Återställer förtroendet för alla anpassade certifikat + Alla anpassade certifikat har rensats + Användargränssnitt + Aviseringsinställningar + Hantera aviseringskanaler och deras inställningar + Välj tema + + Systemstandard + Ljus + Mörk + + Återställ tips + Återaktiverar tips som har avvisats tidigare + Alla tips kommer visas igen + Integrering + Tasks appen + Ingen kompatibel task-app hittades + UnifiedPush (experimentellt) + Ingen (inaktivera push) + Välj en leverantör + Ingen push leverantör installerad + Ingen slutpunkt konfigurerad + Redo att ta emot push meddelanden över %s + Push-meddelanden är alltid krypterade. + + CardDAV + CalDAV + Webcal + Ytterligare behörigheter krävs för att synkronisera dessa samlingar. + Hantera behörigheter + Synkronisera nu + Kontoinställningar + Byt namn på kontot + Lokala data som inte har sparats kan avvisas. Omsynkronisering krävs efter byte av namn. + Nytt kontonamn + Byt namn + Kontonamn är upptaget + Kunde inte ändra namn på kontot + Ta bort kontot + Vill du verkligen ta bort kontot? + Alla lokala kopior av adressböcker, kalendrar och uppgiftslistor kommer att raderas. + synkronisera denna samling + skrivskyddad + kalender + kontakter + Journal + uppgifter + Visa endast personligt + Uppdatera lista + Webcal-prenumerationer kan synkroniseras med externa appar. + Ingen Webcal-kompatibel app hittades + Installera ICSx⁵ + + Lägg till konto + sekretesspolicy.]]> + Generisk inloggning + Leverantörs-specifik inloggning + Fortsätt + Logga in + Logga in med e-postadress + E-postadress + Giltig e-postadress krävs + Tjänster är upptäckta genom DNS-uppslag och välkända URL:er.]]> + Lösenord + Dölj lösenord + Visa lösenord + Logga in med URL och användarnamn + Användarnamn + Bas-URL + tjänster upptäcks även genom DNS uppslag och välkända URL:er.]]> + Välj certifikat + Lägg till konto + Kontonamn + Användning av apostrof (\') verkar orsaka problem på vissa enheter. + Använd din e-postadress som kontonamn eftersom Android kommer att använda kontonamnet som fält för ARRANGÖR för händelser du skapar. Du kan inte ha två konton med samma namn. + Kontaktgruppsmetod: + Konto namn krävs + Kontonamn är upptaget + Konto kunde inte läggas till + Klart + Avancerad inloggning + Klientcertifikat: %s + Inget certifikat funnet + Installera certifikat + Fastmail + Fastmail konto + Logga in med Fastmail + Google Kontakter / Kalender + Google-konto + Logga in med Google + Klient-ID (valfritt) + Integritetspolicy för mer detaljer.]]> + Google API Services User Data Policy, inklusive kraven för \"Limited Use\".]]> + Kunde inte hämta autentiseringskod + Nextcloud + Logga in med Nextcloud + Detta påbörjar en Nextcloud-inloggning i din webbläsare. + Nextcloud serveradress + Logga in + Kunde inte hämta login-URL + Kunde inte hämta login-data + Konfigurationsdetektering + Vänligen vänta, server förfrågan... + Det gick inte att hitta CalDAV eller CardDAV-tjänsten. + Bas-URL:en verkar inte vara en åtkomlig CalDAV-/CardDAV-URL och upptäckt av tjänster misslyckades. + vår lista av testade tjänster och deras bas URL:er.]]> + Vänligen dubbelkolla också autentisering (vanligtvis användarnamn och lösenord) + Ytterligare teknisk information finns i loggarna. + Visa loggar + + Synkronisering + Intervall för kontaktsynkronisering + Bara manuellt + Var %d minut + omedelbart på lokala förändringar + Intervall för kalendersynkronisering + Intervall för uppgiftssynkronisering + + Endast manuellt + Var 15:e minut + Var 30:e minut + Varje timme + Var 2:e timme + Var 4:e timme + En gång om dagen + + Synkronisera endast via WiFi + Synkronisering är begränsad till WiFi-anslutningar + Anslutningstyp beaktas inte + WiFi SSID-begränsning + Synkroniserar endast över %s + Alla WiFi-anslutningar kommer att användas + Kommaseparerade namn (SSID) för tillåtna WiFi-nätverk (lämna tomt för alla) + WiFi SSID-begränsning kräver ytterligare inställningar + Hantera + VPN kräver bakomliggande internetanslutning. + VPN utan bakomliggande bekräftad internetanslutning är inte tillräckligt för att köra synkronisering (rekommenderat) + VPN utan bakomliggande bekräftad internetanslutning är tillräckligt för att köra synkronisering + Autentisering + Användarnamn + Lösenord eller app-lösenord + app lösenord.]]> + Nytt lösenord + Uppdatera lösenordet enligt din server. + Auktorisera igen (OAuth) + Använd när åtkomst har återkallats + Auktorisering framgångsrik + Klientcertifikat + Inget certifikat tillgängligt eller valt + Installera certifikat + CalDAV + Tidsgräns för tidigare händelser + Alla händelser kommer att synkroniseras + + Händelser mer än en dag i det förflutna kommer att ignoreras + Händelser mer än %d dag i det förflutna kommer att ignoreras + + Händelser som är fler än detta antal dagar i det förflutna kommer att ignoreras (kan vara 0). Lämna tomt för att synkronisera alla händelser. + Standardpåminnelse + + Standard påminnelse en minut före händelsen + Standard påminnelse %d minut före händelsen + + Inga standardpåminnelser är skapade + Om standardpåminnelser ska skapas för händelser utan påminnelse: önskat antal minuter före händelsen. Lämna tomt för att inaktivera standardpåminnelser. + Hantera kalenderfärger + Kalenderfärger nollställs vid varje synkronisering + Kalenderfärger kan sättas av andra appar. + Stöd för händelsefärger + Händelsefärger synkroniseras + Händelsefärger synkroniseras inte + CardDAV + Kontaktgruppsmetod + + Grupper är separata vCards + Grupper är per-kontaktkategorier + + + Skapa adressbok + Att skapa adressbok över CardDAV kanske inte stöds av servern. + Skapa kalender + + Möjliga kalenderposter + Händelser + Tasks + Anteckningar / journal + Att skapa kalender över CalDAV kanske inte stöds av servern. + Färg + Titel + Lagringsplats + Skapa + + kontakter + händelser + uppgifter + Ta bort samling + Denna samling (%s) och all dess data kommer att tas bort permanent, både lokalt och på servern. + Synkronisering + Synkronisering aktiverad + Synkronisering inaktiverad + Skrivskyddad + Skrivskyddad (av server) + Skrivskyddad (enligt policy) + Skrivskyddad (endast lokalt) + Läs/skriv + Titel + Beskrivning + Ägare + Push-stöd + Servern annonserar push-stöd + Prenumerat på %1$s, går ut %2$s + Senaste synk (%s) + Adress (URL) + + Felsökningsinformation + ZIP arkiv + Innehåller felsökningsinformation och loggar + Dela arkivet för att överföra det till en dator, för att skicka det via e-post eller för att bifoga det till ett supportärende. + Dela arkiv + Felsökningsinformation bifogad i det här meddelandet (kräver stöd för bilagor från den mottagande appen). + HTTP-fel + Server-fel + WebDAV-fel + I/O-fel + Visa detaljer + Felsökningsinformation har samlats in + Inblandade resurser + Relaterat till problemet + Fjärrresurs: + Lokal resurs: + Loggar + Utförliga loggar finns tillgängliga + Visa loggar + Kopiera URL + Integritetspolicy + Loggar och felsökningsinformation kan innehålla privat information. Var medveten om detta när du delar offentligt. + + Ett fel har uppstått. + Ett HTTP-fel har uppstått. + Ett I/O-fel har uppstått. + Visa detaljer + + WebDAV-fästen + Använd kvot: %1$s / tillgängligt: %2$s + Dela innehåll + Avmontera + Lägg till WebDAV-fäste + Direkt åtkomst till dina filer i molnet genom att lägga till en WebDAV montering! + hur WebDAV-fästen fungerar .]]> + Visningsnamn + WebDAV URL + Felaktig URL + Autentisering + Användarnamn + Lösenord + Lägg till fäste + Ingen WebDAV-tjänst på denna URL + Ta bort monteringspunkt + Anslutningsdetealjer kommer att gå förlorade men inga filer tas bort. + Åtkomst till WebDAV-fil + Laddar ner WebDAV-fil + Laddar upp WebDAV-fil + WebDAV-fäste + + DAVx⁵-behörigheter + Ytterligare behörigheter krävs + %s för gammal + Lägsta obligatoriska version: %1$s + Autentisering misslyckades (kontrollera inloggningsuppgifterna) + Nätverks- eller I/O-fel - %s + HTTP server fel - %s + Lokalt lagringsfel - %s + Mjukt fel (max antal återanslutningar nådda) + Fick ogiltig kontakt från servern + Fick ogiltig händelse från servern + Fick ogiltigt ärende från servern + Ignorerar en eller flera ogiltiga resurser + Synk väntar + Fjärrdata har ändrats + + Synkronisera alla + Synkronisera alla konton + Märkt synk knapp + Ikon synk knapp + Tryck för att köra synkronisering manuellt. + + diff --git a/app/src/main/res/values-szl/strings.xml b/app/src/main/res/values-szl/strings.xml new file mode 100644 index 0000000..c6e40e5 --- /dev/null +++ b/app/src/main/res/values-szl/strings.xml @@ -0,0 +1,207 @@ + + + + Ksiōnżka adresowo DAVx⁵ + Włōncz + Pōmoc + Debugowanie + Inksze ważne wiadōmości + Synchrōnizacyjo + Błyndy synchrōnizacyje + Ważne błyndy, co zastawiajōm synchrōnizacyjo, jak niyôczekowane ôdpowiedzi serwera + Ôstrzeżynia synchrōnizacyje + Niyôstudne problymy synchrōnizacyje, jak niykere niynoleżne zbiory + Błyndy necu i wchodu/wychodu + Braki ôdpowiedzi, problymy połōnczynio, itp. (z wiynksza tymczasowe) + + Twoje dane. Twōj wybōr. + Przejmij kōntrola. + Regularne interwały synchrōnizacyje + Żeby regularnie synchrōnizować, %s musi mieć zwolo na fungowanie na zadku. W inkszym przipadku Android może w kożdyj chwili zastawić synchrōnizacyjo. + Niy potrzebuja regularnyj synchrōnizacyje.* + Zgodność %s + Już mōm wymogane sztelōnki. Niy spōminej mi wiyncyj.* + * Ôstow niyzaznaczōne, żeby spōmniało ô sobie niyskorzij. Idzie to zmiynić we sztelōnkach aplikacyje / %s + Wiyncyj informacyji + Sparcie Zadań + Niy je dostympny żodyn sklep ze aplikacyjami + Niy potrzebuja sparcio zadań.* + Ôprogramowanie Open-Source + Sōm my radzi, że używosz %s, ôprogramowanie open-source. Tworzynie, utrzimanie i sparcie to ciynżko robota. Pōmyśl nad pōmocōm (sōm rozmajte spusoby) abo dowkōm. Fest by nos to ucieszyło! + Jak pōmōc/dociepnōńć sie + + + + Przekłady + Bibliotyki + Wersyjo %1$s (%2$d) + Tyn program przichodzi BEZ ŻODNYJ GWARANCYJE. To je wolne ôprogramowanie i możesz je rozkludzać pod ôkryślōnymi warōnkami. + + Niy szło stworzić zbioru dziynnika + + Adapter synchrōnizacyje CalDAV/CardDAV + Ô DAVx⁵ / Licyncyjo + Przekoż ôpinijo + Zainstaluj przeglōndarka internetowo + Sztelōnki + Nowości i aktualizacyje + Zewnyntrzne linki + Strōna WWW + Ryncznie + Pytania i ôdpowiedzi + Polityka prywatności + + + Niy szło ôdświyżyć serwisu + Niy szło ôdświyżyć listy kolekcyje + + + Sztelōnki + Debugowanie + Pokoż informacyje do debugowanio + Rozwlykłe zapisowanie + Zapisowanie je zastawiōne + Łōnczność + Bezpieczyństwo + Skasuj certyfikaty systymowe + CA systymowe i używocza niy bydōm przidane + CA systymowe i używocza bydōm przidane (rekōmyndowane) + Zresetuj (niy)zaufane certyfikaty + Zresetuj wszyjske niysztandardowe certyfikaty. + Wszyjske niysztandardowe certyfikaty były wysnożōne + Interfejs używocza + Sztelōnki powiadōmiyń + Zarzōndzej kanałami powiadōmiyń i jejich sztelōnkami + Zresetuj podpowiedzi + Włōncz zaś skazōwki, co wcześnij były wychrōniōne + Wszyjske skazōwki pokożōm sie zaś + + CardDAV + CalDAV + Webcal + Synchrōnizuj teroz + Sztelōnki kōnta + Przemianuj kōnto + Przemianuj + Miano kōnta je już zajynte + Skasuj kōnto + Naprowda chcesz skasować kōnto? + Wszyjske lokalne kopije ksiōnżek adresowych, kalyndorzōw i list zadań bydōm skasowane. + Synchrōnizuj kolekcyjo + ino do ôdczytu + kalyndorz + Niy szło znojś aplikacyje, co ôbsuguje Webcal + Zainstaluj ICSx⁵ + + Przidej kōnto + Wloguj + Logowanie ze pōmocōm adresy e-mail + Adresa e-mail + Wymogano noleżno adresa e-mail + Hasło + Logowanie ze pōmocōm adresy URL i miana używocza + Miano używocza + Bazowy URL + Ôbier certyfikat + Przidej kōnto + Miano kōnta + Użyj swojij adresy e-mail za miano kōnta, bo Android bydzie używoł miana kōnta za pola ÔRGANIZATŌR dlo zdarzyń, co je stworzisz. Niy możesz posiadać dwōch kōnt ze takim samym mianym. + Spusōb grupowanio kōntaktōw: + Wymogane miano kōnta + Miano kōnta je już zajynte + Wykrywanie kōnfiguracyje + Czekej, ôdpytowanie serwera… + Niy idzie znojść usugi CalDAV abo CardDAV. + + Synchrōnizacyjo + Frekwyncyjo synchrōnizacyje kōntaktōw + Ino ryncznie + Co %d minut jak tyż zaroz przi zmianach lokalnych + Frekwyncyjo synchrōnizacyje kalyndorzōw + Frekwyncyjo synchrōnizacyje list zadań + + Ino ryncznie + Co 15 minut + Co 30 minut + Co godzina + Co 2 godziny + Co 4 godziny + Roz dziynnie + + Synchrōnizuj ino bez WiFi + Synchrōnizacyjo je ôgraniczōno do połōnczyń WiFi + Zorta połōnczynio niy mo znaczynio + Ôgraniczynia WiFi SSID + Bydzie synchrōnizować ino bez %s + Wszyjske połōnczynia WiFi bydōm używane + Ôddzielōne kōmami miana (SSID) przizwolōnych necōw WiFi (ôstow prōzne dlo wszyjskich) + Autoryzowanie + Miano używocza + Zaktualizuj hasło zgodnie ze serwerym. + CalDAV + Limit czasowy przeszłych zdarzyń + Wszyjske zdarzynia bydōm zsynchrōnizowane + + Zdarzynia starsze aniżeli jedyn dziyń bydōm zignorowane. + Zdarzynia starsze aniżeli %d dni bydōm zignorowane. + Zdarzynia starsze aniżeli %d dni bydōm zignorowane. + + Zdarzynia, co sōm starsze aniżeli podano wielość dni, bydōm zignorowane (może być 0). Ôstow prōzne, coby synchrōnizować wszyjske zdarzynia. + Wychodne spōmniynie + + Wychodne spōmniynie minuta przed zdarzyniym + Wychodne spōmniynie %d minuty przed zdarzyniym + Wychodne spōmniynie %d minut przed zdarzyniym + + Niy były stworzōne żodne wychodne spōmniynia + Jeźli wychodne spōmniynia bydōm tworzōne do zdarzyń bez spōmniynio: liczba minut przed zdarzyniym. Ôstow prōzne, żeby zastawić wychodne spōmniynia. + Zarzōndzej farbami kalyndorza + Sparcie farbōw zdarzyń + CardDAV + Spusōb grupy kōntaktōw + + Grupy to sōm ôddzielne VCards + Grupy to sōm kategoryje co kōntakt + + + Stwōrz ksiōnżka adresowo + Stwōrz kalyndorz + Możliwe wpisy kalyndorza + Zdarzynia + Zadania + Zopiski / dziynnik + Farba + Tytuł + Położynie przechowowanio + Stwōrz + + Skasuj kolekcyjo + Synchrōnizacyjo + Tytuł + Ôpis + + Informacyje debugowe + Skopiuj URL + + Trefiōł sie błōnd. + Trefiōł sie błōnd HTTP. + Trefiōł sie błōnd I/O. + Pokoż informacyje + + Autoryzowanie + Miano używocza + Hasło + + Uprawniynia DAVx⁵ + Wymogane ekstra uprawniynia + Autoryzacyjo sie niy podarziła (dej pozōr na dane logowanio) + Feler necu abo I/O – %s + Feler serwera HTTP – %s + Feler lokalnego przechowowanio – %s + Dostany kōntakt ze serwera je niynoleżny + Dostane zdarzynie ze serwera je niynoleżne + Dostane zadanie ze serwera je niynoleżne + Ignorowanie jednego abo wiyncyj niynoleżnych zasobōw + + + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml new file mode 100644 index 0000000..64ce385 --- /dev/null +++ b/app/src/main/res/values-tr/strings.xml @@ -0,0 +1,113 @@ + + + + Yardım + Hata ayıklama + Senkronizasyon + + Daha fazla bilgi + Sonraki + + Kişiler izinleri + Takvim izinleri + OpenTasks izinleri + + + Bu uygulama HİÇ BİR GARANTİ ile gelmemektedir. Bedava bir yazılımdır ve belli koşullar altında dağıtabilirsiniz. + + + CalDAV/CardDAV Senkronizasyon Adaptörü + Hakkında / Lisans + Ayarlar + Haberler & güncellemeler + Harici bağlantılar + Web sitesi + SSS + + + Servis keşfi başarısız + Kolleksiyon listesi yenilenemedi + + + Ayarlar + Hata ayıklama + Hata ayıklama bilgilerini göster + Uzun jurnalleme + Güvenlik + Kullanıcı arayüzü + İpuçlarını sıfırla + Daha önceden azat edilen ipuçlarını yeniden etkinleştirir + Tüm ipuçları artık gösterilecek + + CalDAV + Şimdi senkronize et + Hesap ayarları + Hesabı sil + Hesap gerçekten silinsin mi? + Rehber, takvim ve iş listelerinin tüm yerel kopyaları silinecektir. + salt-okunur + + Hesap ekle + Giriş + Eposta adresi ile giriş yap + Eposta adresi + Geçerli eposta adresi zorunludur + Parola + URL ve kullanıcı adı ile giriş yap + Kullanıcı adı + Baz URL + Hesap ekle + Hesap adı + Hesap ismi olarak e-posta adresini kullan çünkü Android hesap ismini yarattığın olaylarda DÜZENLEYEN alanında kullanacaktır. Aynı isimde iki faklı hesabın olamaz. + Hesap adı zorunludur + Konfigürasyon keşfi + Lütfen bekle, sunucu sorgulanıyor… + CalDAV veya CardDAV servisi bulunamadı. + Jurnallere bak + + Senkronizasyon + Kişiler senk. aralığı + Sadece elle + Her %d dakika + yerel değişikliklerde hemen + Takvimler senk. aralığı + İşler senk. aralığı + Sadece WiFi üzerinden senkronize et + Senkronizasyon WiFi bağlantıları ile kısıtlıdır + Bağlantı tipi göz önünde bulundurulmaz + WiFi SSID kısıtlaması + Doğrulama + Kullanıcı adı + Parola + Parolayı sunucunuza göre güncelleyin. + CalDAV + Geçmiş olay zaman sınırı + Tüm olaylar senkronize edilecek + + %d günden daha eski olaylar göz ardı edilecektir + %d günden daha eski olaylar göz ardı edilecektir + + Bu sayıdan daha eski olan olaylar yok sayılacaktır (0 olabilir). Tüm olayları senkronize etmek için boş bırak. + Takvim renklerini yönet + + Rehber yarat + Yarat + + Koleksiyonu sil + Senkronizasyon + + Hata ayıklama bilgisi + Jurnallere bak + + Bir hata oluştu. + Bir HTTP hatası oluştu. + Bir I/O hatası oluştu. + Detayları göster + + Kullanıcı adı + Parola + + DAVx⁵ izinleri + Ek izinler zorunludur + + + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml new file mode 100644 index 0000000..44083f8 --- /dev/null +++ b/app/src/main/res/values-uk/strings.xml @@ -0,0 +1,263 @@ + + + + Обліківки не існує (більше) + Адресна книга DAVx⁵ + Видалити + Видалити + Скасувати + Увімкнути + Це поле обовʼязкове + Допомога + Поширити + База даних пошкоджена + Усі облікові записи видалено локально. + Зневадження + Інші важливі повідомлення + Повідомлення з низьким пріоритетом + Синхронізація + Помилки синхронізації + Важливі помилки, що заважають синхронізації, наприклад несподівані + Попередження синхронізації + Не критичні проблеми синхронізації, наприклад деякі файли хибні + Помилка мережі та вводу/виводу + Спливання часу відклику, проблеми зв\'язку, і т.п. (часто тимчасові) + + Ваша дата. Ваш вибір. + Візьміть під свій контроль. + Інтервали регулярної синхронізації + Мені не потрібні інтервали регулярної синхронізації.* + Детальніше + jtx Board + Підтримка завдань + OpenTasks + Мені не потрібна підтримка завдань.* + ПЗ з відкритим кодом + Як співпрацювати/пожертвувати + Далі + + Дозволи + Використовуйте це, щоб дозволити всі можливості (рекомендується) + Усі дозволи надано + Дозволи контактів + Дозволи календаря + Без синхронізації календаря (не рекомендується) + Можлива синхронізація календаря + Дозволи jtx Board + Дозволи OpenTasks + Дозволи завдань + Можлива синхронізація завдань + Налаштування застосунку + + Дозволи SSID WiFi + + Переклади + Бібліотеки + Версія %1$s (%2$d) + Цей програмний засіб постачається АБСОЛЮТНО БЕЗ БУДЬ-ЯКИХ ГАРАНТІЙ. Це вільне програмне забезпечення, і ви можете поширювати її, за деякими умовами. + + Не вдалося створити файл звіту + Вимкнути + + Адаптер синхронізації CalDAV/CardDAV + Про / Ліцензія + Beta відгук + Будь ласка, встановіть веб-браузер + Налаштування + Новини та оновлення + Зовнішні посилання + Веб сайт + Посібник + Питання/Відповіді + Політика конфіденційності + Синхронізувати всі обліківки + + + Не вдалося виявити сервіси + Не вдалося оновити перелік колекції + + + Налаштування + Зневадження + Показати інформацію зневадження + Детальний журнал + Звітування призупинено + З\'єднання + Безпека + Дозволи застосунку + Не довіряти системним сертифікатам + Не довіряти системним та доданим користувачем сертифікатам + Довіряти системним та доданим користувачем сертифікатам (рекомендується) + Скидання (не)довірених сертифікатів + Скинути довіру до всіх призначених користувачу сертифікатів + Всі сертифікати, що призначені користувачу очищено + Інтерфейс користувача + Налаштування сповіщення + Керування каналами інформування та їх налаштуванням + Вибрати тему + + Системна + Світла + Темна + + Скинути підказки + Включення підказок, які раніше були вимкнуті + Всі підказки будуть показані знову + Застосунок завдань + Не знайдено сумісного застосунку завдань + + CardDAV + CalDAV + Webcal + Синхронізувати зараз + Налаштування облікового запису + Перейменувати обліковий запис + Перейменувати + Ім\'я запису вже зайняте + Не вдалося перейменувати облікувку + Видалити запис + Дійсно видалити обліковий запис? + Всі локальні копії адресних книг, календарів та завдань будуть вилучені. + синхронізувати дану колекцію + лише читання + календар + контакти + журнал + завдання + Не знайдено додатку з підтримкою Webcal + Встановити ICSx⁵ + + Додати запис + Увійти + Увійти за допомогою електронної пошти + Адреса пошти + Потребує валідну електронну адресу + Пароль + Приховати пароль + Показати пароль + Увійти за допомогою URL та імені користувача + Ім\'я користувача + Ім\'я користувача (необов\'язково) + Базовий URL + Обрати сертифікат + Додати запис + Назва запису + Використовуйте вашу електронну адресу як ім\'я облікового запису, так як Android буде використовувати ім\'я облікового запису в полі ORGANIZER для подій, які ви створюватимете. Ви не можете мати два облікових записи з однаковими іменами. + Метод групування контактів: + Потребує назви облікового запису + Ім\'я запису вже зайняте + Не знайдено сертифікат + Встановити сертифікат + Nextcloud + Виявлення конфігурації + Будь ласка, зачекайте, запит до серверу… + Не вдалося знайти CalDAV чи CardDAV сервіс. + + Синхронізація + Інтервал синхронізації контактів + Лише вручну + Кожних %d хвилин, а також негайно при внесенні локальних змін + Інтервал синхронізації календарів + Інтервал синхронізації завдань + + Вручну + Кожні 15 хвилин + Кожні 30 хвилин + Щогодинно + Кожні 2 години + Кожні 4 години + Щоденно + + Синхронізувати лише через Wi-Fi + Виконувати синхронізацію лише через Wi-Fi + Не враховувати тип з\'єднання + Обмеження WiFi SSID + Синхронізувати лише через %s + Може використовуватись всі Wi-Fi з\'єднання + Назви (SSID) дозволених Wi-Fi мереж, розділені комами (залиште порожнім для всіх) + Керувати + Автентифікація + Ім\'я користувача + Новий пароль + Оновити пароль, згідно налаштувань Вашого сервера. + Встановити сертифікат + CalDAV + Інтервал синхронізації + Всі події будуть синхронізовані + + Події старші одного дня будуть проігноровані + Події старші %d днів будуть проігноровані + Події старші %d днів будуть проігноровані + Події старші %d днів будуть проігноровані + + Події старші вказаного часу будуть проігноровані (може бути 0). Залиште порожнім, аби синхронізувати всі події. + Нагадування за замовчуванням + + Типово нагадувати за хвилину до події + Типово нагадувати за %d хвилини до події + Типово нагадувати за %d хвилин до події + Типово нагадувати за %d хвилин до події + + Нагадування за замовчуванням не створюються + Якщо нагадування за замовчуванням створюються для подій без нагадування: бажана кількість хвилин до події. Залиште поле порожнім, щоб вимкнути нагадування за замовчуванням. + Керування кольорами + Підтримка кольорів подій + CardDAV + Метод групування контактів + + Групи-це окремі записи + Групи є в категоріями в контактах + + + Створити адресну книгу + Створити календар + Можливі записи календаря + Події + Завдання + Нотатки/Журнал + Колір + Заголовок + Розташування сховища + Опис (необов\'язково) + Створити + + контакти + завдання + Видалити колекцію + Синхронізація + Заголовок + Опис + + Інформація зневадження + Архів ZIP + Помилка HTTP + Помилка сервера + Помилка WebDAV + Помилка I/O + Переглянути деталі + Скопіювати URL + + Трапилась помилка. + Трапилась помилка HTTP. + Трапилась помилка I/O. + Показати подробиці + + Автентифікація + Ім\'я користувача + Пароль + Ім\'я користувача (необов\'язково) + + Дозволи DAVx⁵ + Потребує додаткові дозволи + Помилка аутентифікації (перевірте обліковий запис) + Помилка мережі та вводу/виводу — %s + Помилка сервера HTTP — %s + Помилка локального сховища — %s + Отримано помилковий контакт від сервера + Отримано помилкову подію від сервера + Отримано помилкове завдання від сервера + Ігнорування одного або більше хибних джерел + + Синхронізувати всі обліківки + + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000..e670769 --- /dev/null +++ b/app/src/main/res/values-vi/strings.xml @@ -0,0 +1,328 @@ + + + + Tài khoản không tồn tại (nữa) + Sổ địa chỉ của DAVx⁵ + Xóa + Hủy + Bật + Trường này là bắt buộc + Trợ giúp + Chia sẻ + Cơ sở dữ liệu đã bị lỗi + Tất cả tài khoản đã bị xóa ở cục bộ. + Gỡ lỗi + Thông báo quan trọng khác + Thông báo trạng thái ưu tiên thấp + Đồng bộ hoá + Lỗi đồng bộ hoá + Các lỗi quan trọng mà chúng dừng việc đồng bộ hoá như các câu trả lời không mong đợi của máy chủ + Cảnh báo đồng bộ hoá + Các vấn đề đồng bộ hoá không nghiêm trọng như các tệp không hợp lệ cụ thể + Lỗi mạng và I/O + Hết thời gian chờ, vấn đề kết nối, v.v. (thường là tạm thời) + + Dữ liệu của bạn. Lựa chọn của bạn. + Giành quyền kiểm soát. + Khoảng thời gian thông thường giữa mỗi lần đồng bộ + Để đồng bộ hoá tại những khoảng thời gian thông thường, %s phải được cho phép chạy trong nền. Nếu không, Android có thể sẽ tạm dừng đồng bộ hoá bất cứ lúc nào. + Tôi không cần khoảng thời gian đồng bộ thông thường.* + Sự tương thích với %s + Tôi đã làm xong các cài đặt được yêu cầu. Đừng nhắc lại tôi nữa.* + * Không đánh dấu để được nhắc lại sau. Có thể được đặt lại trong cài đặt ứng dụng / %s. + Thêm thông tin + jtx Board + + Hỗ trợ công việc + Nếu máy chủ của bạn hỗ trợ công việc, chúng có thể được đồng bộ hoá bằng một ứng dụng công việc được hỗ trợ: + OpenTasks + Không có cửa hàng ứng dụng nào có sẵn + Tôi không cần hỗ trợ công việc.* + Phần mềm mã nguồn mở + Chúng tôi rất vui khi thấy bạn sử dụng %s, nó là phần mềm mã nguồn mở. Việc phát triển, duy trì và hỗ trợ là những công việc rất nặng nhọc. Vui lòng cân nhắc việc đóng góp (có nhiều cách) hoặc quyên góp. Chúng tôi sẽ rất cảm kích! + Cách đóng góp/quyên góp + + Quyền + %s yêu cầu các quyền để hoạt động đúng. + Tất cả quyền ở dưới + Sử dụng tuỳ chọn này để bật tất cả tính năng (được khuyến nghị) + Đã cấp tất cả quyền + Quyền danh bạ + Không đồng bộ danh bạ (không được khuyến nghị) + Có thể đồng bộ danh bạ + Quyền lịch + Không đồng bộ lịch (không được khuyến nghị) + Có thể đồng bộ lịch + Quyền của jtx Board + Quyền OpenTasks + Quyền Tasks + Không đồng bộ công việc + Có thể đồng bộ công việc + Giữ các quyền + Các quyền có thể sẽ bị tự động đặt lại (không được khuyến nghị) + Các quyền sẽ không bị tự động đặt lại + Nhấn Quyền > bỏ chọn \"Thu hồi quyền nếu chưa dùng ứng dụng\" + Nếu công tắc không hoạt động, hãy sử dụng cài đặt ứng dụng / Quyền. + Cài đặt ứng dụng + + Quyền WiFi SSID + Để có thể truy cập tên mạng WiFi hiện tại (SSID), những điều kiện này phải được đáp ứng: + Quyền vị trí chính xác + Đã cấp quyền vị trí + Đã từ chối quyền vị trí + Quyền vị trí trong nền + Luôn cho phép + Vị trí luôn được bật + Dịch vụ vị trí đã bật + Dịch vụ vị trí đã tắt + + Bản dịch + Thư viện + Phiên bản %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) và những người đóng góp + Chương trình này KHÔNG CÓ SỰ ĐẢM BẢO NÀO. Nó là phần mềm tự do, và bạn có thể tuỳ ý phân phối lại nó dưới các điều kiện cụ thể. + + Không thể tạo tệp nhật ký + Bây giờ đang ghi lại tất cả %s hoạt động + Xem/chia sẻ + Tắt + + Đồng bộ CalDAV/CardDAV + Giới thiệu / Giấy phép + Phản hồi về bản beta + Vui lòng cài đặt một trình duyệt web + Cài đặt + Tin tức & cập nhật + Công cụ + Liên kết ngoài + Trang web + Hướng dẫn + Câu hỏi thường gặp + Cộng đồng + Chính sách riêng tư + Đồng bộ tất cả tài khoản + + + Dò tìm dịch vụ thất bại + Không thể làm mới danh sách bộ sưu tập + + Đang chạy ở trước + Trên một số thiết bị, điều này là cần thiết để đồng bộ hoá tự động. + + Cài đặt + Gỡ lỗi + Hiện thông tin gỡ lỗi + Ghi nhật ký chi tiết + Ghi nhật ký đã tắt + Tối ưu hoá pin + Kết nối + Loại proxy + + Mặc định hệ thống + Không có proxy + HTTP + SOCKS (cho Orbot) + + Tên máy chủ proxy + Cổng proxy + Bảo mật + Quyền ứng dụng + Xem xét các quyền được yêu cầu để đồng bộ hoá + Bỏ tin tưởng các chứng chỉ hệ thống + Các chứng chỉ CA của hệ thống và được người dùng thêm sẽ không được tin tưởng + Các chứng chỉ CA của hệ thống và được người dùng thêm sẽ được tin tưởng (được khuyến nghị) + Đặt lại các chứng chỉ đã (bỏ) tin tưởng + Đặt lại sự tin tưởng của tất cả chứng chỉ tuỳ chỉnh + Đã xoá tất cả chứng chỉ tuỳ chỉnh + Giao diện người dùng + Cài đặt thông báo + Quản lý các kênh thông báo và cài đặt của chúng + Chọn chủ đề + + Mặc định hệ thống + Sáng + Tối + + Đặt lại các gợi ý + Bật lại các gợi ý đã bị bỏ qua trước đó + Tất cả gợi ý sẽ được hiện lại + Tích hợp + Ứng dụng công việc + Không tìm thấy ứng dụng công việc tương thích + + CardDAV + CalDAV + Webcal + Đồng bộ hoá ngay + Cài đặt tài khoản + Đổi tên tài khoản + Đổi tên + Tên tài khoản đã được sử dụng + Không thể đổi tên tài khoản + Xoá tài khoản + Thực sự xoá tài khoản? + Tất cả bản sao cục bộ của các sổ địa chỉ, lịch và danh sách công việc sẽ bị xoá. + đồng bộ hoá bộ sưu tập này + chỉ đọc + lịch + nhật ký + Chỉ hiện cá nhân + Không tìm thấy ứng dụng nào có khả năng xử lý Webcal + Cài đặt ICSx⁵ + + Thêm tài khoản + Đăng nhập + Đăng nhập bằng địa chỉ email + Địa chỉ email + Yêu cầu địa chỉ email hợp lệ + Mật khẩu + Đăng nhập bằng URL và tên người dùng + Tên người dùng + URL cơ sở + Chọn chứng chỉ + Thêm tài khoản + Tên tài khoản + Sử dụng địa chỉ email của bạn làm tên tài khoản vì Android sẽ sử dụng tên tài khoản làm trường ORGANIZER cho các sự kiện bạn tạo. Bạn không thể có hai tài khoản với cùng một tên. + Phương pháp nhóm danh bạ: + Yêu cầu tên tài khoản + Tên tài khoản đã được sử dụng + Không tìm thấy chứng chỉ nào + Cài đặt chứng chỉ + Dò tìm thiết lập + Vui lòng đợi, đang truy vấn máy chủ… + Không thể tìm dịch vụ CalDAV hoặc CardDAV. + Xem nhật ký + + Đồng bộ hoá + Khoảng thời gian giữa mỗi lần đồng bộ danh bạ + Chỉ thủ công + Mỗi %d phút + ngay lập tức khi có thay đổi cục bộ + Khoảng thời gian giữa mỗi lần đồng bộ lịch + Khoảng thời gian giữa mỗi lần đồng bộ công việc + + Chỉ thủ công + Mỗi 15 phút + Mỗi 30 phút + Mỗi tiếng + Mỗi 2 tiếng + Mỗi 4 tiếng + Một lần một ngày + + Chỉ đồng bộ khi có WiFi + Việc đồng bộ hoá bị giới hạn chỉ có kết nối WiFi + Loại kết nối không được cân nhắc + Giới hạn WiFi SSID + Sẽ chỉ đồng bộ qua %s + Tất cả kết nối WiFi sẽ được sử dụng + Tên được chia tách bởi dấu phẩy (SSID) của các mạng WiFi được cho phép (để trống để cho phép tất cả) + Giới hạn WiFi SSID yêu cầu cài đặt sâu hơn + Quản lý + Xác thực + Tên người dùng + Cập nhật mật khẩu theo như máy chủ của bạn. + Cài đặt chứng chỉ + CalDAV + Giới hạn thời gian cho sự kiện trong quá khứ + Tất cả sự kiện sẽ được đồng bộ hoá + + Các sự kiện cách đây hơn %d ngày trong quá khứ sẽ bị bỏ qua + + Các sự kiện cách đây hơn số ngày này trong quá khứ sẽ bị bỏ qua (có thể là 0). Để trống để đồng bộ hoá tất cả sự kiện. + Lời nhắc mặc định + + Lời nhắc mặc định %d phút trước sự kiện + + Chưa có lời nhắc mặc định nào được tạo + Nếu các lời nhắc mặc định sẽ được tạo cho các sự kiện không cỏ lời nhắc: số phút được mong muốn trước sự kiện. Để trống để tắt lời nhắc mặc định. + Quản lý màu lịch + Màu lịch được đặt lại tại mỗi lần đồng bộ + Màu lịch cỏ thể được các ứng dụng khác đặt + Hỗ trợ màu sự kiện + Màu sự kiện được đồng bộ + Màu sự kiện không được đồng bộ + CardDAV + Phương pháp nhóm danh bạ + + Các nhóm là các tệp vCard riêng + Các nhóm là các hạng mục cho từng liên hệ + + + Tạo sổ địa chỉ + Tạo lịch + Các mục của lịch có thể có + Sự kiện + Công việc + Ghi chú / nhật ký + Màu + Tiêu đề + Vị trí kho lưu trữ + Tạo + + Xoá bộ sưu tập + Đồng bộ hoá + Tiêu đề + Mô tả + + Thông tin gỡ lỗi + Tệp nén ZIP + Chứa thông tin gỡ lỗi và nhật ký + Chia sẻ tệp nén để truyền sang máy tính, để gửi đi bằng email hoặc để đính kèm vào yêu cầu hỗ trợ. + Chia sẻ tệp nén + Đã đính kèm thông tin gỡ lỗi vào tin nhắn này (yêu cầu ứng dụng nhận có hỗ trợ tệp đính kèm). + Lỗi HTTP + Lỗi máy chủ + Lỗi WebDAV + Lỗi I/O + Xem chi tiết + Đã thu thập thông tin gỡ lỗi + Tài nguyên có liên quan + Có liên quan đến vấn đề + Tài nguyên trên mạng: + Tài nguyên cục bộ: + Nhật ký + Có nhật ký chi tiết + Xem nhật ký + Sao chép URL + + Đã xảy ra lỗi. + Đã xảy ra lỗi HTTP. + Đã xảy ra lỗi I/O. + Hiện chi tiết + + Nơi gắn WebDAV + Hạn mức đã sử dụng: %1$s / có sẵn: %2$s + Chia sẻ nội dung + Bỏ gắn + Thêm nơi gắn WebDAV + Truy cập trực tiếp các tệp trên đám mây bằng cách thêm nơi gắn WebDAV! + Tên hiển thị + URL WebDAV + URL không hợp lệ + Xác thực + Tên người dùng + Mật khẩu + Thêm nơi gắn + Không có dịch vụ WebDAV tại URL này + Xóa điểm gắn + Chi tiết kết nối sẽ bị mất, nhưng các tệp sẽ không bị xóa. + Đang truy cập tệp WebDAV + Đang tải xuống tệp WebDAV + Đang tải lên tệp WebDAV + Nơi gắn WebDAV + + Quyền của DAVx⁵ + Yêu cầu quyền bổ sung + %s quá cũ + Phiên bản tối thiểu được yêu cầu: %1$s + Xác thực thất bại (hãy kiểm tra thông tin đăng nhập) + Lỗi mạng hoặc I/O – %s + Lỗi máy chủ HTTP – %s + Lỗi kho lưu trữ cục bộ – %s + Đã nhận liên hệ không hợp lệ từ máy chủ + Đã nhận sự kiện không hợp lệ từ máy chủ + Đã nhận công việc không hợp lệ từ máy chủ + Đang bỏ qua một hoặc nhiều tài nguyên không hợp lệ + + Đồng bộ tất cả tài khoản + + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000..b591b3f --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,476 @@ + + + + 帳號(已)不存在 + DAVx⁵ 通訊錄 + 別在這裡更改帳戶!請直接使用應用程式管理帳戶。 + 刪除 + 移除 + 取消 + 啟用 + 此為必填欄位 + 幫助 + 向上導航 + 選項選單 + 分享 + 同步已開始或排入佇列 + 資料庫損毀 + 所有帳號已在本地刪除 + 除錯 + 其他重要訊息 + 低優先的狀態訊息 + 同步 + 同步錯誤 + 導致同步停止的嚴重錯誤,如異常的伺服器回應 + 同步警告 + 可忽略的同步問題,比如一些無效檔案 + 網路和輸入輸出錯誤 + 逾時、連線問題等等(通常為暫時性) + + 您的資料,您的選擇 + 權力在握 + 定期同步間隔 + 為了定期進行同步,必須允許 %s 在背景運行,否則 Android 可能會隨時暫停同步。 + 我不需要定期同步間隔* + %s 相容性 + 特定廠商的韌體可能會阻止同步。如果您受到影響,您只能手動解決這一問題。 + 所需設定已完成,不用再提醒我* + * 取消勾選則稍後會再次提醒,可於設定中重置 / %s + 更多資訊 + jtx Board + + 待辦事項支援 + 如果你的服務器支持任務,它們可以與支援任務的app同步: + OpenTasks + 似乎已不再繼續開發 - 不建議使用。 + Tasks.org + 不被支援。]]> + 沒有應用商店可用 + 我不需要任務支援。* + 開源軟體 + 我們很高興您使用 %s 開源軟體。開發、維護和支持是艱苦的工作。請考慮透過多種方式提供貢獻或捐款。不勝感激! + 如何貢獻或捐款 + 不要提醒時長 + + %d 個月 + + 繼續 + + 權限 + %s需要權限才能正常工作 + 以下所有 + 使用它來啟用所有功能(推薦) + 已授予所有權限 + 通訊錄權限 + 無聯絡人同步(不推薦) + 可同步聯絡人 + 行事曆權限 + 無日曆同步(不推薦) + 可同步日曆 + 通知權限 + 已關閉通知(不推薦) + 已啟用通知 + jtx Board 權限 + OpenTasks 權限 + Tasks 權限 + 無任務同步 + 可同步任步 + 保持權限 + 權限可能會被自動重設(不推薦) + 權限不會被自動重設 + 點選權限 > 取消勾選「若應用程式未使用則移除權限」 + 如果開關無法使用,請前往應用程式設定 / 權限。 + 應用程式設定 + + WiFi SSID 權限 + 要能夠存取目前的 WiFi 名稱(SSID),必須符合以下條件: + 精確位置權限 + 已授予位置權限 + 已拒絕位置權限 + 背景位置權限 + 永遠允許 + 位置權限已設定為:%s + 位置權限未設定為:%s + %s使用位置資料(僅限 WiFi SSID)僅用來限制同步至特定的 WiFi SSID。即使同步在背景執行時,也會套用此限制。 + 所有位置資料(僅限 WiFi SSID)皆僅在本機使用,不會傳送至任何地方。 + 位置永遠啟用 + 位置服務已啟用 + 位置服務已停用 + + 翻譯 + 函式庫 + 版本號 %1$s(%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) 與其他貢獻者 + 我們「完全不保證」本程式無瑕疵。這是個自由軟體,歡迎您在符合公用授權條款的情況下任意散布它。 + + 無法創建事項記錄文檔 + 現在正在記錄所有 %s 活動 + 檢視/分享 + 停用 + + CalDAV/CardDAV 同步器 + 關於我們 / 授權條款 + 為測試版本給回饋意見 + 請安裝一個瀏覽器程式 + 設定 + 新聞 & 更新 + 工具 + 外部連結 + 我們的網站 + 使用説明書 + 常見問答 + 適用於組織 + 社群 + 支持此項目 + 如何貢獻 + 隱私權政策 + 歡迎使用 DAVx⁵! + 連線到您的伺服器,並保持行事曆與聯絡人同步。 + 同步所有帳戶 + + 通知已停用。您將不會收到同步錯誤的通知。 + 自動同步未啟用(沒有已驗證的網際網路連線)。 + 管理連線 + 已啟用數據節省模式。背景同步受到限制。 + 管理數據節省模式 + 已啟用省電模式。同步可能會受到限制。 + 管理省電模式 + 儲存空間不足。Android 不會立即同步本機變更,而會在下次的定期同步時進行。 + 管理儲存空間 + 缺少行事曆提供者 + 您是否已停用「行事曆儲存空間」系統應用程式? + 缺少聯絡人提供者 + 您是否已停用「聯絡人儲存空間」系統應用程式? + 管理應用程式 + + 未發現遠端服務 + 無法更新清單 + + 在前景執行 + 在某些裝置上,這對自動同步是必要的。 + + 設定 + 除錯 + 顯示除錯訊息 + 檢視/分享組態詳細資料與日誌 + 詳細除錯記錄 + 記錄功能已啟用。您可以在除錯資訊中檢視日誌。 + 日誌記錄已停用 + 電池最佳化 + 排除本應用程式(建議) + 套用電池限制(不建議) + 網路連線 + 代理類型 + + 系統預設 + 無代理 + HTTP + SOCKS(用於 Orbot) + + 代理主機名稱 + 代理連接埠 + 安全性 + 應用程式權限 + 檢視同步所需的權限 + 不信任系統憑證 + 系統憑證和使用者自訂憑證將不被信任 + 系統憑證和使用者自訂憑證將被信任 (推薦設定) + 若啟用此設定,系統憑證將不被視為可信任。這表示您必須手動接受每一張憑證(包含伺服器更新憑證時),否則帳戶設定與同步將無法運作。 + 重新開啟之前關閉的提示 + 重設對所有自訂憑證的信任 + 所有自訂憑證已清除 + 使用介面 + 通知設定 + 管理通知頻道和設定 + 選擇主題 + + 系統預設 + 淺色 + 深色 + + 重新開啟提示 + 重新啟用之前取消的提示 + 所有提示將再次顯示 + 整合 + 待辦事項 應用程式 + 找不到相容的待辦事項應用程式 + UnifiedPush(實驗性) + 無(停用推播) + 選擇分發服務 + 未安裝推播分發服務 + 未設定端點 + 已準備好透過 %s 接收推播訊息 + FCM (Google Play) + 推播訊息一律加密。 + + 帳戶已被移除 + CardDAV聯絡人檔案 + CalDav行事曆檔案 + Webcal網際網絡行事曆 + 需要額外的權限才能同步這些收藏。 + 管理權限 + 立即同步 + 帳號設定 + 重新命名帳號 + 未儲存的本機資料可能會被捨棄。重新命名後需要再次同步。 + 新帳戶名稱 + 重新命名 + 這個賬號名稱已經被取過了 + 無法重新命名帳號 + 刪除帳號 + 確定要刪除帳號? + 這台裝置上這個帳號的通訊錄、行事曆和工作清單將被刪除。 + 同步這個行事曆或工作清單 + 唯讀 + 行事曆 + 聯絡人 + 日誌 + 待辦事項 + 只顯示個人 + 重新整理清單 + Webcal 訂閱可與外部應用程式同步。 + 未找到支援Webcal的APP + 安裝ICSx⁵ + + 新增帳號 + 隱私權政策。]]> + 一般登入 + 特定提供者登入 + 繼續 + 登入 + 用 Email 地址登入 + Email 地址 + 請輸入有效的 Email 地址 + 服務會透過 DNS 紀錄與 well-known URL 自動探索。]]> + 密碼 + 隱藏密碼 + 顯示密碼 + 密碼(可選) + 用網址和帳號登入 + 使用者帳號 + 使用者名稱(可選) + 根 URL + 服務也會透過 DNS 紀錄與 well-known URL 自動探索。]]> + 點選憑證 + 新增帳號 + 帳號名稱 + 在某些裝置上使用單引號 (\') 似乎會造成問題。 + 使用 Email 地址當作裝置上的帳號顯示名稱,因為當您在行事曆創建活動時,Android 會把帳號顯示名稱放到「活動發起人」欄位。兩個帳號不能有相同的名稱。 + 聯絡人群組的儲存格式 + 需要帳號名稱 + 這個賬號名稱已經被取過了 + 無法新增帳戶 + 完成 + 進階登入 + 無用戶端憑證(可選) + 用戶端憑證:%s + 找不到憑證 + 安裝憑證 + Fastmail + Fastmail 帳戶 + 使用 Fastmail 登入 + Google 聯絡人 / 行事曆 + Google 帳戶 + 使用 Google 登入 + 用戶端 ID(可選) + 隱私權政策。]]> + Google API 服務使用者資料政策,包括有限使用的相關要求。]]> + 無法取得授權碼 + Nextcloud + 使用 Nextcloud 登入 + 這將在網頁瀏覽器中啟動 Nextcloud 登入流程。 + Nextcloud 伺服器位址 + 登入 + 無法取得登入 URL + 無法取得登入資料 + 設定錯誤 + 請稍待,正在詢問伺服器… + 找不到 CalDAV 或 CardDAV 服務。 + 基礎 URL 似乎不是可存取的 CalDAV/CardDAV URL,且服務偵測未成功。 + 我們的已測試服務清單及其基礎 URL。]]> + 請同時再次確認驗證資訊(通常是使用者名稱與密碼)。 + 更多技術資訊可在日誌中取得。 + 檢視日誌 + + 同步設定 + 聯絡人同步間隔 + 只手動同步 + 每 %d 分鐘,以及在本裝置上修改時 + 行事曆同步間隔 + 待辦事項同步間隔 + + 僅手動 + 每15分鐘自動 + 每30分鐘自動 + 每小時自動 + 每2小時自動 + 每4小時自動 + 每天自動 + + 只用 WiFi 同步 + 只於 WiFi 連線時同步 + 任何網路連線都可使用 + 限用特定 WiFi SSID + 只在%s連線時同步 + 所有 WiFi 連線都可以使用 + 使用逗號分割的名稱 (SSIDs) 表示的 WiFi 連線(留空則代表全部) + WiFi SSID 限制需要進一步設定 + 管理 + VPN 需要基礎網際網路連線 + 沒有基礎已驗證網際網路連線的 VPN 不足以執行同步(建議) + 沒有基礎已驗證網際網路連線的 VPN 仍可執行同步 + 認證 + 使用者帳號 + 密碼或應用程式專用密碼 + 應用程式專用密碼。]]> + 新密碼 + 您在伺服器上使用中的密碼 + 再次授權(OAuth) + 當存取權遭撤銷時使用 + 授權成功 + 用戶端憑證 + 沒有可用或已選取的憑證 + 安裝憑證 + CalDAV + 過去活動的時間限制 + 將會同步所有活動 + + %d 天之前的活動會被忽略 + + 此天數前的活動將會被忽略(可設為零),若留空則同步所有活動 + 預設提醒 + + 預設在活動前 %d 分鐘提醒 + + 未設定預設提醒 + 當沒有提醒的活動需要加入預設提醒時,活動開始前多少分鐘出發提醒。留空則停用預設提醒。 + 管理行事曆的顏色 + 行事曆顏色會在每次同步時重設 + 行事曆顏色可由其他應用程式設定 + 設定活動的顔色 + 活動顏色已同步 + 活動顏色未同步 + CardDAV + 聯絡人群組的儲存格式 + + 群組存成額外的 VCard 檔案 + 群組存成每個聯絡人的分類屬性 + + + 建立通訊錄 + 伺服器可能不支援透過 CardDAV 建立通訊錄。 + 建立行事曆 + 預設時區(可選) + + 可使用的行事曆項目 + 活動 + 事務 + 筆記/日誌 + 伺服器可能不支援透過 CalDAV 建立行事曆。 + 顔色 + 標題 + 存儲位置 + 描述(可選) + 建立 + + 聯絡人 + 活動 + 待辦事項 + 刪除行事曆或工作清單 + 此收藏(%s)及其所有資料將被永久移除,包括本機與伺服器上的內容。 + 同步 + 已啟用同步 + 已停用同步 + 唯讀 + 唯讀(由伺服器設定) + 唯讀(由設定決定) + 唯讀(僅限本機) + 讀取/寫入 + 標題 + 描述 + 擁有者 + Push support + 伺服器宣告支援推播 + 於 %1$s 訂閱,於 %2$s 到期 + 上次同步(%s) + 位址(URL) + + 除錯訊息 + ZIP 壓縮檔 + 包含除錯資訊與日誌 + 分享此封存檔以傳輸至電腦、透過電子郵件傳送,或附加至支援服務單。 + 分享封存檔 + 已將除錯資訊附加至此訊息(需要接收應用程式支援附件)。 + HTTP 錯誤 + 伺服器錯誤 + WebDAV 錯誤 + 讀寫錯誤 + 伺服器不允許執行請求的操作類型。 + 發生伺服器端問題。請聯絡您的伺服器支援人員。 + 發生非預期的錯誤。請檢視除錯資訊以取得詳細內容。 + 顯示詳細訊息 + 已收集除錯資訊 + 相關資源 + 與問題相關 + 遠端資源: + 本機資源: + 日誌 + 可用詳細日誌 + 更多技術資訊可在日誌中取得。 + 拷貝URL + 隱私權通知 + 日誌和除錯資訊可能包含私人資訊,請在公開分享時注意。 + + 發生錯誤 + HTTP 發生錯誤 + 讀寫錯誤 + 顯示細節 + + WebDAV 掛載 + 已使用配額:%1$s / 可用配額:%2$s + 分享内容 + 取消掛載 + 新增 WebDAV 掛載 + 只要新增對應的 WebDAV 掛載就可以直接存取你的雲端檔案! + WebDAV 如何運作請見文件。]]> + 顯示名稱 + WebDAV 網址 + 無效 URL + 掛載點和顯示名稱 + 認證 + 使用者帳號 + 密碼 + 使用者名稱(可選) + 密碼(可選) + 新增掛載 + 此網址沒有 WebDAV 服務 + 移除掛點點 + 連線詳細資訊將會遺失,但不會刪除任何檔案。 + 正在存取 WebDAV 檔案 + 正在下載 WebDAV 檔案 + 正在上傳檔案至 WebDAV + WebDAV 掛載 + + DAVx⁵ 權限 + 需要額外的權限 + %s過舊 + 最低需求版本:%1$s + 鑒權失敗(你需要檢查登錄憑證) + 網際網絡或者輸入輸出錯誤——%s + HTTP伺服器錯誤——%s + 資料庫錯誤——%s + 非嚴重錯誤(已達到最大重試次數) + 收到了無效的聯絡人 + 收到了無效的活動 + 收到了無效的任務 + 略過了一個或多個無效的資料 + 同步處理中 + 遠端資料已變更 + + 全部同步 + 同步所有帳戶 + 標示的同步按鈕 + 同步按鈕圖示 + 點擊以手動執行同步。 + + diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml new file mode 100644 index 0000000..8dc2772 --- /dev/null +++ b/app/src/main/res/values-zh/strings.xml @@ -0,0 +1,473 @@ + + + + 账户(已)不存在 + DAVx⁵ 通讯录 + 别在这里更改账户!请直接使用应用管理账户。 + 删除 + 删除 + 取消 + 启用 + 此字段是必填项 + 帮助 + 向上导航 + 选项菜单 + 分享 + 同步已启动/已加入队列 + 数据库损坏 + 所有帐户已在本地删除。 + 调试 + 其它重要消息 + 低优先级状态消息 + 同步 + 同步错误 + 导致同步停止的重要错误,如异常的服务器响应 + 同步警告 + 不重要的同步问题,如某文件无效 + 网络或 I/O 错误 + 超时、连接异常等问题(通常是临时错误) + + 您的数据。您的选择。 + 获得控制。 + 定期同步间隔 + 为了定期进行同步,必须允许%s在后台运行。否则,Android可能会随时暂停同步。 + 我不需要定期的同步。* + %s兼容性 + 特定厂商的固件可能会阻止同步。如果你受到影响,你只能手动解决这一问题。 + 我已完成所需的设置。不再提醒我。* + *取消选中以供稍后提醒。可以在应用设置中重置/%s。 + 更多信息 + jtx Board + + 任务支持 + 如果你的服务器支持任务,它们可以通过一个受支持的任务应用进行同步: + OpenTasks + 似乎已不再开发 — 不推荐 + Tasks.org + 不被支持。]]> + 没有可用的应用商店 + 我不需要任务支持。* + 开源软件 + 我们很高兴您使用 %s 开源软件。开发、维护和支持是艰苦的工作。请考虑通过多种方式提供贡献或捐款。不胜感激! + 如何贡献或捐款 + 不要提醒时长 + + %d 个月 + + 继续 + + 权限 + %s需要权限才能正常工作 + 以下所有 + 使用它来启用所有特性 (推荐) + 已授予全部权限 + 联系人权限 + 无联系人同步(不推荐) + 可同步联系人 + 日历权限 + 无日历同步(不推荐) + 可同步日历 + 通知权限 + 已禁用通知(不推荐) + 已启用通知 + jtx Board 权限 + OpenTasks权限 + Tasks权限 + 无任务同步 + 可同步任务 + 保留权限 + 权限可能被自动重置(不推荐) + 权限不会被自动重置 + 点击权限 > 取消选择 “移除权限,如果应用未使用” + 如果切换没有正常工作,请使用应用程序设置/权限 + 应用设置 + + WiFi SSID权限 + 要访问当前的WiFi名称(SSID),必须满足以下条件: + 精确位置权限 + 已授予位置权限 + 位置权限被拒 + 后台位置权限 + 始终允许 + 位置权限已设为:%s + 位置权限未设为:%s + %s 使用位置数据 (仅 WiFi SSID) 的目的只是为了将同步限制到特定的 WiFi SSID。即使当同步在后台运行时,这也会发生。 + 所有位置数据(仅 WiFi SSID)只在本地使用,不会被发送到任何地方。 + 始终允许定位 + 位置服务已启用 + 位置服务已禁用 + + 翻译 + 程序库 + 版本 %1$s (%2$d) + ©Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) 及贡献者 + 本程序不附带任何担保。这是一款自由软件,你可以有条件地传播它。 + + 无法创建日志文件 + 正记录%s的所有活动 + 查看/分享 + 禁用 + + CalDAV/CardDAV 同步器 + 关于 / 许可 + 测试版反馈 + 请安装网页浏览器 + 设置 + 最新消息 + 工具 + 外部链接 + 应用网站 + 手册 + 常见问题 + 面向机构 + 社区 + 支持项目 + 如何作贡献 + 隐私政策 + 欢迎来到 DAVx⁵! + 连接到你的服务器,保持日历和联系人同步 + 同步所有账户 + + 已禁用通知。你将不会收到同步出错的通知 + 自动同步不活跃(无已验证的互联网连接) + 管理连接 + 启用了流量节省程序。后台同步受限 + 管理流量节省程序 + 启用了节电程序。同步可能受限。 + 管理节电程序 + 低存储空间。Android 不会立即同步本地更改,但会在下次定期同步时进行 + 管理存储 + 缺少日历程序 + 你禁用了“日历存储”系统应用吗? + 缺少联系人程序 + 你禁用了“联系人存储”系统应用吗? + 管理应用 + + 服务配置检测失败 + 无法刷新集合列表 + + 运行于前台 + 在某些设备上,这是自动同步所必需的。 + + 设置 + 调试 + 显示调试信息 + 查看/分享配置详情和日志 + 记录完整日志 + 日志记录处于活跃状态。你可以将日志作为调试信息的一部分来查看 + 日志记录已禁用 + 电池优化 + 排除本应用(推荐) + 施加电池限制(不推荐) + 连接 + 代理类型 + + 系统默认 + 无代理 + HTTP + SOCKS (用于 Orbot) + + 代理主机名称 + 代理端口 + 安全 + 应用权限 + 查看同步所需权限 + 不信任系统证书 + 系统和用户增加的发布者不会被信任 + 系统和用户增加的发布者会被信任(推荐) + 如果此设置处于开启状态,系统证书不会被认为是可信的。这表示你必须手动接受每一个证书(服务器更新其证书时也必须加以确认)或账户设置,且同步不会工作。 + 重设证书信任状态 + 重设所有自定义证书的信任状态 + 所有自定义证书已清除 + 用户界面 + 通知设置 + 管理通知渠道等设置 + 选择主题 + + 系统默认 + 浅色 + 深色 + + 重设提示 + 重新显示之前忽略过的提示 + 所有提示将会再次显示 + 集成 + Tasks 应用 + 未找到兼容的任务应用 + UnifiedPush (实验性) + 无(停用推送) + 选择分发程序 + 未安装推送分发程序 + 未配置端点 + 准备好通过 %s 接收推送消息 + FCM (Google Play) + 推送消息始终是加密的 + + 账户已被删除 + CardDAV + CalDAV + Webcal + 需要额外权限来同步这些集合 + 管理权限 + 立即同步 + 账户设置 + 重命名账户 + 未保存的本地数据可能会消失。重命名后需要重新同步。 + 新账户名 + 重命名 + 账户名已被占用 + 无法重命名账户 + 删除账户 + 真的要删除账户吗? + 所有通讯录、日历和任务列表的本机存储将被删除。 + 同步该集合 + 只读 + 日历 + 联系人 + 日记 + 任务 + 只显示个人 + 刷新列表 + 可以用外部应用来同步 Webcal 订阅 + 找不到支持 Webcal 的应用 + 安装 ICSx⁵ + + 增加账户 + 隐私政策。]]> + 常规登录 + 特定服务商的登录 + 继续 + 登录 + 使用邮箱地址登录 + Email 地址 + 请输入有效 Email 地址 + 服务发现 通过 DNS 记录和已知URLs 进行。]]> + 密码 + 隐藏密码 + 显示密码 + 密码(可选) + 使用 URL 和用户名登录 + 用户名 + 用户名(可选) + 根地址 + 服务发现也将使用 DNS 记录 和已知 URLs 进行。]]> + 选择证书 + 增加账户 + 账户显示名 + 使用撇号(\')似乎会在一些设备上造成问题 + 请使用你的邮箱地址作为帐户名,因为 Android 会将你创建的日历事件的创建者项设置为帐户名。你不能拥有多个帐户名相同的账户。 + 联系人分组方式 + 请输入账户名 + 账户名已被占用 + 无法添加账户 + 完成 + 高级登录 + 无客户端证书(可选) + 客户端证书:%s + 没有找到证书 + 安装证书 + Fastmail + Fastmail 账户 + 使用 Fastmail 登录 + Google 联系人/日历 + Google 账户 + 使用 Google 账户登录 + Client ID (可选) + 隐私政策 。]]> + Google API 服务用户数据政策,包括有限使用的要求。]]> + 无法获得身份验证码 + Nextcloud + 用 Nextcloud 登录 + 这会在网页浏览器中开启 Nextcloud 登录流程 + Nextcloud 服务器地址 + 登录 + 无法获取登录 URL + 无法获得登陆数据 + 正在配置 + 正在与服务器通信,请稍等… + 找不到 CalDAV 或 CardDAV 服务。 + 基URL似乎不是可访问的CalDAV/CardDAV URL 且服务检测不成功。 + 我们的已测试服务列表 及它们的基础 URLs.]]> + 也请仔细核查身份验证数据(通常是用户名和密码)。 + 可以在日志中看到进一步的技术信息 + 查看日志 + + 同步 + 通讯录自动同步间隔 + 手动同步 + 每 %d 分钟或本地修改后 + 日历自动同步间隔 + 任务自动同步间隔 + + 手动同步 + 每 15 分钟 + 每 30 分钟 + 每小时 + 每 2 小时 + 每 4 小时 + 每天一次 + + 只在 WiFi 下同步 + 同步只在 WiFi 连接下进行 + 同步不受数据连接类型限制 + WiFi SSID 限制 + 只使用 %s 网络同步 + 任意 WiFi 网络均可同步 + 请用半角逗号分隔允许同步的 WiFi 网络名(SSID),留空则允许任意网络 + WiFi SSID 限制需要进一步设置 + 管理 + VPN 需要底层互联网 + 没有底层验证的互联网连接的 VPN 不足以运行同步(推荐选项) + 没有底层验证的互联网连接的 VPN 足以运行同步了 + 认证 + 用户名 + 密码或应用密码 + 应用密码.]]> + 新密码 + 修改服务器密码 + 再次授权 (OAuth) + 当访问权被撤销时使用 + 授权成功 + 客户端证书 + 无证书可用或未选择证书 + 安装证书 + CalDAV + 旧日程时间限制 + 同步所有日程 + + %d 天前的日程不会被同步 + + 超过这个数字的天数的旧日程将会被忽略(可以为 0)。留空则同步所有日程。 + 默认提醒 + + 默认事件开始前 %d 分钟提醒 + + 默认提醒未创建 + 当没有提醒的事件需增加默认提醒时,事件开始前多少分钟触发提醒。留空以禁用默认提醒。 + 管理日历颜色 + 日历的颜色会在每次同步时被重置 + 日历的颜色可以由其他应用程序设置 + 事件日历颜色支持 + 事件颜色已同步 + 事件颜色未同步 + CardDAV + 联系人分组方式 + + 按 VCard 文件分组 + 按联系人分类分组 + + + 创建通讯录 + 服务器可能不支持通过 CalDAV 创建通讯录 + 创建日历 + 默认时区(可选) + + 可能使用的日历类型 + 事件 + 任务 + 笔记 / 日志 + 服务器可能不支持通过 CalDAV 创建日历 + 颜色 + 标题 + 存储位置 + 描述(可选) + 创建 + + 联系人 + 活动 + 任务 + 删除集合 + 此集合(%s)及其所有数据将从本地和服务器被永久删除 + 同步 + 同步已启用 + 已停用同步 + 只读 + 只读(服务器) + 只读(设置决定) + 只读 (仅本地) + 读/写 + 标题 + 描述 + 所有者 + 推送支持 + 服务器宣告推送支持 + 订阅于 %1$s,过期于 %2$s + 上次同步(%s) + 地址(URL) + + 调试信息 + ZIP 压缩文件 + 包含调试信息和日志 + 共享压缩文件以将其传输到计算机上,通过电子邮件发送或将其附加到支持请求。 + 分享压缩文件 + 已附加调试信息到此消息(需要接收应用支持附件功能) + HTTP错误 + 服务器错误 + WebDAV错误 + I/O错误 + 查看细节 + 已收集调试信息 + 所涉资源 + 与此问题有关 + 远程资源: + 本地资源: + 日志 + 详细日志可用 + 查看日志 + 复制 URL + 隐私声明 + 日志和调试信息可能包含私密信息。公开分享时请意识到这一点 + + 出现错误 + 出现 HTTP 错误 + 出现 I/O 错误 + 显示详情 + + WebDAV 文件系统 + 已用配额:%1$s/可用容量:%2$s + 分享内容 + 解除挂载 + 添加 WebDAV 文件系统 + 通过添加 WebDAV 挂载直接访问您的云文件! + WebDAV 挂载如何工作.]]> + 展示名称 + WebDAV URL + 无效 URL + 挂载点和显示名称 + 认证 + 用户名 + 密码 + 用户名(可选) + 密码(可选) + 添加 WebDAV 网址 + 此 URL 无 WebDAV 服务 + 删除装载点 + 将丢失连接详情,但不会删除文件 + 正在访问 WebDAV 文件 + 正在下载 WebDAV 文件 + 正在上传 WebDAV 文件 + WebDAV 文件系统 + + DAVx⁵ 权限 + 需要额外权限 + %s太旧 + 最低要求版本: %1$s + 认证失败(请检查登录凭据,如用户名密码) + 网络或 I/O 错误 – %s + HTTP 服务器错误 – %s + 本地存储错误 – %s + 软错误(达到最大重试次数) + 从服务器收到无效的通讯录 + 从服务器收到无效的日历事件 + 从服务器收到无效的任务项 + 正在忽略若干无效资源 + 待同步 + 远程数据已更改 + + 同步所有 + 同步所有账户 + 带标签的同步按钮 + 同步按钮图标 + 轻按手动运行同步 + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..62ec229 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #7cb342 + #aee571 + #4b830d + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..6ea89a1 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,544 @@ + + + + + + DAVx⁵ + + Account does not exist (anymore) + bitfire.at.davdroid + at.bitfire.davdroid.address_book + DAVx⁵ Address book + Don\'t change the account here! Directly use the app to manage accounts instead. + Delete + Remove + Cancel + Enable + This field is required + Help + Navigate up + Options menu + Share + Synchronization started/enqueued + + Database corrupted + All accounts have been removed locally. + + Debugging + Other important messages + Low-priority status messages + Synchronization + Synchronization errors + Important errors which stop synchronization like unexpected server replies + Synchronization warnings + Non-fatal synchronization problems like certain invalid files + Network and I/O errors + Timeouts, connection problems, etc. (often temporary) + + + Your data. Your choice. + Take control. + Regular sync intervals + For synchronization at regular intervals, %s must be allowed to run in the background. Otherwise, Android may pause synchronization at any time. + I don\'t need regular sync intervals.* + %s compatibility + Vendor-specific firmware may block synchronization. If you\'re affected, you can only resolve this manually. + I have done the required settings. Don\'t remind me anymore.* + * Leave unchecked to be reminded later. Can be reset in app settings / %s. + More information + jtx Board + + Tasks support + If tasks are supported by your server, they can be synchronized with a supported tasks app: + OpenTasks + Doesn\'t seem to be developed anymore – not recommended. + Tasks.org + are not supported.]]> + No app store available + I don\'t need tasks support.* + Open-source software + We\'re happy that you use %s, which is open-source software. Development, maintenance and support are hard work. Please consider contributing (there are many ways) or a donation. It would be highly appreciated! + How to contribute/donate + Don\'t remind me for + + %d month + %d months + + Next + + + Permissions + %s requires permissions to work properly. + All of the below + Use this to enable all features (recommended) + All permissions granted + Contacts permissions + No contact sync (not recommended) + Contact sync possible + Calendar permissions + No calendar sync (not recommended) + Calendar sync possible + Notification permission + Notifications disabled (not recommended) + Notifications enabled + jtx Board permissions + OpenTasks permissions + Tasks permissions + No task sync + Task sync possible + Keep permissions + Permissions may be reset automatically (not recommended) + Permissions won\'t be reset automatically + Click Permissions > uncheck \"Remove permissions if app isn\'t used\" + If a switch doesn\'t work, use app settings / Permissions. + App settings + + + WiFi SSID permissions + To be able to access the current WiFi name (SSID), these conditions must be met: + Precise location permission + Location permission granted + Location permission denied + Background location permission + Allow all the time + Location permission set to: %s + Location permission not set to: %s + %s uses location data (only WiFi SSID) solely to restrict synchronization to a specific WiFi SSID. This will happen even when the synchronization runs in background. + All location data (only WiFi SSID) are only used locally and are not sent anywhere. + Location always enabled + Location service is enabled + Location service is disabled + + + Translations + Libraries + Version %1$s (%2$d) + © Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) and contributors + This program comes with ABSOLUTELY NO WARRANTY. It is free software, and you are welcome to redistribute it under certain conditions. + + + Couldn\'t create log file + Now logging all %s activities + View/share + Disable + + + CalDAV/CardDAV Sync Adapter + About / License + Beta feedback + Please install a Web browser + Settings + News & updates + Tools + External links + Web site + Manual + FAQ + For organizations + Community + Support the project + How to contribute + Privacy policy + Welcome to DAVx⁵! + Connect to your server and keep your calendars and contacts synchronized. + Sync all accounts + + + Notifications disabled. You won\'t be notified about sync errors. + Automatic synchronization not active (no verified Internet connection). + Manage connections + Data saver enabled. Background synchronization is restricted. + Manage data saver + Battery saver enabled. Synchronization may be restricted. + Manage battery saver + Storage space low. Android will not sync local changes immediately, but during the next regular sync. + Manage storage + Calendar provider missing + Did you disable the \"Calendar storage\" system app? + Contacts provider missing + Did you disable the \"Contacts storage\" system app? + Manage apps + + + + + Service detection failed + Couldn\'t refresh collection list + + + Running in foreground + On some devices, this is necessary for automatic synchronization. + + + Settings + Debugging + Show debug info + View/share configuration details and logs + Verbose logging + Logging is active. You can view the logs as part of the debug info. + Logging is disabled + Battery optimization + App is exempted (recommended) + Battery restrictions apply (not recommended) + Connection + Proxy type + + System default + No proxy + HTTP + SOCKS (for Orbot) + + + -1 + 0 + 1 + 2 + + Proxy host name + Proxy port + Security + App permissions + Review permissions required for synchronization + Distrust system certificates + System and user-added CAs won\'t be trusted + System and user-added CAs will be trusted (recommended) + If this setting is active, system certificates are not considered as trustworthy. This means that you will have to manually accept every certificate (also when the server renews its certificate) or account setup and sync will not work. + Reset (un)trusted certificates + Resets trust of all custom certificates + All custom certificates have been cleared + User interface + Notification settings + Manage notification channels and their settings + Select theme + + System default + Light + Dark + + + -1 + 1 + 2 + + Reset hints + Re-enables hints which have been dismissed previously + All hints will be shown again + Integration + Tasks app + No compatible tasks app found + UnifiedPush (experimental) + None (disable push) + Choose a distributor + No push distributor installed + No endpoint configured + Ready to receive push messages over %s + FCM (Google Play) + Push messages are always encrypted. + + + Account has been removed + CardDAV + CalDAV + Webcal + Additional permissions are required to synchronize these collections. + Manage permissions + Synchronize now + Account settings + Rename account + Unsaved local data may be dismissed. Re-synchronization is required after renaming. + New account name + Rename + Account name already taken + Couldn\'t rename account + Delete account + Really delete account? + All local copies of address books, calendars and task lists will be deleted. + synchronize this collection + read-only + calendar + contacts + journal + tasks + Show only personal + Refresh list + Webcal subscriptions can be synchronized with external apps. + No Webcal-capable app found + Install ICSx⁵ + + + Add account + privacy policy.]]> + Generic login + Provider-specific login + Continue + Login + Login with email address + Email address + Valid email address required + Services are discovered using DNS records and well-known URLs.]]> + Password + Hide password + Show password + Password (optional) + Login with URL and user name + User name + User name (optional) + Base URL + services are also discovered using DNS records and well-known URLs.]]> + Select certificate + Add account + Account name + Usage of apostrophes (\') seems to cause problems on some devices. + Use your email address as account name because Android will use the account name as ORGANIZER field for events you create. You can\'t have two accounts with the same name. + Contact group method: + Account name required + Account name already taken + Account could not be added + Finish + Advanced login + No client certificate (optional) + Client certificate: %s + No certificate found + Install certificate + Fastmail + Fastmail account + Sign in with Fastmail + Google Contacts / Calendar + Google account + Sign in with Google + Client ID (optional) + Privacy policy for details.]]> + Google API Services User Data Policy, including the Limited Use requirements.]]> + Couldn\'t obtain authorization code + Nextcloud + Login with Nextcloud + This will start the Nextcloud Login Flow in a Web browser. + Nextcloud server address + Sign in + Couldn\'t obtain login URL + Couldn\'t obtain login data + + Configuration detection + Please wait, querying server… + Couldn\'t find CalDAV or CardDAV service. + The base URL doesn\'t seem to be an accessible CalDAV/CardDAV URL and service detection was not successful. + our list of tested services and their base URLs.]]> + Please also double-check authentication (usually username and password). + Further technical information is available in the logs. + View logs + + + Synchronization + Contacts sync. interval + Only manually + Every %d minutes + immediately on local changes + Calendars sync. interval + Tasks sync. interval + + -1 + 900 + 1800 + 3600 + 7200 + 14400 + 86400 + + + Only manually + Every 15 minutes + Every 30 minutes + Every hour + Every 2 hours + Every 4 hours + Once a day + + Sync over WiFi only + Synchronization is restricted to WiFi connections + Connection type is not taken into consideration + WiFi SSID restriction + Will only sync over %s + All WiFi connections will be used + Comma-separated names (SSIDs) of allowed WiFi networks (leave blank for all) + WiFi SSID restriction requires further settings + Manage + VPN requires underlying Internet + VPN without underlying validated Internet connection is not enough to run synchronization (recommended) + VPN without underlying validated Internet connection is enough to run synchronization + Authentication + User name + Password or app password + app password.]]> + New password + Update the password according to your server. + Authorize again (OAuth) + Use when access has been revoked + Authorization successful + Client certificate + No certificate available or selected + Install certificate + CalDAV + Past event time limit + All events will be synchronized + + Events more than one day in the past will be ignored + Events more than %d days in the past will be ignored + + Events which are more than this number of days in the past will be ignored (may be 0). Leave blank to synchronize all events. + Default reminder + + Default reminder one minute before event + Default reminder %d minutes before event + + No default reminders are created + If default reminders shall be created for events without reminder: the desired number of minutes before the event. Leave blank to disable default reminders. + Manage calendar colors + Calendar colors are reset at each sync + Calendar colors can be set by other apps + Event color support + Event colors are synced + Event colors are not synced + CardDAV + Contact group method + + GROUP_VCARDS + CATEGORIES + + + Groups are separate vCards + Groups are per-contact categories + + + + Create address book + Address book creation over CardDAV may not be supported by the server. + Create calendar + Default time zone (optional) + + Possible calendar entries + Events + Tasks + Notes / journal + Calendar creation over CalDAV may not be supported by the server. + Color + Title + Storage location + Description (optional) + Create + + + contacts + events + tasks + Delete collection + This collection (%s) and all its data will be removed permanently, both locally and on the server. + Synchronization + Synchronization enabled + Synchronization disabled + Read-only + Read-only (by server) + Read-only (by policy) + Read-only (only locally) + Read/write + Title + Description + Owner + Push support + Server advertises Push support + Subscribed at %1$s, expires at %2$s + Last sync (%s) + Address (URL) + + + at.bitfire.davdroid.debug + Debug info + ZIP archive + Contains debug info and logs + Share the archive to transfer it to a computer, to send it by email or to attach it to a support ticket. + Share archive + Debug info attached to this message (requires attachment support of the receiving app). + HTTP Error + Server Error + WebDAV Error + I/O Error + The request has been denied by the server. + The requested resource doesn\'t exist (anymore). + The server doesn\'t allow the requested type of operation. + A server-side problem occurred. Please contact your server support. + An unexpected error has occurred. View debug info for details. + View details + Debug info have been collected + Involved resources + Related to the problem + Remote resource: + Local resource: + Logs + Verbose logs are available + View logs + Copy URL + Inspect resource + Privacy notice + Logs and debug info may contain private information. Please be aware of this when sharing publicly. + Unable to view resource + + + An error has occurred. + An HTTP error has occurred. + An I/O error has occurred. + Show details + + + at.bitfire.davdroid.webdav + WebDAV mounts + Quota used: %1$s / available: %2$s + Share content + Unmount + Add WebDAV mount + Directly access your cloud files by adding a WebDAV mount! + how WebDAV mounts work.]]> + Display name + WebDAV URL + Invalid URL + Mount point and display name + Authentication + User name + Password + User name (optional) + Password (optional) + Add mount + No WebDAV service at this URL + Remove mount point + Connection details will be lost, but no files will be deleted. + Accessing WebDAV file + Downloading WebDAV file + Uploading WebDAV file + WebDAV mount + + + DAVx⁵ permissions + Additional permissions required + %s too old + Minimum required version: %1$s + Authentication failed (check login credentials) + Network or I/O error – %s + HTTP server error – %s + Local storage error – %s + Soft error (max retries reached) + Received invalid contact from server + Received invalid event from server + Received invalid task from server + Ignoring one or more invalid resources + Sync pending + Remote data have changed + + + Sync all + Sync all accounts + Labeled Sync Button + Icon Sync Button + Tap to run synchronization manually. + + + + diff --git a/app/src/main/res/xml/account_authenticator.xml b/app/src/main/res/xml/account_authenticator.xml new file mode 100644 index 0000000..85eaa20 --- /dev/null +++ b/app/src/main/res/xml/account_authenticator.xml @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/app/src/main/res/xml/account_authenticator_address_book.xml b/app/src/main/res/xml/account_authenticator_address_book.xml new file mode 100644 index 0000000..6305f31 --- /dev/null +++ b/app/src/main/res/xml/account_authenticator_address_book.xml @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/app/src/main/res/xml/contacts.xml b/app/src/main/res/xml/contacts.xml new file mode 100644 index 0000000..b59d066 --- /dev/null +++ b/app/src/main/res/xml/contacts.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/debug_paths.xml b/app/src/main/res/xml/debug_paths.xml new file mode 100644 index 0000000..9fcb6e8 --- /dev/null +++ b/app/src/main/res/xml/debug_paths.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..cf46331 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/main/res/xml/sync_calendars.xml b/app/src/main/res/xml/sync_calendars.xml new file mode 100644 index 0000000..e50cee6 --- /dev/null +++ b/app/src/main/res/xml/sync_calendars.xml @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/xml/sync_contacts.xml b/app/src/main/res/xml/sync_contacts.xml new file mode 100644 index 0000000..18c4eea --- /dev/null +++ b/app/src/main/res/xml/sync_contacts.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/xml/sync_notes.xml b/app/src/main/res/xml/sync_notes.xml new file mode 100644 index 0000000..6fa047b --- /dev/null +++ b/app/src/main/res/xml/sync_notes.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/xml/sync_opentasks.xml b/app/src/main/res/xml/sync_opentasks.xml new file mode 100644 index 0000000..15d78ab --- /dev/null +++ b/app/src/main/res/xml/sync_opentasks.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/xml/sync_prefs.xml b/app/src/main/res/xml/sync_prefs.xml new file mode 100644 index 0000000..2e6e5eb --- /dev/null +++ b/app/src/main/res/xml/sync_prefs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/sync_tasks_org.xml b/app/src/main/res/xml/sync_tasks_org.xml new file mode 100644 index 0000000..088206c --- /dev/null +++ b/app/src/main/res/xml/sync_tasks_org.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/xml/widget_info_icon_sync_button.xml b/app/src/main/res/xml/widget_info_icon_sync_button.xml new file mode 100644 index 0000000..11235ea --- /dev/null +++ b/app/src/main/res/xml/widget_info_icon_sync_button.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/xml/widget_info_labeled_sync_button.xml b/app/src/main/res/xml/widget_info_labeled_sync_button.xml new file mode 100644 index 0000000..53cc169 --- /dev/null +++ b/app/src/main/res/xml/widget_info_labeled_sync_button.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/ose/AndroidManifest.xml b/app/src/ose/AndroidManifest.xml new file mode 100644 index 0000000..6c637e1 --- /dev/null +++ b/app/src/ose/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/ose/kotlin/at/bitfire/davdroid/DebugInfoCrashHandler.kt b/app/src/ose/kotlin/at/bitfire/davdroid/DebugInfoCrashHandler.kt new file mode 100644 index 0000000..27df8ca --- /dev/null +++ b/app/src/ose/kotlin/at/bitfire/davdroid/DebugInfoCrashHandler.kt @@ -0,0 +1,50 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +package at.bitfire.davdroid + +import android.content.Context +import at.bitfire.davdroid.ui.DebugInfoActivity +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject + +class DebugInfoCrashHandler @Inject constructor( + @ApplicationContext private val context: Context, + private val logger: Logger +): Thread.UncaughtExceptionHandler { + + @Module + @InstallIn(SingletonComponent::class) + interface DebugInfoCrashHandlerModule { + @Binds + fun debugInfoCrashHandler( + debugInfoCrashHandler: DebugInfoCrashHandler + ): Thread.UncaughtExceptionHandler + } + + // See https://developer.android.com/about/versions/oreo/android-8.0-changes#loue + val originalCrashHandler = Thread.getDefaultUncaughtExceptionHandler() + + + override fun uncaughtException(t: Thread, e: Throwable) { + logger.log(Level.SEVERE, "Unhandled exception in thread ${t.id}!", e) + + // start debug info activity with exception (will be started in a new process) + val intent = DebugInfoActivity.IntentBuilder(context) + .withCause(e) + .newTask() + .build() + context.startActivity(intent) + + // pass through to default handler to kill the process + originalCrashHandler?.uncaughtException(t, e) + } + +} \ No newline at end of file diff --git a/app/src/ose/kotlin/at/bitfire/davdroid/di/OseFlavorModule.kt b/app/src/ose/kotlin/at/bitfire/davdroid/di/OseFlavorModule.kt new file mode 100644 index 0000000..6db4362 --- /dev/null +++ b/app/src/ose/kotlin/at/bitfire/davdroid/di/OseFlavorModule.kt @@ -0,0 +1,52 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.di + +import at.bitfire.davdroid.ui.intro.OseIntroPageFactory + +import at.bitfire.davdroid.ui.AboutActivity +import at.bitfire.davdroid.ui.AccountsDrawerHandler +import at.bitfire.davdroid.ui.OpenSourceLicenseInfoProvider +import at.bitfire.davdroid.ui.OseAccountsDrawerHandler +import at.bitfire.davdroid.ui.intro.IntroPageFactory +import at.bitfire.davdroid.ui.setup.LoginTypesProvider +import at.bitfire.davdroid.ui.setup.StandardLoginTypesProvider +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.components.SingletonComponent + +interface OseModules { + + @Module + @InstallIn(ActivityComponent::class) + interface ForActivities { + @Binds + fun accountsDrawerHandler(impl: OseAccountsDrawerHandler): AccountsDrawerHandler + + @Binds + fun loginTypesProvider(impl: StandardLoginTypesProvider): LoginTypesProvider + } + + @Module + @InstallIn(ViewModelComponent::class) + interface ForViewModels { + @Binds + fun appLicenseInfoProvider(impl: OpenSourceLicenseInfoProvider): AboutActivity.AppLicenseInfoProvider + + @Binds + fun loginTypesProvider(impl: StandardLoginTypesProvider): LoginTypesProvider + } + + @Module + @InstallIn(SingletonComponent::class) + interface Global { + @Binds + fun introPageFactory(impl: OseIntroPageFactory): IntroPageFactory + } + +} \ No newline at end of file diff --git a/app/src/ose/kotlin/at/bitfire/davdroid/ui/OpenSourceLicenseInfoProvider.kt b/app/src/ose/kotlin/at/bitfire/davdroid/ui/OpenSourceLicenseInfoProvider.kt new file mode 100644 index 0000000..a076d63 --- /dev/null +++ b/app/src/ose/kotlin/at/bitfire/davdroid/ui/OpenSourceLicenseInfoProvider.kt @@ -0,0 +1,70 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import android.app.Application +import android.text.Spanned +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.tooling.preview.Preview +import androidx.core.text.HtmlCompat +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel +import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString +import com.google.common.io.CharStreams +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class OpenSourceLicenseInfoProvider @Inject constructor(): AboutActivity.AppLicenseInfoProvider { + + @Composable + override fun LicenseInfo() { + LicenseInfoGpl() + } + + @Composable + fun LicenseInfoGpl( + model: Model = viewModel() + ) { + model.gpl?.let { OpenSourceLicenseInfo(it.toAnnotatedString()) } + } + + + @HiltViewModel + class Model @Inject constructor(app: Application): AndroidViewModel(app) { + + var gpl by mutableStateOf(null) + + init { + viewModelScope.launch(Dispatchers.IO) { + app.resources.assets.open("gplv3.html").use { inputStream -> + val raw = CharStreams.toString(inputStream.bufferedReader()) + gpl = HtmlCompat.fromHtml(raw, HtmlCompat.FROM_HTML_MODE_LEGACY) + } + } + } + + } + +} + + +@Composable +fun OpenSourceLicenseInfo(license: AnnotatedString) { + Text(text = license) +} + +@Composable +@Preview +fun OpenSourceLicenseInfo_Preview() { + OpenSourceLicenseInfo(AnnotatedString("It's open-source.")) +} \ No newline at end of file diff --git a/app/src/ose/kotlin/at/bitfire/davdroid/ui/ThemeColors.kt b/app/src/ose/kotlin/at/bitfire/davdroid/ui/ThemeColors.kt new file mode 100644 index 0000000..9e6d250 --- /dev/null +++ b/app/src/ose/kotlin/at/bitfire/davdroid/ui/ThemeColors.kt @@ -0,0 +1,167 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui + +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +@Suppress("MemberVisibilityCanBePrivate") +object M3ColorScheme { + + // All colors hand-crafted because Material Theme Builder generates unbelievably ugly colors + + val primaryLight = Color(0xFF7cb342) + val onPrimaryLight = Color(0xFFffffff) + val primaryContainerLight = Color(0xFFb4e47d) + val onPrimaryContainerLight = Color(0xFF232d18) + val secondaryLight = Color(0xFFff7f2a) + val onSecondaryLight = Color(0xFFFFFFFF) + val secondaryContainerLight = Color(0xFFffa565) + val onSecondaryContainerLight = Color(0xFF3a271b) + val tertiaryLight = Color(0xFF658a24) + val onTertiaryLight = Color(0xFFFFFFFF) + val tertiaryContainerLight = Color(0xFFb0d08e) + val onTertiaryContainerLight = Color(0xFF263015) + val errorLight = Color(0xFFd71717) + val onErrorLight = Color(0xFFFFFFFF) + val errorContainerLight = Color(0xFFefb6b6) + val onErrorContainerLight = Color(0xFF3a0b0b) + val backgroundLight = Color(0xFFfcfcfc) + val onBackgroundLight = Color(0xFF2a2a2a) + val surfaceLight = Color(0xFFf5f5f5) + val onSurfaceLight = Color(0xFF4d4d4d) + val surfaceVariantLight = Color(0xFFe4e4e4) + val onSurfaceVariantLight = Color(0xFF2a2a2a) + val outlineLight = Color(0xFF838383) + val outlineVariantLight = Color(0xFFd4d4d4) + val scrimLight = Color(0xFF000000) + val inverseSurfaceLight = Color(0xFF2e322b) + val inverseOnSurfaceLight = Color(0xFFfafaf8) + val inversePrimaryLight = Color(0xFFb4e47d) + val surfaceDimLight = Color(0xFFe3e3e3) + val surfaceBrightLight = Color(0xFFf9f9f9) + val surfaceContainerLowestLight = Color(0xFFFFFFFF) + val surfaceContainerLowLight = Color(0xFFfafafa) + val surfaceContainerLight = Color(0xFFf5f5f5) + val surfaceContainerHighLight = Color(0xFFf0f0ef) + val surfaceContainerHighestLight = Color(0xFFebebea) + + val primaryDark = Color(0xFFc4e3a4) + val onPrimaryDark = Color(0xFF2b4310) + val primaryContainerDark = Color(0xFF7cb342) + val onPrimaryContainerDark = Color(0xFFedf5e4) + val secondaryDark = Color(0xFFe5c3ac) + val onSecondaryDark = Color(0xFF3e332e) + val secondaryContainerDark = Color(0xFFff7f2a) + val onSecondaryContainerDark = Color(0xFFffeadb) + val tertiaryDark = Color(0xFFc6e597) + val onTertiaryDark = Color(0xFF4b661b) + val tertiaryContainerDark = Color(0xFF658a24) + val onTertiaryContainerDark = Color(0xFFf0f8e2) + val errorDark = Color(0xFFf6d0d0) + val onErrorDark = Color(0xFF4f1212) + val errorContainerDark = Color(0xFFe93434) + val onErrorContainerDark = Color(0xFFfcdede) + val backgroundDark = Color(0xFF1a1a1a) + val onBackgroundDark = Color(0xFFf0f0f0) + val surfaceDark = Color(0xFF292929) + val onSurfaceDark = Color(0xFFdedede) + val surfaceVariantDark = Color(0xFF363636) + val onSurfaceVariantDark = Color(0xFFededed) + val outlineDark = Color(0xFFa3a3a3) + val outlineVariantDark = Color(0xFF7cb342) + val scrimDark = Color(0xFF000000) + val inverseSurfaceDark = Color(0xFFdbdbdb) + val inverseOnSurfaceDark = Color(0xFF292929) + val inversePrimaryDark = Color(0xFF7cb342) + val surfaceDimDark = Color(0xFF333333) + val surfaceBrightDark = Color(0xFF4d4d4d) + val surfaceContainerLowestDark = Color(0xFF141414) + val surfaceContainerLowDark = Color(0xFF1f1f1f) + val surfaceContainerDark = Color(0xff3a3a3a) + val surfaceContainerHighDark = Color(0xFF383838) + val surfaceContainerHighestDark = Color(0xFF434343) + + + // Copied from Material Theme Builder: Theme.kt + + val lightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, + ) + + val darkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, + ) + +} \ No newline at end of file diff --git a/app/src/ose/kotlin/at/bitfire/davdroid/ui/intro/OseIntroPageFactory.kt b/app/src/ose/kotlin/at/bitfire/davdroid/ui/intro/OseIntroPageFactory.kt new file mode 100644 index 0000000..fd78cc2 --- /dev/null +++ b/app/src/ose/kotlin/at/bitfire/davdroid/ui/intro/OseIntroPageFactory.kt @@ -0,0 +1,24 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.intro + +import javax.inject.Inject + +class OseIntroPageFactory @Inject constructor( + batteryOptimizationsPage: BatteryOptimizationsPage, + openSourcePage: OpenSourcePage, + permissionsIntroPage: PermissionsIntroPage, + tasksIntroPage: TasksIntroPage +): IntroPageFactory { + + override val introPages = arrayOf( + WelcomePage(), + tasksIntroPage, + permissionsIntroPage, + batteryOptimizationsPage, + openSourcePage + ) + +} \ No newline at end of file diff --git a/app/src/ose/kotlin/at/bitfire/davdroid/ui/setup/StandardLoginTypePage.kt b/app/src/ose/kotlin/at/bitfire/davdroid/ui/setup/StandardLoginTypePage.kt new file mode 100644 index 0000000..19e8b74 --- /dev/null +++ b/app/src/ose/kotlin/at/bitfire/davdroid/ui/setup/StandardLoginTypePage.kt @@ -0,0 +1,121 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.text.HtmlCompat +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.ExternalUris +import at.bitfire.davdroid.ui.ExternalUris.withStatParams +import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString +import at.bitfire.davdroid.ui.composable.Assistant + +@Composable +fun StandardLoginTypePage( + selectedLoginType: LoginType, + onSelectLoginType: (LoginType) -> Unit, + + @Suppress("unused") // for build variants + setInitialLoginInfo: (LoginInfo) -> Unit, + + onContinue: () -> Unit = {} +) { + Assistant( + nextLabel = stringResource(R.string.login_continue), + nextEnabled = true, + onNext = onContinue + ) { + Column(Modifier.padding(8.dp)) { + Text( + stringResource(R.string.login_generic_login), + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.padding(vertical = 8.dp) + ) + for (type in StandardLoginTypesProvider.genericLoginTypes) + LoginTypeSelector( + title = stringResource(type.title), + selected = type == selectedLoginType, + onSelect = { onSelectLoginType(type) } + ) + + Text( + stringResource(R.string.login_provider_login), + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.padding(top = 16.dp, bottom = 8.dp) + ) + for (type in StandardLoginTypesProvider.specificLoginTypes) + LoginTypeSelector( + title = stringResource(type.title), + selected = type == selectedLoginType, + onSelect = { onSelectLoginType(type) } + ) + + HorizontalDivider(Modifier.padding(vertical = 12.dp)) + + val privacyPolicy = ExternalUris.Homepage.baseUrl.buildUpon() + .appendPath(ExternalUris.Homepage.PATH_PRIVACY) + .withStatParams("StandardLoginTypePage") + .build().toString() + val privacy = HtmlCompat.fromHtml( + stringResource(R.string.login_privacy_hint, stringResource(R.string.app_name), privacyPolicy), + HtmlCompat.FROM_HTML_MODE_COMPACT) + Text( + text = privacy.toAnnotatedString(), + style = MaterialTheme.typography.bodyMedium + ) + } + } +} + +@Composable +fun LoginTypeSelector( + title: String, + selected: Boolean, + onSelect: () -> Unit = {} +) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable(onClick = onSelect) + .padding(bottom = 4.dp) + ) { + RadioButton( + selected = selected, + onClick = onSelect + ) + Text( + title, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f) + ) + } + } +} + + +@Composable +@Preview +fun StandardLoginTypePage_Preview() { + StandardLoginTypePage( + selectedLoginType = StandardLoginTypesProvider.genericLoginTypes.first(), + onSelectLoginType = {}, + setInitialLoginInfo = {}, + onContinue = {} + ) +} \ No newline at end of file diff --git a/app/src/ose/kotlin/at/bitfire/davdroid/ui/setup/StandardLoginTypesProvider.kt b/app/src/ose/kotlin/at/bitfire/davdroid/ui/setup/StandardLoginTypesProvider.kt new file mode 100644 index 0000000..1642a61 --- /dev/null +++ b/app/src/ose/kotlin/at/bitfire/davdroid/ui/setup/StandardLoginTypesProvider.kt @@ -0,0 +1,66 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.ui.setup + +import android.content.Intent +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import at.bitfire.davdroid.ui.setup.LoginTypesProvider.LoginAction +import java.util.logging.Logger +import javax.inject.Inject + +class StandardLoginTypesProvider @Inject constructor( + private val logger: Logger +) : LoginTypesProvider { + + companion object { + val genericLoginTypes = listOf( + UrlLogin, + EmailLogin, + AdvancedLogin + ) + + val specificLoginTypes = listOf( + FastmailLogin, + GoogleLogin, + NextcloudLogin + ) + } + + override val defaultLoginType = UrlLogin + + override fun intentToInitialLoginType(intent: Intent): LoginAction = + intent.data?.normalizeScheme().let { uri -> + when { + intent.hasExtra(LoginActivity.EXTRA_LOGIN_FLOW) -> + LoginAction(NextcloudLogin, true) + uri?.scheme == "mailto" -> + LoginAction(EmailLogin, true) + listOf("caldavs", "carddavs", "davx5", "http", "https").any { uri?.scheme == it } -> + LoginAction(UrlLogin, true) + else -> { + logger.warning("Did not understand login intent: $intent") + LoginAction(defaultLoginType, false) // Don't skip login type page if intent is unclear + } + } + } + + @Composable + override fun LoginTypePage( + snackbarHostState: SnackbarHostState, + selectedLoginType: LoginType, + onSelectLoginType: (LoginType) -> Unit, + setInitialLoginInfo: (LoginInfo) -> Unit, + onContinue: () -> Unit + ) { + StandardLoginTypePage( + selectedLoginType = selectedLoginType, + onSelectLoginType = onSelectLoginType, + setInitialLoginInfo = setInitialLoginInfo, + onContinue = onContinue + ) + } + +} \ No newline at end of file diff --git a/app/src/test/kotlin/at/bitfire/davdroid/DavUtilsTest.kt b/app/src/test/kotlin/at/bitfire/davdroid/DavUtilsTest.kt new file mode 100644 index 0000000..0b96e84 --- /dev/null +++ b/app/src/test/kotlin/at/bitfire/davdroid/DavUtilsTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid + +import at.bitfire.davdroid.util.DavUtils +import at.bitfire.davdroid.util.DavUtils.lastSegment +import at.bitfire.davdroid.util.DavUtils.parent +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Test + +class DavUtilsTest { + + @Test + fun testAcceptAnything() { + assertEquals("*/*", DavUtils.acceptAnything(null)) + assertEquals("some/thing;v=2.1, */*;q=0.8", DavUtils.acceptAnything("some/thing;v=2.1".toMediaType())) + } + + @Test + fun testARGBtoCalDAVColor() { + assertEquals("#00000000", DavUtils.ARGBtoCalDAVColor(0)) + assertEquals("#123456FF", DavUtils.ARGBtoCalDAVColor(0xFF123456.toInt())) + assertEquals("#000000FF", DavUtils.ARGBtoCalDAVColor(0xFF000000.toInt())) + } + + + @Test + fun testHttpUrl_LastSegment() { + val exampleURL = "http://example.com/" + Assert.assertEquals("/", exampleURL.toHttpUrl().lastSegment) + Assert.assertEquals("dir", (exampleURL + "dir").toHttpUrl().lastSegment) + Assert.assertEquals("dir", (exampleURL + "dir/").toHttpUrl().lastSegment) + Assert.assertEquals("file.html", (exampleURL + "dir/file.html").toHttpUrl().lastSegment) + } + + @Test + fun testHttpUrl_Parent() { + // with trailing slash + assertEquals("http://example.com/1/2/".toHttpUrl(), "http://example.com/1/2/3/".toHttpUrl().parent()) + assertEquals("http://example.com/1/".toHttpUrl(), "http://example.com/1/2/".toHttpUrl().parent()) + assertEquals("http://example.com/".toHttpUrl(), "http://example.com/1/".toHttpUrl().parent()) + assertEquals("http://example.com/".toHttpUrl(), "http://example.com/".toHttpUrl().parent()) + + // without trailing slash + assertEquals("http://example.com/1/2/".toHttpUrl(), "http://example.com/1/2/3".toHttpUrl().parent()) + assertEquals("http://example.com/1/".toHttpUrl(), "http://example.com/1/2".toHttpUrl().parent()) + assertEquals("http://example.com/".toHttpUrl(), "http://example.com/1".toHttpUrl().parent()) + assertEquals("http://example.com/".toHttpUrl(), "http://example.com".toHttpUrl().parent()) + } + +} diff --git a/app/src/test/kotlin/at/bitfire/davdroid/log/StringHandlerTest.kt b/app/src/test/kotlin/at/bitfire/davdroid/log/StringHandlerTest.kt new file mode 100644 index 0000000..09150fc --- /dev/null +++ b/app/src/test/kotlin/at/bitfire/davdroid/log/StringHandlerTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.log + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.lang.System.lineSeparator +import java.util.logging.Formatter +import java.util.logging.Level +import java.util.logging.LogRecord + +class StringHandlerTest { + + @Test + fun test_logSomeText() { + val handler = StringHandler(1000) + handler.publish(LogRecord(Level.INFO, "Line 1")) + handler.publish(LogRecord(Level.FINEST, "Line 2")) + val str = handler.toString() + assertTrue(str.contains("Line 1${lineSeparator()}")) + assertTrue(str.contains("Line 2${lineSeparator()}")) + } + + @Test + fun test_logSomeText_ExceedingMaxSize() { + val handler = StringHandler(10).apply { + formatter = object: Formatter() { + override fun format(record: LogRecord) = record.message + } + } + handler.publish(LogRecord(Level.INFO, "Line 1 Line 1 Line 1 Line 1 Line 1")) + handler.publish(LogRecord(Level.FINEST, "Line 2")) + + val str = handler.toString() + assertEquals(10, handler.toString().length) + assertEquals("Line [...]", str) + } + +} \ No newline at end of file diff --git a/app/src/test/kotlin/at/bitfire/davdroid/network/MemoryCookieStoreTest.kt b/app/src/test/kotlin/at/bitfire/davdroid/network/MemoryCookieStoreTest.kt new file mode 100644 index 0000000..4a8e130 --- /dev/null +++ b/app/src/test/kotlin/at/bitfire/davdroid/network/MemoryCookieStoreTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.network + +import okhttp3.Cookie +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.Assert.assertArrayEquals +import org.junit.Before +import org.junit.Test + +class MemoryCookieStoreTest { + + lateinit var store: MemoryCookieStore + + @Before + fun setup() { + store = MemoryCookieStore() + } + + + @Test + fun testSaveFromResponse_AndRead() { + val url = "https://example.com/path".toHttpUrl() + val cookie = Cookie.Builder() + .name("cookie1") + .value("value1") + .domain("example.com") + .path("/path") + .build() + store.saveFromResponse(url, listOf(cookie)) + assertArrayEquals( + arrayOf(cookie), + store.loadForRequest(url).toTypedArray() + ) + } + + @Test + fun testSaveFromResponse_Overwrite_AndRead() { + val url = "https://example.com/path".toHttpUrl() + store.saveFromResponse(url, listOf( + Cookie.Builder() + .name("cookie1") + .value("first value") + .domain("example.com") + .path("/path") + .build() + )) + + val updatedCookie = Cookie.Builder() + .name("cookie1") + .value("updated value") + .domain("example.com") + .path("/path") + .build() + store.saveFromResponse(url, listOf(updatedCookie)) + assertArrayEquals( + arrayOf(updatedCookie), + store.loadForRequest(url).toTypedArray() + ) + } + + + @Test + fun testLoadForRequest_SubPath() { + val url = "https://example.com/path".toHttpUrl() + val cookie = Cookie.Builder() + .name("cookie1") + .value("value1") + .domain("example.com") + .path("/path") + .build() + store.saveFromResponse(url, listOf(cookie)) + + assertArrayEquals( + arrayOf(cookie), + store.loadForRequest(url.newBuilder() + .addPathSegment("sub-path") + .build()).toTypedArray() + ) + } + +} \ No newline at end of file diff --git a/app/src/test/kotlin/at/bitfire/davdroid/sync/SyncExceptionTest.kt b/app/src/test/kotlin/at/bitfire/davdroid/sync/SyncExceptionTest.kt new file mode 100644 index 0000000..8f7bff0 --- /dev/null +++ b/app/src/test/kotlin/at/bitfire/davdroid/sync/SyncExceptionTest.kt @@ -0,0 +1,206 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import at.bitfire.davdroid.resource.LocalResource +import io.mockk.mockk +import okhttp3.HttpUrl +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class SyncExceptionTest { + + @Test + fun testWrapWithLocalResource_LocalResource_Exception() { + val outer = mockk>() + val inner = mockk>() + val e = Exception() + + val result = assertSyncException { + SyncException.wrapWithLocalResource(outer) { + SyncException.wrapWithLocalResource(inner) { + throw e + } + } + } + + assertEquals(inner, result.localResource) + assertEquals(e, result.cause) + } + + @Test + fun testWrapWithLocalResource_LocalResource_SyncException() { + val outer = mockk>() + val inner = mockk>() + val e = SyncException(Exception()) + + val result = assertSyncException { + SyncException.wrapWithLocalResource(outer) { + SyncException.wrapWithLocalResource(inner) { + throw e + } + } + } + + assertEquals(inner, result.localResource) + assertEquals(e, result) + } + + @Test + fun testWrapWithLocalResource_RemoteResource_Exception() { + val local = mockk>() + val remote = mockk() + val e = Exception() + + val result = assertSyncException { + SyncException.wrapWithLocalResource(local) { + SyncException.wrapWithRemoteResource(remote) { + throw e + } + } + } + + assertEquals(local, result.localResource) + assertEquals(remote, result.remoteResource) + assertEquals(e, result.cause) + } + + @Test + fun testWrapWithLocalResource_RemoteResource_SyncException() { + val local = mockk>() + val remote = mockk() + val e = SyncException(Exception()) + + val result = assertSyncException { + SyncException.wrapWithLocalResource(local) { + SyncException.wrapWithRemoteResource(remote) { + throw e + } + } + } + + assertEquals(local, result.localResource) + assertEquals(remote, result.remoteResource) + assertEquals(e, result) + } + + + @Test + fun testWrapWithRemoteResource_LocalResource_Exception() { + val remote = mockk() + val local = mockk>() + val e = Exception() + + val result = assertSyncException { + SyncException.wrapWithRemoteResource(remote) { + SyncException.wrapWithLocalResource(local) { + throw e + } + } + } + + assertEquals(local, result.localResource) + assertEquals(remote, result.remoteResource) + assertEquals(e, result.cause) + } + + @Test + fun testWrapWithRemoteResource_LocalResource_SyncException() { + val remote = mockk() + val local = mockk>() + val e = SyncException(Exception()) + + val result = assertSyncException { + SyncException.wrapWithRemoteResource(remote) { + SyncException.wrapWithLocalResource(local) { + throw e + } + } + } + + assertEquals(local, result.localResource) + assertEquals(remote, result.remoteResource) + assertEquals(e, result) + } + + @Test + fun testWrapWithRemoteResource_RemoteResource_Exception() { + val outer = mockk() + val inner = mockk() + val e = Exception() + + val result = assertSyncException { + SyncException.wrapWithRemoteResource(outer) { + SyncException.wrapWithRemoteResource(inner) { + throw e + } + } + } + + assertEquals(inner, result.remoteResource) + assertEquals(e, result.cause) + } + + @Test + fun testWrapWithRemoteResource_RemoteResource_SyncException() { + val outer = mockk() + val inner = mockk() + val e = SyncException(Exception()) + + val result = assertSyncException { + SyncException.wrapWithRemoteResource(outer) { + SyncException.wrapWithRemoteResource(inner) { + throw e + } + } + } + + assertEquals(inner, result.remoteResource) + assertEquals(e, result) + } + + + @Test + fun testUnwrap_Exception() { + val e = Exception() + + var contextProvided = false + val unwrapped = SyncException.unwrap(e) { + contextProvided = true + } + assertEquals(e, unwrapped) + assertFalse(contextProvided) + } + + @Test + fun testUnwrap_SyncException() { + val e = Exception() + val wrapped = SyncException(e) + + var contextProvided = false + val unwrapped = SyncException.unwrap(wrapped) { + assertEquals(wrapped, it) + contextProvided = true + } + assertEquals(e, unwrapped) + assertTrue(contextProvided) + } + + + // helpers + + fun assertSyncException(block: () -> Unit): SyncException { + try { + block() + } catch(ex: Throwable) { + if (ex is SyncException) + return ex + } + throw AssertionError("Expected SyncException") + } + +} \ No newline at end of file diff --git a/app/src/test/kotlin/at/bitfire/davdroid/util/SensitiveStringTest.kt b/app/src/test/kotlin/at/bitfire/davdroid/util/SensitiveStringTest.kt new file mode 100644 index 0000000..7ce8259 --- /dev/null +++ b/app/src/test/kotlin/at/bitfire/davdroid/util/SensitiveStringTest.kt @@ -0,0 +1,49 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.util + +import at.bitfire.davdroid.util.SensitiveString.Companion.toSensitiveString +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class SensitiveStringTest { + + private data class UsernameAndPassword( + val username: String, + val password: SensitiveString + ) + + @Test + fun `equals (other object)`() { + val password = "some-password".toSensitiveString() + assertFalse(password == Any()) + } + + @Test + fun `equals (other password)`() { + val password = "some-password".toSensitiveString() + assertFalse(password == "other-password".toSensitiveString()) + } + + @Test + fun `equals (same password)`() { + val password = "some-password".toSensitiveString() + assertTrue(password == "some-password".toSensitiveString()) + } + + @Test + fun `toString in data class`() { + val credentials = UsernameAndPassword( + "some-user", + "some-password".toSensitiveString() + ) + + val logMessage = "Credentials: $credentials" + assertEquals("Credentials: UsernameAndPassword(username=some-user, password=*****)", logMessage) + } + +} \ No newline at end of file diff --git a/app/src/test/kotlin/at/bitfire/davdroid/util/StringUtilsTest.kt b/app/src/test/kotlin/at/bitfire/davdroid/util/StringUtilsTest.kt new file mode 100644 index 0000000..525da43 --- /dev/null +++ b/app/src/test/kotlin/at/bitfire/davdroid/util/StringUtilsTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.util + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class StringUtilsTest { + + @Test + fun trimToNull_Empty() { + assertNull("".trimToNull()) + } + + @Test + fun trimToNull_NoWhitespace() { + assertEquals("test", "test".trimToNull()) + } + + @Test + fun trimToNull_Null() { + assertNull(null.trimToNull()) + } + + @Test + fun trimToNull_PaddedWithWhitespace() { + assertEquals("test", "\r\n test ".trimToNull()) + } + + + @Test + fun withTrailingSlash_WithSlash() { + assertEquals("test/", "test/".withTrailingSlash()) + } + + @Test + fun withTrailingSlash_WithoutSlash() { + assertEquals("test/", "test".withTrailingSlash()) + } + +} \ No newline at end of file diff --git a/app/src/test/kotlin/at/bitfire/davdroid/webdav/DiskCacheTest.kt b/app/src/test/kotlin/at/bitfire/davdroid/webdav/DiskCacheTest.kt new file mode 100644 index 0000000..1d05e7b --- /dev/null +++ b/app/src/test/kotlin/at/bitfire/davdroid/webdav/DiskCacheTest.kt @@ -0,0 +1,114 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav + +import android.os.FileUtils +import at.bitfire.davdroid.webdav.cache.DiskCache +import com.google.common.io.ByteStreams +import com.google.common.io.Files +import ezvcard.util.IOUtils +import org.junit.After +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class DiskCacheTest { + + companion object { + const val SOME_KEY = "key1" + const val SOME_VALUE_LENGTH = 15 + val SOME_VALUE = ByteArray(SOME_VALUE_LENGTH) { it.toByte() } + val SOME_OTHER_VALUE = ByteArray(30) { (it/2).toByte() } + + const val MAX_CACHE_MB = 10 + const val MAX_CACHE_SIZE = MAX_CACHE_MB * 1024*1024L + } + + @Rule + @JvmField + val tempDir = TemporaryFolder() + + lateinit var cache: DiskCache + + + @Before + fun createCache() { + cache = DiskCache(tempDir.newFolder(), MAX_CACHE_SIZE) + } + + @After + fun deleteCache() { + assertTrue(cache.cacheDir.deleteRecursively()) + } + + + @Test + fun testGetFile_Null() { + assertNull(cache.getFileOrPut(SOME_KEY) { null }) + + // null value shouldn't have been written to cache + assertEquals(0, cache.entries()) + cache.getFileOrPut(SOME_KEY) { SOME_VALUE }!!.let { + assertArrayEquals(SOME_VALUE, Files.asByteSource(it).read()) + } + } + + @Test + fun testGetFile_NotNull() { + cache.getFileOrPut(SOME_KEY) { SOME_VALUE }!!.let { + assertArrayEquals(SOME_VALUE, Files.asByteSource(it).read()) + } + + // non-null value should have been written to cache + assertEquals(1, cache.entries()) + cache.getFileOrPut(SOME_KEY) { SOME_OTHER_VALUE }!!.let { + assertArrayEquals(SOME_VALUE, Files.asByteSource(it).read()) + } + } + + + @Test + fun testClear() { + for (i in 1..50) { + cache.getFileOrPut(i.toString()) { i.toString().toByteArray() } + } + assertEquals(50, cache.entries()) + + cache.clear() + assertEquals(0, cache.entries()) + } + + + @Test + fun testTrim() { + assertEquals(0, cache.entries()) + + cache.getFileOrPut(SOME_KEY) { SOME_VALUE } + assertEquals(1, cache.entries()) + + cache.trim() + assertEquals(1, cache.entries()) + + // add 11 x 1 MB + for (i in 0..MAX_CACHE_MB) { + cache.getFileOrPut(i.toString()) { ByteArray(1024*1024) } + Thread.sleep(5) // make sure that files are exactly sortable by modification date + } + // now in cache: SOME_KEY (some bytes) and "0" .. "10" (1 MB each), i.e. 11 MB + some bytes in total + assertEquals(MAX_CACHE_MB+2, cache.entries()) + + // trim() should remove the oldest entries (SOME_KEY and "0") to trim to 10 MB + assertEquals(2, cache.trim()) + + // now in cache: "1" .. "10" = 10 MB + assertEquals((1..MAX_CACHE_MB).map { it.toString() }.toSet(), cache.keys().toSet()) + } + +} \ No newline at end of file diff --git a/app/src/test/kotlin/at/bitfire/davdroid/webdav/DocumentSortByMapperTest.kt b/app/src/test/kotlin/at/bitfire/davdroid/webdav/DocumentSortByMapperTest.kt new file mode 100644 index 0000000..7bbccf2 --- /dev/null +++ b/app/src/test/kotlin/at/bitfire/davdroid/webdav/DocumentSortByMapperTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav + +import android.provider.DocumentsContract.Document +import junit.framework.TestCase.assertEquals +import org.junit.Test +import java.util.logging.Logger + +class DocumentSortByMapperTest { + + private val mapper = DocumentSortByMapper(Logger.getGlobal()) + + @Test + fun test_MapContentProviderToSql() { + // Valid column without direction + assertEquals( + "displayName ASC", + mapper.mapContentProviderToSql(Document.COLUMN_DISPLAY_NAME) + ) + // Valid column with direction + assertEquals( + "displayName DESC", + mapper.mapContentProviderToSql("${Document.COLUMN_DISPLAY_NAME} DESC") + ) + // Valid column with direction and multiple spaces + assertEquals( + "displayName ASC", + mapper.mapContentProviderToSql("${Document.COLUMN_DISPLAY_NAME} ASC") + ) + // Invalid column without direction + assertEquals( + "displayName ASC", + mapper.mapContentProviderToSql("invalid") + ) + // Invalid column with direction + assertEquals( + "displayName ASC", + mapper.mapContentProviderToSql("invalid ASC") + ) + // Valid and invalid columns with and without directions + assertEquals( + "displayName ASC, mimeType DESC", + mapper.mapContentProviderToSql( + "${Document.COLUMN_DISPLAY_NAME}, invalid DESC, ${Document.COLUMN_MIME_TYPE} DESC" + ) + ) + } + +} \ No newline at end of file diff --git a/app/src/test/kotlin/at/bitfire/davdroid/webdav/PagingReaderTest.kt b/app/src/test/kotlin/at/bitfire/davdroid/webdav/PagingReaderTest.kt new file mode 100644 index 0000000..3a300a6 --- /dev/null +++ b/app/src/test/kotlin/at/bitfire/davdroid/webdav/PagingReaderTest.kt @@ -0,0 +1,209 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.webdav + +import com.google.common.cache.CacheBuilder +import com.google.common.cache.CacheLoader +import com.google.common.cache.LoadingCache +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + In the context of these tests, a "small file" is a file that is smaller + than the max page size. A "page-sized file" is a file that is exactly as + large as the page size. A "large file" is a file that is larger than the page + size, i.e. a file that comprises at least two pages. + */ +class PagingReaderTest { + + @Test + fun testRead_AcrossThreePages() { + var idx = 0 + val reader = pagingReader(350, 100) { offset, size -> + assertEquals(idx * 100L, offset) + assertEquals( + when (idx) { + 0, 1, 2 -> 100 + 3 -> 50 + else -> throw AssertionError("idx=$idx, size=$size") + }, + size + ) + idx += 1 + ByteArray(size) { idx.toByte() } + } + val dst = ByteArray(103) + assertEquals(103, reader.read(99, 103, dst)) + assertArrayEquals( + ByteArray(1) { 1 } + ByteArray(100) { 2 } + ByteArray(2) { 3 }, + dst + ) + } + + @Test + fun testRead_AtBeginning_FewBytes() { + val reader = pagingReader(200, 100) { offset, size -> + assertEquals(0, offset) + assertEquals(100, size) + ByteArray(100) { 1 } + } + val dst = ByteArray(10) + assertEquals(10, reader.read(0, 10, dst)) + assertArrayEquals(ByteArray(10) { 1 }, dst) + } + + @Test + fun testRead_AtEOF() { + val reader = pagingReader(200, 100) { _, _ -> + throw AssertionError("Must not be called with size=0") + } + assertEquals(0, reader.read(200, 10, ByteArray(10))) + } + + + @Test + fun testReadPage_LargeFile_FromMid_ToMid() { + val reader = pagingReader(200, 100) { offset, size -> + assertEquals(0, offset) + assertEquals(100, size) + ByteArray(100) { 1 } + } + val dst = ByteArray(10) + assertEquals(10, reader.readPage(50, 10, dst, 0)) + assertArrayEquals(ByteArray(10) { 1 }, dst) + } + + @Test + fun testReadPage_LargeFile_FromMid_BeyondPage() { + val reader = pagingReader(200, 100) { offset, size -> + assertEquals(0, offset) + assertEquals(100, size) + ByteArray(100) { 1 } + } + val dst = ByteArray(100) + assertEquals(50, reader.readPage(50, 100, dst, 0)) + assertArrayEquals(ByteArray(50) { 1 }, dst.copyOfRange(0, 50)) + } + + @Test + fun testReadPage_LargeFile_FromStart_LessThanAPage() { + val reader = pagingReader(200, 100) { offset, size -> + assertEquals(0, offset) + assertEquals(100, size) + ByteArray(100) { 1 } + } + val dst = ByteArray(10) + assertEquals(10, reader.readPage(0, 10, dst, 0)) + assertArrayEquals(ByteArray(10) { 1 }, dst) + } + + @Test + fun testReadPage_LargeFile_FromStart_OnePage() { + val reader = pagingReader(200, 100) { offset, size -> + assertEquals(0, offset) + assertEquals(100, size) + ByteArray(100) { 1 } + } + val dst = ByteArray(100) + assertEquals(100, reader.readPage(0, 100, dst, 0)) + assertArrayEquals(ByteArray(100) { 1 }, dst) + } + + @Test + fun testReadPage_LargeFile_FromStart_MoreThanAvailable() { + val reader = pagingReader(200, 100) { offset, size -> + assertEquals(0, offset) + assertEquals(100, size) + ByteArray(100) { 1 } + } + val dst = ByteArray(200) + assertEquals(100, reader.readPage(0, 200, dst, 100)) + assertArrayEquals(ByteArray(100) { 1 }, dst.copyOfRange(100, 200)) + } + + + @Test + fun testReadPage_PageSizedFile_FromEnd() { + val reader = pagingReader(100, 100) { _, _ -> + throw AssertionError() + } + val dst = ByteArray(100) + assertEquals(0, reader.readPage(100, 100, dst, 0)) + assertArrayEquals(ByteArray(100), dst) + } + + @Test + fun testReadPage_PageSizedFile_FromStart_Complete() { + val reader = pagingReader(100, 100) { offset, size -> + assertEquals(0, offset) + assertEquals(100, size) + ByteArray(100) { 1 } + } + val dst = ByteArray(100) + assertEquals(100, reader.readPage(0, 100, dst, 0)) + assertArrayEquals(ByteArray(100) { 1 }, dst) + } + + @Test + fun testReadPage_PageSizedFile_FromStart_MoreThanAvailable() { + val reader = pagingReader(100, 100) { offset, size -> + assertEquals(0, offset) + assertEquals(100, size) + ByteArray(100) { 1 } + } + val dst = ByteArray(200) + assertEquals(100, reader.readPage(0, 200, dst, 100)) + assertArrayEquals(ByteArray(100) { 1 }, dst.copyOfRange(100, 200)) + } + + + @Test + fun testReadPage_SmallFile_FromStart_Partial() { + val reader = pagingReader(100, 1000) { offset, size -> + assertEquals(0, offset) + assertEquals(100, size) + ByteArray(100) { 1 } + } + val dst = ByteArray(10) + assertEquals(10, reader.readPage(0, 10, dst, 0)) + assertArrayEquals(dst, ByteArray(10) { 1 }) + } + + @Test + fun testReadPage_SmallFile_FromStart_Complete() { + val reader = pagingReader(100, 1000) { offset, size -> + assertEquals(0, offset) + assertEquals(100, size) + ByteArray(100) { 1 } + } + val dst = ByteArray(100) + assertEquals(100, reader.readPage(0, 100, dst, 0)) + assertArrayEquals(ByteArray(100) { 1 }, dst) + } + + @Test + fun testReadPage_SmallFile_FromStart_MoreThanAvailable() { + val reader = pagingReader(100, 1000) { offset, size -> + assertEquals(0, offset) + assertEquals(100, size) + ByteArray(100) { 1 } + } + val dst = ByteArray(200) + assertEquals(100, reader.readPage(0, 200, dst, 100)) + assertArrayEquals(ByteArray(100) { 1 }, dst.copyOfRange(100, 200)) + } + + + private fun pageCache(loader: (offset: Long, size: Int) -> ByteArray): LoadingCache = + CacheBuilder.newBuilder() + .build(object: CacheLoader() { + override fun load(key: RandomAccessCallback.PageIdentifier) = loader(key.offset, key.size) + }) + + private fun pagingReader(fileSize: Long, pageSize: Int, loader: (offset: Long, size: Int) -> ByteArray) = + PagingReader(fileSize, pageSize, pageCache(loader)) + +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..e3102af --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,13 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.compose.compiler) apply false + alias(libs.plugins.hilt) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.ksp) apply false + + alias(libs.plugins.mikepenz.aboutLibraries.android) apply false +} \ No newline at end of file diff --git a/doc/.gitignore b/doc/.gitignore new file mode 100644 index 0000000..ba62ebb --- /dev/null +++ b/doc/.gitignore @@ -0,0 +1 @@ +javadoc/ diff --git a/doc/DAVdroid-Linuxwochen-2016.odp b/doc/DAVdroid-Linuxwochen-2016.odp new file mode 100644 index 0000000000000000000000000000000000000000..129f030014601a8b12671cea2c848481d0c96a24 GIT binary patch literal 14791 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 literal 0 HcmV?d00001 diff --git a/doc/NIST.SP.800-52r1.pdf b/doc/NIST.SP.800-52r1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0436449b71e70570288b64f2141d3d72ec73fea9 GIT binary patch literal 585486 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

N_NmQ-d{!yFB|n#(woFAx$T*%r_%i zC0!{^qUZIN=gm_NS_vKM5Dt1_pQbz_rBmCPlQAZ1%Y2%uy<( z?A>OOyn}}>!kBGSWf*Qgss4HKc41QdkeVqDhIe_37!JJmQ0%MFi&-nffbPJOEm1Us7r@HdYQn-_!G zU(ZKZ6&4EEGt9rF6k9dLJ9qkQnA^XnmZix4A9Lh0utgjR=s@Sn8qMEfC;Wc*f9Ib= z4D8mxDLrtXM-kb0^SwkguU(R7oYx1Q6QeLinO|_oGq$hIsgOujreX14RJA7501Jt> zytA$u_^P@3Ie{&xh<~aS1Sz_|+ML8a2q2xrQEU%s%vK!zU^@^OoLM4fY;ng9vkDwc zv4rl)xc6{>Xz`bXCciF-Ai}_#VYpt3tmsy^GHvTC&!@@{_hi*7Ajlr$kyM9}f`*(+ z6WEWaXMOW6@C1`OuAl+b7r$Mk7zMr%twS0|pAca}c<0thD#uKE6V$U&RW7Rfu_1L+ z7iryvxIX%$Yh3Bltl0z8wv{c}Tx&o?7O$E}Rg^%#G;{cxUSEKW-fy3{`nOM$T5WZM zo;Qa8F}%vqv(JcGhzjEMi|AgIEt+|tv`5vw>>>i4{|dgp&rsDv8eX;fvB#Zo4V-SV zMw4DPJIKPP!8<%sMMuPvXE)!Z5q!ScmvxrO0V&Ex+>{}p|Gjl8t37!sJ#k+9`ypIZ zT=R!D$8=rm$NiCQ;-90`vQd& zo(*gfQX=`yiL>xMh6P@>ea@3)-mOemj4dP2KGv$9P(s-+ah>1u1S{ym?$c6^;oOYv zk`b=psJwR9!7~>}X>a$?X##8R9I#Hx1^WOS>a>iGM1}!e&(VBND8E{-e)xtnsl5C3 zoQK8d4EK^D;meB*T(@oPuav975p)*7AoD+ksjO+BEfhm+_@Zi}Y=kTDUa4ovZDC+s zcKP>}8_oADMqI@!SP7*0<_0k>el(Qirs)VwYk9JSXaUOCQ%{Sn4&|r%2U+mG+?wCV z02?_JJJi@50u3PY%Rl`j1U+}+zA5H`;0G1LOyU6nid%@Cr*+`2q2z@n*t##JBX}!1 zGccx|y{MC)$7^`4Gixo72?-IwGH%SH_EmRfZzD~xwnI>^#(f`snh>>z&b9T`wfa<# ze`+{r+uuWR)jhQevPrZv`3dhT(2W@w;}ur~a*yFjYy6fNQFSNpgW;$(z;`!Ls+-VG zG2hg<2zyQ10{IVTA*Lg$@5BA>j-d{nGF>Jy-~^Oc&Cxhy?qh=D{t8{#ns#6?j$q@( zVI&g99njRCUL?^dFg)D}&1_};i~6<|qJ6rJ^_PAmkwrk-9w0b0KoQ41sip(vn~}CN z2}OoOb?ywFm2b#=#FP<*rW)TJiiMZ~KL7x5va?ge?a&H+CxbKlA`6ya-*~XhHa$|< z{Bj-pLb9J}$iZ)6Jt=U;Z;I-EazbZfAlR~CnI|jZYWvR$?4V!Yc_)#w6VIKoDlqeEI1 zfTk|w>iS7yqU=Tk!aVSAC7ut?W`8so(&rE@)rv zH%$65Yt}@fjaq>gnLzhIJMpK_K;08t*Io3-%r=im%|S!geGGxa-^;eF=Zf;?%P64>fn!)v!^l+aUggp9G%8b)c89juhDK|9T{M+N z4%X@}A3p;~imuU>HexYf%=kF5cKUaj`wkSVRI{@Lb##OTUD5uX-wQuv>j~+LTKAnk zk}(FK^GWM?Hn;Z^@4TzlVVHuiC<#}qiNKXAT-5cDg&WoI>^LZ}%()IkwL-38ca~>o zZtQ{-PMJcgJ+0){5o%lg9qq2n`;?a%x5Kl?q4qOY(8H-F%%73A>Ha}v1#$y!u1W0G@xvlrTGy2 z*RYWdL>5ZFSNdm-Ok%0NzgK*=nf6U}km*1H8!q(Z?MKz)9^ImnGvYhL?tDa<1s zR20=S=8I&l|HVJB;H90pG7);EVj|X@L35{#0I5-8azvLLH%L5KecW0o2kbaIsZfqO zZcI-HQjB?Tlub=EE4Z$uXhB=vgr7+Z8LF7$cJO?C9PL!)RUFT?U!^lmjuUc^P%+b6 z;51{@k)JG_Z`rLl`pTFsZ8}3Exh`rEUs8jDDkDg_a%wJY%s(NfAn>n_HlZYAG`#k! z*G+~I)J4+aJ>o1^F_$Zs@@0E0(I&8CE#wjVv~ka(dDd2zScSQjpGjU~R_r zMF129;nDT*>cL68hMa6pHWv@wX)M5N^(Qhlm&N428x)QBW$@??JE zdPsLpyG*7t>RTb8krFa+U$-mBQC=ql33i9PZ>n(~ioYkfV@^86xV!DaWTU?>=^kG< z(h*7*2o6-xmXOwwv{w7d?s!&x1q~Fc5bIvpSkc7#GKrwVJcl5|I>1~muK73|HR`rY zRiGv23&bO9H<@fxuKU%vUO#~qP_Y)_8ycXEuPiR=^8LYjQN z#V27}ckTc^+aElWkttR~kHzjV;HLEiGQ9y4dghvU1?8Xf&A*|wgc|erBDB~1gf)!a zkbm(uyg}k-{OL|)!X6JcxxFUol)a#bwktDozO=fi8&=+W7bXwO*{0r2u)H^3-DtYC zlNjVrMue+^eDWL^nc_ruVDBxLlfN+!-$|sRmA5wB@9IQF(Zm`~(gs&M{sfNQb>vBXvUr&9E~@y?>c@oOi{@KT7Z4&QalaLni zLJs6?N+!}7?6MHvJh|6Sgx5{x$}{m}y_h=bOu`Gs6Ew+E@T$qpX6@m`$Vv{PU zFU%&6T47imQ4UbmpFE7}YC`5a?RUHs1{h2%(Jb7)TgG?^`8N%-2v51=CQ>z&iHXzl zOMv1S8=g2@Eo)0EPD1m2c(4E}hb_TbC^5B&Ubg7&ItZj+}Mx1 z@EK#kWkFs%5vhBJy@ztaHB&c7y3xAfGiv#;52p^vs#uilIqNpa9E2v|6t9Pl7`_zR zh>=QS8;`6NOZoB%Dtr*lGE;up!noX7Ws3vt``T6~5QgjlUqZ*af9oK|<%5zmkT5ZL z$>Qqg17H=qO{P?AB0_X39$*-k4xe^y@?Gm?CAGvoi6S?$2#PqoyzRzoRS{wBrrejdFrl;a^ zkT3&?G)Sc~_qDgF#sh3bo|JJ~9YE%$rC?2?E_lYcZV7G2MYkGkV*Bh05-JJ>Et^QNRO zmspO-tsrv%qr|?%^jCfHt1=^%-o)y^Dbqkk?Ime3iJBTZiBx#S;of*BVZU;Rqxz-h zpy%g0=am`#@Qj<4RllSom~W&Chj-_)@Go6F0rHs_IB?TVIY38(A0hIG#>`rEP%#%V z&KDt;C4A}=NeyIZ0ci&LFtl9*(0hl89@`}5=xr3dEY1QDDjvtfALR69FHk2`Ax7$? zl*7Qe&!2x|kI`UWH9jG62afqf+RlV_6lfCPZREi8dgvn1=^4`NLCX z9RWqSEvefYkifyugf$=DajQog+aCVO>bg#isdwv6AQu}g`W}>xlTBaLp-&{^GLR^6+gQ1 zI2aiZ?~(mMeZi>?+32xTyZ*LSvX`fx=ekjEQP7B-{ibW;LN3WtZ%{_fD)KW)K?rVs zfx9s=38hp;*G>lrQcL2L1GN=e|AZ}F-Wzuc=Xn4e$btaK7YmGZ3+?*8)nhIuglWV& z-2*}j4^pc~d?^qV`DY0`u_Lc>(@GLRI_R@EC1W$kXX$?5L#JwVZc?ty&kPTyL{&0G zCs)?IrS=SGcebhRLqVW8TK$-LhC!Ym_!?A*rUOatb-y{!~@%!sxjxh=|dS!eg-xR zkV=z+NK>Q~;sm89UQrE>F?J}7AxcQ;{*t6iPlk_L_2UISe9DkU*$;{g?uo;SqrI{u z{E(fSo+zB4oXPLeyS48xbDXWbOWMkwUgQ~D!;o!(x|abr{?sRK2>5-xe8^X1X2FeD z%8rCNR5Lyd%=>@R55AUV=!JB0N+`A>Y-R}O69R$eQt`qCuYaOT-`OL?Ae8Il&}f~n3Qc9^GoK*7`$z`H$C z1wv=Mu{->a0DwO>3b5z(hY{Hh7?DNUAubkCxlnT13{ZU%h@1wGkqkC7PHIL?iNd5q zwDxi7bVXuxrP+niq~(=EQC(99_Qde{wu#3ezy-9bQ9^F@64viykme4|$JOvl3q)Ma z7&8JCiGmE;($jQ}aSYuwLZd&Yx2tXP4?9)-L21Meu&?SI(S5zdJ0B07dfXG6gkcBN_n8xhw4vExbzY1G_ z`YX2LjC+6)@E0-mUu1CtYT%{e)Zk7n$YIsK5>=gUL9U3#3qKu{Z*ORMd_~?WamX@r zHeuVb6det!4}&7^Z3bjJd0D9i$QU4h8}hE$mEgBYKP`nh>4gWR#C#TKpHi$f#zK<< zhQMS_r1MkklKpV??@89QI4<#i!L`+poHOTBI9dhdNa+4Jb#U2BI!>y}=mXwssWu#G z0wfi0&=g2cS~adQCg2+cQEe0$FnvX#Y0W^uRfvAob{O1m6>Omud@Kzmsj>!lNilB% zN9{T{!u~#fB5)3ig4v!{=C4V6{^wd%=V?{V?%L>ChT%GQ=yD0L_$Oow&L;sm-Y+!% z(tsjnz?Dpe3JEW!wlIC>7Ynn5bJaj3C`Ru+K?uKjQc# zr8_J|Z)c@AM}e56ssKQ31q|@;^NT5*?RZAA5JWimarT(2&wO>}p_+)KQs7I8kI6q7 zhp8+0hlZ~bK%&foKt=iPvk_cUQPdNuT8Ci1GP}Wt=Lgpt<=PdV@=NvgunIxCbl>YF zfo|f0k_rp(fYlxu`Q5_ic1y<+|plZaE&nvT@KXsudB`^Ov;S^66zF zVh6^%ZxAIJ0S?+vzRhOpwoar{3##aPcTl8H_D%?i`64Zk`*W7D7mg6`*g3O~Q3O?@UonoFCp8DJ%CL}sn63rm} zK1tiqzzxeerbI?uQR$1sy@Vg#Y}Q<7dPUq};{Qs*EqL7RAkwn&FfF8zT$|uXAg3-l zLs{C=)cGtwHL>DclKsaj38hT(QIv#+9#HiXTO>BDgcr@e_LXr-UTcn~vZ;|y1x(Mj zJrlPqZ8NPM5EwdECHuR1M1cy--hqAFWFm3SVOCb@uxI zq3xZ5Gzp`1&9ZGZ&E_T_rZQDG(&ptC}CSuQ?xtYAmyIheOkzeNf zt@S+o2EQwt9TfXB(Kug-lgrfLH4IGN1Od5?E&V6Zk)4@o=R5L~m70*JiqAZ*(%;(s zn!ToF#SOy4P2Rc-80FfD($W%luof>8^5az6i)gseuN6E|i2pQ>P;P=KZ*MS^?I5R4 zy#^1Vd%?kU!3ucWhCXk`9hfMrW?_XGUyU2mUsB#%%6c*Q_2n2VoYR(~@`47yIwp1M z5uLX_l$Rv;O2&k;y%~*I50PuVe0hO`zzW8re*oyv37nMyt%t`IgJpuh4!vSudGD+f z(leNFP<69j4jtH*V0>#*UTa^sLhhuxz-Cl%4W-`z^oI z)=bX4$Wpi&$(sV+>pN5fiagyfCctoyrT<89ju&1d%m)092$1`w=C@T#&5CD}GlkWf~0byv6X+3Wx*~VAKUd7I=feFs3VIEZhgARB?#}WSNtxSURSqp|8oNaRO4ak zAF-py=MapUBziZlgE*s({LN&XiF zD+3yYpw9*TvzV)h77SoL8qhSSgpxBX&oOYsDIMw9LSirKUi#}Qu1&99 zB<>7rF5=ZK7NhVqU_}$>M`iS0nQ^jV8#dJ2%zoNyKMI5klyzn=Jdy?_Y{-a@2b7Ky zQd-Ihs#6;$7ER`I2VU}=_IJ`EAb`!JMSN4tWSyB%N0k;*Pv<^JwNGm|`YL2|K1SG? zy=u_WQCf6KttxmPv8Hs!gM{KP2D_BLAQ;9q;>*ku`Tg3;i$j zvtFH8kP_j3imD;W z4Xz>Sw;G-kD5=AayBujLE6{nB@dFV!ab-(*pl0I{W&Kj1?{XoH9!sX0pCcxPC% zLJJ{HK=q=2L||Nu(NAf%-DXExE0opQbx$kWKGL2XGR1kOBGeGyA}7$k1K300;coF<-m10(ae{qDf!Ie25He{KbEtAsUo;zV zqzQjLeKn=ITb$IR8 zPncBtPV(Pr;y~}KdP7K6ucciG1(VPKtu*JmG=aKP%L82W7H>xMINE09>PyJ69^S#v zH=v^l&9C@*U(T8z6KVrAo;ZcbE$0{G)_!Z=SS`MV!1r1w#T{Wv%TcOVPXBgyenI@a zEBrUH-f^T@4q#tqs-pcs(!E3azv+#jhSWFb>l#1edJ;t-ulq1C?Ybz4DWu>1K=fk{ z7+K_vA(l_mKyYQEsCRh_I$Jz{W~GHeD;|!l&wl;#y2)E~n>>D9r?g?YDN_E@Gfvy9 z1QyO9TNR6~V4teNBU%h;qt)xW_I>2;>gGR6^g5fxP+$3!zk3QC{;e}5_RS3ZUQY3p zXv+KONRgBWen4v@I968$b{G|y<1m##4r5QlzE-0z@hWh}&J7lMyyDs=Xwg$;((bAJ zG)WXo2#hV;V9IswM!gvQ1XJ&WL%EqyR^9qk7<*>pb1<(DtBP|Be0df>m4hygDsuyy%c~d1p^@)x8 zvce33f`4I!w2X)0cb7q?V}|PWKW8mD^`ZU9u}Oz^X4P2{Sr)mwptI^YL_nF^UaKsZrV#Oco+IAZz(zX!q`aL7^4WNN`i z5Q2$j9-;&8(-)9Vng#o-v;WV-7t2rDUzP307?Zx9-9_I!(CAD*q&$AebM*^I!JHKw zgvH?wp!0V|Rzro-m1YHWwRJ+CA#W}{^YCp2js3NSh1A`R#uPHHIvVRS&Yk1(>3ir5 z>|$gA;pM{y@zyGwZXJ}0rMeoWw?AZ2&e|&r*+F0 z1BcH|85PhMIxxCZ|I9GUaIa=6>yx2>TPw;2Qrxph<2&%7n+Hn?ZX?gQT0kmR)jqrJ z1G``|IY7Nj5BBf(x9gpU@Kls!WfGD|!^{qTckt{af=fM=W%R?F$(pq zjRTA?0o8TLjO76#BGj3iHDc&xL&cZ5LXpQ0ahJIFLgK`WC5%20m3Q!_?Z}r(dl(Yt z#vGiC2`);5O-Y2zDvY&9n2ji1g6FLE4v zUyY{{Kg$sTW?MzD!1!MoAd39p9qZ9K+o1RvAMaUT)G}^Z9L5@V;HpDD?LR~FKpg!u%4kBS~UI>jhJPf+}6tRC%OmB2W`x58V`Qp3nuu(TSo_{Sic#cdNP z6^gtgDwSDu1TLQZYTM|l7_~&^Aag)O5Qas^^$Yze{}r+9jV{A*>Q0brmXUS!)fUR3 z&V}zs4l<%^FF|s8DN=hjYwMXA{C_;0cE0}P!c0AG~-!-zu~ zxWynYe4RN@2t>2zDLI*IF@k(GNoYmIAz2;CEK79^IYym0dwYK4?7-N#1piN2uJMkT zsOxdOQo?#c6!aL1(l5u%IIYfSveZm2ZH5Vu;f9+&nLG?$} zT=MMcizI}3s$4uvx2T8bu47Age=|ydo;&p*-(X-#(ZuEL0n_vsoKyUsdNb;@^B=}j zZm~tg?)ngAxS0A>APB3zTF}TQQA_?k0>cqX5xrg(7>b#I6>vc*PGFw;yq=pxX4=Cz zEm%eM@z?lyETggq=zM*rc+xR-uUE&tZ}Z3pDa`l}i38 zV#JDDXu%*WbUp(c^P3HIC(ae zf(oX4FcT#_TN*z^D=#-zDz|*0Ltn#}3FyVf9?nLBNQt)eOJRCt=yLQ#ZHbFjXS5%Y za&K)F8|OZjbath)wZTi$+;kasV^>UFhtJ2I;L6eTpfL{D@O>_a5LXgEz zQ3yaNKLz~XLYpHBOg~K9PqD5a+|yr}OwX^LHNaCpN&?X?Fa4hLkXP3MPf9;K148T& z@5l)2lE9FBsqWJ}L4e}>$}JSt*cam^uZ_7wARZw+G@4mnagM-EC#5t!lnm{{!IJ)F zQ$w$_xTvl9udY?x$2yNZ0r5sc{QSG0ZHKsFFSL({-5v28p-`i}IKXw3aC0%o56-(F znz#&AZCxvhuad#5b$08tHq^9g$#y+tYg4kJS(0>%<4xpooE_JT#L%K+Xm_nrqznwZ z56BkzUdTaICUW)p#3Xzdp}$CuL_5u10GYD+q#!9XvzRru5yN1mZV=jpG7%k4677`R zakF&#nMGg-iuw*Hwevu0w(Hr0Zo=J?3Hw%kzC$Li1)dDKKXeinFa}A432}?+vyu65 zvrKqzYq_bsRn7qUgtJ8P23Nr7_yj9ysY7Cwx9r{(SN|mK0q8#$NftAm%Be!ueO=`9 z58J(fI_ZnclL0vU4yf(<7p#8^A|s-jY|xnko;AO;XS7fysnseTk#Z}Ox`_=rbOQE# zlu2+-qMjDNb?bay>&9jIms#R9J6<7Cl}4`z&IT=wDL=oRE&g%UrW6*Gdy&GBk0;hk zv-=0hb(8B_RC&FKZ)QGlnJRorCzVsb!b{l@T|4V6aFh-XG}#jSd<%`Fcu;Xe zJi3`PZUbV!gJi1xK+CM(FG;cO)kN5xa5QR3fv)ANAi-&EKH8_lbP^8oi;iKX%>2a{ z*(#vXG=!I1V3(>MNsPF>BSDw|b)L+6}xK2-x{k$@j5??UELl*_tI@#Ojl1gFUN zK~`G`_9lb2#9zHCfsRpFazIIjT6&F`TCjKlyuu$TA9+9g%pa|f8g<_U3f43uIggtj zUsR>zOPGZQ1w2?{q1??y(rSrEE8G#A$S5pD+_7g4z}Q<>@}k%&ZnztbU*^kk)KJK_ zg^8AmE*$q{86l&Cw1R0VaLQ~R+nDYf=A6Bv8C#uu5Mb!{huF|yy*{o`_QsIB6rXn3 zQQ=%|*V{eoa?=Lqt8?`T9UnB*^;PO8Rckp1av!xm!jI09&|E@!lXGQd< z@sa4^G_kM!S^mnQPzObAtZ&yiLn_foilFP?~(L)sb#v zWj-cs^W6tnG+tuLer`ST`=hBg5|jghx%Agz&!u4^-$Y738T;(Z=a^T}M>#St8Je2U zt@+c<{`%AXkvl44TDf#Qe4w@(N8AGca7u3v{ZtwFLh;m~vxX|TTPjIL&uGUB<9boC ztu%=Nb0uL}mT$=W!^7kwt91E_$wfDZJTI1&XyM|!gd7}H&hfpA*#wqY-x3s(qLHqQ zivJ*31Oo&Z_D->ydwgyAy7xh=5MATxq}lQ+{E6R*sSM9*f2kr2Xv#|5lk4V^08z|z zRcei}nLY#WZ1H)&_a}3cIfRTah$lKRZm(7b(r4E> zRc(xzWVHygh4O_16hU+T7^x^ul|>yZVfC68+B}tffZ0@w9DxuIEF}~LQ7H-&1qey3 zyg1q>CW$J$La?A)H?T2|e9&KJ)1u@wwH63yF-iug9P=2|aOU+u=|zqjG|P^Ser@jE znq=2k?qntO`%;q;EKG`O7?93f!N5FP`!=WDAhsB5Y%qzL zKoWLsn@D*fCV0t^$PWX=VDOeYWBYR*RcoztPOWq`-+BzEw8$8Sply!LRQ0>CN2@xE z4pJK{g$l%6Ri$HD1~evPYGBqxEb9D^R+!~`L;l5-i~6`}$v$#dafVS#9+xipE{+^x zcom|KrKUIxo0z)RKmtBaS~gs|Vd!iCoJCwxzAyOV!bBH3#~#=%ARw-#huC33HLRDV ze$3&R)x@!QOh?7McU$$%L~3t6WQsrM(Ty8QX+yj3`zc%xIt zp8hL`XMQhE*CV^Iv0GAUEM*YtRM2W$vE75PR86YpoG&zS>DKXy$Q;?iRlLo7l*5RO2oEni$ zd51<|xGr&)t3^{(;Xp&G_~@71m$bSL0W}mR1a^2Y5+m>wb|^aBArA{w2pWkRKooP zs+eau!;Pdzm=E$y7k7l1KI(hVqwfgsYf%cORwA!2NO!}cCz36;-5S`;vph0sREYjC zL1ezvWg9IDR0d|(JI=;Lecy|o+?4L5-hZl;oAMBXty@pQX*C{t%15@hF;+hoJ7ikRb^H&~e zu#V{{I*U4#;R(5bQq-i~!$rdvgs_S1oU&cWE~)6!vt416fSnD5Ep}Nd7)_Z8s4~Z} zx*_|SdQ7|heIAz**{Bw~QNGZ_RpY7Q7)uI&wCB-@z%R~AKlXx&&`&?B}E-%Z#+ z_Q7o`#tvGdk2#v$5nQ!vT6$HJo32o66>M;e2E~ zXgb4Dmu_f;f+2YCp^X=Kr=OvVl&ls%ms)Hs0yaKp{lUTaRY%PpZA)We-%wPb(r?)u zUL>5gw#~>3UEb~DJNf(K6Z{KvUcuvA{)a_clm6w=GR}*1AsQk%{Zv56e>;gdL+Xb` z%da$JDuC||7fF9Xskss6T7g~_zuX-Bv|z;rLS(pVmbiUMF5{n>{(LQ2UY`uPYnHhV z!wq9I11^)$&}xKW@|1(vy?;2u$^j%Bqe#S!08OPFMss4HKs`}S?*2@wp;kYkfj zJ8sgabTeum6~I#b@px`dVPCO9`Xe3VD^Z%_N?vJWkl_S2emBT+GqIh(~kSPQ!?=S4RB+_@n?D-psdScYw+e}h1h@nf%|GduI&y-i)uggtr2 zH&f5ayc}LR1bUbk-|BwW1fLc9b3{6J6n>UU-R7YB=HOJe^MaT>c9GMEb^QtW1rFwm zMuCfRk@^Yp8Iw?TRH6$$DyF?&lZwn??;e78;tz-nS5DC=Z_+-}hW@sbhBOvBN_1mS~F z_&fxVGX-rtCG{kHzeLNBz00W)=bq%eBco`4UcHW>#i5NLtt4CVu)T%`9^)EFDnUK1 zC(FZl&oJOlnC(VK>`6#NEl|!}=PiD95fm#j{VqIBHx8GAs0B#(GyKt;Yl*T3x25uQ zW>B$qiTI50QwOz&AubIgm6e0?NXP!0j?zzqhx@1VAo*=hL8v+U9u%o&2F#sraa6bf z6MI`l7AQg5MkYZ7Tf0Cq5#h!X=V>*^5}_%>( zU+IxISDXYH=#G8ix?#CebX79_01)o{E?GkMs~tBxloCJ)$hX=R#M&1!>YV#Z&Q#zO zqr+jcH7{84Z=g*6G^*&3XvC>+U4RXIl7b49?Yr{#+CQT~5^&3uQE9{09bK;r@#-dU zL0a@wG@(sFGLp4CqGG-nFJEeAd35B}$Wkq43Nk+CZ4jJM0kjXxPu^a6$G_~CX=$)x zkoWjFw-5S^DN};v=LTsgpn9WQ%k)F`M06pGjG1DsWHEX|_KY41a!1kJ6gv>$ZL9IR z*U<=sN(p(sk<^k&PK2THy6BDGR3$1fiMG$H3^wJ$;57d$@C|I!gO;UP+Y z3LL1*=Qz%;z-98^s;8Ay-zpk{bri;bxri5X2-w}wG5A-L=Alpr5h2lh`zRr~IjmU*_pGoB3f|4TnYUwe##2mwli@rKJR4J$R?KwW8lZ(@2bfJgOHXRN<-9l z+o#cP(Qpd$m+f6YNcHcRvCpj^$ri9XFIjMuG@{>nIc#S@{Hnne5a9a^@Y&8y{Bru} zqIU00Qn+e3p7Xtlfgj;x@ZaX+^iSBOlHsrK>n@7GZSMXF9e>?^*{1m4$)ev!%cFvp z77iH>GvvA#AqbhJpAK5}FM1e?16+SfpDq}fM%=~08%Rsgdgt%~f5`R!xcmQsUuXSy{-5Y~RaLhCK)3%- zF8hDSVrTuYSnU6Cng3T7I~yxISmOk#iA27sq{8X;T9<`Sm#a^}czUzz0 z(a|uQ{)@{g^9sFQh^*tXDrk4n7mZ&lV;?wa5P%MyHPGo^<7r z_$m>mbzmIl{U`W&z>g1IgV5TGjKBH0R!F3p1~ac_VA-#txl|g=&RN6ygC1U?BjDW zhumN;fdzrf2^%PIAO6pIlU>O9IMNvVxCkm$QkD>8M>%S`qpraFl!rceWWfmM9<*eG z9Po%O*mS*6fEckdUI+kZcaAxq$IUEoH*A;W94nG_vVh@V_}Tp@xb!_>clfF(ju;If zxCmQXp^BQxEl>Dcu><}XH^?Q_(W1N5)%ub)iiuAeg0_2wyc77V6s?Wp|XCcFr53?YxN@RdFHKT%bmj zhofE>$nV|F!Np3)LkTBT_bV!B9-RHl3#t^x5QwBuG#BqJLz^(vSJd?m{?ap1aV>sr zPx@T}JOkKvy*{F4IL;9`a4#@KaRn~;4vs#2K2FmOn2Wa4GBOs51VE?M8F z1qFu|ih1EF)H6(qYb5zDpPx5PsLABDV)zf5e0q#en4#oyk(;?2n1C)F)UJoDc`7Pr z=vA^p_ha)dvzE$*{11P~2o!NPq4o}Nw$50=T5`#pVQj?{81;0$$!HIG`^hsu87^JC zCG>zEEc*RjD)}G*kja$$Fbb@v9N)&3IQ;v!Tj*_S8t*pu4#Ss{8;Hf&@$Iu%9YMM)s-MI?sRkq zqvAVlQa-{8q!y9*NzO|Qj)8T8to7I@qG6}_^h)125r$BzcO{kmK|9vP@Gn#%bLA2W z*m7Sa8aLsQSv0Eg_?}ZBG!7Vn3eni`;Z^flm|7xdg~9^w46q%)hnm-3d#?IC0Ea>3oG1GxN2e43_lGH>E2-=dV5lt;(P`d@-2GfK;X{UD~ zl?cCAO@L@nnj^~<#X8Wu-YT-LtW>b$)`t=T(aqCG$ug^42X|)w#D%6v^*6x2V{ggZ zQ~U_N?78?{?PuPqsYKK7m8csd$(Ho67V{BRpr@K}6$67Ye^Vx~;u>?Rg!#28Q>!t* zpDORjyPCEKcWd_o%-XphYQUJNanu#DQ`ww>sq2%}mLpt%K4(%uj{#{p!-tvO@`YnR z9E1X$-U36^D{zn^5Stv3kuYY==6R%V<)!quYknP_7?@EC`%_s%TTfRpP=o)m2F*ft zgsUkKfufCD^60zZ(r1*V1xKth7R<(DEoUDuP}{2ZCqvcb+CZU~XljH$U%BofPBb$iNE{_Ae;c3Hn)!EykFmVTfTwl`u)l4#$C20wyAA$MHguKGUYWzo@4* z{ZZrP8N9m&S(APtESJa)g;wW4;VRDAFqdaTNK;;{c?Byoj}DB9fy}`pP!q%~uZUqr zc!(71GUXi9?3z_#-15GpUoH*%x6dEW<9;72*m_9R+MiM8LZrWkd zKmQS`*~@-}M6bIy<;Clak;GgEVNo#i!x`f zcXM7>`T}pV%TYAx<&2W+ixAZvz!&+|zsZTLHP{b{5wZ4r00e&{=5w;2nT0VLs}wh; zADJdZ=os-Jo@_EED>m{_(#Nz3%-CtVj7yr{>Q5_^*ADCRSI0Wp8Ny4acDlTR0;yzln zSbJtVV#_a3v0i07LY*{!qd=~Ps;f5eFPrW)|4vP{vX|r-$GDcmo$z@%=E7B zAmB_c{itdUE(aR)T~vGT2?FrIHDMKpeZO)OWZB&HBlUuA&{m7$4djV~fUUIRaX79d z8rE=`Cg*13-*-JDL|%YZ{lkqHm$WrPs;h(z#Hl}XqhVsAnHNK2l3``2Q8Co-%BzIY zJP306gHc8H1KjN!ik4gHaYX!#ZSpOKiv4Hkx)>LJ;`?fK)e*UDmX`R+fWDo%xQf^J6J8H?3}*^Tp@h zCkX)ypfM^2iBy$N7-v*w{g!+?1jKqw1Z@ft7fRXiTzEM)oRW1YCCG|6Pl2qzO|m|q zx2`VmX>hD2Yg2g%JK-$Wda&;CThrr@HH;{54`L4xZCi1#FU@ZciHI$SGsLzKzh~W5 zTG`=B6uSSw;FVOQAHRetByoHU4>yZy#l*Yf?`@lewpOxMP*H}z!^ivMzsdGKCuK?K5q+}|5BnBc>P#$ze%sCTChc-f_%~x7NKzF=@ zzJA{&A@x6zFRrl_bqala0fK(MgUei+Gof1Aena;;hwXxVwD)&k251ufG_|p6EdYU5 zL!dNiPQ`xEB{4fFmm86$p+PF$5B65ljywOGT1y-EfG!zwTv*ST_T@2} z>)O&n@luyvD6fp#HTNs;)5 zaA$tM^=5JJ28FRUC6t4~;Qkx%kBMBKM#@ZvoH+H%{54%QU-fNe^;6Ow__(L2=XF6? zEUh01jljk|j1KTf!_yrin7>+00sF7fiC^3KJ>w^{RMpDdz3t1cq#~ZYCEsCcYgZjA zV^obj&14h_;Yrt*7xU>MnqV(gDRF_5cq`3n^r3Q&Y6CZ%b!Je}8af4sp(vt~be-Vw zFB|S(1Fo1*uk>=JL@d9*s;KNq4992}_F(fWn~dTa;g04E$It`nclfyY^On*qK~B|CX8&(LlL!TVL&Zz5hMMMlQKMK zn~YNJ5j&QZA3oI-MEJ@Yo))_@=BlV_c}qSxbCo|Y~-{)gqpHz;O}hfh?rW6A?w@J)tgAG zWF#4!)NrP#P95=39WQluDWFwddHr6^^?8*`dt<~EMGwb%x<>5jiFnoNO&6^yk5?&= zDz7#=E3a*#1F#k^wP3I_!y4z}*^`nh-q&#)j>In0EHsueTJ2o51X;DC?%ZgWS>J*B zcmlE5)#zAF8aA%m84igx%UBJiJb%5ro#6-9wNOg|M7N|RG4vN(&#o_J%!9hhU=ePU z=J0JU5uuO@aD7cUH?M&e_~C7oTXV)De0IM)JL0|;eFzx6{R>HoyNxo#V{$lkc?*mD zgIe)W?XqD+{snv_k8m4n@vaG^6&uCQTw=i(P)S+MAuU>C9F|ho8Y1tnM zRk@0(Z=mp5-=ss;Uzm6=dPNaab6|rvgPFbqbv#{<_zN{1BFb{eO*x(&0KN{skxV1% z5ccEueD`oIuuXm9CQ`6#>Ep3K z8bE*4(9P(q^M+-KId7eO6NuNn6G&oYRv4+XHC$t_@bnaH=b&xoTSX2rH*t=TN#oFr z-%PY)`43&OAx%o>l$~>)Yc&jQjI2R&ra8z$O+st;VizZx$kJLCJ*AzkglpHk{sBg! zNA4-jKEVF8v5zG1e)VSCMlmDKx6M&;bHzWZ@hN!6Eio7oGE+X>L`&x2}g+ zD4yq-c{MK9vNU0^aDdU-*gRcZg4tupOk}@IxFIE}q;@~0e6eXFjY!MqIEjyPTjYjQ z^h2nW^kuQ(JyV-!GqXn$_+odaDEXn1g-2KV*bNDOvsDPf9Lb7tRUcCfLu^t;XI5ZS z!$-O$9C@X_)xF?qh#(Laa##-PWp#@e z{T4})qi`^u+ao72@{6{Np*LDi7>Y$gD=|diMGXBh?Kzg_D(0jjhQ^womd+YkD{zCT z@;>Wy_?K5*@fMcaprZUs@pv}FS9b1?MoxdQ zKm;CT0$VZS%J&V(-nrG;CeG6p@dsvMU=oVDOWGZ85if(+7jeO6##IqmYacl($3!Qn zI8$)zRx;#Vkv>eO)MF6~2tS(LYnM3aDPK|U=VfUM_nHEYYjM+I78+f!EJ7O!Im)ulL{LIxdU`uVBiP6p>CV5uMQ3uo#H?r;Qyu ztXA^^=)~I$KI#k@5iv5#&Qsk5f__X`7cO{f={iR~Z^9lK@WX)-{DS=3ekEYHgYbf@ zyXQ2GyDq0W)0TPtc;^uI^QB@M8LgKw_{x=o6|tqr{1kuM^<3B`o0*`^KARd`q)i)c zT_w{^EG_IK*UBSb;ZC-)mW}qDYc#yrD@3h$A~-zCIUtLlTrW3lDjFD)GH1}m9k}E& zCt*R&zXS^v3b84=i2XE2Xk16rECZkVJak@7@1%YOcA+nG)2Me)0?JZSsfa+LS-)(! z@YlV~HZbCiZ<`!7;$;11Ic=JY@2;jXxhJJ{NeuS0Rjc;;4pFhFLG-3|1Zfb6B{EN$N$ROnTS}pIR5WJ z?P99Cs;UOTNYBgG+ZTkmp`uXaxED(xISOncgqWyU5I9)EYN0R~8?q_dC5)zVG?O*$ z+^UIcbi<&&@nu8=@$iz~!Ej?BII@QO6}l5-_}%4N)6Y$g^G;`v(~P(Mn~ymWP#Y;G zisKe4PS_)7^RJ|PT}3`CMbE|}ujeCIe<;W4v?pn|vsW(e|u`3>MlmA%=MvoRBVHOx*1kYipA1e4!Rp4Oj1(Esf7+Wuz^q@*k~ z6I%tBA^Ov8CZ?4KoetJzmx}&2LYen1Uqo%QDAuLSscKzAq;MD|nQAQUlFSW@vS|e= zV*RX0DRdNljD|onvUut^6p2$ZgN~raB`g#SDuoXli?J1Q{#1FpCA4hwAibGGXGrDI zpKHP6bK?>yKb>lm$@`(U#Hgmpx(w{&$J*#qat{f(q5*C6wbt>ZW~GyfMTeGL;;|_Q zhrhXmbdf`x!ZxYhBT?2+C|cY?vFRN`z}0q3<<~*ge-~0$7IY|I#o*iqFnsRDWtNAm zXR5I;@S34wQ?r{;m+tw$AN2xD>Ohw-C?I10#dD( z*a|$xN~L@7g)D=S$np`H>p>?AzE5AmH^2;SDQg_tkMmIlsdDjo@z% z_c&-gqw>Qg< zP78|nq{O|9>+)275m#m*{yHg4!f)8Wi!i7Op)PO(jtw$+Xd^=k`xE zNvh+T6*py9Wn*Is+c2~av0A!@vA(>)#*`wFWQP0sVv5|CD;o8id>uMX2 z5%3a(R1Dq@?g8Ne{swWhG65|QORz9%p(mT`mwS~_QC&)tHnS#rJcn@i3rkFF=a zOqPX8(MsZK-d~N|#b9f;=&4Qm3iL_WC++*MuX<1U&JtmzQ)Ov02fJ`IBdPAcAv?}! z+JDslN@cH@v~?3(=sy*I+8J6j)YiW)fm7C#E&)gDF}=mTSpjBsmI6xlE)L2Lsq@=( zGn6yGAxiX;`RKe(bp9<(~n;!vFL09OxrX6^@>?`YqxfwY`Zo9E}=`cjp*$y6|@ zyZ|l5bV#&!bb7#Jo2Q^hq1p?eAkYWg3-(WyhfGd8M;YcM4OVG2!H`a*2Ifpp!_pbu zy3(WC+&)3!>2{omV>_i788 zcbSjkCLIMe?YBD7Sgo1A#uacER#hD*%Ns|}H9uZhg zwG-<5ZN~HBYCT-WS-jOts==HLlw-f|glkhSOmo{xUJI+F8&VePy7vuK#Cpvm@e@)m z)2n}{Yfi^)pOSJE^z4TvkzixR0jSiIbqhG#r4yWD3RfwH#%hj=dCN5`BmuIKDHiB- zGXUjpiq_r4!Xtc2hCAVSV@>>OaiVTPzUl#+R}}aL;5rl1z&mst6P8RvSZzZ}ZDY*s zUb|o;MBxTKWNQX(8tsq++~z)C(OsS9Az+tYWS7Bz36OjE@XK%1kvVLEd{|#mjqF~9 z(@^iheKO-)mVF19tvlHLJIu#HgE`Q$JB~w`h;Gh0-(`l~U%D>ym{L@|JZ*udXvRpj zB;YHISYnbxt}3ZkXrYB*H+y+lv3>&qURVJiSP_t;gCeGT$iuq;ihMLwCMk58*x7hG zFuQp@Yq>p_xb<0|$oxN#{v;LnEoEVk;oOR7uEp|?>D$GYp3*rLu&yO1kAcz^xI>EA zLz$~+(p%VQBZ2!BkT%J^dRoc~>`H}W18DO0N(|>yqCV*r3{NI3oemtHS$bTdxsJ5% zC!Dcm->m+0tnrC$Fysq7m*PRIIm)X+%4^a=Ye}IFYB=Ldr+T&WHw~Cx3yifwR+l1Y zmofuuam=fEc)CgA4slE^8k zop=*xGXL>0YnIL|o}HBQ8~l6lfF(ie{xaV5FV4UXC$gzB@Xc6^`LfMio>`LE!lB2E z_qvv3o_XeL(Fv|hbq5RWF+JJ?I#gQJsevi%R4MZ?k6`kRY^yOQ3oR$QBsvvZm8l|M zi-ual&irLgpqme;ZD-z_lkk6si#w@qw2i-udr_u(CIZA z($^lp-wt(`U^W4j%FDF5Sabhmo?S0bj zeadeXq5u@KJ`@rOa@Bq^T@o_sLUM6qa`}C7ArvOe2S!5JdZeg&ESY+=iF!PmdX%bq z9GiNKi+TdwdgQEcJs~P~uf%$Aod)=g22h>`*vuLi^qM)No3Gtc(z+PI60_x5i)f80}u_C5jw zg?!3E)B`DXT)v82UIpO_JcSO*4b)opsv7Ig^$NGDzt%n6rxB`QDXsTP-Q#ttr7(&C z)ROp0ebl1(bgp6{m5d4vWxXoJ{c6FHs!G(_L*<(2W&3J@(yCI_S|#PGDW&sjq2sDr z)H+9%yeV}sQ!q_Y$Jl;q)zXVxuKIe4X^ZHta(WsZi*l|yTWT*$pihc?%5h67PqKU( z42v?)#>PXFp>(i3^iyVSv}p(Tx&bs!8l0j%Mn3zj%iMV8SK zY|s>gcx(m7#Zt`e6r0)1a}*Q78LX&A^K!{!E=KgrA)TCL zqRx|e9BUN4gBib3O`K%&BUrI1*1$4IDbhgz;h&>np8)?@MaB?CtPmBjka?xZVk&Lv z2}`b;{FLTM^#vSlnF~w4U->c35$p>X>(U#RJXiTS%~5U(1nV*%mICV2wNpOE=kvOK4~YlimZGN7Mp5mH?{2`?px>`xV58OE<@L;JQsE< zaD2s@o`=$#`0mqNt|wc3?sI$&X`3SMYkW>?^lK$;s%ghgTUHmpZnOx!cHY+^4`nc` z^L#A7u1IkW2{!~k1|inUp2NQ;AT})62l-niu3&MkfBqOfm%w8(;1Uzy0!)jFlVdQ< zOoD$^TjEj<4^Pmr3suXZHAsPYO;SWPjU1_Ek(69PdrXx|V|yvyQ^k$YwC zi1laY`hIl?fG{-$%(?<-T``UB07rME<9k%fJsagAuJYiC^1#aS(2Meb|44SrgF4H@ zKFs|?&4WkG153?APs{^S%|lk)(QM{We+9U@!(Q(Jd58S&5qL+)-jJ{70rLuoI189M z^Qk%uxIFX8JPX)9^65Sb_`vk>!3^j_^~pmG*hTe;MGcsFN723ikrYvSN7&v_e+S*( zaQXJczJl}ap?rt!-f)Z2Bw~-OFxfyPg1cYQP_GdBR^W`n%=e_P;&J@#Y5K@^NRv zo*;Ou#Twgihj~4jKQ9=r%KG&EzDdB(>e%J|rUgE*cnkZE^}lh#&Tik!e*%P_&WZ_e}a&ng=H6#-qLfY#yuhNR+TtJ z~W>27b!kC`5XBHmWwPsJT{~_X^1$k@G9CCH1%$^Wu zR;4=l=?(>ZvZJ0LX;-LvMm%}$F;AbDtO8e=c&pSLB6O!upD=mrRv$onV(BiPy(R4S zCOwOGirFq{KJZ4WKsqZ9BWG5lpX&5M9)Q-ukEsU!;0LWUm@Lh!o128SHWDEo zS_hL*2Mk@^*|^e6sSl#!7Z`2GlUY=3#mJgjn77~eszoQqX|>?k#9U`?(e4wbNOmCpSlzR_YrbFR1NH{ z2B+zv!QEpGjM_!Yex-^Z>V=2BryU^pYB`_iMbLg(&8Pfoy}jck%v_4F^$WWdL_ixL z@qnQ5guwR%>pY=$o>Ad{{3$!5%Z!ko*M^2yH2#x@POcf*KR^>24#J>J9BG|E9NM69 zWtBi38l_5zuNj3Od5>I3-k;=-`7e3;P4gAtD_gzDyZm>8@IlfYtwxV#k{+aH0G?lj zfo*RHn5vJEYCw=`$ROPw@1H6z2t_;)V{ARbYTXap9eTYMnR6Csdq)JUGmmqA769xH zWp^OP9xikiCH*9Z`e6Xz8)%8$-{p>VyI1B8H)s!ycd+_~!~56n9_tI213hB&jvZHo zR#YE3SAgQg2$xbmwr4Gfl6+|ojXgGw9%GRnlx|-{dQ^F8uI^IrjO}^z1Lj-mJM{-e z)sRNjh)2}`XC)tz!aS_?%wM@V3>?P+C~r@WJtFQN?F++i7(IGa-5c)xjdp|}`t%;# zbRTqTs42ZhZ@KQw<-_DV(<$#U-zmwd%88EE5R_G)gH@oAL$e5FgT0Nbjl)f!Z|5&< zcdXA^4DVTt?%BEbcE@4|)a{|#8;5V`;2VQ)L=gl*I6$aTy;Gc1n-kPeDBpq77oguL zKJ<_qJ*LOZYIfGK@O!eiX6l$kqcSg1UdrtF=`oaJqql-=g3>Ybb)IABv?&s6ST>27 zRa*1phRl1Tt4detcA}FtMXNH_k#nzJ>OI^McL#s((5d;P2S(oL=_9B&f!+|>BZ)VK z-iVA14BaV;H-}!o+JlNWi{9YsgNrv$-4WKCXs-lqa@3JwhcIo*WHH60Nr*PBT6hJw zR&8?Ck!6SIKQ(dWsX@VZ=l#>Lb;WMpcg6m9_Z0k{z&E*%4DNnVv137-N1I2dN4rPI zN83l|Tl-rlqZY81`kj@J)HFQfWfrIF>h$XL8V$P($Lk6W$LqQ!*5^ekmJR#!S`FKd zb0^X4n8l{7Rf{YCdLDK|W+jl})BVKsXKhTF&MKPCms56ib=j_EgnfD~=ZoFP&S}cT zmbblkMpX5gOPpwblXKVB{A?g|$|P_8bII~j9ixW`V{>fcTn4Lv?daE%?FUcpmDMJE zMeefH#jtCQu_o+fSy5dCxG!xb1IJR)JLIMh$r@{OKy?wSHCVS=ZQX)3k9hcLz{VQ2 zcmHWr-N2g50gtBn=8Ds(^$Q%}E!amGGlG~;psp{&nz@QtuWSt?QDn{I!2!+6bI}|z z7UxMv_FG(9UHWmDA=gmhVP*}GH6*5cHRF&x$Od!ROxFje#ye4caowG@cw8uTMdgzJ zcFyIBnxLxrkE>+YwB?vs&PpJ=nE7Z~%d9$q8~lW%s@N}rY`u~jZ_i`g2PeZo!TRfY z$9f1-5#Lw;D;c+fIy^2Pt>4mN2ay=}i@7X4y|?m`l=&1~6>Q96U?6yaci2Vc32%B4%c2g660pEMBhqLl(9S<`Ls6nb?KH5LwVBD(mRl&fXEI zTk*?MDy|;E5PI=5F~kyXB+Jh%D7(lEj8?^o2g=yNjv&dn;0Dr)>mrdUSSsf%rXnp_ zC}LlX7g4A(5LCLtkSft-A(A5%t{omBLxgHeL?odoH~QA~fo8#y&`Bq$w!xx$bC4fA zU*_XO;VGn=LukYqOz^q~HTUJhAEC;DdL81^ET7e3Sp~@wp)4%ZB%vDu_I(7;nnWZ5 zkOFK%ybq#LA(CNQ1motATGUu#!bbh$v~-Nh8V(lRuaT6PkGoc#Dc$EKXY$@-jHYY$ z$@AhBpdHO)8>Zz;^pj*Gt(pj%n8#HJlrWmxjKx=GhvS{}*p&AnH=M;WNd1d9ZO@)6 zmAj10)A67UyhNkX=_1R|Cd0gg z*GyH#%w%DNqSaq42Yp&M3LGe)6F3y}jfMz~sZ7;dljE~05=Oc8f+V5RSfG-;jG1o0hDPc-4f|#k)#H=-v z3t^ef6~YNn&Js|>1SR3f2f{L11t*zDVL6GB$)Vw)<>m0c$)%PE@%ib@@Ia__n|3oj zR;W;av_9XnGnmX~rm{JlW_;4cSc%0%OGlxvv}{isKabojZOIyA{(O8fBKRqIuBwN1 z?j73K99Oxt`5HPY!%Eu9K~8K*!4)JgYa|I75{`nPFzvHF9flsmX;?Zv$=A;;eXoadS+OEvYE%9FMo4V@nYGC=i~QYF299nADek?(_*p3L9BgLnVAaYHI>7sfkr8uICd}R@2cPz6`dFG;AfZmeMsUR+ygLe{#5YtZ@v7JuOnm8}BvWtv1HhbyKK_ie%J6lu|%t z*%F#KfzwHYEKRa01|ya@NRG$%=O?@lAtLhwW;T`NJ0_QAQdJoITlgcj_6%^Gh-EZ$ z>c=};1l*-iv_B<%QkvA6w0sfR2AnuA9uAfj(FVRKQXhhocaNa|k;DTx5>6a;Ht$Gc zj{>$%;VxTkxNyrh7~fKVaaBDgs%SQIJ0g9&zZ9DcFkxLZoq8PN)$4BAs07O@s~Wl} z;vHtn&i)Pau}2hX0WVDGwCJB-6Z;SOUj;}{X;}kVvY60d(NXdlKAv_c<{(@n{|L+E=QO=DEvrIK7%~lN zg!i9fPGZrnga_Duc962&-$&w_rr7)eOTZkLDwvwe3kv%EcKlZSR+L;DuCBHmR4L;Y9 zcnpg)m0Iq49H(rWrEIg|s@3Z3!d>3)i(jaP<$Zh|cT_cA^cF&&GCACR)q2|Q+FjrD zupX}DK4JBu8!y^Vc!2F~`*`sxh;oYRiT4q-Q}`VYT0)6I6o%oHv%J)6$}k+0cnbQU zT_Q%$2K{67!P%KkO+7N=PK%&^Y zF3W!)0Zmp)uCXo{Hq2r8n`NM9TF+C||5EX3(OQ4c3;$9E)LT9p!sjwjW`qpHoWC;+ zorb5xwtf8w(JKY$f-$|hT#xR)Li8g77yJ;~s2lmm(IsEq$30pZ0F zs;B?$TB-CST=GDmSU|7B6Yohu=o7`55_I+(kLqqFFU;}hOL6RR zD}752!Gn(`A+#KzTl6fweAqyUl>qhp1>clt zy)R1qm#p#xtN@w-mXF1ffB)q5Ts1i&`@GTcUB1!v2>E(Yy^(FRx69Kh^@NY3=jVj- zVK))Ocg@SMDyVM=7`&6B*lU|)Oo&*I0>t`Iy2B~N=;fJ9g zKNee_=<>nshgYHe4ZK;jg_bQFGK8Lc;cSo$G-Aah43)5_{x?t;O5rJ`{(Cu~mq>KO z-($$54bjSES$VVvuMTZ#oy0WEqY7n0vhp3Llnw4vbI2Kio>Ri*s*)wrK8T?Z?pYmL zoD$Y^o{!8Wf6vAg++?)9Ef~dw>^P4zDYNqNuIvE|Jw^4JlKNMMC%dA$B0u;VwCgH< zuLc|XMAu^olj+M=AM{O z24H7m7<&}?bwzz}YkxSWOFpqTen8&PzheMSi1+}We*6EfaqJO(^ntbNTeAVPssO8; zsRh8{fMD6C<@^1yi92OJmV90UxDx#RQ(TUR%VA zPpnhu7Lb7I_35%F+m)u7oYAbBkMTOKRc68#32J^9 z{PlW8a181F&Gk%CH-9)WdxcaE95Q6fMiY3nxCnz2F_Lg?kou1*fkS)C%TPJ^G=+b_s%Pk=G-) z1(==`9Pe^jt7QcxJU#G^>>^o&q8--$6P}q~-BBakBf?!&_(EG?s6Yn0mRH4PPOz%w zk}hc<=M3>Z$Duf=a4L)wSREf0y(9phkS0(uxFhQ9l*vG@}+RiTBrvTL0^z&j~@~zaCd1Vyxp7`Jzu~pSzpH1Ywo9O71}N9D%h>W#EWjg z*X4|-b<|4WS|iWzY0p$pRc$T1uXo>Z53!2&oTy+Bx;aVp?k)3ATdVJP*biC*>zrAzhewlgo?DfzKRrr# z3BEJl?7#MD%&$-$Zay7Ql7P_%(d|l}Y*#*=bK7LkO`dtSiC_A3x9Yb_XctjW;k7|G8QZjwzoNjC}+V>D{N)%8xi9lU>+%0`^TUsAu0K)qMH?$d?r z`F1(;b`+D-4R_@0bByxwejGWHwXjt2ccr{%x}J}8Z$&%UI4B9LlO+DFmu8r@@#!Bs zV+r|aUQKef&1O_e6U_H8@v=lcv)Oytb3qNdtd$9yAJs?mIn57wRawp2Lr=Cb{;$2U zC7HE~ptYEg2cbCfMYuV0-WMB!jXmp|Bf*=~_3pk9M##!$+-m<#gV35bc(20eLT}|d zRohpvg3(U#48NS5QIjA2j&VJ*q_3TYv@i4CPb(gGXwtvbT6L*ERuyVgOg25582i(L zMYD!}rt7!&b1k= zACJN*Qx5N81_4D|-+9xr0!yx zZ;R=Ro>$Lo*>xjgo^4+Z3$jDdp*!?IpB|jfN3lO3BSvCr0Uuuo3cNg6zR+?UJ@936 zolk)3#T)$_>%Re4?Q8?wSvhv$R#>A&r>3V35;BOR>H9u1i{vxb$g>$idRUJ+=v@-7 zy#%CI*g`ouIdG(`GT3mSEdP4YeuOee0Fw=2mT=3B8^vIgO$%%IfO1W!y zewshtlZ7i-1?{IT)oROJ%@+B(uG`s~M z&bQf%2#dOWnD@SNXg;vU#_wG0gf6ct;6SkvEFx(N7!-Pa5WOXHasfo;?cgRzbi5Xo z$E^wWiBa7Cb(1Mxbh&!MLslTOjqP%s-49$&%Nli%^AuW+M)ctm1B=;jr=)zBm|fZIh`$o zffJKO5n^{=!ytAFF~}KH`v{Po*K#Y@7&XF zg)@@qDx1ItyROs43i~B;(vCCNbb^p8{f9&wrp;T#lNOpiccVd)Xd!3rWLULMmvH;b z#GGk4w=Z$wj#g`D(hqYp^jfG(_5E-PN5JdHj=+rcWdi=nq+x`lqRH5teqDOmbNsU* zY*xva^R^Nj4||xTgq5`-;ZHx}M;8>Axc8x)ad*TMmyqX4nX&T@^HX`;&z4AD2_xY< z?FRbhG&`C)*433YQMQj9YPygB+eztT*JC@*D#JnVb06^sXG`d=ByxsePebki$IZHMWz!PQZm<7awd4N7KAJ@yLxFYscj#-Y^x6 z*HJx#80^belf?z~tUexohi)Wtj2E?C$0s6F(3^GWx?s1W<>(eT9)0aknVb6W>C`;} zO@7dCX7XEK>yPXuw2X2VhorkS<=5$51D$x=P?Te2j71xgP<6qL==qh6;3ReeYs9a3 zmVSA5fN5W9dP~kL+eUNJONHe}Ra&Gm+813f=I;E!hxVx&*1Qs)aP9s#Ek0eTtKa= zY|x6mV5C{YsaTRwdJA8OA|sT97lQXSm`*E+Xe+Tkw|2WR|@S7EytDT+2G1N zt7qCAK59=I8-1DKW9@H%GRV84!R8awCr1<2JfX=BIYtCd9pp*DCu@OGZgRj-N)U4^ zv}}TGKkJ0rd?~h9?M7ux^h0d=`H!LXZOqj&<9XH^bx8N|gfYs;2~U&uWr$yykKaaZ z>8xpY4d4PU49a==tB2Vy>5FIHJERZVgCD{D`tbrEUHk<1l@hxO`UA53oucHRDLA=&D zZKtk%Rby&ND--nAz9ha43$62BxrvB&2=6h9HoL(a$=mt7fL)V6a8GRQ-5b@@3tNgr zX&%7;QaL8~s;+5lGdZu1NsnEo>7yZ_JsmLIR$C0OhxA^HlOoqlc0ibr5yrELXv_A& zxw!7I+K>3#oMJVR59aU2#d)MXupgxy9z5UM=6fW2jN`I|7bopw=M$gN(FRU$W98JF z!v#F!V>QkbQRCV+U(M&RMb}I)o~eR6j>m>lRXXz=*99K!zgRlNXrFG6vAYI!s^jW* za9)jJY@etOWRg5z!R)pbQ%|ds1D`m*Ht|sh$azY~I4ODSK3jxpdy{x2zle4r3aPwWZu)!&%v|E-Jfnw4w z^dsJ=Gpx@9T-dFJ)zL0AC=o6>hg6%ydV+kt#<0Z~GCDf|N@7-I{1lw3roj*gV{UG%N{}Fc1+LjRT4r)pKU}gYR$xth!<%@wrI2%Fg8^>{}ElcpL1VUy0M2tCpZl|F^2S}PF`<-Mt?S9{K z-EMi!^1NbSMnb&nYK1#hOm#%kNPY5(iAg6sS6N7xVs-i=bDA=W zC34Rl!>+I=G)fm@cwO`f^{(A@Gq_#xx<#{WI1=D#QMU__X)Ys}9q6iuR|Thr9dU)( zP>C0DJEBX`7}sE??kDR7zmdnV?`wB4`~B-4@p@wGW-W5#I_rzqz5N=>wG}_^I*I!|v;CTRhLrPu4p_w0n}k zmu;2A7VO>)vEhXN-h}PmtBDt{A%Y!o`cdTIRaOVlA^1%vXpJ-UReEb%?CL#xZ84R> zC0ru!F;7U^Qs#H^2@d}Tmh?p$pA?6GRcomD4|kuS^hS_%FfP*Oj_H@{2$J8n;*<61 zJG18n;pie?)Y?kMJ@8x;B#+ZPlmF>%ff;CH^upfPe!*_e2cSYl2=|@1y@wJAI={wsG{P6 zPeP9shaW4VjGO$M;yHa5esrlDh zVNTGoZFB(?WR^+PH&tU0A+;-vTQUZ)8cTvv4ONxg2B%|k*WXKW zA&6#iY0$5)h%z%98@zNFOQIMryCnVZK|_g6X6^U&2%oa?ed(Gl2rhQ=gX=yepgna%REjhmm(#wtm-okwjmtBJ;|iNtAJD=g2~(v~5yh?l2* z;KUmkQyqH3-!F`Ao1hjJ7o2VASu!}u!qJ|Pobq(KCOl&e5wqXjARn0^@0sJ*GP1Vn z_$}Kt3nLXY=d!`YS(p>+C3P6m(vEybUMJfMS3*d|quqsEqR0-mKE+xbRA20950v=1>5t!dY$-P z_Aj0qt-NKS5{OPR1h403%Ldy}1_bZUq)wP%q;$H3a39?#ldY<$%}pQji`|slH7HA1 zb{7~IEIfl31aRei=L8op>}9i^Ld3?FuwA`Njj;QhL?ui=3qupAqZiZ(E8;J&yAM=s z0-c+OZ#Q`Bmpp5l)CMdna1CnbPAi`0CU#U+eiOToQyrN=!*A1o-UrCq$?k2LLtZ__ zJxoBqu>)?a5uy%< z;AfqdWqXF9E{$GX$d@K-YCNM@{z$q+EKA}w+1B?07vr{Qm+B}Pc6&#tO%hAutZY=2 z+O0`Ft9%gz?~-M>7d_l3B>k*Q7}P0sv#;z9W#e2;S{@+r+VqrzUQhI29*F zrgT<|n2{_fvF!a!?deJ$T(VtFkIq?JRj z_`q1PMc+c7ZuUK`HkWet|HOrvKlugn6tRyw)jPyG!{nr*H~n1rSm=xJ!@6y9daPZA zG#WE%O(tzRBkIedt)f({kCaJC@<}`gVljuuyH{p4B;^{X4=6vy-ZyajSNzr(31Go{rJS zv)dF}Eak)1p!<8}bFF`C*G8nEiltn@+*&$eU-gGqG>aXt!c*Q2eIQ=kw-Nd$RIir| zabi@@Ea`A2b^JIHJ;ORCv?bav>`@*hi9`d3tPGQ25yn?r2Ks=|*V?dx=IBD6c3I0G z`nkZ4*-=76D>rqo>=*dnb zG3rRrCe2J)PU8xiNR2Xne~v0%?)|=&|3Wl1QOK#y(`Sqhig>wVHb(_NPWXjLjXFN8 zpPSDX?QC-F0HX1E2$%A zhb5o}#V{^k|DL2zW50dIzBud$>Q=o$vY$!c^-)oU6Nj(YsViG9Yqv7wRyyjEMOrm+@J&tc z@z99vo7Rm^L`kirmlX)BJPWS5o0S~E9;FS!nuo$bGj@5}QM@$m!Zhr~Y=UFBe)r%s z;lnNg8Tla5VZUgFYIG&PM|vydcVN1a?E8%A(?{>TRS#3QPA;9GK0KzksW!k&o-hM} zecr881MinYT6VE!0u}inBWCXxb|$xme3U$#BBxWRy#CWsyZi6K%f}rs1BsE-AxP{j z$9|53u=qsSJswBc6^OV)w>|hDFdP5yH`b+yB6t+NP}771w>vNOUXbURwOyc{-vh57 zjvu}i=$;$A84B2cchBtEvH#B=?|p#)AhvrkKl&z=O~e_~Mk zImnxmp!Nt?AhbC%$8u~@4FJ~$9Nrp*XIqZxXUuLbA4{^Htq9rh!00 z(zdA$KQ-9A@>QSyb?ca!ZpUjt$^`<=%tz2ukgn?Y_Z<^Jl{N;TwwBVL=qGo=)Pk1A zC?Cy5MIT86(ph7L5Lz>9%#1CGEn+L8h?U;!Mreon*9CF|qs+)+pW3W=V8baLT%&GS zGiM#r^t2n^t zY`!Shv%f7%lQ$Js^>k5H@~n37d|?KzY}(t|dX7uCFS>4hxB9ktUof@y*nlC_9-@?F zZIn#KjCiQ0LK?iuCqfduQ?uY4ctG#;ba=(y;hu|0y+fq9|bs1fXP-vXFQ_(#n^7#aHASFDD;G zMPg*Jp1JOd@PI<$TeE*#8OMV`HlD-QU@ZyVP4Nlz3e(*%jKNJ(e5BGb^u^gnb|~vL7lp-|Lyl4k3QOaiZN^s7 z5wQg2@iGY{tqC2JT&^4U9!wgw#ZqTyDw^~r(Rv|$?@j9O1rMw5)~PF$f~b@`ND;L$ z6VjAHQz$iPbM=U)AJN_+A)|!j+91N^kB}@<^kgRj@l*w>*zq+Egckbt@)o!nkHGuR z($xAXE99glvP7hK0lZurE&n81o&o~v zME}P;2^^tU#4dlRn!9W`)~IS#E}KWT3PV{PYl>6N8_O(sXO;)7bTo#okw}QY3|)hQ zOy0EjD9X%)OQvlXliE};Epy3?75S`LLT#EsJgwzkGXbHJMDUe*nu&)j@}Ten1^ee{ z^F52_RjO_aj>q4YWg+fcUb%UMbhRBQ%Wbzop4Unv1L1a*6q!+81J~7 zscokrz7gMOrm_g*Qo6hrNpN4_4QavRRB}}yw#__YhKR_u=ud!4k$i4dP9))%AZbLW z5RyzlH0C8F0yM@YBnSzIyC=+Dv;2VR5uaUzJLCA^<&m15TRn?<0`<(yEhIa0_~7=5 z<&`QsC-|V>7I9m^c@p3hAv!( z*5fml)w%$f^6>Th$myxZ?~1~B&JKIqAET5%*C7UrD;VLF|6(H$juua6d)qK+0SN9h z)8X7AasqwsmasO%jh*L>{XpYf?PV;$l-*}1lKqHL7#hYI$36R~z${DmDlnhQ z^}Oed;1)J8|JWZltm-V+CtO>vjTQ1Nx3Zki|?On^SFrIjoIl*z%}QX*$=E z`CI>DEc+L9@X#wi{u{>4@@{4S_%}{H{Q2WwaGtfLz5a{f{0BMnex{QoZ! z^gq`9e_%lWUnJ;%NB(5_ugIVO;|u?{=uajlcIN;5?^LVFByF<8_Po~ZDQs9)h)6z& zO)n{v4yp1#|E^zPW1-gIeF z>ic?l+3x9l{}k)&&e`4y(dxF?d^c24ySTSqxsoWl;-FaQrZ-}tb=JH{pd>TVOcjlp zbnxuG=ezJw!K=OXbo-637*;Tm#6j!I>RP8((wxq_Cl&~ zf62pADn9Hk{%xA@9ba%{hU7CR!aUPWP3i^e7`V3L?X%fvU3axTt;j{Dq+86+o;lZz zj=@Y=VanTOlljb?)yOn49u166Dzl?x;G`MTjNCth`}=)Nk@3sNNHk3 zmw^>g%k-^&Z_;wr`hYkt)1;b7b7hx%s{@Y^CwR<|jsdBagrTApAtNEFij!7cM3CzX z^OAdmXSDe+fYvyfbw1m-&3Y%34!HtjKs`Ppq$R$1YzTxkbBgj4-YRBT4rWKAXmuDo zf3)B&ja+ubu%BACwDC(yI_juTuhJf+if=gW<^B^aFhCAj6GwdiJecZ<`KqaKv7u6Z zwDy2XOUIxkF%Fd+w&3>PX)7?{Jv}Y%kru8GAqf)W=n(>IVA{c6u3G{*AVf@?v@0z4 z51L1|p-<&j?sRQJ5#)eH%ywQb>yA~b%C6yj^z1Bqk}(Kd!7l}U)>I*pg>HTOfJ+8s-X<`9ndLHMxtl){o z&9>P%JNY@Q*u*T%H`Or)xTFxlx2OW>#27p`U6uaFyke|62iOkDJ=IqDdOgk($Ju|H z8V7EF6rQ~yU2Cl;)XIi4iqval*Z9b#6Fp~p#qsIdfE^lbrb-GisHJM9%OE>;=sL26 zJ754=9}w2r40&|x_mOIKLe8bn2gpb0wAE#oMecl&+5QB+qM`V50Ay~~M!dR105Qgi zB@YI7KgZTy5~*Cjz^A?K)auEjiOt(g(S+uqGOS`Z=9IjLfU>ovA>(Idmlam^RMD=` z8h6Hx(2{|B#fIRhWl7S=DMt8|6F2LW`<%iKMjr)=P+lE)td(UYKq`Qv6IjJsSCA%G4&vY0uclcN+SY7i z4C}r~s)yi_swv>2W@-Tc%91w8G!O&`asYd?@lLxpA)Tje5NvCiHj}8ABwbqQJ39r7A;AbX5KYldE>-e>Il z6?=|VvHInT3$m`~oi56=;`2r-)x!GJn-4&$y?_&WhKFvvb(v@a$Zmo7Gc@&qrfw}J z^kUb(wboD-vN*Bx>HgPRBs4W14k89_^Qc$%Z;$*^uVRfnWMkqLB&mCczx-fc@@Y>9 z^>DFzBQbgyvs&xnWFZFQUyiBI5lL@T^Cp}k+!`pOk22P8{16MHsrqr}mh&&M1^V}D zp+f`L)2t`*dfI8U;G&W|{REe`xz0{IGmc=sX}qw9k;-T*#c;Kf=vu_qF*qH}`M{GE zfbt)h`%AIJaLK7+xs{|#1wgfNjSCyJ2mBx53QC`e0+J1hq6{NDy;`w?j|XN*cfXw2 zwC$1ccUv-?8>!_sc|_3*3oTnk&8gm;{JU6EP_yM)Sg!PFE4mlm(C|(wE+QXlvF!P#Kk{s7;5RSLB~mBk>=QHT5Sq^|r$(tK(bObjvCx&OSo#BDx>ymp-C$Y^K$rQRyDVE1ZIDfIk6 zCYst^K$ch^q!9o4NmPd5@@I0OssuwyQ0`>_Z`$5Dn0vnbqj>mpL+{QCUSz)&-t27> z`2A5pMj=o5kaw9mnOVm!7Jr1kl)_b!Z$w8zw|GbF5!)`Jpo!vjTJ_qSOnYNILZ!?T z_Z55~O)JV1uuqJg#EbVWz41_o2#%oHqc9{?ne`kDs0&4m=S*?Ep*|QuwPn8S(YK3u ziE-`LbIDmfJ#6GKI-9R7s_xC;N_^f$qHlnWNINoN!xH~C~y)+u{uvbyR(*+Gu;M!g zMhpbD5?am$5$B)4m!snbv46zDmdhjV2`=?#pBKR2#Uf&SoYYTBKA+0krOE2U$uC*< zR<`CpMI`iTt;h$Gdz%bC&(0+|y3U6aaRxe3ApvIR&~N)vctM4SVzFVxdU6Ye?lF3C zv>iT#QDX^?jz0TqNKUk~8_O0pVgU)F#EukHeWME)Qm{4>HEay*Sg=zFCkrnlv8esl z^ts)ppT>V(2fLTy2nH`p)0BX+bUiH<2L422N{FJ49WpC%;9aL;7c#UF7ds{`Yom0K zV2vol?kI?U=?y1Gh-F*KmSryLTZOECm3`0D@ll}G7*(!bv)LBt9;*4upI%D+2t_O` z_-X7|*&H|=R3w^7&JO~YV(`dL61r{QlF{Z_zU=oSRqGugKKPq)8ZZJ!>SsmSHZixI~U42xL&cwJ}5tt@Zl=D$B<(3)|k$Vl2 z*!3@6Qze?M((@C0v0iyi|5lpN5N!+ZRHQnF@bJ}J*yVQ7v-L?w4y8K>TN94f)lqLv z6ht$a%nXCvOc@q{ECj=p0ZW==)Rdz}6Xl<(P6tobiz!D{w4gWJ!UnFmQoP!EzT-H@ z#kFY~y?ftgmH z#Llj9w?%4zaM8RsHch7s--amHiwTsGH(S-tj4NTq4f$l2Zmlf!pM8S_K>Au|BUdNn zNA^UbRL936(nr|ac%|WL-4f$|YKs3+UiGb0A-ZQz%q+5*CfU)X8*5h5qB4&Aebiy$ zfdLYDx~bcn5Fmu2wqGiZ$b2&FeqKr~WoDflGDq_3VX_O|E(yVn-mK@^wL6hHLZU2u za|=4GV?xUOV4`0CWQWVRCwwcm#G5u=!aQ&hBpGcCpT+MGE@72KbVBfq&JcOPg3bSh zDC>w9O$cQm)5$3n#DQB6k@mjzOx+t?$39ZwX&EZbXzoI}cHEfJtPK7RnS<{kYXD>s zw-}u85R+o+Skw#13h(it6mqXC!DDlCrX>fMtLJQ-(wRJ9^K#)(aw==3R{<|I%6FQd zadzx9+^$bh-5Fl!cGFwcCH7E>%SjiivIC+vgt2eQoMwdRRh~)IWvx=-Gun0iV3~SFJ4&ZX$+rrToq5s=x;6KikyR71IT4U7jqNtUj z&u$`2q=Y#J3idE{_~7aYri4RSy-ZMEy|v}u_~reErVlXMDeUuKh)xEE|4(&_T39=q zIMRt)8#tQ?n;6*{o6!BQlAP_o_0gb?Re%blM-KU&@Tmsx$YPa&7l5h=^H!h?#|?$S zv6el-eb`l-Z1b9NYxgFI^5;)AU~n^Z=Kr&O7$8SR@YydQh=Dtd2daQ*RN~Gj^uaTA zHnGr+2N)G$#$S~D1}-N@Ra>IT8j_-IUA55%s#9JI_4J2LugZIwh9%Uj^hp+${d5 z9u=^)wR3jDXJq*gvyOi%+|Jh7#P(mq|8&3j|EW>O|6{`Of6H>#|LoN9KXw!TJBiNv zUrO}(T}1`+od(Wc;aSKbWCP zYJXVn_CXU*1M*O!|u8(7o?g)X~L zWVAcv-lw!h#Bg

=Y&ZNSDBXM?$67ez%l{|kxSC6vGf(;+A1&#ef}QEr zIaBde(@J%%WPfDuK^Z>XVWzR|>{k5{=k1bIT5VoD_?AzE2&uLR0WRp8X9gtc+BA;M zbzz%V)UWvWq184#Ujh`>(EXSm+8>FpKiFy4(5G*whOO;#AZ*}~;L)4h2~YA;Nb@7! zlvf!Hz+@)OY(6}+Z_~js<-i0p#@XG*ERclr?gGf{ieC3GGtBOCc7_N10`0htIOmUb z^{;>01UBa$bg6j2KkGv0n}|L#70`t0c4v2VnU8}~!bm>HQF@xJrJ95a98 zdB($S;y2_VJC9ltPogB750t6tgLqtHMnFv#&S_Zyn|uK+lbW}>Boi)Y$S(5@ek!B@ z90BPGe+~;7cQ9uQ5u$c^clL~bSh#2imc-RlnGCo|9<<2+@rA^)@+7l4gWM>|!LX^| zwXsca83nU}+!>OVw=D`qlgopeCxbjj?Q-q?P=@7!Y_a3*g$on~Hb{pc;Ih>B=&C?n z&yym4IFx@zSoTI(7DP0?RSXR%a}2%z6H438sEmLIF=*ANF<_lHMe8s6qa+bWT_TAD zq2%ycI31OE9~6=EBX|?PSSZyiq5@Pv1YD5JgShepe%JhG-C#js1p=F61il4&kb$UP zr&Ny^S}G*rm?nfk?zl@llht%pmN*0+T!B9e{?MI82L0FuPZl+2Ie-)NRLuz83BMH}R-oXkko&KrQ&@4#h5*lAR{oyi5TFrVtD&?B`LOedp{d)H@_)efBES@fBVRSRF?sJ5GN{vNL)*03MQpt)P2) z4dfpXBdkKzE~=iYDidZB21%-)s$+jskrfv%)N~JHSRbJ81-NFl)V5dYWP%<6_>KW)@@{1CMn<#)!4G_+v>qJs|0(abiE&72Vcb`+B$yANd^8gzSHw&W^ zax@Ta=sQ-{Z{txIjJ~f<0l~7I!KIiKBZ*hJ!}3gK@l2kgD#Q`#r}~UV^6ucfUD?>#S34}x<6z~zP2XaHUM%PeG5cR zbpalfzS^FXo~TiRoqZG#L%DK+NmzhYWA6Cna;zB%8S$ zY87QX1}Frzz9kxdnD*p;^J!>sv6st;<5yaEO#8B$m;1h+B{!oM9~d$@T1+qkrxAQD zD=*MP_48Ldz+uo5%?92I>{$De*gP8HAROL7D#Z!*fx9LcizGe0^$Zh@YwEx2eeZy% zulmCR8tb1L>qmg45&2uIf8oWSb)10v;JW5D?#TnCWvF%#xA=6vH;G(d|AJJidxX1w zR`bLq__1}&+Dz%@49|ML!+^AF6Va={^%=-|?lHi}ytjCUd~dqYfNL}n5IIDhdfMBj zTrQZ(ufRfY7(kZ*YEtLF$_3PV!xO*j1KQtDk_RP<;p69vK%Jw#gb3f=`-N&^Lr5Xu zY$ghkRP(Npc*n8;9wGmOH_AgD z8RCmS?K`yt#bnD6)rZ1?$t?ooW0Sbf`v>C_;P35Y*4UB6b`V`~L$zZ>U9W*fm(+bM zFR%3xW6;f0_z`Vc3kqJ*^%;hM#v=9#=JMCoB{pob?S!i-1*9pUY@(nMqg~G8N`(=> zS4&Pt7e?EPrHq{!ZTnu)HnmnI52kBMv&IF+9d0(C+5}2dB#|kr_1;RLWbT3N1+piH zaoRZ-l@qOd0wp2MZ#k96(1X;4h~=G-K}@(#LHzWO5w4N%YxmkBa^uPZl&t%cz(_K+ zxju%S&KE2`G;Qr4Nt_Qhd~X47(8j#pJF|BSGGq15IE`S?1#sufI~=R z%b1*4Upqi=9f0@nu4GZHQOjpdz;QY4&u-03q`cB9l=9yM!fQ$v3~IMTWm(b@4+ygL zr+{}4kSyHI11fd$qZK^@3ZPnpGrcd|eD#d+Va7aPll3xtDrH?C(l49}P1Z3cP0V#p z+`k`pENoiWwtMR=!6R9kF4Z)HqT7y}oE~y4`3C@b2LlJ+*va#eVvT9)6n_0K6eTt1 zrh*r%8^R#YpMk_>GtxYnd&#j;l*X-XM+Ii<5+FfCrz)S^EGvLCf|SJJhmvxH`x@VY z8S2*SB6^QPF>o_gH_P}GsmM)Y=v!FP9X0(mCm83^(6@$3jtQ?~&VuJfT7FN(Bn zb5&vcEN9F}s+mgltl9%;{eUJQujDTs99aegtjSmh1!H)m?pNBQprRw9sgF?+YPO+@ z6b9HS6G6lgRrhAe?H$qoRDNl2E!uEz6&IIFL3EMbVlxotf* zcuSEmoPDf1T*Kf+r#0d@Im*hi^y0PJ&-d+ciJN^#(X;x`h6LuC^N z<%QZH=MIEhPv9sBkd+wPGjE#IFaqkwFx&w0^F-y!)jsZ#v3i6NxqD(ddq6i$*$g06 zW7*A>D&z*xxQtV7a@+**SD8e@>1Vs$l-&xe>l7slf3G6fi}bb;MnVXuPOw&W=b&6U z)c#SIyQ-Z+L2!hL zKpgUxnG#`kh)h?%rY)!*&-kkDU#}RKl_M|BpWq^zti%j1owV_Wv#^fMB-BZe6vm{| zHVR=}AHJERfrh8~Ooqh0Li{{FTVhTBOR_-0l^$6XomH3gcz%i8I2-->vuq=?C5AH> z$Yl;BP{K7czSHl`pij^k35kLog2K(9>1+iLKZ;D@O(yueImtyPk$6A(6pC__*Bm+8 z5>@`}6KXSqy~>D% z2<{OLTXi-WTLcUp_s0l{0AjF+Km@#vo#)yW3d9)6c;he=TPxFS$EJNYYIY`z)R;{6 z2=eP2+=UU_eK86Rz~cdbpp1OE-ejmsiPODGEx}&dq8l7`c7o%=kNnIANJM(69)&>o zO*2YOMHM(_3e#D82MW%ovSz5Dm%WW=T}h}~MQgTH%V}vsFf+o>27Z1L7R(7VTHn)u zAHx=LBigLudVd+5068MZid)I@Qh^L-Q+4WL(TSNk_b4ki_7)CHud2!xxK$)O)!b)d zCpY^63g?oHk}*HSNf&`cb3IN9Uwxo_1AS!|4yf0hjxTxW=}vlRDITR+jD<4U7NA%3 zi;?lO+w=(QRGb#=MSdy7{mKm0MuL%?OjSQIZ0TNYrPVC`aY$0j&Z@uS$00NF84891XQ@MkqQMHcDSauf0p{{B6PE75R|Cr zcS~CHOF;*tfb@U(%EXSD!Oi^aYTgEaZL4wd~(^9eNb!V63#P|V7Og+)Cs?-Q&B zeN0vGLg26+$35w2)&gY^)0=8}4IwFl0JZ5FQSsT7d|6sPG9Hll>GOmkwzKvX-SIlk zPy)^yNy8pFBE>8mA5ExO25tKV)(vg%PbBdBYzvt58v4P53T_hTbfYWRyz$SP5iYErA1BFI3&;t+id=Pn zUOKqZgTyvySH7CzxZ?6zZ%qJb8T$!1K7Q51f`6j-PyZP<(3F8kmrC*jbu6OjuQrI^ zM)TqP!%#xwLMo3Vl84}pgCnfs+-Vh~2?h{1UQ8eoy+h%l51c2#7m_N82Z?ZanjH<@ z!d^`OlBz9uuQ@z`K~YDTJg5^hCVPM!&DB^ImgAAzK3@cvFRgT$^+zAbWVe0LJiB|uvQ=ty(_>-?DR%;oYJ9$RoPJ;sTYmt>torr04II;>j-6=a4@rde+<-G@EuqKoyA-wSQJ&dSiHj>TbLS5BCz~z3UzUc- zA3!hRQ&K!gVls-nYd1^I0U>A?NLysvntN=o5zHvSP7mE|DhpYK*uoV#wVmP-{uyLw z-mKO;2Mn{ze#cdOxx=Fh3x!#1(wT_WpI?x40KrEzxc?3{{YwVy|A4*G{bxDML@jJT z9Rf&=GSe@sQQvFkIj~jVD0DDN7p`jVqn9gpUaz6-$r?9%$J>M6*S4#Sf1LB{zOOBJ zJvCN)Us`^jqUn0w4*u`MA*Jr?nCkWOJ2V|YP?0dvzuUvVB)9$(m;F67_W$6rzcK{= z|0QDiClzD(&aeHu#^x^(%fFzrzk+T5wvYdW%UJ&{F8d!vCNMG3GyE$qn@G{LT^IQg zeEmuJ6yG+J?nwlR3~Hoay17<|KWYw~kM>6NHZv5DPj0@U`!xw8D>=qATHs>9$JDyf zZ})pO%@qzjWAk&;tJaYB)5*=L;ZI*x5{%VIyk+;p^>qJf@z-TGuA$4zyBC@*?omef z>TV<^&2jT{`RkQWr@^-h#)-=aJm0YedlC+AlNhXh)5{)>cJ*=B>Jx&I#4P+bp5N9Tnon|5FNvn-^)bQ)Fjh_Ok#0uIiU@i!z(JA0^`k@|KBniJY< zP3ms^BrY7+QpTq<9y5HR|8u?Z+Mc6f?AtTLsH0TOs2*x{ z`JFinn{0<@QtnrkT;LZ23Ir<#%4(RzAsMGz$Ft+w?w*j{t|&%ZOlZgowWomFJ+kTn zHHHFlysVr1!19BOcf0@d{ct-e5T?VqsBtpylQ@v*xr8();YXx~9FOSRA;1R=5i7~` zRFScSFkP^lF;%_)LEos&J4UY%I#HHS{t%n3wfmXnH9X8!zK*WvzInARH`E2%KB~O7 zzLKlTZI2ZH#b>gAt^kOdcNWcVdgph+@rA|Ym;94)# zaakeQ_E#Z@FpFSZR$+`)x%ZQiqwA&PhU7izz4zG}1vkQzx%5+ib#xSONetBo4;xtgZ|q zp-9Vm!ZQF~tKq7DNx?k8#~{ur>>Y!!k#RU#v0}+j`ds!1?Z-Dc+N>RCA>lFfrN9ao z2_3qE^B(KAo~r$$0#o;swPc*#52nsC)eBBhakoYGkTa@|L>u?m$>pOC$(sP7hb{Vt zFoFt_JhsUo_Z4D>WJ(mJDZ`uO_~$ekIGA&h#S%CXQ^346D1`gQQn&!3ph?+vu8>;5 zqXrbvrFLgrwN>QYic=mEGQegHr5v%>%^WtahWppG>it=DkCUdkexjiUS?5->YhG2k zJfhkyoSJYn-|c6CSl_w!pr7l#z>W3fI*?1p2S49ohX!O}e%=sAzq4+|IQmzF4hj@? zkn}5LQwpe4Q*Wv#oHV5mC{d2u0yr;1cUX7m7ne$?x)mu&9D&&$ue6l>qT_;44hned zx~)XoU86gz5SGHCHsG{~^MNBbRZ1cq3e-B&$~Hq^X&wz4(T-G4AVA>nAv~Wrkj6xV z$CnFXj?m2tIM@0`1o#Tn$Hbg3pF)PGSVs^sAmU5=Ob%)=Y%LVEo;pP_c;eAK8sj{( zWgR?eW7q=J?C;A$>}nsqfTZ^$kQvBgb5rb1uavXib;~FcSx)pcp8i1OgoPAscz%f3 zIk0S1ogsqFHnRgqR6eIX1E;Y57lVGp3sRG7VgySxkZ`0t(idXJfN&_n>u3-ARsds* z?IGb(TK%dnF-%3OJw46G-AZk6IMHx{$ulJLAIAxI$y?PqSZlAM`my>tVY{#{f|do9 zT5~&pcm?u``T`be+_`RX5Vrh{(a%48i1WTAvG_tm7b*>XLq)G)ts z0savVldK@qon|UH0vcaAlVUd#)s>7Pc|vJPZ2JMP6j;sbmCQC8^R~w`5>LnXFo_?T zy_Yg0_>>`WWQ5n)2hhp6lwjv_U|EBDIhy%OykujiKQ_>i%?-5tVf79V1!W5aRormr zeU|)_rMjhaUN(Gp6cW;ppI6CGOU@&Dc*j4#fnA9z%`64 ziZ{-1oEOY2icffBdQyln+-I3n}7@dQsv_v`ahXIcP{Zilg1pE`Nsj4Pt zl}Ak^IMnJRE$>B7#gdt$aoc$t;;2as+UdkHlu z(ge6R3q9jy$sxz6VZ2~=bqj-*w@A`(6L?k1e0l>NXDsNEM?<>6aMo)aFLq?f2S-~~ zB1=z*_5G*qL|=9!nCq_Gl;t_$YDAKL0Uv8A?y|y+pqp`ZhA9@OtIvLdP337lZ2z;v zf?O7uIS{JOya@E1e-CB&q8XW+%c}$k7P~h6mg$Xp%DvQpx@+!*`6Pb|ifrzS`)$8k zxVoPC5%={XwYe%KyfOiD-8k{SaK3h5`|63X?Ta1H$edQn+`D zP!4Hc-!9#r5VaBrH?irqV-%z^;PZO9 zM!aTkR8NXhmOe3a|1ix?p%yP3PoxdCfL*YR;0zhR&y_-xR3Op>T(sUI(@n(mn%b7g z48OJ6OPdI>%d5(#&i%7M7?X1+)XuF6BnFML*eL(|kp;O`RRQ*aeJh7`P>E<$pdSjF zSge03;&+)cLW@|AIyqo_#w!$lM{7btdZnSXbu5~XX)m?cdQsEl>WHlo}fx4#<~@ieMI>LQ9%?1ECJP%cj?(np z-)X5{zzL~Ac!!W*Gs-gy^gC8CM>$prR8N|yPdpd#kF0m&1B(stuBUHM%v>*pxO_pX z=1z-rY!9=6VeNW$hdUl%bE<6>IevxD&j}K) zX=_}{vX#JPH0{>7#OO2c@qstURP*X#5mS7HG2+FmZW^!zM+)-&rS)w+p004kH7vz@ z7bO*yJbSO%#~FX)iex`mY0R~1)<%z<1?!w4ANI|U4lk-F=S552VZZ@%LY-4Z?38zED=dhiIphx!= zRycHyH(VULZi_f!6yfj6%92q_N9NfFQ)Ru3s5@@z`;ok8_2?t94J~{01+ta%63n%E96 zH8>|qC5p1sI>xfvFh`~eRx(yF_s_B3to2wn<&wil-;lzoKE(%9w=l^wCvf+{YHwWH z?ReC0Ti^&z<-~OGy;@B2hyb}Ir@z&KKUX}s8?bXw9LSvPJY>3x)5HD-o&$F50ldK> ztu*n^C#0bmK$HPG;^gx9r3!ctw)+nFJPnW<^xzzP!=oYQkKnRPjm%e+Ioy`Q@S@&! zc?_%by4AzXoJ4-HW?njm0nvI!B;W>;1Ws@QfH;Q!_A_)RCp%j|w9yf)!4%#8>iYNX zYz8quYNyvaAepB|Q;@aIPNP2>{A;o02bml0N+H$(IfYPLC5kO!k@7M_QBHDvDzT%VrSqG z@C+53K*293+IqoPdytm&lb~080<7?OA?|=mzF;1I-g0-GJOO3VT@HF@n;M!aF!Hs7 z%xHs*xUm5a1@(zz^WjVbMwfhaY2^fwepy6&bbp)+N2$rhSa5SeOoyx%yv_h>`<5W8 z0jCncA4qv<>HAmqO#pc|D;*(;+XPVH{Qk2`Rm6OJV#+jH5 z$>=SF1rm!^Z1Hn@R@!0cX#pAGgharFeiR)Pn4$v4rYlcqVl!BBfM0R~Yag#on_6kt zC%R3!;gX+lv#0qXitI`FoPh{%ezQ_c8s$yX!zHwbCph=Q`Jnv(ylMo&6pa@SGw5@M zRhAPf_dN$$fTs?tCCnH3VOL!CC++DdJvUFehu6~CE%wwGOwE!XGnh-%F)Xs1Pw4!K z<^=!Q1Hf7p2tW{+5trpNed{#@nL=42Ddh$n)AhYB~Km;rh#N_5VGTq7_u6{|htyO_jyNLo4#V9xg6gK?YqqyzdarzpY70iSDnj{BLj7 zccA9qNz}KX>;J}8jDHuR{U<`j_HPN*zgW%w4S19?wlQ@w!((D$V*OV_RiP?ryDo;* z^{)C5r=^D6{u=Ms3s9SIVMN~xQd{FWbHFEAH_#|V!9Y&>oiLC4NkDA5rw5;xK6!Ok zR^8?C@G_ZOi904t>GNvr-=i{`aKE#tJq34MBCjBc^^aC zi(*+>yGdCwLf5y`-IKCv%R6v*^1|Zc=WUVo_@GH6^8IbaYJd=rW4wrX8I*hh!iZ|X-WY-9H;S+PN+9o}&gLBJ29dfmF$BJ*{V;|Uo8c5&f& zVMz3>o^FZ6k=<(3`}ZPq+Ep$E28kjA5IvNFi&}%nh_LEUF&VS8NTVYY_3hQ4%|GRq zW14?%0qKBEtV%I>FhuPsVaW;((9+l$U7Ct_i&K))I~bYt9^+q3kM}d0$XErc%^6eB+H>2G(x0TfzlXv|$akd0THHKOLR43m9nJzbG-Yj#|w zT5olX21jeyu@p6cvkTXzbzxJ^J9|+}AXM8Fss=EO=eub4oYA&WzLE0z#4St3PMtsa zrDa*bYpzhTlMOQr|7*()F3SG0XfeFf+SE{nQ(^g*99P<*AG1R@F8-Ir8$oDAD2TFsL*cdIMg!A9<ojGYs*vY7@Y+ zqkxB|Uw+epSconFv{BIq#f8oi=9@!q*R$Z#mYFql=PD;3g+0%~8*Pk|FR5_PNjA)4 z-f&5-Zz4B8A(58m8(!rRBvYbm&(|(5$cC6y)k6#GRua=^8kBvTJm~m9pe^Q|gcb`= z1|endJh%c7==SV*kq=76VX?k#6ihU5RqBqW@}`}3)S*jx`FIy<{KF63QgYOg0cd-X zmlh3V9OddNnK?huntV#c!1PyvVM;c(Q-dRB0}Cy&yY%4XMqnb2X<}FV9=2> zqmn?X;qI8`No#vX#!F~QCp4sT1(yGm*Tq0ra1-TJ^K{CAL?4VbF=k+EZ!$8+5yl)E zs0$%htNRecZTF#9k;=0?F_M*~o$?`&hzhY3V;Ukti|Och5V#j$p!W)2C)W7t1eAzf z$g+joRpe}RodP(TH5cT6u@~2Yq-X>*98CPRhTN4^Mc}E8bX&+=u37EHuaB`*?Zd2b zW)ZAV@>D79yr$vq1r(Zhk7lyIowB1KNk@?m8%eCpM&thShOdONtUu|wrFt%G&6J}M zZ*cFN%RF_#{3`{hu2rPzxYx&=2+f^-Tkhtx3LZss#psuIsV;QNL#OOMVc5~yQ4-j) zMYi-g#ZCp#X-GmDHNbf9RQx;J;xQ#bp+kp01JCjmCb%o_I^g+2dqE326BJEn)pSTd zbeeuaxY(1~w~qen*dN}QNji#wauf8NLddxf|EiHt`%>Qo*)(f8@YbihGmwfDJb_`V zey5A+cDxkwt^+^#YLHxW1-HG{UGO~%gNG<~Hh9ZQuya8V3G)hdy{>(sU^^+ciW_!= zDeG~CHi#o`f8!b-S*xQg%cR#gq$;^h1Js06o{H>z7BKS$g7Kq|J#Ox2E6~V+|Hy*# zT=J2hPFr_#t$clQ(qjW0zBj+(AJEAe_?g?1B4JErZt31lQn1)a09c~}S%$kzM{Hf< zCk{k?0W78$FLQJ|&eZmT2lvMsj*7FvF_Pj9qT<`jEaV8%m4IjN+lG>ABg{h<5#Eq| zBDa{b?LD%T+JXSFoA%Ykm+vXvDpvXfTN82)6Y=6@?eY?!EPM76L^qPf0D3eg?d3br zz;P!z19-g{0z&C@DNc+LP*| zKVHW1O-OmPYbPW@gmLaSr5LoA6EoGIb~;t#4wsWOb{Cz255VO#g!uB&i%d(UY+*<~ zmNXFx6!PK~GPIDz#r_xro+x1rWO;$u>FEY!1UQCp_KKFcQ|Dj(SpK@fk;DPNR5Fm> zo4pxlVRgG*nu6?8iiVtH2ZnZuXC>JjO->`j!e=tUdJhB;GKJ)Xb7quH&5y1n2$Rp3`aQ9Y<-mJU=id>H zH3Uzj-8BRPi?%t!Btow>O>Jjm|FF zl9t%}uXiKEjaH(iS&}+YBfjr8JO{}Az|y>;-F9n9QlAYHy}3LN-G_qr%8O)_3UE|C zjW&g@Ai5-+I`b(W`oyqw&eqTpZGD^_wXAYn4k}5wE$^2x5t?_xMER^4XFtP}q7lwY ztfrQ1&8w*g&sHk>k(u+z_E_PRJy>-Yg0BaQB+PUR^z;ad$Iq@N zfhaZq14n!9s!@yr2llDrsdfLCCu%vgA&7F$393eGSvhsy0A@j#lx z&}>&;6Qr|5tf!C11opUigDf;=5Y1V)i}_(<3G29IR(|*1Ffnj8G47KA)Sve|u$lGC ze;yq2L)ck-L5G*J{nh3L0zP~U!|n_u*$Kot2JQmIZEAB#97SiP8S@H)9mS<#4IQ9p zHza=m(+ad|pJ#@f*XB93dtw5l8$|8)gloauv*-%CX4zW=kDmm}@by|7BRwohX)9J) z>o$1EtN@eAl58T-^IFw0 zlxpD`x8$`F9EeCV^C`uwX&&0;Zx2btS1-`~PHhp^ATdWt`ojiZXf`0~id=te|1~`E z!=;po3^xF6Ct%-1mN*a_Zka{9A$+pQGbW_CjJ5ptEa4P+r;JK@jld7uxMcf$Uq4s< zv*I;t__3|a*7_$?jnXUs^$+mR`9a-THRdWWrPRXVkbF;$vQGkS5+OpZ`D&2Wxeg!6}>=3z%{@rh)z_ZL_pAVd?*JiY)@E{8*TD3jiw8DhrdJhB=dk!P!eq z@7RFN=HYdZrXm@>RzR4*+v-&AEZFw}cIn%yjdmA1l}w^xhC}P<$;?cTVIve=fg8(o z&t|#Lgf*8?*hK{_$Zh^DbGjxM8eFsLve#?Xn<5_sfhL_|&C&eQ6abohFFhDEY8sG! zw?QQDl&{nMOUe}pQK0_VtrSDvRDZJrd(7A%=aAhh-)E8i6<3ogz6`xWS|n@p-T8b4 z{3c*ZQf8|`OqHvSf7j+z8Wo7Mq#PcPvYv}R>YDDW!o|3LPxy=xf@jIZP{$#Pnltwf zv*q#rEO>l+&`#=gbi5Z<_MF+ifv$$QfZv@zboJh##u{7ANzeL}$Chr)sHUl>Z}vGc z{^HvpXc(fg<9nV1>%$#1=NK3(BE80{qs6=~Um@-XV)Bkr1B9|%(-&{4tx)M(#P%n> zxc3kF8OsVwWIQ|IZvH0!9ZwucJxKWBm+SAnH5XBAi-|?MTF28jz|qmUf`wV1P?*l{ z)AVW{$!y>4qA&D2Eh8qZRD?ONhqst<q>|s+6Mjz zpM^K00x@qXIpO-9ZS|jvS@jb1E7GWZs)0vgidBSZN3RLpk=3F@FRZ~qDX1X71m*erRF&xj6XY_x|NS^4gu}ZT{My~4iQ}t94LKvSG5&9|X1ofSd_EoAA0ieCK)EQKeVNmrLU%u1RO38BW zDULwmgTvX!mf1)?k}}usDCaN7z}zYyfbh64@Yci=Yg`>nwNz#=GcISi77iX;w~iXp ziWfyj*U;OM_g3MR^=L3CmTMg7+fKp1?cSrMdk0aIA@D~PY^^*sqkggCtQ)D(^APr- z)`I!m!E*n;KxmGMo(^SYX>$c2>gs9Lx(5&Zb((`hlQLUz;h;bVuVx!N<)QbvwH+o3 z9D`VDw6@V$KPW_F7Zm94btB&~FFk~*#;VV~q9>N9Qjg2Lc2iE%gUcQ0I40b|FSOK8 zEx`DxNH>y>cL;ji{Yz1y%aUXBUUk%0u4iB=o9R2oj(cFzsY#!zaQ&nHYQaieG9O{C zR{SlDjuYpN*uPCDKFT_){n2(qy`Q9xT+mL@JJ7*PlGa;rx#bj3I~|i%gE;IWOS~z# zOd^hxehgJ^Cm&jufk)~|C@<*<%^%Z-&HD~5o}r4*d~~#cnTBrbQsY_|S%mI(sLkZZ^#b~%Sm>^CJ)l}O59T32(6nJqN7y|%R2Je=8r!W)FgHBl+DiND)O&;rZn{wKuF^2RV&`Kc zzWo6uA1#BeFIhi809o+d>VHGy82-{%{rC34{{j^mS^pDLWME)cq>~F}VP*nU;D;&- z9+F#wkiZf!2S&hwM$iLT+<=7mcPsc8lj}cKX$*fEJ^x<U*D4hL=gjXPi17Od3bsz)mRvkKT16)ZXG14BeTTo;R=xr2+mF% zsiQ&>*R8KgR3RY--uSDOBfv!zqORH;LGr${%tNP%(0ffw5JDPf5*hl>SpoX&yRkF( z$U?mrrcSR{S-R3(h?N}*2-Qa7osdZ!KuMA6J(3;4Td#MbI_3a2ih%&Agilp<4zIyC zsJ`pgZ<|U4i}Hh5h_6YMt%$E)#vEI}2J-l}wRn*>04skP>jfs@xunhU+xF3EXZPCp zso%Ob9qEaF#%8tM1q8X?5LRE#w2$e!fqpn&P;lYpVS`|K#pbx*01ra#b(wd+e`|$L z;&c@6Vi@IMu%2J_&}H?xJqksyz#&h!61->QCq-t4IXCbbc-7p0dRKN>K4@HIZBt)a zHMe{OaSI#DmYeqr|uCrH+S@?zI5QV~U!z{yEd*3nN03h|8jDq{`LtV!5_jPWl`(T>JA>SHF z!oK#BqPw_Cp;nMn&s5Co2N+JN8E~&)f8aSEg}_+Sn;gNU&5g+DscZuhR6yrk;{Yt; ze4xV+0ib$Rc5r_*@x0-9W~~U|mVyHLgOd;m^QkRVSFy9H#AJ;HE_xTpRaCE9^PReg`a8{e_WmBYGYwL)VBTn>dB> z*3|nP9QYud_7Ij2J2vt z0ng3zje@@!b;|6F_?f1Z&BXTHGHGl@CoexmssETNlEGMf;i+mL@L^1}z+Q+$VIFZD z12=~r>m^R}lLRyyjZUj6l@|mZYm|@BG>$XMhfK4WX&TClXp{sw@C5 z;iuXAK(EsJo;yoRrr9c75J+KD?PIiL$wDS{czK4dgE&@~TM1Z-U7$CF5~aMH%))*? zNhf`h2f7nbmo{NWDT!BqFX273&>hZv8xP9{H!AHwO>?VXjk)V{l=T|q0CVpg3Qrn+ zp>dktOz*85^VA~w=1bRhO!?Qro##&W8EvE{4cgl&tk`z<%T66K-7#4L^#wi3xld?2 z?wRp97!Kp`BWu+qni6b+=hLA_u~iOghpX`LHK<8Hvr~F~DFjN5g8`3mXM|fPqI~c4JTfD85W1>$dcS%JC{H zk6aU5_sR!OPl)rL^8m5=q;I`lnHielM~}4IXdXsb>W6S;g1w7Ul>_FxE+MN#m6hn3 zIdyyB$@u)90{Caf+u+~Ts=sdg|8@Sr-&;Zdr*!>e9)W|coq(;ICLPUph5df9&@+4= zS=qmj+O*QfM&|ne?Wcb)A>Uml#{UPU>%Zw;{ZHCe|EWbqPxo)@*nbws|D%pEGqceB zYaJ_4UAOt}QF%YBJ`h5tTD#mw)0GG<&5Mc^UUK$E%WkgqMNku)M568Z8vpW3iE zM}WQSrCY>nFDe(yWVN|BVkAF1GluH=w6=C$c1O79MRXv0MUi}UZFy~N!tyRvw!F%6 zH)I@p+!NWDG~CqssJT4WTyWl=S2x@0M3isy^c=dlIPYh;F~d0Duh0y9m@HN6i#Dkj zF^D_khcm^6jR~umFwi!T)kzfn=(k<>n6;77>~#NRN1ap|5U|R)Ix}I32tulVFPBhh z4K;A%b5({Jf};2Af)ZkTzn3Vk6Jv1W&`eEIGjYG7{VrMi3KN)stWYeUBo68->Dy`G zUoiO;D?xE^$@)ql5p)XXGkwf(DlGuzWl$Vm{;JLTiG53PbG_cOwzelBslB`?9q`IG z?2!&`hvyf%Ryr8>^l87ruXiR1+~AZkDrSeET&MK#;a{ry7jxnAiTvnxV`wIN7_QLg zOW2Ck0obtu zN2e{YV3qX+bC48?x`E;sT*4YGmJ}ICD-3FTumTs=EhM$GC;jvd6}bK93ARdhKUlv` zde349nek|qPmPSm&(T5j8FOwBkP__K*nwVnc)3z;r7^!V^hV<Bl($dkn5!?RGXc z5#~zF2-Jo&u=B?;r9e2$mnci%+)ToGYkSLD$R7Y!;Xyz#K7kx?Leq%%3^mtO?yq7t zw{b^mbcN*ACdXQ~pcR5qGy0f35Z*9Lj1;}-W0gH*{)azv$0l(f8ehH}iEHW4y{_GX z4;5E-&7diFOiUW%bD)M`i;=|x05`eI+<$QCU7 z-PJ(cc%-d>C1DReL``>x(JiBjkfK(H6MEAsqzj+Ks4GRP?Gv7VKfel~7J7s3#ex__ znVw?YC#p%gbmv_onTsRsNu!HD(HVwQIxK>=UJ8beW71&ZQGL)i;Ad0AgVhGl`dAw8$y6-HU`a0a3gNqEy8!x zOg*mO=ABbZ7lLK*xR?2p&wpGuPhm`##$}LX%?OQ@QbFq2~YL;l)9MANChH$sVJ`Q z?9`)drpz=OEW0z)Jti>>UWwC6#J>Ihwuxu9iHoLEPBByNvh2KoKI3Ep&@2e|Vymsb zRFlc@h7@1nubff-Bfqv$B=uj*3}_ioXMdoKDy15{Q;WX++_X+`8J#jlQhSF^xvOEvEG8pHj$riHs(h~7jGi|1NFcCT@s%kguZ`eGL- zt+(eyVB2j_PA_t>rhzFL{xs*uZ^~iJXs)(=&cZz^KF<0Ke@T*(2ghmRQ&U}Cm&Vi- zmBOBT2}$!u9l+5N(z5Qs#|uyiip4>2WEpy_*$7rB#junnn)e6$w37sBhdaZp9&Fk( z<{Ms^_sphW4Fcm-I&#aUfmb3*7en_6i0V`Ph)U~#xD!S6--)5jckl?DeGVeD1LsYKcq zkgGt>#r<<*k2%5JWLhQ*DNbw|r|2neBt>=F6%pk(yPF7r5oI+VMg#{)qGESZXZaaS z68U2@C!{MdZM4$|G-jA2zkfLUq_GabiV+yXflfS8sNp-8An?}yc)PgDAwyS7Mu}tJ zUJE3F-z?MS?#N!%wKC8wG%!W(blkbb3!pD3Z#DOn+S;Ei-;m3u!w9QGp=Gmu2fWo8 zcn3$>^}e*j5|yCzk4=;OCV_MKpSx>YJD{DByhKFp&JH2JiRlk6H8ExdCvLEMSMk=G zYoiVtreuF%p&V@zv;C}axpZK}ak z1|3Z1iTm{Aq}@flS*4v}e7iK602q|2EY$a3)U>tZF*=~SfCwuX+ADZtgZjBs0rq*E z*yRI;U$E_DI9KpsNfI^Xdx-*u7~Rc|_b0#3(9MQ^fR|xm%mNdEZeqit+H*~wr#GmZQEF7 zt+H*~wlVefbnp1~j(>EY%tXwbzDK_|G9xqNdhYvrU==MoHhW{HahzT}6deM1ePMn@ zQVF!Y&rziw%Kc(Rw9=vvvylh}D{wnoMz5Y|zRW-KTTw`%0Q=pl!0QUmiZeM<#R1vf_(5U;AlnxargRhC(gmfetQMt60P&bo>sdT;^TyC zjR>Gx4NY`eoA`TBFj+%8o=}U-$YpaK%gyBC%t8u0UhWxA20$4COwC?oxN(ybMmPp8^my`K|X6<2O_Y_Sup;9SuMcJM1Q=q$xAroEzFC z3T0m{X5~bd`3Oku#%YEFkU2UDx@87ZIe>0wwohxOJth(tJ$;tGnNFuJ7w-L~s z=|H+`gjT@TCON#xIDxNua3>g=V`!P zlMyc~(!01K!?efVaU15M$b8K$e(?IJAhTCJ4hPx=MJcj^g9{8srVS_euBpf{19Al{ zV-9}m@V+B5mpNZe?`kx@tF)|aTR zM~BpWEprx&>YIg6V+;R27KMxaxji%S?_ z1o!L9iBN2<(xt6c!)t4$^ZBQAv~Um?)3sw=#P`+EP}CnG+=Nu%_%VeOL@lUSL@@7; z@^c@pQmAWLftxw`o_6~MXMe$IfVTlQnDC2yUJO=8u6wypy~$u?uvu&wDv#P6UX3+~ z^k-+lORKF=qfoPr0v&$+CVJuWC-kFNvI zn|k+EI2#D~3^Q(n76k+Zlw24_ajocO{(*tznq*P(bEhnZbmm+}DQ8BVDvRhyf^P*m zc?k>~RW0^(S5ef(g$?-cf=)@-b=JNc9yvy_@;VDxH{5JGasMI)f_(HA@QfnsUZnD$ zorax0-!?ymYa{|Dk$C2NyX`*q@ko@Z6V$^_@T3hd$mpPnLv3#ixzwz)$>xsi(4$%8 zi$ok>=MAZ23g`#{58`*RG?iLz`%_>gtF16A*EXdUoZk` zjb8_p=ctKHu&CK4uI@3GD)Q-YK~90rR}$b8TpzRIx?P?VA+zd^Dws}1OszpWWbxN7@sK&H0t%n7W2%{o01*aEoJQkI|`(J?j?qaVvz+ojrc zyd$#hkmuZSxPxaE?Lla{*TKhzT8d?lWpLC1XJLyD|x;GpX1qP z0P!O6qF+!7WUkH6a4`UFZZF_~#?f;5;F)6hrF61Ljz%jP?;U(Tb@7f;tOWlyahn(5 zW%U<{#T=pf+0#dpm^7KbJfOIYtA^Mve$<}>iNDn|*r1z^H}fS|-92-~gI^<;WLRM5;6f9X@v^wE35T|2VPK}yqVG>}~$-qeP`(8el z4M0^nf!dfPTObY1Ops?DU9sL)YCiR5q(wc;X}G{3ZHjg?YkWOx*}GsYs%cK_<^2fj z&_?hY&{~T4@wjP`%Acy~1iTr=BJdn)S&>#vb!Stdb6AV>i3tOiQo&Epy!e~TKM=p0 z5&t+N6$-v*R!7+)H8Oi6$yLN58(=Lz%t3loeZloFESd(k>>fgJqfBs<=*xvv6h+zV z!a0NU?$6A;8C$(EGY9ydZ`H{NyEM*xP@$BEiBkdLDM0_b z7q+#F8Q@O@pp<{Fb#ggB8oo^FR#AeUx|`3p2M^u1Gj7Z~w)DA|=)SsO=81RzO#@ZQ zG3qL_dZXA`Y>kTWOOlBjfj}(!;I`l@j!9Rub2~gKjKF*n9elfNM%DB{HAC(j5^vM? zI%NMSO%GzrgIjmA|xEGl+U(0E6!KALG($Qw9`mnHm1^3zcEqhT8Tv8mWmuufrK6>&l$P&ANfFTo!`xy z^=TQBbu#8-V_d`Z&X?I~+K8k#F4GNlx2=vDoP0v>OlxM0>0>sIoKz1-WoxYq+CaQs zgkyR_4KLm_QR%aLBr2<~k2aDPF~aYM?;Rd}5_ZbqtMD?Mt4cT%Pr?E`Kkh3omGs>@k!f0U#q7abAfrVx@vixPE0G?n3Ui)N3YZ+O}1HBeVM|)p2Xx0IE z`UxWa1k>jW>~cQSD%5NBoe6qq^Y#bzUH|~MbKOA!ehZ!Vw8SYl7F-_&8PS}IfyqfA zD<-kFUJ#^5Q^Faf?c|MW?UBSNjd)ImV?+d}APf?OJj$$qMKg!V*dTx%Qk@o=!E?Z| z99*E^0^Z^iPd`d>5b3)oH{804k$=AMlZMDMVGrgwre_1%dW#hLojl=wRU4C;jeh2Y zd$<{>0L4s6yY@rj6LmA}`}mRAXfFBcP_s>wG`7v^i7*r7!BI}y<8w{Z zl_&L9F`&QW1|s(?zb9zYm84M!p^%{`)y3MfI?nFihvC1xFn_=fF|(S;@CHv=kqlXF zShr{b0$!DnAjEMS-+F>t-5z=t_aW#@+QcHH4zYly726w26mX~K-eoZ-m846|mh!-z zRzf6Vc>S#Rj>cxiykT#Jbj(G5$U46%fS2l0SJSQf6b%qzpNT8b4lqh)@QPV>Hnkq7 zwP{|JZ|jb#wtUf%ZejiLRc)^_IP|N+<(`1K+!qSfjP-EzEVPjJ#D=>5azha?xa_H> zktM_XWz|KnqA2fqozM5JocV^)v2V24WtQ1F-tHs&kb)E@ziFk z6<5dsS2#gE1Il(M5!Kg@3^6sQnC4yQAH!(j@WR8lw{3%IT$u{u2ftvz_gF(03|#jL zs)f0bRL)tnP1|MLlI(_kFd4z%t^%S(pmu8fD9A^Imh{d%J;P!^&4xu{|Z-7Cp+l8pm^Oa<=fOxuY_r z-$i2SxA7=(hC`oou%HMz_nNX_Ikw?sJ%P6y@S79gn(lC==vLn60oH`0t=)dlif{6UTx zPllv+d9ACeWVxQyYf=8L?iui|3+t165J$Mper7w0)O_!!X~4t z1akK%Ba0p7aM8@k{>DljnShW@YE5r$83XL~Chr`ZEwTLkY@lli>i3>dK=DcVpKr@WASe0EY~Bckn-k-8bI_vGm^q zmV)|=2{?>U!q1Ao>h|kyVRGum>BC^OO^bouhoE5GK2+kWn1nMR`29o+O{*082j==} zNu(_bVx`{jf07I6r142BJ#)Osh4F)`0B=x6r4&V6L>fVKdKyTb>iMQN;P^vonVe8} zO+3rzj<8le8Odt~Edp#nV|@=K4@xb+f#-NSl^m`5h5a$FQy&+#UTS5|M-(;h0mrpgShHBldIj72+q;k;-HS$?| z&tggd*aITY@LcPJV?6KrADe@iQj6zDgwZ$*QLf`NZXipx_2Xd{_KoMqMg*ECM^}M9 zoP7Uictfy{xc&))|C?9jA1Vs}$I$o}%l|MmR{M{kaRb1=hQ^{t@@p`XnSvI`6gDu$ z(Rq!8Nv!@eAt~}$*19zOKXYuIcqG_=I?}&+J^oj#5lpCX9^#Nap|hlF)y(M>yDi{X3b@Qq%ZXd&Kv-_C(ClplNkd3Iq)JU}Mw} z=%+k#zcU&{qd2Td)W&pZe6(zb>!YBS;KM-6+EH0i*LXeMMB9 z+x>cjMyGF*|IBng1Y-xUHtj_GO8AB%S2BzJumICVc0?OeUZTFa&lV*cF-z%Dn+H1y z&BGXoGEbsuFYSgQ=n0a5mBO}jmw^UimwzwUD2Na0PclqPU?FR0A%={TeJ|Poiw6MO zAY=YP5ld7}h=NuZo;Iwpu;k8CScr+&^i;~kI8rDy)eAoh6yw4L!o;I42|TR{1yCEY z^a7;l3%n6g{EM0-k7|KcI_l@beOq?bb^T1028WcOU zaaDdAnJ1Us16=(xo#s^M8dMO6UqZwj#gYqyI)N4W~2> zcUjUx4hs(V5ZlublQEl>l5*}22Hgy>>v8~lo~q_LE?9cqANbPRO$nXt-RfFeGAmSf zEeb6dU|LxaYawKa@=GX%bFS(}ovul-QVL=zHX%)%J0exR0um^LJ|oY%U*Knvkm*g` zqo^mRA?hJ`J(biHH#6uXiV$PQ$jpI{(*T}X zD~E7#OOU`0S|CE0?kHS-vY~WQgqYLIrzxAJsXvygdr)C>#wo5qAZCK-gl~h1kN|z zt&}>7-jke)Ds;Zlbv6}5X)a!a_M9yeQYz+*yc>ZZ&lM5lh9q{~oIXsuC49Lf8OjdM-VU}k`6r>t=+`$7 zd*>^j1!dshFuwMBQI8_f{9a>DhZJT48q~g6Kzdgm5@MfYXri4jwhQcR)IHe^u+b-T z;%L_F+i+~VIfsT7>mu>z32t3>T5b_?4^e`SdJ+$nlcoJdW;4n}nC@v0~Fb zZ+wt@)l@Vtq>s$cx#FL-4cOaaY7E=&Lz3z5m!$B3O*FXC*kH#o%5|Tpoon`=*(*|E zi$TNJwCAvd0}!P$d!uyR>u3w)2md%(IKHTv($QxIBXcizIbbT3o(e2f1Tq*Dr%*l) zSX6eq1Krz*o3kX91$_2D7<-J4^1|-@;gJ^Id876%h7|OzN;H-|6>m3ll8jS56AB{V z=G0<8g_ecFFk2}a-;mZr6;x&QJB7vDI-y>Wa@`)hM<*F+e zrc+^NG)tO8gDmaSmQ&enCu>-$D|wdTSNB_5quC0QB)A?b7UD7- zOja}4dbYO1ekXQ%Vw%VtJG~^dAUir!n%Z5uuhg~F@1XrQ3>MP6ht*lxH;TkJm)D+g zf>@hzN0JJ(R?dpv@~Bc%4`}v$U+VAe;v(LQ#zfXr`nu z(R#>I(tnIB6+q8Mo*ZO%!ka)bPMEJ3NaTaYE_kJAErIALH?a@M&NSyq_aS~0=%6??gm3t8$=+ z)s(R@6|`-amrSZ8H4hFXGouXsJxyg!8K}XHzEM37h^|jO1nHWg1-6$Grx@P}=QXlP zYlMymcKfK{TdA1hsZB1#kWH;)L8as~a0spRef;q5jmSYq*gYAavS2B33qjD?kYn8w z`_2OfyaA}4ijevVQlO;K26z-7)w;(_1t~zBSsMn4VT^whOxIR&xYXrTpSHeT!T-YF zGrAyhjAi0})X6iKZ=?ij%O55WcoG ziw|GOCxS;GF{?wHMI52i%}@Z z0e~N{A5LNv!@6ssKq{X3al}Ol;{0&Iqkv}CWpktDm9)qs@t7Crn2n6c;}3K1nM!78 zY&&x0{FAI^%BpW;Ysw`koMJkdd}{qhJinUG4s!WRs#G0@88*rOjEuFY0l?}&8n~YN zfuq|2#C%lvYz!kwRZ^8e3lVSxiU6381vGoqpK*CQLDABPb~*Ks@v4~4hmw>@%-nu; z43r~{(}Q(DSv}H;GVmemX#=W8lq5iY*^c%J2SUru@pomSM$2D*?BGIX7S5uRv0m_V&`&mtm{lhc1o==cn#I0Y6_`_0AJp?ormz+OEZdSidn8*Y0|(43ESQ z0Gp{UqH`tb;2LBS zEhpI5=xx`P(Szl17=~H0w$M>f4>*mpJWtUqBY8}{zjPr%R%sF@sb1jBerZPYEot}kSN7eF=5C?^U+|UU?R^#7#7?aJC#yDXb-(!)F5!F9m!xifUKWq zUvqS~xdBOk=AL%$DtSuzXOpMTh)9H(H5r_bskuN)*W54AzDZuGmkKh$TTccX;b7$9 zVm{np>bjfF7w8^SpH#VvJRFBP(g*ErC9|mKR`n9SY(L|bBY~q;PI3pRW+Ae zuI%-^DO}+x&%bb7J{KlspLNF1$3iHGUpCix52l_+DEConkzZago!;vMW>2!Hnt348 zP-0c?RIA;$Mip$FxbFmR<^D25lmh316jO^3!%`J~5>1sN-JC-m6^t#E2gv&(3!#hC zd}(V-IL>69=J#(~iL!F8_aaSRa5EEXWHeA;A79g36voy==W_~FYaDwB8ZZ}W1$9Ms zpei1<0Qkh;4IRhJ?W^F)0qk=NyWh0T)31C z*)Y*L{Ll{qF(DGTZ;NIbcK=9%*>Plt7>E0W95XUA%GR>uf)SjDAi8{cY8>&)1I|l8 z9%V(nfzm2zx7Li*;K@wC3MZ18EChBD!F7-$+$>6~Lq^r6`)J5->v>hylPw`c+u+>- zGn6XV7aGrISI+|G7rB8Tl4zJKMyfT|Y|g5lM@qr$NL}}mr(|IZl&{A_j+`N`MwyM7 z8QB8%khdkq5}9dz%vsXt#w$GEf$9+YAnGG&Ma+bpv6F-w={3?pt>^ir9=p$Pb_hHa z;M|3mcLRB7bht4}#@UI_Mpu)pKkAi+YY4rAdzFgKgYLgI%USTd;~30%T12RV?a9D1 z)Bs9Ys7t!5i$dpeAE89mI#TfbKKCMZw1E+Lh6r(V?pJ6Gx^a7PsB;%7Pn^zlz@8y` z%(nJHv1D4O$x5_eYO{&Dn?Y%^8JV|G{qC{SZzV4*W@HK;W`~*6*(2z1>PyERlsz4X zFgT=K_g?t*cBXI8;BZhml3Ns&H_eD3CikC2(r4B<)%5Pep+nxZ#blwTGZwpc6PSit znnTlVuC`MexSovm3eAY4QWhZVO1vH+Gty1C@JB6}c=ig!&Ie+$<*8Je|0;TM5lG+P* z;SJk)CA{!>=3fqmA$w6{X5XMw+4^I{WO1x!4PgU;DXNX1k%9m457F2k&lWvwgyhn| z*YF#O&F^s)XG3VdoOxP+wc(%E_AL2ij zvM@I6+qEaplA=BvZh*(Q7gfPTb3O!XWOSNfYj9cO+x81+;>oIQDpT6CE+2fS#RTh(P+@xvu(WZGNoLn z(iP)J><34ddtd*yH=q6{A(`j}vzGsp_OZB;8?-Ami*=iWQ}tuw7;Gg_gaMA)4% zf^&Y=7`t5eVs;bm-EE_|mHq`;E&UxXTpY?x!EeSuF}CF?krIb(R@^XiA+-v8a?gxG zSgq@9GU|mB=8ET25K&l$82e1q8^W5U($d~=2S3;xHX`<`LdQ7cLf}wS9 z9yJ%8>_t7f>FGtbN5!Hwlys(jK{neDY4NQ z1v&KGtN^AMD^U)U5KAlOS_oAr=(0|}APRu8uoJ{t%hU_d z#Nan*UCOE5cLqJjI%l0xYn~|5=3fKeBs8yAj`!K^$;sjGYQod}VU@A{?K1toLo9-S zf5*dooOHU%V74HzuYg`Tqc_fC@i6mF(YnYJm~p&l$+`peq0A z4FBex`CkO}U%&kuL1q5G3FZGIyVd?*&Nly-E&9I=H#7YcLj8|F`9GF)SQt6}HvvzQ zn#2!6Mf(x(EX3oelQy)5^A`p*XXi{2Y$6W;9pxSHhbI-s2wPSSj{ePUSrCRrDs?{umANa7@*^@x&H#ryq!6ds zJM$~DHumumQHKwjw{MDQCFGW5WfvDaDXUiy??x%IGL#C36tERiZ;c@+Q^8QUPhP%i z<=4%R{l{loSn+X#-=Pa_q2tQ_q?xWJJc_(roGHRGfrM#1X@t4lQ{c1f9Ih7BrN}US z&@c(rF(IA|(z2i*3B6%tfFi|#b>Z&kA$$Be49>tmE_i%s{vyuTm3OL9LOM;LY9ap# zgX(_6K?4j~gJQT-iwOo6lnGV}CiH+qCID)O9aVDz_hJ{EdmHouEyx(4UJf!XkX4GH zSwLp60fHu+q^+Po_4O>i zJ$at;w5uf5(xALH2W8fq{;Ok5{Q~zqodod$)&dQ*Rkz>H71@SOmhJZbrZjx)6hFje&!6)~zr)-r?!cGuk zY0waRujQ)e3?l>Pn1%_G2&iJ1UnmvE0EXL@>+vbawmK;XO57UV9~Z;<#Krn*o9=}! z0;|pFaqAJid+e^NcH+zO!7LPxGf96=MnIdYVlm`L=uwML+l{SSzU&18(JpGc(Ll{s zne4U>yn$#ZmMLSLT%k(f>J=aYJJ3ZZTqWm+vG&jlFXrc&jF=>no|)kFN|%qBcpa@v z;A8ColQJccH*scjnQ{vm%H;{eAnYY&%j&5xoL{3oI*sy5--XFwTzd9~@+K38S>x6x zE$$y-T;}OTkVw}>f70N#-AE$0sH69!pak3bC!q`jZ7bf**Otz)xk~AIxEv;C0#HW_Fgt1cKj8HvE$qhp`hIvX`wY zy@w;~Wl z;L4h8R4~3QRRaAj%Gx=1dHlt&qWRwR04lM3jgjHyeV4znMlN3QtJ_7!;*cS7&MD&J zPN~cgOmo*jO-x0q!Uwh-7MsFF-nd80YMPy@nOD3Ntuao?5hfKx-&Z(>hpbP{VjZ0m zSf$P>`?tW#Bet9K2~tYu15?XdEp9YDm^bu2hfsEMk4;h63yKv2x5zDq% z0!r0OZgHemg(;^0jnK}H%mtHW4RbS4-Hju&pnLt+up%^9d)XHiytLm!#5e?4nR@;6f!FoD`uc-d28!;Z{3aR=?#-CiSFK_xG}{?a!@*Vt#g=d zPQx#D;xL|lX=ewVJv^zs&%06xBVpK>hxo%*c6g?if<&u6x;vPp##SU%EE5~Ii^p}m zUVP)~{R9wG^I)me*$&>{Im~)u^^>&wOlZzo~DO;4@xsMT4gyD z6#>jbMoIf3IaXwJSkjE5{-WRfCkxy%>_=zE1dIj<schvT zR=LJmG&?8llee|7(v2`N+{GC8 z88?IXU{n0jjZDA?_rOVSK=_quH&X{0TD& zy#B10NdIRe^@29!y$CRla#p_(9?=7+kvJ|<64ReGM>(@($)oOjp~osxNE#H|2&IoI z>}iaxE@(c3z)&Dodmw=tbq;~MYRXxT*zobM2)zZmn0x%v7_ML-eiV@);R1oT&REc8 zourkP5sU?fQA&`Uu|x=iu_h#QB*`2Y4WP>dW#PihB|@;qOrteih6VMc^wMGY`Zd4p z@me^WkP2cE9^Y$3{IUQ9v9xy%f7R?lFt?)hv8H!|1(ch)4}=U$W|occ)}a_mKFyjB z4eVPKTS=hDsu{b4J~};k(94*^aecnQcVoN3w8N^G)=k4m14S`kNkzRQ6?n`oiHWt{ zYZtCRrFtGn$0mmOyt0p4hXzcVw2N(}x<{SHBr~Mpf@S3vNrBZQ6ARX+=V<@J)m$8S zk^|aYvs;%SO6*&-x=|?yi52ih9cD|gIFkus>{l!1V1ROHv(MK}{vmQ8yOgou&e6P9 zSGOCVOFW|hhBD4+7O=45zU2LZkKP$B_(;$M09zz{#BU0+*HzWqg?}WCM3)4!Ch&bGj-bQ zLEKaMb!XYu?kmc1wy!*cRz1q@_(Emd@cyh}>;rTI=nHXDlT0dL1hGB{N~dLSbjbvY z!6XC{&SA~W67mk(5!WoZ9*I%7Ppt#iNop_O#^EVx`}9h5T!4)5=QItChPI__qF8xa zYm9hC$GfL2Jvb{BuUg06B4jN^+bL!ICd}YHO93F++_z~SF_TLi%$%dJds2U_b243L zS487iMIF8|A_sNlBM}V$n%gpo=(0vs5PlN+LCO`D7X*wN!$#U zBo&2EkP}e?us9pNg|O4eTmkUdP%64ymQ^_v;+cL%c|k|gH|5W%-YkZoXwb1-hzr@u zlfa3EMEIoX-nCLe+w;>X#UGtSiJO{G=456bdUZim^Ni|iDv8_7ga&CZ+cIA61C@|4 zHG8(i0J1%%`y}3R8g4Q{yC3F=r-X3QR-|a=ouftFQ_O9FbMMB^r^}1gxMTk@DX^Ze|}U6_5=3;2M%Jwvo8GnwhfY zwd72#jgx@!<0ifN24C{MyPrXZn0_)hFkJ3iCjyrG-^r zCbC@7$Ff(*RB|4df4BuxJ;cL8PFg9sf*bZVfSy_ zpiL;c&p_?Zp>dve_vxQZZ$CrGF7DpiVl_A+;{V;}k1cV1dp2U$3|<6*Cj>LPqz61E zuq}#BvH+i|q39YOwcHt#Ex;VoO$!Cqev`jVfb)(QMwwHXQsmCinoVE7psuyJeCi)^ zT^5ZR04Kp6Cd5jK%tqisLP6Mi+E z6YE@6(>CA83VV8Ade0;nzW_osL9`p%<7o-8FdjUw2@{YPbvx|LOy_}N+(J@6=-r$= z`-gF7zE?b@VU&r-GVD=fEa*Pp;&(Nf=PA|l6hpX*E`BjRMb3i?0p>!rlZwQv;i|xf zUDhY3TD#QyO>94Gp%d!uHCA=U{jr;c;IXFM+<*l<=PpmMj1jLl#^Qb;oD}GUDD6>S ziU9%AjKL~G`cScs6|rd?epz!06@b9sXA~Jldii zK|zgT+(8j6a2*BeeuBL`_bn9`W;o3VKkSA^qEW;;;~TzglU>GMZ@Nv^9~5jx%p`M%y2T@9ew zq&*r;m?^u-Jh0CzOxOheM8dWe*)!9~pFAjf0)+rs1pK3iO7$@DzLSzubFC5z}xKWKWEou5C>B z-p)AbH}#ilaj3v4v)LIs`DP z*>|aHoK$&(%h)v(xOo(Ki#%kAd5Eg<-v7}u`I4xDIM(X)t~HZEGm!Gl~dfWmJnSydE zj>2R?CMuQm;kVKWK;fPvvvR6)(_5@nZW+c77Vfbgzj>0=B#AvD3`Lo21_^K7=i&rz zIjd7qu`PeX1UKx;rB0|2+22H15f=<}FJJ4KdOy&nbU35!AD|8MzsXbnXK?h3@xM>v zH7WlGIQsJ+Nj%ekCrAG)iKo9GX!tLPv>uPp@=xdam$&8rD9E$^Cp7vmx$}Rv+W-Fm zk^X7@*nbO@nEw$d{hKx5A1=~=wdI*u|2Lpiq_%2H+=1wmTf1XKZ9gybgr}{opF+ai zt8I(`JrZP(zow|AXiZzE_2=MR3A$zLplvj0oi;%Rtx`6d)8+lCZJm$hT=plEciG`q zdTp3|)=&n*x4-pw+wPj~c{A3D+V*?V`SL#B!xyDR3hQOg`gyoyJvZiuC~f<=({Fto zvblF^b+^IBy05!TdM?`zoVj{%_Q!aeCXcer7gPLEE~!B9NdAvdo>lrI+}8?g=99(z^yY7IO@T>8#1KG!@t`n(kT{jf5xLR zC5&}ZcKNeLNXiEsX?H|=OSXjxD>d&kR32JHw8c_IstP}e=n5x^R5cd*n8S1I0Yp`B zaUB3-yJ_}dnq!h<@MIg`(tX27Ka^&VgCnlc8~~+!^}e!W)s9|%$LNXV>YHHVqE$pp zxep;)WNtCY`dTSvfK%&M^o;cZob!w`wf3-=McvyZke0YoH+JpP!Ge%$DN&==lJ%>qxxVEP$z2W#s=LDZmNQ|{DSl+ zGmpft1~Uk)L^mO&ZhxZ!0x6LIS&neAy*ZZC3sBH_$Y$saY0&|W-A4lloQojH?x9uG z45gN}HSVW&-=Ut`FFRqNc6~wa)gyj_{J6<{4q(ZPHivf@@*{scIhbJQiO=>&#YR63 zN?qMC@eXQvp(WLf2u*fZKcoRiUT>04di6krCXwA=*gBaqY>) zD4ME6rfbi~#o(pR;K|M1;Lc(H5X{oSh`ttT5vq75Xp8t+GveKb2$Cp+agKZ0xjEbI zjl5BrFc!j)6RuA*zd#26iqRG0&2@RBJW2%5RzNy5mDr0_;{ad=<0ic1 z9iQu{cLLuCA6|JPfO=>9)~?@L$()PM)0l&31Jax*WZF^U2QwA}H%^*F^QS)sfK> zmKNc#R;Lg|@fuyWWGqduJN)EpI1bNcD|BqI|G5>`pJO+oY&Ttbya7&VY*Y*#{U8FI ztZWEw zT^7d4f5wdk2Ec!9#`D_2ERY9%DQ-6fhf@k?J;%j1S%Rwpf1Cd6W(R}C$UTrfUnAh- zM2zT&3!KpWZ{hax@7fS|hjEQjkT+!iO!3UNeKqNESe+m>xEzqCpm(tuQ<2x4G7!W^-DJIF!~S3<1#wug;%T-pEYV)*oc$KGnxInw<>3MJv2SIYN!Rg6Rq*%}C zmJSME+Y3=&a!9-z=!x)i$injrgJeEQUDbkYUdVWWF(P zR9vm{sN|u#$3Yb-&P>|3djC*%FIIy~*F42B&_(#fHa5Uq4$cKqXD$I^AQ*N4wQNiw zwjOR7M>{^v+D7|cg?7YgJ|@p($0O-Vt#ng6=qPpc)Bv;uEvd%d z2?(Vc-)Ff=k!O@R_!o7*$x!kH3=Op-?nc>@)D!sC{cDiP~!a${xqA*nr< zG=m}I4si;gE4BwKo{w{UE_QjL68`JOwM`i);=MnKNIo6pd>`a#ZMe}uHVg^s3nHH@A^PTS#KaM5#D-=f z7jhRloA@)K*i49!j!olSn&|^I3vfRDnMQWcGF1AoT33kISpE#=S(br_l&M5?B$I}C za|0*bsi$aLjeMP8N!LZXD~f$j>mVp=VuX1u%ruwmFMREcuPC%Yzay80Bs2^h>`TGy zp`GDpF>+no8u5+r62yUV1XANik73k^f(n8CU{$B`+$fv)Re-Elf0v$Y9-;+{IJS)Y zp-cs*<^`hQ_Jl=_3g3woYbsC|R@q*HZAF#wPJf-oRTEHMs7O5Uvh=njI64um$V3N< zTK{2Bn@KAhX`6iN;Gn>UtbFTC5wta3=ysL~VS;QUjPgX6kGPCG+ zP+z@nr`m?Um^IZ)0n54bv3$W?Peiw4d06t6mN!t`PXO)u6=Ui9MWd4FJuVNn%k?nL#A5Y(jqna0a zm}zHGr7sdbU(RcGBVNI(ZtDf$`?!oST4+_XSQ?)huR z4uPAIV=#&rZfAZcplyT1_E|XM7Cd5Uh@MZ0p-qHAj8jB@R1Z+btoLsD+ZT8`%` z))l-~CCW?UUMHr1Yt|V$@7ua&D-LH*_~5oD)Vc-w(8P%TPF!Ud{1S|1&PtHHjB&z@ zqzkivduIbtw;lUH{9xh}4Zrb8VuurpeGSMf03BuKh0y1ZuKwTE@ZALg1x*XjT;>wtOkmfulg=LdqZ7(6Am_kWYrbnMoLesor~ zD`MDT7PNfA4HQrVwvk}w^bwlypc}mMM3)%#;)_W04_|LnS*vmp3G)E)q>gT8u16DD z%mT{St+P{$qDj52{Dy}ufHDFewK}I5^`GYXw=9At5PU}4`cMnIijbG%T>w;|6uVab zoq09&N%)d~dWLVFlZRXH-y6j_sL_rnJJ;n3B;^+wH(694d#1Mr52GwCiU--rM?ZHe zO2g8UPUetq*#ci0-{4Viav7nF?QC{$zQG`hFXIry`oOIohyY>*xOz<_|ps zxaHyCWn-8JcLmo2PadDHosB9#6y*pN-X`Ym;L^N;+7O^)ar8&(SR>%_gKM-eyEdd* z;Byi+F9l#YBTba+yAZaw!sv9~US%WjeK!wCxxR;$bM}Yi;x0CckJ*~3lAsu`Beo!t z?*%>cx=j7uLfxEit0#>uK||afcQ>}+dCo!FV{&RGk}4i*rD{eiztaXyT*H`&2;fLg zwYvQl6RnQ3yJY>6U+0E{lTv7Xngri>sz#k9tGDl|00& zz`tWl(P8SB$hP-7Tpxqi8O?$tA?&R0y}1>UcdV+|lk5~E4^hpjuo!NAnUi3{!C~%z zc!CwDo>}qU@f6brzD0(kM5Ni){LaRqq)n&-5Ng>F*H*9L0_3d9+oy|WjQ;ZQHhO+ji2iZQJVDHak|w?(p{BvuDn(x!>6{x9Z$e z=O4&}dMZDx&wH&G_sgR9tz-u$5PuFVo`bJ7oppwC*Hckw=o4|uTNw5y#71)G*C(RF zGEMrSFn)q_CaEb6akp8$9ml}KtxLKn_jii@!VOgTP0A!wOL^v$kR1G4myt)XU8FPm zCa4ad`qA=?seBg2v71phM{-b7a5T3FwNds$%~hDPo-Kx^KJMQ+Ck^G^SiGG*CRd^n zrjoid_@i3Jydri!%PL&zh0LH-)jzA%G74k(qb!aP!?;Y1O&T;CzN3<0HqrF1zsgSA zc}eCRa>+ze4@g=+1e-eSHq){w0@)-vOu$OT>^sCYKT0mFw5AA=rb#l6&%{tYK@Xl5 ze~w-;-v?m}2l3IqR)-KxHlFv7RcUtKxjK0IZq!Vn=gOpU=c-_zvSfgtJ+ba|I~Dkw zoidk26F~mtw{L)s&y&~R<)TgGQu6T6a--#96?bmEtIs$ezO)=EPoER zIvzo6wr2zC%9n78>mwO=BDY#7lSWr4_zT{+`pAgcAKKL)9Un;aI2JP%f`9(YqpJKFIT(cr!e5#f;>_Pi&`y1nMfmtX-eTq}YH47-q8ej2)4-TtI8Y+0ZO#~*uhHhgY1UhN&YfN;aA z&+d*M+}`qVDruJ#>YOsk6k6yCRV9>={=!ny=Y!Xq*;-|Ge!yUXw<=P0tMBn-NE`%F-(6lz-aNd@&^gG-@_3ML863IvOrQ8N5R@viuKUdyBK?#pKN!K>fWQW%N{?4eptaJtm9tu^y1)QtN$*DB)r&a z;>x;x$}VbE^MI#Bw51PE@#da4G18IrBQZ=QKs_8`mOM)r!J>GqYev(X{WgdfK@uU4 zm=2;MmBM%9UbygQ=KZ8W0&Q87jXr+fNB54*Ud`fw7H4*gV1O|)I7+(^&B(lFTt8f# zrRwLIpipdVdCH)qK>n=b$3RwYoyX1LaGp&0{JvB!8waR*fXq0q4BCN2+hk}}(!~$H2s5QM_?8L7%PYOAX}y9B z>Xx4m=&N$loRq4h72vaPsbxuF7`BssSJt1Jc?;qs+}#DC7*1jmeNl*Uz|3ALT#X;d z&jyVp5`DdnIWCUJT#l>+Pn0&;()vbqLoV4??5O*s%P#OeQ|;`gzQr?qDgY z2=A`LnQFp6n2*3h;PH`X);-LqmP*2pcL1gB7_F=|C%N?rmfc6#cS{?DTh znf7{Xt8&z7BI(HA$G@m#=xAIXAfmzMa|oYD#KO=BkrpWxPTEy|AX5kK_Wi-V2(E~9 ze}#MhbqDl6iQp`3{~A9t|NU;Uh2ih@aI}$suZKg~+Ci7l<~MUj#DV{r6+FrN*8W#- z<)57Ne?MPj`+L;;Pwv~!+vkOb-u*r+vTL}jx9(iF$s~Hb zn|ln^a@KNWk%m$;ql5<)e6oy_ej+60cZ~VY$d^^6sWZ+`SEnylvq$7;Oz+CgtZ^Sa z=KkZVEX+Jqny4e#?yO;_ro`!Ic3{|(uMSBjz0wXJZvD!H^b^rNfCyv!0cF;z%nnq? zDJ=DiOX^&<=*=ucCU_ZxRFEx3NB79=;u_aY;-W`;nqya9?NX>lTGHQX7# zebB5)e#CdAs9kFeRHq62d;i5JAzS=|r|8|JY)yza1({&bi-zD7XHEq$m(>9a{ZK_L z3HE+r>|-Z$4aSjS1Hy#w)`$MN^1ib}Jhxi`j<~Srx~tFIJ0q+|MwtyPqvfTR6ZbrZ zqBF?!<@jB-eP~M*h&<>t6ViOs^NBb#iiU`!^v-C|baw?Q`aim7pYwJmK~GzZJVjcj zh&1wA5#mje%@9c7bdj2GB={!UW13-ZXn*ZpLhNucO1jD@ML~zPB=mFh1#e;p49{oe zo&Cm~u5&{;M2GVY{qaJL0#1_zd12sy$ilJET<^mVmoH*nQTQODC~a=HbjY@Fp1Swi z7cTkZ-eUwn?&GH*KFV5R2wja`!7bpKXoNFz2Fg}c3TPQZBHNzMJbmd1ChC| zP!^9?(E*l2a7$?x!5p73i#uSA)n(ZOjOCHZg-P#}mbfXcFA7Y#5?CQ8=4%XdCGU@@ z<;Pt6@H{asMCGmw{C+jmnA&@)icgLAW@RFNyj7 zn)!uaJT>-vC!M+391SOy@OoviZYq_xR=t|gE&rV{h{Sx(iz+y$QGTpsIrh{RG2<$OtlK+SxEas{;uF8Q1Hf!=bJom?SEzG%Ci}(7`Xgp6 zOe5noUNw&B850^@E=dbBeMr3-cvp}-b42+h_V5gQhN<(BPcDHP(iu4*5qS?WT}Jcq z1ib;EV>vXsES1U>O6(SCZ)8=>Cd2oja<_6}1 zzTmQGYE(L-m}a4{E_V{NRTv0V*#?SU>kpSBgWF zv3x>b_6(GnnWW01E{v)kBL=k049J6TNzb&z7f-(074NkMk0c;!Uv*7Tqb+-OgXgbV zYs%doA61}-pGn9nf*Ww=+&a@idS@z#^Bq#8+|R&pHhB$optNY z&$a4~bD;JHlj5qpDi#yK&`c5(|H*6^`n4!6#Vg=cif6oCR`UTZu+K`>)wG-BiWR?j z6P}qyk-p01z)qZO&?feQ3%AmLbEL?=6GPmQ$&31Vt|y|vntZ!f753fZ*LOC0XlBUp zFUopk5fD`7g;VRTs>n=Ea4X@DoN#^Rgtu+W8(Na+9G#wIs$^oSs>5@2l8d)=4%p_# ze9Q3|`B>+)SMK*+&62T*R*{E_1LKZ$k4xf$!sxt*?iWfArZ+@eiC8uio>^VOmvSza zGvP9-!lHDf(NrM`7_(Eg)%q}5ir3-+Z|DA_8RL3qgFux|WlVa(Mw>4KaESaYoU1MeaTr>~#KSmm z@uZUxN65T4;(}4Qr4s0WI+ph+|8PP8wBxL?9eM6ezMmOb)ge2oZgOUaW`@JLXaQ18 zLtstZS(mq$pH8EbZMsAHzGiP}ZFKKg9#<0=^7egtkCQFiZix0GuMO5uN{H8Q?~i2! zCG-w3m6M=#lX)o=k+P9$CFlJnK$*8B6MwZQ%lvRK4STUmNw*DrHE9x z%87m{omsv@o}_n*VDpmUeV-c0+46E-L&j_PJQOMV;4+M$bXVUDv{1!{b9w*sF6G@(PU> zIN{2yXf`bW8SPX_h0+v&U+X3Y@KaD-s|71r2C%Gnu7mPMC!xjJ=nG-lUbkC^poFm| z(~QY%4#QbPZ~u%)4k={ucyAtf0Fwq^Pyq33Bn`5QBU;3zB`6WO`8C>fC!5?mnEO#o zzM8ZUc&$c7f{?k{)cK|}Z(DS91M6*pVxSEoX3oC$nv*-Y-^-7t@K)NdXaxC-7;{Bz z2RGh|qj&+_Di;AbE$99uInA!UQ_Jubtc-cQ#pI;qTS+zNbx7IR6xTTo>oJvsv$T{vJQuPv^MMY0{RzT1ODG|538L&>GhT&gve{I2(8 zebS~@Fl7f=vVATe^Vr19!L*Gj3x zrUj-aBus+Cnr+d3SVEHTCYdA7#o)Vm=>Al4iV7N4mX(DW4-;_{!**>wDZyee*kaTc)?3 z-JUG(n!m>!)BEk|st2cw1k4hoYP=o=rvEyb8iqongtDiCkdm}`nta(qQb?VYRPLM( zag~U`%VUKG%t(MRynZ9d_*9MD3Z)XVB;I_XuCZ1ztP)IVa!bk55rjG$$CwMZK}sRT zt8xc1TQ5JgcB%QvKU;m$?py+4`-IvHT4_PD4L`ulm7IMc#f&97fCZK*DuQtch91$$ zp3nb$UcT*H#ySbag_Emlj`B%5<&kf?sZy0d`U2l^Sqw%2Hi+JjRbMVWj&{l;!<|Tt z!HvV0?hx7D%i-XULFn$1WsJ|QUpdjsRlxa`JaMkzzROYIpZ$_ABM&MWny4*#@>Z<6 zj!gL>+EZEMJWZ%t5KPU(470kvKNLczXkCMgODXNSM|c$vGwwZ)_39}#RZx*jRymQg;Z&xu_(^wf*Mmw@~^y zghHO7H9{qS(+Ue`1v%z92DgAar@rOuu0-wP0bkV>pcfxOP$IF2Bl*aJ5mCDmS?yx< zvd!XcbfBX>-gEM}&sKqsBKPH?Sdk+%x3!Wmbyr`vj8KNnz&j)h*RBa@NZ}{~{}`S1 z(3=~$IScV;1NquVemE#CU2l9=8kWOQX4$OyHb1xcBAIRTV%t{i2wy zCrbu$KM?Jn82Y`aM{^%^AG?Zv%-S9h$j)!vc1!ktw=@S0X`3;Dn;ajUJ`z7{~}=cy_7>`Vn8Bsww z&km!i4m;mRTT?Ak5b`Z!HRLE-Ct&EB%b_UIleUtOU4N!hOEWf~i|KT`sd|w~8eoeR zu9M|9W$QJF?k){MVz2GA3HG>E_Joh|)3Q1Xlo>sHLyAVBxQjUI+SisCF4c=Fbuw>;{lzHB!m&3)4LtV~+zF7YSZ zNVrqAokEp;n+}iX7@Ks}Gg>8dQUd?Z(AAlPhsKTWW-|E6H#p{wlTo-cWA+lZettd@ z*Pi15E>ZSuop7cUCslyj?FU=;70v-c*=>ExGu3&wS_D0y_cKMV(p> zJL#=Jci|u(ttKXAcCEz5X@)cgzDza^xgEp0yKsoLReYb}RP7u_LGvO0CEZXn6(FiC zbOtJqXewL}zPD}ls(WFXiXUQrz=m8E-C@D)pyf!XA@iMa zQSDU8%oL~Fy)PRP69f6ihgIdHfoe$n>~Wo3JoHnGPwbN+^x zI=P4|33acMk=v&Wz%0&S9ll+uFs{;E0Fb99^`Z(SpO{IeZDHxE*^9~P@9hS=32n68T6CWI}ZgM`HhyW4gWf{w3P zO>W1D6;d7)O<`nLt122yha>onV;HWNl;uiwb#X2ek!sBAnk-kly~NzPEQW54YzQpv z&r=-vMUD&$Tgsq$NPDP$$of?d3wA8Bx;DXlbMiP+e#^XWr7X1ElZ{?`z_+iw{1&?! z42_i-VR;>{v_NYR^84P_K{Ju+MwIEMul*3W8EYbEaDqhiJp(oDcY%J+eac%5PrNo( zJ#`1)yG}d&mFaE6Crn#5vwiw3SN(n4B(68f`S$_lhXSPwVCuMF*|IJ0m`yIa4@83E|)nk1$|yGEGQBn#k@v z^~Mr73nK6_*W1mqD$U6*?1}2}pJrUaqu6%Ir{IQhFB);e0gi#P6RdI-1RJ$kqLW0E zm?xHHKX(4Br>YLbb>7q)| zo()H+r%uEg*9%Y}yYi(pFxP(RKIp#5RJVPo|7k(O0|XEL3I+b_2I7CBl1%?io|5_R zN3ksoph|)bK)?_{=%t|KfAymOq;UTSfBeUa;eYVQf9TZzhxz0GS~dKCtwFJJ{M%dl zpEf61{}Mm`?Qi_|!ZbS@=Rf1eZ1r)w%`vp?H)<vaiHiqmyv{%|aRK^d zy*VV)GfkpL7LMDW&$w1Zr0arcrId{t&nvhyoX<}mpY3{V_eC#9X`Ar)uh&MYXO-39 z`bqtL{&s!YcyaVyYFnS%IGZ1>>->?Ml)NuZo;vQ+j@#~k?pyh{enw`+`~Nr^d^l!f zx&8F8Ij(XeG;3S*gXC_U=1SA7rB*!3m{OsP>ilk6BC0B5!pHDlVT*S8*}1&gMPHGQ zFG4kBxU;<_S5~25)Y+XeCP|ryqBplq0&bl0$CxHUl>c%sa@I5j@6Mr{N>V{H#pIJv z$|!9~p8?;ZSbNB#Fy)=^HbeU$mm`~}lyhm0OQO;mikv7ytF>(IhrCgdEd-$7EDC2I z*1P_#IcfQr>-Wc)R-V6Zi0U1ZgqHry_)MFoHs&L}Z#reJt}e8#KDP*GV{3N?hM$PI z=JN5f+5a=CCG%1g4O<_>UHbRq)^LbVbWz?I8LCGDxa3u9$&Wew_>Ywn)}+cAQ8fj% zCYmJC0#Qu~UWs#iUwB<`PvWKK18|OcgoR`fnb#q%{Mo1ICRun z?-70I`@pFs^U`qW{cnmhww0N53~!>*7-{|}@hmnyK&miRU6=AFA9LjErJpIlhBnbQ z=c8x7h|RWbhP}q|W-RkRoCI83(Dv9vn&+c_vkf8)UiH2UYRgfMIg^yum$YMaaYU1C zfPLqqT8ntXd=#O*AI~Z<>oRfcEo(I$G_r*$ju>ijhQCZ{T(ZwBy8b<8vYR14sIBQE z0TRH`ymkdSgcrlIFQ3kA`?T8q8X1KaS2THQssoI=PsaUnl^LG?&K zt>~#t$?LE<=R0}@{G{6oyri%pWpjcu*+N}BGo0yei1SjV+xpg;)dql19tORZD>gkY zIGC(G+p@A%CzcPD%)|`v*w!%PheZC3irk@nKV!T}BP64xr~zeI>xRv>Z~fJMNN0{y z&d^o>_G1F5UatY+$#C>SOk=)FuY`wXOWtPn;b?pwyRLwgzM=83G{828Tu^=^c4~xE{0~VDogud4#uuneZ!!o;3^a{vZ`T%1YSrBz0rdUa|&Ot>O8l&L6xbgX3 zmV4NsT+DCoq->h=Jj0 ziBKS)dXw)})%eBG6CI`+d;&58Q_)>YNh%?(NFA(h-UsvUTW8SRc3(E?6F7G~qZ|AkqoZ z$Xtm)1C>sFL!*K8;tcMt?Xzk~3+|eud4TEvvnb^#o-sBj8D=#IG{==~+{B5aMOQ_X zBpk5sunk&B6ZS2nu^>SmCUx1_cv2V>jqjBc@MM?UL!kvXZe~t5u`>F*{Mb_*rU>b? z)B~|S5BL>D_2+d%so_F@ES+g?jyr|#DzVM82v)%yKc|MfPz@u7Jif+2Z6UZLEFTH{ zi1RZ1(RT6sJS!kH*|FnsZd(^eEkECyzZ$GxVAPF!HvE*BkGqqc!If?+DeS4n)GX!q zFOQV{ zbWny9Zo3@~itNwie7Ld>YT!u2__j>Pd-205iX`(`D7xRVxZ`)zux4kHDzGu>7?1^u zl75scvi(BD4n1LbeXbUvLtN=sm?zl)b)mk%M7`7pW+L-2`y#^_scrQ+UAKq0B1p#b zQxCN2OqB-z6U29*toeeq) z5p90sq0^*Wu=ct?NVN`QASlW*Bdqt>26`%}q$wf_iyjXZiZ31;yc9=zvPt9GW>43c z9Y5@?vb|41^B41t$W}OKEaPdxS*9}s8lMeHi6)-S5MZO@R(D}VHERZc)WvI*X%uFe z`as9{X96Pc@Vu{IHNeM#tVV>zEW=}9m93*b({{f;WFJ1c&Mt6hnwJh@)5b5`N1Zu6 zlZF^R4%P$A+X9!E@np*bgdxNdOu3qw;Q49>4Zaj5$x?ascr_zzDm}T zU7CuA$Kd9pEIzG8luVbYZ=@b?X+3Ef858W5ip-)yca_=SsfPiCk#io5$t+%niaRt; z90J=pr32I?pOnuCz`mLydxf)J?LMY%t-`Sn_~0DI2qf}~V-;0*U~xv?j8}kdzQV)B z6Za&la*pM=uyk4g`%P@;65fIk=B*;xL34TAH{!$-i4yE)=J2~1w^Id8*RD*N)}UT< zXALo2cCNa}37S(%X)Vg0ykbmRLL=_F4S_s1SL%TU8&^|sxwj-bkbiO_?7j9nRV;aV z>=nAL%5#o^g5uMaB=CiaDRP2_o4<8*iHpN^qAU#Q(=ZpV|`Pi;&vmLX;foa|tw`t-D9N!!i}#;|_aHidp_W zxjTIw^!CByw+PxT^lXZZEhjF9N0XrXG)Tsw+}ji)Nrve7Nw7XnAe~&~x5erb%j!bH z2Ci3JR-D~5yiRr~_0(~%d=A_U+L-uRI&bn?8a#Vj>>|Kr*NSS@EGhV9i&BT5iI5wT zOUFD!Fp8482dzT}lnU7*7tilu;825C3GP~&uz1KddzTOp7Q!CCLe~o{3Z31Sf<3L& ziO~_UtYgJ)d}@FEdh1&W7t(0K#FwFT=Nb+HZb0BSNqGUwqH=oCgkND&XBl$NrJrPe z-#f^3lq$AO?olt3D1?EBoGs+s-^~3|WdiK_3Wh)Wiq$9Ouq9Fc#uP1y$`zJdH*+J2 z5RoY7!7PgfGr^lIrv}mBY=NOCe$M(XcTL!lVOwl&FF!k8b)49UtWUqJa2MKDpZ^IB z-&JbSs}4aj}8A&uBM((?ni4;jBF=$M{tAganb6DHibnlJ-_kzS|PhRdr~O)KlZ&B5syBy8l~ zFX4fy4ujTe+U^mpPlfG0zL@Q$!-XAN)J6zJ>+{3xqC{NSV@3Ups4U^u3^SOgNv zQ&CNQdbxbaYP%qFc-MIi_6HY406Ov;-&3^Q&DBG>Bs+7N(b5!SMiYfun&@o+sKVog zx4s11r3}TK(q8Su;18){5|Lz;{=4|ri?Wi}w_!~n?UlkFgF*NB!cc`|MnYoU6mVJX zd1jZ_jT+v!s7@JB_CuAvB%UsmcvF5QUWcR!nG2e8ige##&d|jmO`Az&@HW%(Y?bU zbF$4=suvrC?yZdC%{fd0Y-uUyuV+#mInr20I#wOo#M6A`4tYvl4;(SDIY&R`@v2<~ z^A__}IMPtX4Gg;+P}oVR74)~F8rOKV_01bOEtb2#RZWEidK*~GFf1Xrlj#3a51?BZ zj4S!X&V|dg#`7$Qgy;|ACsFFwNX;$G5abX?kyp>38(8UaakUo;8h&5dX%OR{h>F{X zDVfcFkLZCoQc7T{V@;EsJXfF>ZbZ>PG8641;YvuXCvr%BZQ_!I3G=TpqQU@GsJi#C ztD32_stlRb$55QnNl9ZyjF<5I^ zaj`$LMfHRRs~)uO^hurcI2G!oF-WUr0y1B#q3ptF>8xe0fNC4pgtzMu55+3t(C4I6 zNkcriqiEPG)`L_?F3Gz}=!s6P$*BD%&%?_~B-SpcAF7s9)KZh*(n4Gk!RbrXfCv1f zCvDpq>9H-;3uFlUX$`M29&g|?SfSLFrG)*x2&;DE9MAtJLcnj1)Gil$6N+ChwqqCUX4^O9_UNVv4Wl3TU1Zc^XVcNJt^POM}!x zA_K{FAd+*9EP`aocwTS50f4EX!NVfQ^QA5F(p^X; za{1j|M17W+S{emOhc{NR*}Y6B_WCg%eearb1VK7V(&*GJ?ZJ-=`E4#-~`fx3wO zaK5vmo8%l+>4iPeS|t#2%)5UVv?k+kQ(Lqw#8%cq0xgj-4%N2y@Ib<}vUu&~X2!az zFB4!u9S;&r>!W2sxEiL3OhkNA7JWt)|26I1ty~h!wUHN-2AR%rEPZu=gi zIP&cWAq%WtmJ`B)#tzr8sfcW>1@?U?k|2|s7#>n!|05SQ`oczI``^8!H8$~UV z7}pkXIEXP*vrcqugL+#7{H^l>8U++h*UIyyimo^1 zE^peEnm+`wD?I$TOJjJRM;q$%TlUvy=bMwKUba&OkCsVdrS zb?Fa5#LvYWp0=O8xv%f;XsT;CvS)ulqrr@F}s(T zTGSz%8P2OLtd?7B+u`-ghbtyKGdUX3j9n=s}n|v}r|{YzYWQ0DO=!b&L`3G~IpP1aEb{2p(R(8(gLM zA&}{T_c7bY|EVq>>&5=rt+{}N#=#9qc;1-1LjE93z0p7Yn1t`4_xs+T+L!uu_)FOJ zBc@j0Gtcm3i~D77fO{X;n};(rbn2iCOMP<> z)VWzClr($NlPX9d%<|_1|9tn!=&by103YnUG)oM$7kq751(zrI8t@_N5QJe+o>8#j zKDc5w1xt!Yk!Zxx)Y~RJGwOUI(JRO#G<`Ih{N{pL!_EOdH&@>XsTXChq1bN6dlVY= zZOrrbL6vw+=BcR7QO{lqrcN;ZfKF*gRAO^ICKtCbiRuU)BW-Jr_ny zCDKW-m1CWrCwPHKuUg>+F2?xSl2Rx^w#u@|j`tC{9;My~2AXjl=Pr>Ewx_m_sj38h zvm&5km2@=9Sc(tb268l}P;X8eAYfi$V6&2X*(|yc{_%t6(5P6aQv^F0bIuuT9x9cYf!GL7Wa^S?n1_ikXv{w$vqP}9w7 zx`!yc#uUm36AV^Nz(R^8a=ZmIZDp{5rPfwBcil`JpYAwaVwVAq3`4FWFywd~0kiQK zz#sphYeTATkU07Ht#ydYJNI_+O}uh#W?thuo|1W2;gmtm%t!PW)2FA{bKmQ(Km-bw zv8lDe^mZWl1^%uyaCMjcTv`)97#sj%5}j$LIRg~u6t*4vX_#qpnr}eE=aFH+{{^w= zC8pi)@hnTkEOOxF$?CCAB`(FM!3cJe z-fL8c%(qR6{$Bq-52_r~>F_s>m*y-)Z@>xAXo7H#_Se9Er&{gUq04YDQA`*TS}`JE zuznvCEtKyP0Li7kc!2Vd*d4fMCz=DBbC8ucsqAA(KrHPyut78=OG^tQZb1!s+%R8f zI*A8o~@a(3t3{qQi}QGsn^|FfI>52 zLA{33a<}$C4l)pwT&mNR1$ND*N*X`;Y5E2ABVQQXtEzEd_t7r}wzOF`uR)%L5=C#4 za7UEVrtvKQtY40V0K|$r)=FeW)`j)AZl@2g+A7vSnI6pg(X>?1g{FG4|Fx72=AL6e zc*&I46oS@Le$$OLiYUygOQ(8hL(Fj}Qf~rvMVmD>e-{}OqL@$-qBwHCoYMn%hEQ}n z4oVrbhM+JvQ^<`gKgdHt&Omh@pQxCCsM(S>opA1@m{<`*0s<_a)hQxGuS@R_KIo+B ziRc$RAgaq*Att?Ie2VpqDW>4P9(ZH?Qtg;tNF7WSI@oepluQ=i4Ax=wR3;46sD`7m zOv4(+Z`+dS0 z@!fPi;+zT5)!}mbGM-=>cJJQV86}LF0%TH z&e|`dbngiri)Gy}H6J<|a#i(KQ3+BVK1oN8tcy!lij9OVJS^u@*y!PajoC>T*hzn5 zg=YEPekn?4piHa5syYl>+Q?ybU7}j)VAJ)}_5_yO^URDb&KIsd6kmb-Nx0fN+FqD< zGbJ@Bn)nP1B=*&A>VVJC3u0Cv0qLO?2@GNh&G)#^nE1^ajbvJr(xg&4E{GCB6@tOL z0U;6Flln6j?g`xaDz|pVyV&$(us%4mSN(oZd}B>+d`RYaWM7-M0PvMD?gc;))cHg~ zRs6VjlI0siN^K?)~ZVd7jg(1H3W+}Kz&ZcQ5wgC8kr4N zFr5T(CCGowYPb%L&J~IVTVv$lWH*uuT2*2U+tx#cZlg^Q|9Z`5xQ?u>SH~hG1XJeR(D0HfKM5v@E*-<3zU4tv@ zdO?m=U0qm?{-6!NwyD!5indp>5_DGDOIl4&n{y>}bnPL1O^zz)1t-7m5oT5l6DIBU z*vG<0)RICOXIm9YmDx}ncJuEaS~ji5S>*>8~1wCxq6>k9G@Wz8b;mIO?uVo{{T z<^{O&tZQ~I&E)8It@H5D8)B}#SKxM=@P$6qDj;l+xf+RvmT-~?)VZ7;uDwnpx{^x6 z^DG2sb<-5LebJ%pC9^um5HbUfNO%&dj2^F*VWLDN>B1$cBcF(#+oJm29F=uA$WukW zrb0JAaH@{bgk0Q6uVrTV%@ks2j|ZdA=7qi`6yy?l8U?Ydla0eQK3j?F7w#;e2(}Cy zXs}0yA{VX2%Nq$o9aIj2J(-~C=1pZ5F*gDVC2V7dpG68;Xvdm?kGfO+y}ENj$b;quv`7_@y1xSiyHG;Bd@IbOCK_O zuHN;rwXEi2S)2GH5y+Wn>@A^75te#{I+bAM6jpMcY-{I`Ab!8$f!wrRLZxQyn++)} zab>yFqFqGHDFS8uxIhQtsmK9_g)NEaaWtu?B_-)NmP;+WX9zz|+R)&2BkvU;?rj8R zKIPu+qBjHP>JuMx__!zqn9O{&1L5OJ)39|2K)tq!agdAHcC+G93#sMBu`Qxq4JWC} zza#33nl4S-w%eTn)|pX~r!Kz!kVvOHHk6%b%4QN~5m6=#?{!i04NLAE%k>g$Bb)&+ z8zBP!x2NTf(^Dh347Vi0Ru`I_Nq5QiEU-O!HI%&p>Va!@>LQ%R9tGQ&x>&=&-KYl+ z=YmsnUgglF?~_V30J;;j#sb1LQ#X+Mc*!OqmxIWDlxoJ{vG@Mv{4h7BxhBv0jbE?h z2Nu{L^B{CUN@{fGfN37Fl71Ln)kg+)L7Fs0D>FqariuD&bXduT7VA+?QqlyfB3E&s zp+>uH_<)&h%(Bb8i*8uk%9kr<-6`J%hXHRq8?*^%OJ&S64(t>y6|(Cp83VOUMhYoOfxx^3{KYdl@YCUa!LL}3Ap_okf%XE5}4lv=yC zLaEzthoH9&*>g$*-> zFgp0l8z3HQnF`z2%KFX>mV&K1M2Dw+WrWwfsj6|p)AtPw?(HuJg!@}&aKWnA{?yqb z{cg#4c5~fox)stJP-dQPU1ye@67eqvz51c~MIjNt zU~s?=SKorCU^!lEuMeLZG)_BYwFDC93vDzGdMGeS@CbUYO22uR;6O?B+$8$K=s!jP zzHNHcNVU&2(mSUyK}0HsmiZ5^s9S!;J%GyxMK@oXR#GexT0L2(j9Sev9enFJRS{VT z@1SZEo1N^!%{pTlTg4M*zeTqYyM0T4T9qh_ZJ8))@^DQ#*9}H>e+KFWJ_cm zn1`hQ8T=gzih68wjyP4W-HPjnsTxF3P>2r66WFL-uu`vV0?*-fWweje0k#=&4IvPW zAONXnFsDPIzQQ5X_B>H!~}=uP*^-F64{=o2}uWcyI%|AVyr^Y1DYy zXNBFvUo9tJ*6U^4bRVQ;{@_H!j9k;ETN1jr%w!iN;p)(fJhDT!u@VwRHR3aU)h+mA zK2FOSdK+C(3?wN(C=W!9saSKR5g%2B8+49lG`I;ud$>T~s|0ZHH3)yKBHaeC)t?6% z2%%G_SLox*F#bm7QWM6>oX*4733QxZ%TL!rhs_%e^z_MmN$!gFclwGW>6mNztuN_E zbKHqXSl3W2o{d9YvsUcqZGZyy;z5W>0;CJdeS653&e_T@J;F^<|euyGL7J@ zq6h)w-5+pgd+zLEhI4CXk3qCy+w+25#u-Gdq^NQNwY{@0(ET!+39Bu@A3X_*Nh!7!A=zeYFGHymq zQpbISj~S6?3)W{$wO(AhD;X7>=0_!E%_@3Uc_WRynpG}vh9gqmgTy+ERtZGrcQd3N zBg{5Z_4Wrx9w#|G93?hdOYToZ+U8qR#XtxNILk*v! zzl^Jq8%_?gpE~9|>`aF7)qj3a{{R{Y_xAoPSoTk~#s4_O`?s-?|HaP!&EEarlA!$m zMFjqPGV@=G!0dks(*CuN{ST1F%*M|2&mb-Pj|jXkf#h3PHzwXh)j0M5P8=&%dA_rK zwj9Hv&o}gCHMZYIJl_% zCQGBlODol%cX0YEK74l6^m(-x4gRa5^VL?p#}^geS?i_8Yn?^4%@X_eepVizKccJe z3j$yMzAOADVzjnOt?p5a2JiS1kLE7Jlx$H(ewantv=SyEt%IgiOXfafsxa8UqRREd z?e|hRd>*-xD*`ehpz8R_kS`C4^mx{tnUi|R1H^xB(FP=3cG$-~En@z?&z&o2ktN*! z#JY*;?+efek=Zg*jQY?X3I-3lD2>^cJ53UwoLIAo2Nu;y(-u6kD@6?xiCBE(TPv{qDQ9T?OV zcfDzSMc#eBFAs=x1~!H_EP6gRxjVP#o^~|!iKxT10Xl~Wjpi>~)eMTL)rL^4A_`gA zFHm&5Uo@O<>D#MMw#i1Ft_zAnUsz|7>BCEAj7REyB=`pU2|NSlccvybdH)Y{?-V3k zxM*vZwaT__+qP}nwr$(Cy^5=B+qTWswU6%ZzB}$|+=qV5hnzDbBQj_F1K;546z6h+Or!dzNa8@@~qk5KpMvR!BHQd77Fw%I`M_9@-fwP8&a1_9k9LnV^e7}(omFkJBmWZBP( z6>2|#pif&eb>5M)Y3-dE=-sb$mtl5jJgl%(-8qe&Fr|c%;H|Kwbp8n;YqWy*Lir15 z^Y`a`0NJ>Vh(y)Q+NG7u*ztSmJ74_CO|T5@OPx2IAy>4Ffw;cJ;@o_mlp(w}84=8P z3u#ZLx@NI@G<9tJv=Yvr2TE`a*KRoZa)c25N7kUBO>O{UmV}V0ybGYgzg*se0P2zD z96cO18e-f=ZQHwfOo^R?rqYUL@!RX==ZCm%@(=g8^#+EGPxR(0!$U!7u!#Jl6%d>` z0wI5%6+w^k#x*Q~MVy?j0cm5ELoc11tu08&_#>K4I# zg0ykJnzG-B)y&|db2&kHA;!Zs>Qv}ZwisnAJ->Y(2^vG)$(GdFP=pH}3gDtv+pidA z-e2f`HMldNJWB<_Xj&0yb_1D?3IrMY&5lGA-A-^U3VkQ~$B$V5XXg4WiBDvn7a41z z_?gz49i1I$u{!h5@$LdZp?b#gA5B}r1r6?gX#KJ>O~>OJf~CS7b3o@UWg4`D*!Awq zI}bTB^mAAjXrxyah6Cd$DCek7d~ILlw#|4NOa8V#5f@jjwp07cISskuRo)1H*hqg| zgg20Tm)`>_bJ%BIXX9aOr=V_Z+S&j$;gPhc#JJ`e32m&K_@HECer?Dz}7tq5W~IUEV}C)4xD5`PA)VjOZ9TuzwKHAPkF&f;fA-jo;N(yi~1vbe01nLym|qta+E= z)zJ!Bq;$D=eE^2B0j2vFojubI4?4Sn*gyc+GbL2M@quNEe~OYgCIl@E=lo_!=SS7{ z(3D7(uPxH4-Pgs*s8T?tCtlAa6*BK6GP!k=joG71462JbFE*YrQQr?emY-ewV5wnm z+WoJLez4iO0^z$Br1OIP&Z%Z4$|31D*NKMquX6znkXS)1jet8A3Zu;cR2?mj9LX*% zg!f5@uP!^L8H)vW#xAQt8U#)ElR+YkVUmaUJRCKmYZ5Vwwzeo3ar<;_(!RF<|hCr+sZ zE6cfN8Y$R1T<4@$ni`@fw<-um2Ple6QJ~lzyqZ0bm;o>wZcOK&cJNaJ775Q#2tL!6XmZ!>e`IIGNRqKAx@IDKN$<5wC?Qd z8HrXjy3Ty>&<)3o%qUoKbty1tJEPMHC)N4NWb!{Hk5P>R)yq`5>WP{cs;-Lx9{9?{#k9-mHVuum% zaPDPJ6Inkg;YUwET-%V&i?-%kwW_=b$0&53QGAXSV>TNmfq1n<)ZJ#77 zrJIFralR_z#W)T%JY1G%x{JpGL{fnDxU0%}i{2}Fvl(Ga(IU#Zs5clW@Yy7wm8@mh zp&_M?pC%IdiH@lmeNm9Rlqc^am{kYd8&7JFVQD-KXwOKTZrL;cb|e`P(EPcnlSi() zhxe0Dwj$n9V5iSe5>4;A!k_l8_wrdyS+jM5H{A z&}L@+pFwiK52_Ro8vak647z-tK;@8${Ij%+wpPL_5-fvlDF_kPU5 zcBHv9cK7TjZJK2S8Z?NzZy>}+u83WW=k0LgH>`9YGk2A|g=hl%=zMTO?A`#oY)ewt9!TP5f)@&OAcZPEd;aXkX`!zn4a|f0x7}nbH{s_y2#2f1(|$2eu)s0qxLzn3mcD^ zm=vCmz{{_Sh{Yf^n%C5^z%}1_5dkqhv&U247)cj~1+Q6+fZSuWj)KOT%W8vkhbvpd zQn@Z&+=lm51)kC&lFgE}d;bo2xK0k$x^M5FNIzCTua1Hi#`fM8RvmURKfyR^J+fR6 zeCsD`60(Asn#YluAuK8x_4;&-lB4JMb*K~HBIiU~P@FW&Lg+b3W1-G%`}G4^J26US zyS>$~%LLoF*$3d_qR1-k>Z{5jKEhia%8h4FmmnMV60`@doS+q7K^q^^IUDJD(?Zer zqnao!@87=fm2u_NYdVfTdwEUgE*J$7v2BCRryU7#R$veg>4oYoq4I8C1SR-B5fj@v zg`;ZIx%Gjd1&MIuBn<9U$8K2uDk=4U{(#P^-Hbjg`J+hN} z@3P;W0$;sYLcy)0)VyqK;bMIE7$73^)PS}7_Gs8d1-mI^;RSv7)Et`v!dY)~n`fH# zVAm@}bsP+J0y0G0Wlx&b^37Qj5!G%_mRB%K$1hw^KJGSIDj-NUzsA`a+ivnh9x0@) z^_c6RztnD*B{{t569B!<)pH@FObVAgseB|NV%@4AOb;J2_>;BoCPFTgN3S*%(HzC& zRxM8AHJWT@k@929%+cd4R}?WYZS(ec3c1{!elS2PI$$OKKB7_yp-UZv7w@3VgdiM9 zNTYuO61dn)Mg&4Yzi_i~8u0HqP>8LP#|L~JbtjRjF>Qeq!NArJTbo~oX)&Dg>^WV&zGO7MsfQI>}nD>*QDgE~Z4J$cF-M_l?KM}J2 zOBDDI5#T@DdCdQ)4gQRs{{JpeL-&t%-hb|y{=8tq|BqdY|D`~{{x1cB|8+I;Ka(~r z?Ej~vO|#lY;sz_?&lE);Q3fLei2F{l;?acZRkF@er1Eb#NnCS@A_eM%M&TGEKc9D? zcmNR@?79bfY=ps*IiL5(vyHM5TQ4;_uDb!awwc@(zi^Sls4nEs=q|tBt?B8TPPwp_ z_AkAynz)bmSKZBNi(Q-UHCC548?3v7xEWn;G`7FKz;R{|Sz0gQWjbnfH!D;P?Ky4W zs|hpIN*TqS@FJO`!bgNZDH$|PWDOFfo87bv&lX-?zNhCmYSf9O@DO&ItgMYUZdA$1 z8(T_^Bf`i%;IB$U_GGvC-X1ux?2S%K}2wj00BK2@87}&SbIM>HqZ0u(gXv__ z`^Vaiq_g%!C7Y~v{ZdnU%nU1d*yOeAK%X==T&{e>tq4^zB8iL2NhJuIq@%c#)_D1pM!4p5gPNVK|`iIrSIlJ zM50Gaojpf5fpuzc%qEQ(Cz7z<+tJ5sL0b#W`229tJm`EG8kYOPA3r^$4EXk~F}D_) z`q#EZ#CL}fme1g_=7EP?~;FLv^^Xq8%P ztm_$cU((C8zM9WLP7SflFLnv(xk|7=NIZ$M7eFfTgtKqtUhY|H&vTfo0IOBvgjcn5) z1MlmGR$yYCT75|u_FPD!)xeyFaQ1_vi^M({noVs%aQvH6Z&#ks%(32Y>i}m?^=*^{ zl_Z7Jk%IgP+>+ScVVvulApbIGpM|5s8AVx-{?r=SkBl23WAq=U!+^KamVIxsm?PhS z(#%L(;bhb4COzbQh4)8;>Mx~Mo=@%q(kM;BNw|>eu9*~?xK$8 z{P?Syk_|yoF53b8V|X@=I@e1lWG8CB3+uW9!f;sUO7aHQiye3*S-f{(NZE09pEMYB zjRw&epv$|A1C2|oa$1*c2{{q$Kt)f6Pcb?Mgu#p&hY0!SGAcplj48?^p3ku6sTFvJ zP|IorrFOkDEFvfCPZ8Uiyk>`3O?jptU*+4!8$sb+1D}XP2uStA+#PStAR3|`}%QYc8~^kDNRzB&^Q6zUVooJ zW%37A`SL1d70}T}zq}@h4a$|@Eel;K3&@B@dI}j?A3Ac?<9$FIr!q9J=KbYzUAfID4ms?Bk*FxCmld`XOd_JagE zg*p;fV}^3Ct{vj3oH;l;QgE<+eFliVcVcHW27L(iE>hD#W=l+TJZ)ggC@VtTpv%)w(prMr9JRmWU__hcu@R9q40S*6e#|W{ zS)%V@>Dur2zy-#@!`jvS;9UBfN#QS-)?S4yR&K8Pc6h=*u6=|f5Xh-2Qif`nBR;a< z)4+lRxU8p@qJ4DA?n`6QdSzHwsO7q(Nxe977=RH+0pJOkCaw3V$d;taGM_hJnin3$ zW+Q9arm@*ityybLCqitn>vUpb9iG~)LQpi~>+%Fsuz-4Z$qT2~BbkhD?>CVwugd%1 z+}M6C+gR!Pw1!em=(TttUEaOttU+vbVv-0ERE?W1$@@}lN5r`O$yg%PkBtkTq`{d} zHymFj!1ijW=1@$sF(o)?oUyrnX~eCkrW0i^=_0mMWW-IUIa@nX%j-)wPZgtlD|y)} z>~>4ev%h(2@v{TUW@EptTMt-VU!}t&J}4u z^{p{P80DmqYS;tLgcuwqthE>dD3RgeGSVTRi}p-t2U9M2?xR>OguuoZ?}#t)E!EPL z=PZaK*n~6X<>7y)PT`WekyIYrfExC+HM%XFs;T2?4If%@Q15QPTAtIF8rM}ar(%&Y zK`Ewo{kId)yMFNs(sE%N;LV}Lq_tFbl*r^^TH}P!ws8XgO0-e&vWao96tph_v8U)MG~Y`uE#cTQMrAZMc{J&qzjtm6g}Z zhU*L@C?M{D12PwxU=J3*rG=g)M@jOH+@gSqYNJd=vw0>)=2^h$Q&6$ODGF6ASx4Qs`?{jV3I`g z8ZnRyMU~^CWJzt0bD+)sDp3f{(^k6$xXgO{da!1nak##5VxS=I1aZT*Io3V$Naf4x(Zie-@OndRjj7zp01A;PrMrzQu z;I)BPZoFD%3X=ZGI%-)0@FEhr(@x>$7GQU^XibC=v)@B&C=w|xPlj-~GdBRg8s?+W z(tXrD`^c;qHVZ1(g%!{(on}YHHw;Y9^)fbaj8u<_K9zzzuQWa`<86wXTX*<55I&Ucm@hZb2P*k?T zH~jdW_RAY!fb_N7L4*PVJux|ZM0V$?_*GDWwY1F&C%AXa6ZBi1oa7S!!y}Km`qkq#u{-Qhs}G)zcLw}6+9d5s(GAArH%y6H3x}a zz7NI;gSoq0@iw@1I^aW)gPr$&pCrT%Ld8nB#v^Y?_!M9V7>pTFGm>I;`o#R=jiC&k zXMEvIlfZ_B5s<+zYFe*=0@2Oh2C06rvP(9H?nHY3EB9wum0~bxUID2lGg55RfkYa1 zcDUw1I4dei3L|d+CgG?=4ge^{g<2}$9fx;p#ERjNlPb7q&%a_56%PjIh&w}{D4sSM zqrfh2jLa$w7FPkOZ`#(qb`E3N%JZO9Rg2xH#v2!Z3cl@B)h|((Whw$o;PG0IK`fck zXbM_T@!8m`yQShVUeoE}*uMcf5)grUb?fu}ab(h*IQWvd7U-8n&*Im6_sZXpF2)Kj z#nqe2B}j75dRIId4PNol!GxeSwf$KcO`7~vC)*L`slqIhw-o*oO!7~}0B~Q?afj|m zOJgQfX1>|OhUwz5P?Pk^)A@5Ohs}O6S!H!cU#i|DBklQf+1;IGs1(Vf_d&yx%J;SJ zksZK()gH_{@yY{C!?C+a6W-##6)S!av8rV6dN$h1!Er(paQ4X-$xK5@33<&mIUIq% zN;Z4~rC4je8*6$*#8FeBWYa+1otLld1VNTI8!f#@BFqlvi7}7zF?Id+z-rH$4=d}@ zIEAE|^6;he3LQ{gafaN+S)Pc0hZfUP(7_z3CsGb>>P(iGe9%ZT++!TVhb8n4F{eds zFa3ss;)O&r6=TY`a{*lHh)bL+ZH~nxVjlXi(OiIF}1DYUuU6 zqGBBsIbxG#Mk|r}G^+YdxLc3GM!E#J!Cwq0jigS00)hLob<<{Ub>_Qu6=#Y!9#hy@ zJU(J-(jc*CIo`0c)MfAE15SuVWfq$75zRy>S2=IG6z7|YlBh#OzlV+$N65a^NSS<& zcgttL?xJ{R;PVxW?Ss(g!wHUxT!oc8@V$-69Sfr4>jyl%{X`}M^C zHcejwbzcBWhGs_2jq*O7a_opTQOgo76PO50?cYqp-a zg>FAn!=1*-iHBQ?2^+<@kVrVLfFmhg6s33xJ*g1bg%2(G#tS$*ARRgoE-RzNQnt6R ztW7(j-Z(6fOSA=l2eP~022laQED zI02)Q{g(p>a{r`%3>bYGsfAa`si>fNIj|Jz_0FUnxxykhxiq^mU7&Qe{#9Zag8keP z?|OhMVzK;-|DB^V>xpEIwFttlNa{iRJT$}v4lsi>+XTB=xhHh&-G~jB8swgEhF8+6 z<6uXa#6CogVYGfGwLyxX%6aGd<&-SslCLmcV2OK6a0Ylk6!oux&N982rhy*`KBMx-+Uz8ud~<}rMtz__9-IuR#~mmDsiIx_lv}YO&{X%un!brlngFi#AL9!*XB$uxZB=~_w^0dJ zREhhyAk^BY*bhmoEU3EVU&&_w1hx9VA=v$QBrS!16YQFQ=4Jn#q_qo7^RI6DPdKUn z((3-99R6r?|Ebln{98cge~(u8@0b4nLh=`4)T75|q5t;_{ZEXo|1l!_KdE^C`3~kE zMepAtO8-kmkK95 z#29Hql^XK8^7!Noq>!j0J@$UkdKboi_)YPBYydTUTjO8VzWCU(QZ$ zYCb3n@?{dFuWpO)FK5Slx6OZBX@;i$ntnL8a+md`AgHpc^%{G-ZqkK|&f;rF|TM4tg6{dk+9BHDv5_1nGBwk`il-IehEN z$R-DbK^F0$k35P$I#>*EU!cE0N0^{d>oK+Xk;XUP9mF@kJ&#wiy@^*bO0~y!tNIF% zTe-Qh3*`HL9!I$9S=`)KZvGT&I|%Ykal@wP0ryQFK$1uL_xK=%gE#-6Y}-pc!_!L7 zWE*uxeQ%n9Tb#RReRtt&5-$Wd7}Ac_7n3+>1FbQU! z^)6iA6`XxT&}phYku4X&G_n2&zDdXquNqen)YaA9iHS^JFr($rhS#IzqeYf)BNV)* zorj$G8SXjHy9_;UYm*F49C`3X}E3% zcR_ezqfPV2+-+X=kxTnTN=A)e1wlkU%gxWcL=}Mp7~~!>)C+O z`RmaDurAACDHZ?L0oTBWBkkBH>gAf+wwYOsUK_1Zmbs(3&t7J zJ)DZLdHfl}+7Zs9&tb3OVw+sz++f}8WaOb3qs9?}#D zX@^2Pqa}P!qHxRn@bVFg!o322G7M$VcYCa4AYq`w+U)*Av7A{P6RXD!y_|v5<>t7) zuGu-Z_{{j5nnm%ac4Hj>=p;b-(3DeK%XWuQQ<^w*kIFX>kH6QMd={)%)F_&OPpzsG&t7M?$Z8g- zp1W(&A>$m*I9?nBQQ$D!@3a8##&Y@#jA-*ze8UgXv;Z%x4_RBLlYY)sI*gAu!vrDO z1Rdf&xH&jUd|m&G0n>ESJ2dkoxp}`YlttX}9(|ojuvQ^m7IEBY+(Y;c*OM#QGsl+iVb z2<)K%aetIaRI`B(pyQ|Av&<*QfIw(UXA5!75qz-bNU5BZQ?jy)eG>`V z968ESu3nu2cEKsUDf+O61o%KKgHFgCV?f`a6;gmI?cvvvB-i}E=2lA=TnGGVXPE(5 zahinGLa&5KT#f|?T9ewKxI|<0*9+IvYXj!X{-juC>Vm04ZjQ_eL`TnJEeg#>fTs+m zOPRnoMJVLCKY^i$7dG2v3SrNhOE~T7a(lXL1wR!mwiH-^v-pqTO{D?T=#Fht#)#=j zV~KvN(~j$OP>g403}o(&z)2$41-9#iuNWQ_Gx5j5SuUY@Dw_||H^8NyQsZl#Cc7os zQZ<@w%G{>=#}yD|FGCEMwaie!5y(_Yv@8;a>RlrJOn|Y(+kVIr0aRuXiCFsgh04BT z$KsDO;3DlJBatu$ARtOQsOi#7W9|q<77tP>|i)O-g#l9WoU z_|Qxt)JEsUy9MkUrq&kQVsf3YmuuJ6ogru{jC z=m>h0oPM>MdawY#fLFV~$7vbtDI$nYZZ?pPBjo8OQ<1XZ5u2H3UUaWQ)+jXvf|E^^ z%of`jQ0D9)+vYG$3upQ=UgC8L+MveM0nye*+%zX8q$*K9lPs-}8F*y=7ZB3Y4S_l{ zbwZ0juKq?WKG#l3UwS*vE*TU^o=!Y&|D54;r=h;$se!bew-ltar(%8>i1b3r>B(+* z`gW^FMef0{q)}GAV|;1T8fu=6)t!2aod)2#45tXke1P?nMDsIM2uK}k3}zMUV?_)# zO~oMUf^Gv;WiMkw&5gy>vaUgHUTQX9mW_6$0D8P;w6W;|c@_u0`KgezM_~r+`~d}U zP;Ck%r!F=E<>|s`KKfz!I8QkjI!V_~JjIpq?2-yk2ee7L1~R*Z4-x2~=d=X2nJ|5> zMs-u5;n+w7FF|P}fl7HN6M%J9&1K=#%4_1Y=F$MDO+!syN-Xz*P9#RwpkfLd%B2uf z&L;`;vxFD)5uE(<3XFd!3zgVT@KUVV7Nt>1ccst}q~YH)ulojS&Lt~XpGD5>_;i1^ zNFzDDs~i#MKj~5U6??oam~VXAvhoe=c?gusm;J*D^#{Q@q~mohHrAv#BG1Z))5qdQ zBWocDO!6dA7Vy77+w7nHs*gf{>je zqEq{-6Ejzw%@P{V@{u-)$-)Hq4AQ8XV}e5iLVb%?Gwv)Tv*1c~6$QSA{P52Sf$vq- z_4G>>(k78n>H*a0H90=iboB?x5M;e+jUkxDzrtwX)h>6>RM$u@@kW7Q0pmG#rhuZ& zqJUHecHvM3tj3ksw3cz&fUr(Jq~2SRf+(K!H3^SfHmPUjt!E$Clxh+d+ASc$ zTa61H`yyQ2JG)mq3DnMf=Ms4nu0D!1Emj1xUV*s6?_KRyX{Drhu9H1YTbvU-Rau2q zrsy7OS*zaHmk;q=-`TC(P?jN7GV!;()HOgSQkry@INNhiZj;!&0ZVGd|&51#dL`~W8F!z?w6a1jiXY8>3Z#l1& ztJWCfxfgE{9!YaWumwlb@cUemqnW$u#j?#d$F`H5JnO0E%GYT_W3+?fB#BQ0%P~ zjH}3{Vcmto4S+N|Ni^a4bf68zZ<<(lr2d><&eUwHQBeUw|R6h2Z`DOyr18HmXQt%5Q)dqR) z2P5*MSr;Yy;VqU~m3DZ66&wl|prI_H0K3=xMe{jMtU);tAR%5WBNUr6=#^v|0%>=- z%sJM_>m0uX3C@7*gG|>)A2X`tdJQBnA*7@-s6qX^ zS0+HnnB1Em>Xd=p27fj1VNCm~Pe?RdNwzRju{Ax6vB9Z{q?2cPXp0eUed zlcU_ph`E8pE*yl3N=e^b7>ug(3ceBkiZM0$|5{M~et^|_c3tJ36XLFtJ;?Y(pX2Rg zQksZ`8R33Me;{*rEw_W0Q3J#Zq$CF3z*5L3d3V7iNxQH*j#Ak$1e|38dbTQ=?oIQL)*bx4!e`3=6Za(m4bfG6G$jT%s`UDX?!5jC>d<>|4vObrk8|Zx( z>JIT8rreUOrhSCrUH)?-u@hFVNR3GF{+__=$?I1k_1C&XXQLeI%PSQa&Kp`IOL(JD z50^RtojkQ+OQyq)F2|BiR{x!(@t)TPM(voIhz5cDe76$9b4qa_zpV&sagp8Xa!sPh zM9OAGW3n(YR4s#(bQK2!Gc=pis;ueLp}UrxiS(q`F#OAwUl5vv<7@?+)RI~gCSzy@ zdNBjGK408g^-de~>THp%>kAbdFfUo|@v!eeE2x7JPS%@?@TEW+fGT;RR`^xY#TL9L zyY_WL&@df@9dk6^ItO?|m~Uk(U-tnSiHqV8*;BYleh@jrkW?&J%ta>Jh*0pYK=M6UCV*S@&`iGa!=BmXNh{?UqsTG5cOPg(02}>mSq7^{A z1vYWU>t^8hxLok!MQ(tcG`Hw|!x?Yi{j{PgAGQY@DqLPKu|!UF?W(<669xAmmuT6R z6}NYOf9|XN7|5969IzXp#T_3iF9PgSNJT7C;Ea|g*zp2v0%wk#qoc{$Z42^{U!{J9 z1$$UUwXu8bjR1W|;Tan$EQ_UW#RAfu<6pKR;1A&tkfQzhvq-sgJSPm1I(`DH31FU| zjv$Cr=C+LU$@&acR{dvgI3Ty8m5Z+kgYQ6jrCf#$$#|9I?`ES;L(Ik|QVvWmRgUo_ z$5hhu!O~^QQ1QU-0XL|z>4^JzWviU(_kt^-*#N6_%yX%weYb;1oI*}bAjreU4cs#~ zWE4+Ja7^2P!0mx*Z=+9*zb|?k?+t$x`qSI^9Td&W~(;d1;1G?^$2N89 zh3W6$QrmV<7c50ON#I7}^&*IHq`wYpg>=mw8*+CgD zZhtE^XKBDh1^K3*Mq&*#&~WGx7vYQ{V@4~UCYp005tZ*kk0Ahw$+PG=vKP6`f39&$2Y)ex3rq-u*src%}qJ!nHdEU;O(^{?)VkuzXhii16ul z3tYmNP`ubxBwV1dkrHx+{#~MfHVE~xnvzK5ipRbU!}>d8Sm|YC>j1cskzC7cjckz6 z87fgJ?3YOQT$uTW8RoN~SpIO@DH+;kOFZ%{U0a;{TUVLOEN83)&acJLfMLCGzHp9! z=#84;0(4)Az69|%Ie7UWs{Hmtkn4`ZjC850?L;UUcUh-3+*(m=D+QQ2T@_#W zDN46vh&Zm#T->9Wm0?Ib-Xeg5rRqg06%@u6&>EH?FS+JvtMoe(qO9!2d$WSAzwQSy z3Ag9K8LE3v@}?l3c^cs(3E z^3N8_PRBexL#a`b!%^Th&c}m)S#su^LYk1@4P_uvnsOL+>j1EWVY`OL3@Q(-O0zP{ z8@lqCD+^sn)JBZ#(zK2&>+I1zxadz{;aybsPQoXj98v<{^4Tlzj?WoQQ9MDizQtHz z&;w7QvwL=C&%m`vmQYfvC-CntoG;RzP4k_{szf*ykZxnt@XmHRdwO0(5}fZq5<)vn zb*BRUT4IM*_vpjF27Vs@xQD3Ggnfjs>tUpTGFu+~sy|?!4|X(fp%AkWG(r|na?1Es z76`JA@do_PFrJF1ypQF-0;1|T)|!fpz6f#rI~2U=cfnz0qnzW+Ffv1o&+!vBD(RQN z#f0hg-)rwDiRqP%%|h8q_<&vgX{M9Els=nX>d0skwE!v+q==HH1)(--$GE3P_F<`i z+W>$8WCBS)|Ajfg@=qwd|K5>crT3KN%eva{$+a1ZLi>3ZZ>cfJI47h9=FRQitVC^WZ9Q*vBB1fDY?wAxvh_wqLBe>K zp0iAzuli7Yi4)9BjBnfHT}{XJaW~V!OiMO*Ol&M$sT2#P$sRQ6;2>!ROlET zmp%?b6;T~?#($Mjn#r1Ss6bn3Ye?sbP!BKD@N#9y7!-^c`_dq((iUcrp4CDLsuz@2 zH4FGTz~`JPqe_4^C3Rp#n53S3^CM5N(+(Mv?uzsm=n4NLYn*9u`|^`D*7?aABZ!L7 z!~JB9VYm{4uylT~Y@|7v3R7x;DTp}JbNwr8YQ-sTY@4RR&%f_@I`E{$SI?6~SlPstb zt*1=Dgg{Zz*pPZt+xf<(d^d8@#wPfJKlhihUcR1X!dPK<8O=dEna+T%PxOvEZ+QS> zrHP?-joqPoE;_;E1895f*RKe$&w4f0Dx zxLYK)Q@?d-aAu=k3XC$*ywh@C3BxB3q|)LA#|}XQa?$A6Y_N;1t@(U)aHdrPU$S<> z{QF(4(NPssvUJfVi&E~-@bcQT;1V5!7;pxb@CFO}m3)A8@q65L+{+43ugFNyschR* zW*L+0tD&-1FJ`^t381C5{R4yFW;`|#opl? zF0Dys9Jh5JlOq%Cw`Kdi(mInNe$n2S8&;O>Ob z^}T#ez-(cG8xi5z(9v!XX|U3*TA`zf1Lk;2B_&R!gktJ&To(@=_~0&=QCpr{9I;5D zVDf>L@7x4&7yynDjkWg!opB4pRN08X+4iDb+7EQGrC1mG+IC0zGO88x%6Jwrni2PX z$pE~j2MaD?I>c;Fk#A9@$Jfy-|Ca^ z$`R4YBR6~Me(;Nff&?c*U-*d^B~CUYIVdzHBHVS4YxdWh7mm3dGrERPU%XbH*^lAa zB=RXYM8=rz9CcUR+g2~t*5QeeOH!4&Ik2J!JPaGh`V_yKM_f{CXooUw1wXs4v*A5C z*odc|fTDR3)jM3Io1>R>?|7a^t1l)hyUq8AxO-oh3M#Zp*!OP!f}d&mI-fbE^T6Sw zB)aX`ZTa_a58R}3$0WxjcM(dKlFGIJ;onr$*Ke=~@7=*1IM8x{1gCCmEv*l;M$5A} zDR&k(dGhJ)B2!>dIut0zUM(#ty23q&8Nyz{)_sz?*2gue+;Z}@_9VIR9xk!6N=JiX zBAU77imkOSPG>wA3oZ~12wC~C{!qWroCD5#&bh2Bzm)0bSnYKsit|}%bcRDJAAH?f z&y7`D-}Dd6qBI=9mDo+}&km(thhyWUOUD_hHs%#sDBl?I5#6+V}u<#Jg{SxkpP2*cURvV*{wq39?uz3>+VyGsuSE2LAeby!eDR#~Scn)wAs!m~2Ielf?O{s)3b1H|fwWA2M5I)w>ChF735{(WCmR(xu)?1L8k-$q50!#fX^aG< znS$prW|bT+!E3e7PId{y3L*yG3-!y<_sQBDq!GX><H z5Kwg)_YS6jg!FK{E&hqkImbWEi9;QE7YSxiy~Yj-{&bN}y&Ft0-;&Vu0p;%Cy;>k( zyqPf;+SYPTKbxo4)IrMTUfw~0&8)Pb>XC4#2{LG@U!RGBZ}J2Ah#JEm-|j4cRy}7n z*;-g5jg!gna!t?G(k2;^%(8uf@<*wRV+~RB6|2l;sarjv1+BR~0%&1%1)QOl!&~Y> z%y*by*LVL1aqkplOS`t|mW^3<)hye#ZQHi(S+;H4wr$(4S+=X|i(aeykLcCCcSr1q zeUb;`Br`H2-*=30KlgQ6nUhNIdMt!;Y2$8K*_#V7(saERj&dyxcT{;#UHLNVT2I(- zh&`yjh}pQ43gIqc^#aA8jvs3`G6jz^rVTN2G`ek+|<>8Xl5cqxAP(LqHs z2u4w+?~#?)aqvYdbbBk9J1_-+qn+|WbS5{MO%3f3Lw#+q(3ik*fRC5na{#B1ordCa zTeHPrtfvNhqpRciM&;%BjBwA6#g{L?Wd@K%SzXSNN#lr~Rfox1VRqls1W|WNUTX0S z_YX8*$Bg9ft8ErN(k$erRxFalEKycC7rfQeK_=>Soj!9VKDw0H`!dHKD4-4i`BF2{);<8NjI6P@1;a z%J%Tj&-%Rrp(nA^z~Z0Z#s5nyy{26HL_#qfvAoLPqQK-D+1Yaf^bNOalf7sDl(@Hg za|e5)RKpMWAoz}TnTSp$6ReXnHqjX!i})B|vf3%be+{K>9eKVTCdSXL5r`obYv=`O zS`{GlabMIY33vwu?}ax&DN@5TGI52r_7bWZ{q>Hs`{LF4Ik_qaG2hpk%27>OV)oGu zECaclvxYF!q%&X4qh@Hy4q&H1=__k}Y(ql6xUmRi_K3Yiw?hK9=rAObx+r8_+9?B% z@|Ec_tlPf8ag#N?jyxed1>q(t-x=;}I#ux7>vCo6up_knyNWc$bDQ()yZ?bNOx1sw8<870s z8XC(v4qkB(Q47V5{GsW?-v8+wZJ&7@pPpMgYUS2AW#~uGiSjJ-3}IUboO%3i(XtFQ zN2(!FX7Pr4mI`^t0DpG`9m_r39hra41o}+cNPV~RD6TTTd4`#bp3P3YvgYQUalBd8 zw&zzWwv_c%TD8SSIKKk|l~tEB*h2ZZhSTMxgZz5Dz0-fR3cyG#d7m?t*NIe5gm+9K zqiv=dCoPF?q_uG*%gN+)$e*0b^J39rG~u9p3}7dITu`}YZE*4;4e>kRL0Gohphrj_ z6k^2|y(>h%Ew4T+Eh|k@qSE^ak4wMGFHQj~TAiQZrPyK@Dt-^A=T#{-iZuQ3QE`sE zMGL%*+;&YWD+^7M>Yc=z0nWGVR%s_k@#&YJ8?ZR58kR&j|H`&s^Vu*DgOWj3VGa!z z_>)tNy{j@5lN|Cj){Qa2vGa@E&7!3pcXZ~jq{agzD1Sp>)$IKZ73Pcdxdzm_J@=SY_{Xxsw7E@4SxY*_{l<{EdUz zH)7b#wQDc+YiEMat^x7`_05DrR%>@myO0gYRxdX^xlw1T@Vg}A4T)0{+Pq5p^e*H# z*~hj=Pm@<&BO&b>um=}K@{H2xijz>XEHtY>2bvQLnWae?9M;O+lXxwbd{5usN6_x< zz3dB<5$H^DT-#pEB@w z_#upasHdBLT7pa+a z3*x>TN>;k(WxBcW28RSWPRf-moyA-$vCz2zDR}U)k>gUMkE-&&Xqn z*r${~e@#L)-Tb!g20Vn^b%yp^vEkato}Jmpc=$X$VaNT;M(*6?1v7_0dwR^ak$O3c z^BW6DBpcA%z*J$&#eN(VroRpp83^(}%XVV(8+S>Sk1EE9 zeP0G~-y+AksnYd88~iU$J~`A4Km{FLl3a(Xf!A{t$Kd{Ra%`OrHt@aIdYQYTrj6At z+FS{PaU$_;*5Y7}nh_{-Sj8u^tNm2CxFhd?^M~fS7>p$Z+;g>uA$jlW#w@0oeveB- z5q76aNP)y5!HGIfvYlXm)yVu04eN9T$B|VA1XKv67d+m-v+~QEF42Zk`UQAbkAQ9J zw5S#lzKnG@J(8G!%yx`-;muL;@hiwicJ=u{?;ggtV#*7aS}*pz?&b#E3O$j7VfPfB zjhIkyWeKI3@%@1gzbuiu5=rb`a2sWph@Z^KcaUVp$bQ8M>i8?!I97(L5s6xuRTr7r zY4mL}ma2n9&1+`#L-O3E#97eUpS1sioyNo}IcSfA(I5*y+7yE}w29@1?uuKj zj}WaV9*(X2C0{E$$x|G;X3J^##5ut}!Y=GudFy(iX)`T{gql%@shj&?r+hQwO1P-f zr|b$l@s(~ntI6ch(ke&BnNKdwbabM%U&S_rt+e-EPJ5!DF_#BtF~`-UGfMZ@cC$2U z{bwj8{suJb(Q+mc36QkLJFcgwsSDGcX)6^xVSM)O=ZR zHfL4d8b=_{XLNHpWMDq$i?z;4t|V)1l-8u}ZY4##lQM`;gS(HQvD~WzHt$}gFF4np z9q;@^)$Y6;>UW@Zgz7vo^@3v*8ej!l5;L(Iq;E^-pW~$Mar7XWd4OW@YI@D=>Aym@ zf2+e{lB$&*A2Q$Vrs06K#&N(76@9n_qwVtpe1=*G;Vy9)h%w&&?3fDmd{7&~{Z!9S zq&~=|M*u$t!_k=biy4@=C`|_>`$_;iB8`GV^Hk$E>P}IeE4?~$(}DLMmB6Vnp@d!t zaZEL@w)xdEZ|4Ne{Zwv06s)D%3V?|)NOgG@8ivs?c>tI^4n^pj9}ZfB?E?Vzb4+7?B-M=`1UN||9a&8k)I{;HAxcBLK;8;mr zsrirGJaLqZ6El9hKFtTB5P#~fhU&> zU}&{b59Jlv)mb>13r$%32)Wf^vzB3gi}|x4`Q5mozibq@Zq?BOx^BrT3l3hmRFi@9 z3?1%zmKiD_5q4SMQ;;w-##^)0=sV~uKw&>DDb`~gME<v0z*U5bP|KdMg}I?1WW^$Q^%(v}g3IG+a#rFGZK1!*a~oN#MH)C+ z17zN4UKoKBqdn7gILgS@s$OpJbrPV`0&wv(9~HVn_Doz)8tKI9-Ktsvf0CI@L(W_=W z=3`Co;L|pp=(F)CAJD#L0q#6&i*u@cr+PRZ5T+rTdpryeHVa^8cyo&v>DeosyC%DI za;cm0)=!DuYyqR*jknyb$i4*7#2Fbh9@?8y^4oigOXOQijW{P2A-ry>);`a*S z@12&{fMuz5xu4~kDL?Su#*D#mdz!%GKRL$d#>FYLz?(g2tFG36e98dtf#w1T>MjTX zd$b!SL|;(DWk8p4I>d1@q96a_W27Kc60;#wDvsC^B}s0rh`pW{6g#MM-5So6JoQ60 zMlNZ2Jd-9$g0sl|i^Adey}=Uu%=IFKY4!D}tF>Vi)*b3Ddz&i*0j`+&JJ$6&objs* z@15JkDq`DYz-V|1mgkzxEZ;K!`4eTMux?Gvv|*1Yh{OOrR7c*jp>FJ{T>A-MXClcS zUT9(`w|4$BCLpz~u|nEw6%s|Tbwh0jO=7B6=k+(W1%A8`rb-;08QPJ2W(PkRyP#%Z zn)1|9Vc(E+Y@5t$rty27v?QP9h>@OR1a5ejaw>)FcQEt|3hC!Zu(%$fu~3_b{8`?I|H5z?TVFFxn*xn@*KQ?N+EC0s zFS-)dvWLf8I2s4{fme5mgylHzN#&f4r$PhdGK5mrU*w@=6zYS?A`^W@mHI*wk0AmY z$3Mi(jEJtUGW1v^MSL&i(Wew@Fl27r04$( zS^dAI*eL!}ieY2{`k~mE4FB5$RRNOwpU(VG=<}~Q<$qRT{uMI){}w9!FNK%?f~sNo ze}k%Fp#P8A(SJeq{D-9PU#J>(X6FBWu_f*ITTlpP62(NkVN6yeHvO2u?Vo(IjA)^8 z)JC=^T>X3Lp=(xEyKU6c#l~I#+{}$fg z>w9>(cE7tcATRJ9cN{%=bbm@pnLg|qec3lURPs0`Oq0+i^ zyzVdD31mu}W?-qsJy%KCLCBjI@7et8@Jjew6czMttf53 zS|T3%oLDA;kQ*ifI4Yp&$NKnOo#8T)3|zcpL!F*p`O+#EQ7apupNVFy=j| zQmH{U#nc{b<&Bi9Ua}evOPmx)UC)GwE1VQa(9VR?jUZRJ+WLRo^2=kGy1OMUXM?@- zNM-R8uw7k6zJoA6Mt3wxW9A&>yGig3zV}b_);SALrej!c4N}#L>cp(ABPi8Yd;D=X zTbMal$7e#Mu_Pa+SsJSts*~pQT4EG3upb(l&Ws7B+%{_H+XL{eqC5D0g zV7$oh2^J(qJI9^y3#SuTY2azTo$0vy)O>tik0J7%u?QtzKHJBQZa9ECf>Mn_^|hfU z&jzig~R8NmF)^eSU8W?QCxrhN-hi!tbBLZ^#! zdhsT9s@iwLF6woi(CmFT$N-|oM*}ic9;2zc#Y?h^&0kI=g!uj}=yFuQY%}mcgJF}W~U$6QZB;txazvW z@TA#5W4)_XURC6a(KA8#fwJ6Qtx3-B#I}qbj}lb2N_gs z1G6{|(i+Qy4Zf_sG-A?zm7MPp4#6WD`3YmLLkOGJB~;r!o&xsAh3)7CxvIrjj<#=I zZdL!0Tt1CW%bk_D%F8ZIoBOgh2RQ8%mgVQM8_y}2PbWvWM9PweRB$0iRvBu@@^_mF zT!C>Q&U*)&%yv29S*0vMy9^fFUU|Z$RgD%*W&h z$Rs(rIkqnkV7xj!zFsuLFI^`(IXm2h>aq?9S0)E!t}Q!oZ5h@Dn>e41bJU%e=&qtY z)+PkLoWolMdlosSBMDWQc5}_kv#0xESX;$}f5EKPnlle9t0Bf|z=O^vpKuSH$Qi49 z!^3?biiOfz+vd*7Sq2GjE}}yh=g65ZID%(By)HRbjT?OJgekSdg#!BaqxwXdeST+5EHHL~ zvkLm|2A;lt^!Qe`iwzAAmT%zkE3^I4LVM?Ki+GN_$L+%zezh#;Z=4-eb|(1BCpQAP z=a2=NDq!Z)=q*6WoV2Os$oVK-C&vrCXj38S z;OrzG83m`eTORJbJa1&Eh|LK(>8>`b3dw)e7kIGe6R%vT4KIb`NaScbXD`SkL~nC8> zS1Km44ggKU`{18$UIt#0k*jNTg+-!J0hy_(FAIU@YsPEW<%F{7(WZA>t4un9Bivee z0szE!>nN~J>xTReYjb~yb9lOy+kkc9BCA&gl0Y$Q>X6%)$=(P?U^S@0A_-ou;9_(% zx{^J;wNh1`2pomxxEVAw;Ft5RJ}a`M0bBr2Ve!Bt8ns>YQjCv#$mgnI29uJnZ~)>f z=jz0A&zDF%Ce~ZIU+}4C_Q9gM1fhS3x zZ+mRp*+kR>!W(0r-j#nb0}BjH2xG1^2LKlwAKYy85NX@?jSPqA33K>{NWde|%{k=W zpRg@!`Wbo#lc0{mmX`FhL?K{lg>55_xOG}Ldqt&uItkm$3f4OjrS(o>*m z;6~!FwVD3K^cTeC2|`^v$fIFZx8~|P_~>Pyhmu+Zi9HE_00^6@fpDvjg44#;w3+(; zh14US6l}nc1c}*$mEL(+xJ%Au`n-0O+G0aOK#GwbwtfRwnke`Tb+Y}#j!4b_?#u*p zQ?@JExEX|+kZ5>E0Xmbm*gPef6B5O@dNC+Pyqk}fY#5i8T`L4?(q7gCUE_Ny3Z5yc zmo_&chsKSiG~hq#7DOz7@fqqMK7olP29B=$q^e=}q+oPcmFMfH)LKIf4?jE~uQ|Xn z%M}FoX>WHWnrm}>D$HrFWI9M6u&m&7EDe^2Hra%w9RKR48!JE8r(Z}GTyPjX0>+c} z?un-FPB28bc8*l&M8Suo_lVxQUmJku4>NcLIh#*Y_dz0=quS0lJGsYKI$^gzw)NWI z6Wj;OQ5@NRA;}#=_b5mfb4gMMy@}f zlD0nq9my|pV2u4rfBIN_d&9L#EU^lI2@IdVK}qk;C@R1RNcL$5eQD##CWS0>ZklEe zb%RpVOQ~b-h+9-3ML6N)LEtnwXjKgcF=coeaWXRMiUww-+$v}`vt7OqE}uZGKzF2q*+G|43cv{F0JY``v3qJZ6g;DT8@WOc>YmWnHJ1_2`B92{R`$j+) z?l+OcJuHa=uHQMTu+N!Bt|ICR%_zzgO><3}Axi%yHiib-k%V72500aTVY|J6VLV}E z;dkF$jwI_#n%z+x&W~1}nFaD9A+v`2TVN!zZJT*xQ!^>s?S@jyH14_I)}csG!Q{DW z;=e?u4^%KLG@`?PiKivCU4+)7h-Au-t}#a6&p6R>SJI4x1T{HrV`Q3P`B+cpHRY|! zTdq;zOhT_2?wzhoiHG;tkOp5MIB(jcp$ewvc!IRZH!b&`283gF&BzzHl6RX!yjjq3 zODXU618N^R&aMNNixq3wjs-|ICn$~6F-WF#^z7Jox|s zTs>m}EW-mpzb*RGu>fSJ%UeT#Kr(P}0G*FQ+-x*gy31BSV*&BEn?rOKHz?$+(tgi) zHvJS`h~{+n3fO8JFOIow`3-&t0M;jZ5lh(A%qs+~0WSz8@+FH_~#NFNTxsrN##9}akP@mbLyR}AtnILaia4Kz<; zr=58@KyH2@${j2*fLXk(qp2@7+f5wud_5xJJ}s)Q z%v!KMz%d!g!k)X$ZQ0B-rAikuOQ#3nl|n^84^esmEs{ivQllyb#5#kpSAG?gJPTQt0hJ+n=OU-GC6;`oKn|6c$%-SAR)G5~7koaU6y000$ zm{<3lgf2>@?gU|6GgD}y4n>Y~B!Wqtmk_f7mY1~@r{W%?2rLOUZ*3iQLy&cg?tYiq#Az`&# z!x%png~ju?fQ_G9A`Oo;fsL=9CD+aNp+j>Qa;7R)7!_&`)zmqA;8nDVRddrE3%HHk zW)m0&SXHNki3hho;x>Yos#}d@xAvfBz1D2D4DNwFj^{c_A$MX6xs`d-tG(Vd#2`%V$}cmuQ&Yz|Xy7 zi)R~BF#Ty^4bJOGNTT3WErn;OwmiMencUAU1;Uv6z7dhIdALm#sHvW3(v}FZ0kpI^NfwJk`dC&(~CxSb;t7O@e0Ze7F-{9 zq!ceRi(eWEq^bY*8TMvnF1zd0=yU7}QoV`Q3f$=dp zE#Dx@YkOme@olCmJ})+HKTI8?DA{(lK{%<f%35o=G8zpDLGz40w5;IJ=ufJ|(>V)G)5U~l*$bjf}zityg6Hs08lC`$Z z?l|3#D~1J6F;8l@vKIZ8QZJQ)Y+-$T0{>|`6^=)dnkcPg9H${_^he^zSec8Aq}~oD zYV?$SG9Evm(=IQ*h|4^;)rb)4|855BfuPN&dS(z~*7p*Oh;#xL4UQ2Iq1GE-kPJFg z!kk{_7Rgzpn#e9Qqu@=EZo^oq)EIvj1y@tGaqCeK3K@H~SxCOJJ^t{Se$`Su{s>C> zi+)5ZtXzh>FT1wQEAvz<@S;P;a^f0xr&t_-~a(AvGnzW>jU-?1I2-KyT7O4md1h6+rh_ovkELR~W?V%$xIT^e3 zboD{#XVlyFckYTW^v|B1P36o(%cDBrzY$cUcfozq@4(aj?(X}P2iZaLNWzNIH9-T@ zQVAOPQ_BNp^Qqf<@76cq9Ab`)nS;AMo>1s28)1zYdhah_Kv#k6l`|9sKBQ)j`;v@& zds^DY!Dm`@Tgo|myz96)Uw3VEG`L(>W<}C5oUDW@vW}m!jIy92B9@o54B9Xsh-lXq zfDjH_PONQ_wy^91ynK(qQixmUaterE%jVoUurbf9hHarO;3{AbaxD9D#6o&5u*%JK z*sW>Y^PBG)zJbNpZMh4#eyazvz{e_hr zqdhPNvMX4{xMs6Lw6offc=E@(cQnUe;Y@aipV-necIlHpa@G{wzc}UQ$+*BUm3WNP4Bg?%L^BL}TT>EL3o#7G?tyijkUWX_CEJ7^0K zdE}$+5rwTw6!NinIRd1Ca*?HQA*okYqqc)Hjf3$egpo1Fb%ztX2X(ZT!=L?VM}@Kr z8qqw%KlkVdM;uAw>Gs8syLAXcRf@wc(H7qwdj{v*x8an>&o@9#J4XJMC+T%hA~yY2 z+N(=sGts=GL}jDfS4~?NW8$H_SUdt)XSb0E=Pm`vloz89#M;c_5Y z*|X1_tFdK(!(*>sZCARHkbUf7Fr^y{k)zB|#tLy|@rcShy$f?IQ#4XnGFY*MwjQE0 z@VJPfdJr{x?FzGx#Z~fL666poxD!JWWcbH!? z0o2B+Y}yZggWv3h1qIXc^rh&6mkIZVgrd@Yr)aD(V0gc|EL`r4E{BaR8L#N7y+2%Y zuxl}ge{stgwrTB&!ikDSw45qbvBmeX~OEVONFlq9}tA z$%;B%CfxItxr7tI%FR7zMJSti8{F?e_udAyr3B0-fIzoU_^Z#Uf&pS5f9nyxFkJyzjo{5tp#yha;1Vh}*?Jp{||=-mj7oL08jJ2xjZ|9ta! zz@pjia`T{uA$dr+8J510p)0)rZ9&F{6F~|MX>ufjV?hdEDvGD=XzEqdrZoFm{n%I* zsEeliQ2%lnVO5pM-^oYqEA6m$g&g@cD}bmH4`g{?#1KZ4>8>2`S}e}+M{?;AW)HZ^ z21tyI8_EgB_kq=-H7UuRJbxapSDlwuJd7k+iH? znh|dNJ#Y*ettx!Gc0?1INMYkQaYG-&W}l2=OTXwjs?Il5yM2KWeL{I~WY95zzX%ZPuL4UpQL~1{eoq*1aSSiOa6+p0|wT< zQEKFbzwJ^mZCkNgE(*1pbMuu-$`8=62Eh>cVHjiP{t(S%go@9BZ3tKOxQLs*+_Zh5`}A@RXkb zYjeHrA1@m)!uux3)tJM$4F5<(hfz|k_#3q=F)33z5|(L^Ci}+nsDe41V%N4TVB8e~ek=I!+|3?3*$?h?$HzU;0aEr_U>(kHd zkcn2(eJ5C>y&v;$;+nl;TA3N=I@-tc}(hV&jJ`T6qPFH5%kQ9JffADirat$yNmMci$!ZHW4)TZb=w-^oh;>?eM`S zSjgw~%93<=qEPgay$c>y%%VX<$5Lhavj5jbM4?ryu(WCO86L(0REvr%Ri0P}Sz8|7 zAl3J%M1Gi50#Hq;noq!CH|epOaV$&o8uo>28%;UFRe`ilOnwbCxHK7=veX>97}<*5 zx-+C*Sa{uVcpXV>0RfaqTt+w$J&tGdy-7)lH5+I~{={FC3vMu$rn|o3C&#T@S3EwM zpl4V*2arFXaxAL%`1WVfiqGwbDo@vYdy21Di*6=9X1I*dXcm`)i9M z#cXlQhxtOOh1C7DQUUc8g;|TM{53De!>SlbfHfQmV5zYOQEV{^=4QwF*>A?t!-nT8 zgM+b3rJyeEoJ@8s5+F6Ld-W?|DeCKX%9gZn6z$iCdGVZzb3&$+8jB+c@8@9&XD>QK z_%6uTs05DnJAhlgtVF<4zyCzS?bWBu5s#%&3q1@k2*3{1hKw2UA|_6tli{h?L7mty z0?SJ+<4dt@aNKaVuTYbD4ta(6A4P`?HNYNqO^nkFZH^#~vjh%F4y66DpF7bIt+=TG z&ObFWkOE|a)eICn5aZB2dLfC<_Xm(ZRY*I<-k;24C$ehF3$9`*93A#CU2=p$QMWD^ z%s%(Q06_1p~blBE@E<|5pBS7xZgtt%$oFok?_Dj5BNzeG7}<{#h#(PxxNFY#m><&@irtO~X|^H5SO^XpGaS z{Pw%sZn)FgsXy&5vD6=Yq2VkeD>L=_{Br>5?c|pLP23LvB32YrX8~4F1itxmM6l#+ zeS+Yh(qpMzH|%y0g0TP_$m^BXP~djI3O~7Xc#2yiL8adqB{ad(uyXWbg|F3wUZqE; z0oHAzjjuPePjBnek(g)!8hD>#{<5aI1xXjsW7E&w26S#_12>{wfX%pSd(?u*Uq|Jw zu^PzL)QJJU#_meHttkE7KKW5p&c!n-;XxlS&s`uUepiHK#=GIZ`c=49EG3~xY+rc7 zF<#g;Q`nqgtXLsxn1Pwt841fXaHTf`ms4FVD_Yw47D?NJ`amev`jMRkV*cHqGGWO@ zZg-aVJ7Gc}2T&4{jC{dzSn|)>1WKgU?n*o-)n?x(s*6v?COFe`J$6ZI2`5R)2=oRr zewYCZGotHRF_;-)#@G2C9|j1dW8{R8yly1I=s>6gm~1iVnh6ITKxAK0HZm1_uYq0F z2-FdvTx78(tZqwfNioI98ApR{6Gl)K%W>_J2+EopOh}Xm44j}Jzg?+E6vV5Pcdxy> z-}y(N%6E&fD8RN(>JC0-(+`j!(isiiM3aBy6G9>6Hj=d2_J$(WBRBzK0F7rv5uJMd z4FTPHoIJ9;`fxkBnP5p=_fnm${4MwmAE-dm75bpm5r_Ly89-#J*ac1_*+9%3gAb>_ zRZKU_8=#)p)}``Y1|8Tx*_0AhOZWHS@|qDUJ4y9=ayLBavY?B*{bfDHXSy7O^BZdW z12zuvb4hHKDt3`pPjv%Won;%*uP}-(nI|HB+ZWi*Ufa4Vx@+M@PSFiq#LDJKZB8k$ zQ@7X$6EF^-d@oH?|+dr;j3%gXIN`gd_R&+e42 z!M+A?{+=Wr`yR8jM->;Jfz*+LRTK}aEah}O-vRwzzLA0bAB3&0T-xmgD@v8O3`cLH zQuMWCqVi)NuA@{bcRF?$P_ui!o=E(9+`SlL?N|Z`<5wp2gHg&8XkVnJDJA!1p*K6- z!Iob7lAr4k`g;f$cY`hSm{29jZs-yS%7z;O*g2so;Zkzj^1Qdy85J1cnV+naGI3}3 zT0-xYLd3*69ruhZ7x}H`t_!}e&u|UV1zI?euSs}1YmtI`4PFAkloU|uc7`=3D4TmW zh}o;>2vcwWB>`sdYczp779!wcEfalebcv~cr?=Lp`jdmbRdpwJPV?4c3r*Wf)^$RG zc1#-K=zxYSgZgKy)Oo!BtL=DUwd0YggMZjVVBpL~ zq$5Ji_t`=Nth_zPKL4Iqh^gne(W6Y!#4^QXy3rUFB_oSMr7#sG%hQs4SdBJuQUCx%_Nk8&>UpMof26zZ+AIIb%n0#!wA_1x};-j3Nsk zteQYwj>pVHi&KUeYzjDp+zv~zMV-gYs8^b53PT@G8!FkCv6c}y2g&0J)OXT_Dwkj2 z(!~bhd)y{y$FVA4J4cZYKHC;m%{RHz-hRa#Dk2y!4b-aqIg_<$4b4H5*7$Q!>e%YXT-1)&<6_Bx|hEoima1dV}1bK_!eJ; z$2eEs(?bzufZbDE;hg*PGhTU-r-s3T8dc6yMI%G@G!I;?_%k?Zftq!Roz5^MCqt8E zTQ9R%mj1KanEOhpOCo0`-hbhK28o(}`gFjilAM?5MF2E%a11=p3!yn);+ zfh%AYgG+R-<8E$)sqVtus~^+k0Q z%B2B=lgrRhf<69{9rKqwqN^JHdUm9jF%1R6lv6}18>JbLLKA%=u_fGR6Xw;6f@{7( z!M(Ir)^$?Z5Nf(x+=DA4KJ!%zuvUIr*Qh`Kn!T@>L789EH}AV0o1C>tf9A>msr!Np>ZvFs{?iT4s5yOlPg( z&{aQ%K%5o3IwndoC(i3gRcnh6yFscHor_MC*lap8!^)LWwA$6m5;@sZlyC z(TA}`v*bp6v5$uV@9c&DBSj?v$ch;0gA|r?6n;XL<=dung{>{i-t@OI9*%?bV*UX!KWZu1 zZd6<5Sm_Arn*0mI?1N1iH`t9y{Aj)5k`jMTepd1WVt#B$<%+{^&%Y;7N`i9EC}^_9Ef3i$qm|*bU^YXuV9Wa8h3KH!(`#g{;=%yWMDK% z;B0n{R>?&Ty}h_{NAko*Gl5sybX#654^d|z+!ckSlcp~P$kZ8SCLF)-*4*9e;(!L+ z9v;j-CNf~I5CDH)9EvWN?G&17i*}Vf$uBr83n~L7|2XYLT|UmwgDe z!`AZH!Z(*9gj0>9V|+*lChKRYuhYH%I#7qk>E_c>v%kqtx5QINAJcAjklo?tjIzrD zS2*GrWU86bsTThil=jmD&YQLXnxO*Xsrr!<$ty7{s|UJZo7Jwyff`^>bv-d%G+m=y zY$}JB@aX-N8&I4T3@WHhoA7MeZT~!M>+)*tBv|+tIrF}l z@5`LV^&HxB>NB+cac(q{6N9+|GHuEqs1Ghow1M#Y#cGZQpc<*|a1`N-kcvNyVOe7! z?sVoL-^d zPl^Tx>Jugz4*%=NHR0#~ItsxM-PzD6-&Vu2)NL_d9wWu|i#c8x_9`<_I>)<4wrA37 zjdAKzQ|r}c{61IJi6@>5@@;6xPs(!@MY_kyAXx;^RYqZN-27UQqwn^Yzt)hC4BwTP z2|Ya-2XLVh9j7)MtrJ`xq9ID&JS2T2=6Qm`;G$w1FcckL(vbnppuP}QVyT!A_MZ0Dl95WiIp8+jD;DkvMy@H8z*#EHdb*%3No2e!WOsB8zf}{)AWRpP! z(Z$7io9z-H0cfV77uPqlEJ(z7VPN(^q~XSRK1ABT2S`Q-pPB_mi$H*%mEx|5fZE*{ z`-yu3P8JF?l6^Aa=BmF4n2FkRJzjaWNQoJQ@84@a_P=)N0Js!yJ_c^sKc>$xp{pH- zQSzIGu(miP)J!PDbZHM1BbBYki~@5b`ctS| zZAe{r2wzW!p>+TBq#`w5CJF3<3_RVSTW!iI>>hAr$dZti?3^iqBXwA1MBZFMy(tqR zx;Rkqm@HIs#K@5+ZH*|!jzaAFyDYP%-)ZTwnPt5Rgcwq&Dpxhhw;u>CMn__u3Fux@ z!f<@-HDWbkpKjgx)O3u^!jLbQRG)DkQw#BLvp~iso3y0u5ZcFa+BUpG)G&I~)CvsQ zpUSKv-?SHh{@r~cncTT2c`QL_?C7{k!1+ca^ekn1_60+lexm0Ap~2Y|brBHqBdx-M zn?xgUEv8?>=EtJFt+`8${5VJQkA^LFWXEZ}k|(7A&zbLKeL1RTtEnSQOqjY|zv3%{ zM*`7i-+(mDI!@NrvMdj7NZZk&^Jk&s&I|>Y7IXTV!Ddu9aPyrvS4H}8n7}VPKbHOc zPsd~&XUX?zzWn3f_9-|RLaTl9eFxPBb?SN>XW4i4twf((3VFVi>Avks)$hc9(cOL2 zOM3(n@_-(U)KNSaBP=Pa0XZnX87^7pk-e0>hr0r5k zDOJMDMEW*~7~WG13twSF-KW8MMp5#QJx+>0)LGN4nCqUK^fBT1fCQq%Bm z>e&F4k$w!a+(luwBvOGrhd<@~4V9^QS1Wg>SchbyC@Rw#dlero2*peY zyUAHXWKs+o#u=lwr`Kx6WnKH(Mu>;xZqZeAwLt;%13AF>pc4ao5a0`?B!zsB*p7Q_ zHENqJw9AT-RWz*>Z&j(F$5Q*1|2Q9}@^-qwwBPe+)zUoGalZmydTprdWpmAVUu!kJ z4r;2oh|&kHhBE!396KATM1t&C;mJwZ9(U&yaPHWkEQ8lK$Od$sl~-r!7Y$dOEJfnN z#gW&Al6;ILe3GQ-DE99tKalLfkpm0O12yu^%~#fBli-pOY_V_?zOzMRf5vu+-JK|C zMmRTDwkv1>laEd23y#t0u;p}l07t}O3Bbq@DRyL57$6Z~3$bF)t(}vzJ-(|m$KHlv z-%hmIt+W8*$xMH3!tQyHRM<3C%;i#7pY|N1z%^`fAq97;50xbURuL}7d5ke0wwtUq z7_qC#LZ>L$wtgHKF5wW6D?`ngu$pXw=pl?Eq}+j^%fQdWAh}PpUgsgdaq(O)?6eR- z4R&$MeudN$y7c**$j3h5HFQzaKoxVfVqC>oGtrVQWju`u;{2^&qy|F6)Hv#vO!rB8 z3=D6k-q;bM1v{fkGm0A7A<2`l&Z6Z&KA#d@>OnAWp3#e&uHUtV5EX=lZY(5OQdK^j&&76i5zH>Nz`XlRkJq+rg3E4}T z#kgbPR-bZ`mtQE~j6@TjE1mUo54+jd_s;2rQ&(jdp&bANbp%OTq1)uf{gv;E7hXrI zl2xGBvYWRPK&v8Gs}>k6T?NfK7Q+_t8CnZF)=sNaq)|+eGn;YEh45$88?t~UVg#9l zl678ten;zpDFB^e4bF^=(I|4IM$c|BQBEiLBzC7_Q1{LtCsmcjgkCMYK z6;j_8{;8CPa$U#1ov-WahE@S1Nl-hyLseak=kiV1HJ-dMns@r{CCn?36gM}&K}a;6 zXo=c^Nb0WlF$)%12E~L=mo~OCqfOKVA{{_l26ZJ&t*xEqIo-C@p2p#<7=vf`gg+z0 z$jU=N2TLL?T#lF}E??{HCgfnp)x9%G0G2`j#|C#@fOS9snPkp9Ow^_~{YWoS7|O++ z^ZV#^KDKwn%rGGA{F2=9r2=;8F|fp64V&C zKo%ni*X1Vr zwWh?VtPFs|xVC+>Br`&qsl7aM6x-RbvQuTzNq$pS$L-jPy|SCQahwrv=1=#{wRu}B z$ApL)IBB6HfY?u7ZIqQ`fTV?mAsORlqUCfJOu-8w?EdM!oPm@xb1#;JZMV7&WShoMwifk<=|d zOGh-V*_%62DXH%J)@5ggVsKaq2holQ&Y5ohyww$CdHgC1i+W}pmONSm=m?{+;~FU5 zd6S)?Kkne%!4DCK_<^NktForJZNeFS{FNhhsT?e<6CnVq(IsI<{>;t1@NC2r)J3aGVJOyD!>H9HGrY z?+38KE(&Kiwp%*xQa`0C{V4N4Zs#JQY@pSQ7A%h)nAhIM3FEubP_#Qok<;XTv7U-0 zVH)L!w?CR1D*PMt5=actZ$*kZGc8_;5cxz^zqh!;Q^(kVi2s&8VoahfRz5JUv}Z<% z8X%frRS@D{xpE~h?xM&yFLJECoOznv<|1q0B}m@gBHw2PBxJMS@Mpiz8&Y&!r`l+$ zvqH@|?D4=%SzsL9I}`x;sb&m^c}|D)I+bf+uHeAx8U{?9ajp)T$BKY6i-ls0%t(JX zyDwuY^oZGo*YzB*&o)sMI_+|Hh1NYT+=faI>}XYxN3CDH%W}x`@6+KBbvOd%oR}n8 zp-9w!Q%jo7yXW5Y?KZuIn8^f$RYbHz`W0G|Lgo&*)m0r|jb+E=XISfe2oiL@0X`6L zGP2%GX0Jixz3&cO0{drGH>9u&_qfq9ayT{{hvKZwq51#|g)OaZ!cZo@wtGzWoqULGu3WUtd((2Y7+@`@Bs(Rj|e4HF&TZxE?7=NE~LJr3|U##n5H(#C2 z$mwjFFu{9WU8T=5#Pf;6%N<}$JD+_}YqWf|&cln2t z;H%041_iH$N+Gi^cnvJ?SYml`)xGe86vu=UI3EKl@!;M$1F)TBB=ieU?M}u98!o1g zP%iH38Nj{T=%&p?R9x90{y6$cL)IGG;}btxYt}e^ZfKOrJjYAfcBJ@Xy+HGep$4d^mwkh*)f7R?6wJq5lZgE0+<4C> z8=wA+E)R+C?=y;-is@v5Eh7yayXqZ2Fgkh_a2pes^JtP~nKuk?^R{X-dMAy>jyyGP zot`P&x^bh=eB;G)f@lb_G$FyFDkUM}N)U?3UVOTKYkb$=+fzxGZZ6$fc1f4u9i6pk zlP=9#frZ-f@xY==k|;}gFNx4U^^H-s)|TcyD9Dym0$>b+Azjr#X8*Ni`u;o{(#LmBFun4A-kTy z#+5<_F;vOixo*;Xxj6IjFO1nxvw=@I0iUTb9miZ%tE8I1i_JYLy~1v<*Yz&s@q|50 zdtOHKl^a}gOA|N+M66_IuG;+-DB@u);OWDOMGxiP69Q!nzRs2tNf9KlZYNeP|Jlnf znwaIJ(p(Lf_Mr6I4WoO)wSIxUB2)3P@Eop-s*hadq>S^^2Td@Q(8#xgbcZrl$txo# zX`do7A@kR6LL^;CqLfuF!vw1^k7KN!0%A#BPco|0`jPwFFSiL6H2?2#;=feA{Tq+x z|7N9SVg0YtT$z}d)&6Ovt^eCE2W3ijBEJD6nI&j}Oko345|iIV7#<7y*VKCs4O{(h zX2SLl-uJ)9xBdsn{GVWx?H}2*|Gxp7|6P{r|Dv4#tALLAA1L%6Yp(d0i~heVonvDD zca-y=EAijvy0S7dar`UgOjg^p-4sXic~yHNZmu(Dt6eZ;@Xz5$x+3EWQt#B@hk(&6 zq?IvNv{I6Nc-3;Lq$hF7IO=CvzLIj$nc;MCcwefyofN6^eLp_9sC+Lyk(W;1&GtuJVNtM%E8ioP(ONPJfuwIq<{TGW~JX5>4eg)vEH8w`wAg0-9 z(Wx#gPScxCr@AIHlUt-0I}}8+KqBWuF-d~U@Mwy{f zK6jPR(wR1jTd;QWWrZ$k#}I$`w^|5?p<2GUqm|EJ%{FqtMv)ZNHkM#U4A9JW0v}RD z7e-{#qI8>YJXK8$j8q31$Toz{mkT$P=KZhA> z4A`T}qls4Rkc5E!KF%|Fe!R;lc-oy9H_GbFFIDCvH)5de6l~~OUA{K6#RktuJ4s$n zF~vCCGV#bDb(m^)pbdSAU1l*C>Vjj%Kwy3{(mt^tDW`#wjIINMnRTULCRFvi|HQ}r z`G!@e?*?@qb(-ypPbPLho+2_7PIcOLus8Lh21~*IqBO<5ge7O`2V|%UZegHG~j3+YaCF4O*brF*q zi}GBm3XCv^7_cb6-c4SsXLykU^5(pDKTRz9C#?md(Y%tuk5Lkst6t<+jR#t7KeV4) zYgk;%^a#n{TJuZpH{Qsi!`uk#YTZT9mMeVs;{8?;5re%y#iN;DYL92YP$fg0-qkl%WtF`6hV!+?QBM3iap*w$%jiP&tO{+#2nKGz zwat{&sG^cx^sfdnD1Ah(bKXk&gsY?^0*w2x0V6*!M0_^60VH5Jw%|lbwv>GCBF6wE zrGnsl8x;~%dU=AAsd$YVFMf-__k-0&a5ZB&?jTXs0t(L}MHaQat2ek94j~_ipVXgo zF_|w`vJRfs-HbX2)Vp0V!tmq#xB#Gy%#y7iBf%kg7GMOzxx!|8iO1Vl#^IG-8$d~r z3+#9xY`qIsbFrHFge*@DtW+wsPgztui7DVp8tm1kL3Js(7UONi(Xq6i(ZG9>O#TVNy*8P`Mm|1XG)}#n{r9wwIwW%7GoO}qtJjT z8mZMMx~H*=8CLF6Xl0^X`F@>UA!sU(U;=zd%~VtS9=1=fCXB1Pd1QkuY(riSU{-+@ zTPKI0qq2c35lg+rM7WuO1n(O%3JU@pdhGl#n1w-dCyjcxYziX1ZV+(EvI%oM0{P~_ zB_TVX3AkQWE}>eWyDi2qIp-$!pq%*(lYV}JyAt8`{!A*F<*_dkTko47#y^ObK zy1lITO1U1;f!oH1giv2wy&U@aezJ3a+oD|VvFg^wVdmyB;jpHLS{SynpI}#*Ex`85 zMx`8_+6t?ytPvvyO;QiMPHc5X?Zcy_&gSF$z*WPy!ft^(%nS)1R0gW7Rgi{Ov&!5TX;vy2A0aH;N-h|w6KHOdL+$p%X{ zU2{3X$X#n)S77~mX6;bzbp!b}Y@>#iU9YvY5^NKuD-}gt7(!FWL3q?UBs=ZOzd)lG z%_Ux+FYiRYt9moA);)MXG8AT8P!~XE|A{8)jxsto2@UUKSnmd99PZSd-FsybVboMi ztfl8fvg%4*31`(?u{tWQWYtM0JILX7A?v-M7T55{<_di@r*H-L*Fp3#$tph@qsuGxUU zxNYx#!nW(?o>%8Cx2#KxpBKFOuXV8|UT+h~ao*Zqq;d7+9AhYJWEGgj;*k=HA*Li6 zHc-=|dlvUuO^6lhJkCJ3~96;tjV?%T8s)n^GV~DS?f!*}}J<7OLqGF?P zI_)dJefcr|+>*u@-5;3aIb&>rZ2Bjds})zeqpcmS8J+jr(c&6#;WBDDG9y+wlP6u`08lmT~|939rxa2A(9Qm3#VbTnmF{rzkU z_OMKtwQ9Ac-<0qkr4OGdI8tlQdYpUW>5&^dsd>t!tth{Sa4L5lmFNP&q}eu%lw$?-wHDXx*66x1 z;c@(lo*4_B|FZbblABT5+4GpTQNHu!1t+R_fkO+T#?KfVIEEyInxjM0*&9O0Qe#tJ zuFueIR!q3as3p68?ftFod}Gs}91p1Vn*_8`s+w{zpjATQs3ua5A`-5wG*xL-0tpRM zjEX%0DClMERQz3HnBY|}{xiq1-EAN>O=$TG7i3dlmZylx3}u+NWrHlVMWTbukyE(; z6Wcsq%UWWCfNeYWHOdb4$AhGm=(hdBt2YRu=Dqc@{iL5A7`-Y{oZo-cxbFAmB=PTZ z^loVyG6+ain<+-{kQS>nib!a!MHg_UBt0R!Z(hAcU~Kl zf$RwA;&S%h7N23)Bc*2^?BrgB6V4ctPe1YMU>~8W!8Wf-XJS3g+n@I}aF4 zfxWtMS5&xiwhA(8fBH6Qd+vb5*%rJPWoe(|&dn2NXuaLuL0iwvBZ?$XwLJ#iJrI92 z^QGd{Q*y(PnX3|Kn%w2WkSg7@*~50ivVeF&~}r zXYGy>=ZJM3tq1Y?qBiyQjQAW$9VHr*tt}I>Wf~`vJ3;^`o#eSW;&2$Jt3VM2CN?*a zJj(@|K6n5@+FrFM0Ds+5Jz5F8W$F-SHH##YUQc1v>GvIW5S;#QEBb-WeZ!)&0>0rFVUjWYtyBa> zRuQagYQ!@gi0WX^f88Nn;$V4q10*Kaag}>L1WP2WA@A}qkss|>wL!S^lR9o5`5NUCmX1gFQM1KJ~9WdtzcQ|NXBg{ z>a)v2!=H_Z1}f~gurYS%e0KwqhbWNyc~-J5r~f0t6>*XhbM4_C`oY;P@tZ}(6CJMp zUg6GhEWztb${>q38aLP(Cq$vmeWq-yaOcwUS;*8Qhd}pfZi6tH<`qtkO?XJ5{x=22 z)t5OyUVY{?2NO1a#zlZ(-{RF~A8=pmbv8(K4%KEen)^1S5dcb6{RpQ61?ul(p_^`) z4&uyVqk*-c0bJDUb60vZQ7ME_g%#_8a+K3-HZq;6Y@!9cnqQamf`l5M%er03BT@Uz z_rlP(Y+YnS7@1*Q5S=5FQ~3ne)UJPnisjb)5co6Y;{CI@7%Qu*QM}hiChJ&7U#j$I z!(*Q%MMJFswvyA_8sjerx{g)_v<))00@J9aIm z3Q0@JZq1-BZ2nCM!-0xbmJ`_UkY$W1mY0jWW-n94VyIzYPUwU%7M4KPaFd!dQB0|G z;b4SDzUP)-8vXR7gp8xo5`e9zYbfS}8HCk*Y-OGtelQ9=2MANTEvZFap|2WMght&d zW1Nl9vL5>fWLPFr&Xv5qwIky()vG4}veJ=t?7g2-h1+b5%8z9RE)7|km(--4 z|N6uRb-Y{v7uSyVbnaVxPahM2gRc&9FOVO_CowxNQu*&V4k5IMt) z+eJ%5wcD5ITsT=w%GLFYi25gJNRG5*N@FO~LyFv!7O>eo)Zxu#OlhfLNzZMr zh`BZF8-qYrOmAG$SAhlvV+ILPKelN}jBSX>TM~FS#Cu;12l#*oKZEN?0DR^fxz!&YzgOvLt+%Ua&>*-DCbtoG)Vv#fo2SA7D5H z!IZN{x_7|InHZ9^qq21<2!cteG6GDQvqH?8;4Cyh$Qs59Y>rc4CHzKaRS-MkR+{{# zllCwpoUNFBQ%Tz#2Y+Y@#vifRdc80y?`m<~FC`gpG<|tWsTzv}X(tSC5*LhfW*`XbLn|0Zv)Es=J-0~{RJXHja)M`}e zY!7pxS=A2cZ4#kw#`r?dR$y^&UO-Nnp)S-&2{eq!bkWJl9W$(M z*tSIz)C*;j`yrzlabr#!rI>+Fi*>sa%GyQ>(-q&<*UiTeG;DaQybE38_n?~3V!s!o zKr7MOJc1Lg{epQgYZntz%}F0y+MG`4ZDhi4U@{ zU_rpV*b1dfNW}J##Q3X#{IBmUgxJt?q4@%dgc-G`&+&bQr~??L5IOZpJHUT33O{Mg z_3TLi&_po1xysvR^mit)N&i|he8`7fTYh1FV!YK^l0qLF-C?Ts6kfCoGswbV_LD$M z5hXU643XTtO=dyXN?bmZk}n`uwO9@8$Ii2nGPL21D=PP3&L#tNv5&`ak&Tf95Z;{UZ+Y{}2`aA5o5qCQf!P zjz%Umqe@h=GeUsRa)??$g^ z=WO6?Lcl<;3;;cXGeT9CZrj4)%98sA^J&=U)dIn!QH*1|ZDMBq*zrrDWC z$v3wjN;9vAK3_-6%BKB>Nme{mA--{4zJ2(5JkxJ-*_t`NHLowOb(+7FaN;cX<1Sp= zYg{KyX?eXlc=(XT(DU|BoxPo}^c)&CRT@Qj(31`w8UZ7`%@W2q=G(`T)JLe0EfOYZ zR*B@KGMwpps;fM@dAu+0*HuVNUy1%U1hw$~!iys=7yW~WY&Zk*D?5MhdhV$6kfmnV3lPZ%#;>M}#}h7mKlLx^>*gAwy! zfES~Jw0w&RM5XJi{M*@OySMylpX`5@u+T3f)YoUeQ`Jd@GYr@UskdSP#a=WN9$Y|Bvd((PClgg; zMpVINo8N{9bz#xC$?9gKZB!8{JVYtN8q1tfs%c!Yl#~Cx!qRyf_-jG(1fiQHO++%Y z97ToEL7`oGSX?6IrTLHZ##EmC@T^&H9`tsgupYKL1xIEn)9n4(C&fe*yginF(HobP zm4UFr92-<-Hn|csAbMLOv0F_4H&U#kFHga1ZC!=`n1#q^(45HXKMc$i zP>v~13Pq&D;21X{R?Q#>2N%zvpBf3KpwB{5%D6LMrVgJ8urE-G0{9)%pKm6$1n8ML zz0Zt;cXT@DKE6y*udO}hQ05)xjpk8Dw<62ysON`m+5qIVd|7uhwSJqFm zY2Y4Q>M@X%+B?f*tR@5PQcYc7A>qwxfbJrJ*42n!D+V%I68 z;`+1C%bL2OW{AYXKDQ5{*aMfO59SB2rQQG@z>_OzUw#CQT-%IVwYbHY_1fb@?I)^VvAF*f9maPhpY|?pCuC{HqI14Hdoab1K-rfe}0*RNyvf` z>Wo0<4Z=4AYC||WBZ!CL$mY733!HLu_JiZLczmlaplZd_ip9tazHG_*a1v+B#ZKha zefLhQjBP4f`@#gBscwLBuJWo(77_Q(h9A@>zEPS721IP5ph1mJl9KPe`4$c!b))(B zmEJXMH==j%A>^L@gz~0Nyow{s(6L9I{i^uO-qBioOw)R6`bxs_fxr<17sl+=*d~ly zMhH(?VbP-sE!hFZ8))_%B}18-Px$L;Xgk#Brs5E^g(&gPe4LtDdG3r$jiAOYdtBRV zXbbtTD_OP-$*huq*4`(_9riixqjYEGQe*o>cgvMpO>*i6w7ElYYj%Yf^+a(!Tp{ls zd!jUzaGCH-6(#J$^}0Dw4RM!$fxQ3KJ{cS>oc=6>w}^a7IoH)gti90sSU38!qg~L}a_sXFy_F6_qgtoAhi=WAk!o70BE3fC>uGT)k&U}tp7z69d=+|;x8 zPmi{0Mlx)IWBz3?AV{N%uD^RFMxvY((D zg}DYdD7O5ly|}uw8LAGPbQSJkiat>&8|*RQ@IPB~j{4m9W(p6d-_Z|9iRIPp7b^F54x!o<=W6doajslG64TsS6CRASZRqfjd2f$0cil1%B<#R9Y3G<) z%ozz)fS+WH@6m+GPI}n)c zkNt7#%@J`i~GVI1^-9 z;@pH63zdJAG(dg!5a26XALg>=UGh6B@Fhr|HHAxpsz0fjAYnOvD7E~$!plOa4?Z7N zT5Eoy{69MZefk^I+JqPc@c(&-Q*rQDWT4t^n~xwX*^t&?5t!~Y!r)8Pq=A!VUZjme~r!RRGYq6OKXhc(#kxZ z^%+_e=3o2E>*u9^=U&)BZ}me5efSNf*|}KnL_rx&A3; zo9*#fYg=t&2Angez~F5&U|OD#WEFA5?-A(W@G^+E_NuW)o6r7yfgqhT*`=L6tiVCm z;p3E1;uKt~`5j$Aw3_T!9bc=#vUGs+mKZT}B_bVD;f{TR0sT=JqO~X;I+v9Wn*EH? zHeVrV7%6wnSrNa1&9);ejtI)>@#TYh=2KeEQHM`WsA_X)_TuNo6ql{jQbemuP_Xt3 zK%tJR!RVbKN8E#>76`^^57?h59nfB-;-;Gw%&y;f=G9B4Wjxe(n~p(4`l>Akkp{b+ z$+3H&ojj2)H7g0q^)9UM#s*P%g^iO9nxYeB3lTB=a@?Hcjrrp^ucs6^-rES^)peG$ z@JO62=_0QUl*1#6*5QFTgG^?A(0kn_BG^pBkjdi>N&HTuZOq8Vk?+YS1?d(-?(b*= zt!@KY(CE7klt9esuNtg>9P@$VrJ?^KJ_$D1)DFKxRzfFWS4Pdw^q|2VtS5sRmhK4q z=?KEaP^7fB{$eJZw)?GfAQTO~+#ve;=1q(fTN_w7tq7_bTOnyanGqU^$Ij0k+7Rhx z_#m_Eec5o5zJur}wQh^c6fyq+tCKfzI%xI`<(vx^ z@Y9o(u4F1BtIH^}GV2!EaF*ual6w{^!$+;=%G|1s^6ay0x$3i~@XC0nx<{dH5d7Vn z#|^(zO#z-bXT6(lEwxZf5CV%0JQ-3rDjmmP9Qer!@qiL>lOKj&cwF_kODVUrsc$G*$a9ajk8RlANy#CPZ*%q^KskH7RN z2K6_0kTg9FFH00)9{f2cEuZII;v?BZB~uLseb1B^Gwn9O(|*K<{6q4c-%KIVIV&f? zRS5@t`7Ec>vA2B1RN=SDJn7I4y5-)})z*n{=D-ehlVw|{07Ak5Q78n8gZegmj|RVF zfqzD7ij-D&gHHd4zLhTJZEvGS-W7I4%*RMXAa62g^hRgP!Ey!N+fO~z0w!Zs$teWN zLfLWHWR)3^uWnN|rfTiUA3CY#*F_rpiWd%+EwL@~Nj;kHEbd3&T8NtOir zrN{(8cW)ODcazUXk{Gem-bSYw{>N1j)@f)XS1Jbe+T95^bX06NAqNQQZV|mktMHuWHazu^e zoQsU{#-f3INH0iiafn&ml3ds0fnZAqL#M}C2T4ZV*k{A15c<|^hgzeN{dBB&GtV!k z8HzpT0l3}yUnq-^v-xKW$sI#Hu$Ruoe2#s7WgdC$)1N~;IT&ciI(iHAM zwRL7v*sXO3&v1VVOZ;Grz`=VC6t-g_9j(tQ(;)8$5HsZ8{E2TwGZi!UHrkfm>sfCL zM`}E_fqRpeS7zv&H;c2#=>{mL;o~MOP-?t9+_6R*6<$BL-P4KvgQGZN5-U)wJ_OcY z%;DS1h#h?oc%|H-fa%Mdh8|J}EvM+@PvP3=9bk29IB(*lA?&Uc)Ai9>&asz<18P`V zD?M)lP}fvTeG?p@FcPAzA5*~XU(@2pK;N%yrU(ko`OZq$#u&fQs6k_gBJ`|_*szb? z(F@GCa_q*?=ss<2o2hhB-uNrh3R8tlUgR<1SfdQRiZ^SU4!oQ8T*wJJCGcJ5xn<>w z=u)1fvPE(j3ZGQc2s=B7pfj#cT#39!e0gADf(H@B6Q_&P%bnQ_Jc*FnvA2oq=tD10 zRaqeHt2gv#4|7EondK?_;Z0R530rG9ND)_Nx!2@`Wk7OCZYPBuMja`H$62B%LOnb7 z^3J(4rzL`f7enr@Iq^Be%niYE`rx`0I;vZ>50f7J2tg4@b_07P+oOv$a7nM0Ej=M%K3l4pB1zl7QqYgm||) zbyR{23{WOjOHAcNJ;E1xn zO{wX1p)1{`JzB!fWuzbSwz4*llCdXg-EnO(3=jBE17j{x6nHz3P;h;jj^#Z zea^oxd<;9L$E%c{WAw)^TD(DGdPYhK!8mXYJtCmG>ViOSc(1Ycmk(&ZY{_vBAgjCw z00+fA;D zSk_uY(6UOlloMeGot0rfAxO#{e;BxTE>u__DJeb8Y#iPk1+x|~ZTy>o*#utP0gT5f z3>Te1!xOUh!OTqXB_<+olvUkhAE=!)$T(7D3ag1X!*-g~&DJwme;gx=RvnD)f4&sF>o^9+Da&7@+N|q7ZvO|DWlLdv{ zfF-EqpG3jOknaM-q8`JULY8Jielvj%c*!?>KrjFx4Y-bfM<4&C?&ZJ7qAV=`wK}#@ z`5)@oMwWjT&7_9PZvY5tsS8>_Q`o?jO!GU_@c)(RhJe7x{=13%OBKui#Gvf|G|2zu zleW(P#Do9guowD2QA6|p9fSTqT>t-~d;L%J`Crmb)_+So|0Vw5-<*;Epq-2itW5tm z+}ZT6xD$u@KqhUtA25+++qqSzsXl-g1Uw*)n0&p|b)~*@x_38&kW_)mg3IEizo8hB zv(@ed8!xYV<@NF0dfNBr>+Q+;JxPaBr-hg%@DKvM$pD&cS7Hgg)#!Pw(tJI$Q$ZyEwqsk{Jfi`B@o1uroj)u+#ux>FY` z9P^mq70WWQ&C`s90;G$*bD`kMI*kG!NFwtX`zl8OvT5|ol(>Ad=)ZZ_&#aiTO_12J z+qsrJ{gMiSIrl_>oKO5SKe1%ofz%r)A?|?x{C#$k_=MHshmN=m6JyV2!-fB@N8r0> z;C=FQjnm^Uyy5CvQYVG<8W_f|vfczN@Sx0`=+Y}9Hg+!)WwlIJ2_g;W2-l#`+DIy# zj+#h})9+;&XCNgwD7ljNNcqFhLSPAD2!h7nCK9%{Fz7V)>jIV4z3wBxbY;y-b&x6a zm;E-yyG`9LL%9=+VMNx3c@5);TZ>6EysY|a^vANbWZ(xHsm`Dz1>f^rdgN&ebLc36 zzek*g_l_EmQm~4b+G6aZm_DgNL_vi%qGJEwW%45DP@LBFLdrQ#4Dh zvl3iFU||W}4Ys#j-g+`IN$_B>2w>I%m~%y0F#LuoILz28HMCFIA9XS1&tC`ounLdb zoWRD#_!qu>Fev(=e+~!CK8I{Sr?oa}mven{TVEb)t`uVO6KBH1*IF|j-?ArY46?{G z8RIK$B_h-EXsu{lvXuLh5T#`{-r7jVXv}NQGsR<4RP{?zK?Vwanx{IG-UAY2g6Q`*slZ}R86g1pb@5s`Tq2)92K;ufkxWTD2isSY|G{wCa| zVScVQ2lzp5t$!n*4u0Il*xq&j2z-L}1A0l~=J>43k?V=J(+m5_!Y zsL_EzB7?5+Tzj~~D@-ESnjbR<_yA|~@BZ4GhqWEkd3E{)%5O|y$x^+&FeDnZu`*>i zjk$neaN;^I+ni;pYKs9JrYBjBjSMHaOW}66=`4xXI@kn5KH~J}1t#d@=Ag^yw+<`Q z(*xBxt6;;BV2Mx!?ncRhNG(EWSY#oOON#}xzlHq+O3H!9xTAy;08V4Rg4Xw+$rdKA zontNH5)iWA-emsBmMc?;#Gir{1E(Fj5Fw-uuG1{O+VD1)^C?CNxD1#1}JY{{Z( zB@EjE0!~_$9O#1hed~~&03-&|lfG-;qS!0cZXAl^UF{h%IxhO(;5nBOGjt`V3h4Kb z5%^D)kRN`rkw`-3kx*PsQ+w*;$tM~L^(sD3DYUV&AqFn379B?Mu9*^&G_klM25NJUMuYw zaV$ITpW~86QefXroA)?DZ%IV#gYrbZ3PJr7_=R##3gviLK=TZ^H_55mE%g)yrEuXQ zL#`=<*6Tu$89fA5khrr)!^HgqM8@Z$zVI;gtQE_(0`)ZT0Yc}40zyt#Pnd(tmqPh2 zBa;j8{h&F_3`TM+3ftXasG15z&kFas1FahJ?FzxIMHh^c^C)cfMxjQCiA8kQpV>w( zK=%TJrs&(1yk-KLRLd+|a>|e7X8rB72u_s0<}4swRz$S#8{B<{s~!v30xbCYz@ z9ps-S$-OKdq8YdP@JNlP?yK|aX3a-Pv7UMqVAe(^Xx7~YEqJa~ zBMVU8Q;TCcx$75z6p%Hpu(DD*dQKvmley43>5f@8$&RD?DUtQ88~4#Rjol54NanrV z?`Iwl>lW#ni)9ntH`w}wgRT5ZLSXI{#;?Bp%2uqLI3dP2g-D30bQu3IP2 zhR*mdW*IzO1xIF{5b2F@Vrnbg&tHOXdJF%iYPtX>ow!))3x~em&U{*(A?iX|%igKl1og4EWbV#k-|f zjkq;q>Qky?O|W%- zVN!cyI%!`kYk6{T7!9v%v5c*m(ad@d)7A5JkA~qX>s9h_P*P+A8pZlqNK_8hAmD*# zGqAzCTL}RNAd8U)u06a|3mu$EBWUg5=~Tb0DI@6Yz*x9?Skr~mx+yeA6#b3Iz64s0 z5RuyvCL^=cj+@A_WArfcmcB_7Y8Uy6mv91*fb+nU)NbY01DHmitMebCUyQVs-TV2V z?)m~?e;Wq9XYb)Hv)e~-pj9kD);M^^(&T!2LGj|Q9-=iD315Y0;T*fv*!^%nEEnvu zW$lHm^#a`LzYL>_e*ExNQ$ag$sq)|}aQ%o5DL7S6vtgBXJ$A&+e-L5?uh3QxzH-wH zF5e3Ilquhe8nmV2Q$B~Iy8Q#I@J>%z4hAKt{7nsolY$@5h*{`Z_VM?i$+ZAw-|`n+ zWju$jk_VhQloChkXhn}!Ht}OsoU(_p2iGGqhwKUB$<6m!%Q!_3YY%3T^M*^iCUI3fHdT!^m6u7Cm^g$nme!cCn}ico9r8M~L8~|t+WbcBklWgE_nQbo_{J9Dy(Y3Ei&(g~ zXupPbi&u>n!wt_6pi<(tbmF>USG&t~1og@WE#v#(moN%%qcax(Ycdi1k__X7U-Jbp zr=xgrtYc-khXHSRrLE%I_r;ykJ3YeNZL+z*ps06P(=1dJ#qNm!EPf|O5m?}t(vOw< z0DxkCqPx<6c7~mOJa?Wp;1{|lMj~iv4dZIvQ1%8qG+>>aGAHZ8+E{?v8>@%i?xT}j z0JDafYrbq62bUHMHB!iE?f&7X40cRV}P?Tmznr zxL#AiUN31@TKB^T_~HS6Vv*3Hh{RyQ;)l;|6_FOxLt9*-G19b`J91VUXxhs)?<@_r z80VoX{yX$dA^N;X3DhU&x$f@laQfJ zzq0s-P2=u82i&ORK43XS3wh00{bnb35}Vy8Hi+Q5jkHpc@1%l^-4IGTC60@UC}JDd zF)_DjxwP0i1??6y+%4?r^=doq(?L%Lb;FaAZn5F8RoZe1OeK-zn&*^hU`@ZyS+;fu z$ef1s>;L&vORj|g3@&hQUAu)j`m&On_nb%(d)}B7FcimrI_#8x;C1oQAhlpu^@gS;Tm(}H*KL$07VIM$FY9v3dj|7 zAmdEL0+?0cGw2@1sW7175yUg-565YFNx3E9IW#xZOil6 z&?>3$)(_mrm8YRB&^ec=)!D2T zbud$pT^d)7R0eoPt<=r|3ZOqO19Fz~0sj7JXR0?U#i_5=@SRFKjrSRTh+RjG!QwFO z90SZzJYs4}!dn*VWNVVPAv_-iVHGWGX_i=u&BbZ{ty0W(n{95cl>d)`ODVr)4@V`D zQa;PgwWMkKVeN%oSFgrgrD7djnBg;-v{6cu;nN2CP9_E^ zI}+@q8%eTkZ@&FQkA^u01v zep#}J!~VEI4os2A#+rx*Ta(bg!-|zj;A4#fV>45)hMF4RCSjbNU+=RhN1(I=G}!@s zW^u=;I~6R(l&?k&tue|bQ8mug)`US}p~6*Wog;%2br>MOS868J@2lyniSO;gl#*O% zYH;RO$N6x2YbK}YJkBjG7^ehXqc<*Aez~>?lG;0O@8)quD!}kO$v03k;9OcRoV%^$&8(kX?P*{qze$vYC=L_U_v<$X?&K#gRSV{7acz-?er8Xv_t2 z$8482$%CiM6MB?We?mU*H=wLF?@_(8&DCq$E$-gY+1z7TP z)_L>Z_F>eq%gxg`3NPKe!;?4S8VSB1IP|hCxi|XXwt1d{r^Ge~CuiL%z3=qAiPf%` zREOVjTwf?XU(m2kCg@J8{l2@vOx6LuZm8JNpM)=OynKv4h0RSAJXf+TR8W(@NWS$yNvyD?0iBExMY<} zCwyAR%+Hn|)iR=>Rs71a4q{9#W>}t2{rUs_fVFhuSymgS1MNvDN4NHxyx#&6J41g` zp=EvhBNUFxOG|qc1d`;kegq20xvAxkU)7&Z{T2hV@X;}@8VBpEMQRWC8EoQeo2bEH zj{jb111mDShO3F1b0c{cv-EHG;Kz5@T;G2}B%e|o-DgZ*Ik6)zDhL&8@Pm6MOJ3rI zFE_e1_@D*9ZDUA&z+X+R-0d9RynBv>o4{5l<%Y1ScH+$5jIJ`&S6GHRXe11Pnnu=2 zJeMGvGUU>a)!G!hx48TE{iG`J7%sJob6X*0iw2`HlI9$vMyrR5Mu0xrT*@pm!`;xI zm^6eAQ;s`kmT!GdGfisI8nZ&ip~QHw+4S@v^m_Nea_y<@8hn?-OzBZ_)+)qsUeqx! zPPLcOWV#zU1d9t%Wz+V=26JItSIPpL2JzMDvxVG2#zGBM%sd;>aqsiN#W+rxc{mql zJmj<-Ptzrac`;0RL6fLUt_i+yyfCQA(tRvI8tMIwT{WOgTP^xCyT*l>(-;~2O7juY zxoY?lzfIO?%`jM;`ci%^F{w`4g3Ct?o)~+>jvih#W3(VI@d-Hk*-t~)%MXqi9hu|3h|MRFVM!@ zTO+;Ac)f37X^;dp`Qh!aQZVMYJ|Va(l1BaM73563L|RqytaoW;d~~*urGdA965Xre zlOMQX+OM%2>SpvM@8T?Lp;VAn7si6f_kyIXmb^aPpB{upgmyeqoJeZvCElID z=di8|JxmC#7eT`e)}i`=|M`8|z4GDnVYYp<2hdvA?7wsTgqQ$L+=-fCElcF2DNnzx zjoDG1Ctk}cHEycQ>!hm!+0kvUz)BmhQ^BLbbrvO#&6B(m#`yM#nJgdA+_A$<)+Rif z)a`D}3{I2NwK&p%*AR6A8mxg+!69&Kkia+JQYL&uq~>@q+fmg5Zhwl_4}V-M-afBy zDY#cBPET`O`jpg@`o{Q(iv=VyE{$X6(0lS)Qf@6nGsC>JE=I1}BmTO3Fy8CkuFud; zMI>&>3{6s2!3m|jUB$sOi#)?w%`#Dm?7H2W3+TAQ#({F^Q$D%tK+&q&hcPO-2#TSP zGrZqU)R8vO0PRhhkc?tVP#2z*h|^@8ppz+#Ir(F;G;{mI>8uI|HzgDel-2?Vl4HD) zB|_7btJab%I>>Z++F%oll%}a3Z;Gxu$aH&Y67Q^Qq)7#z7WdB7=lp(AFQyN)dX{_? zO9#Dr_B^?{=-!Y`jHQr27GjuonayP6)}_D*!CmRDxrnt7+zX1JTtovO@i+?siIezaRr`kBJoGS7DBHfZB8e*>^7 zVcO4d=b29rp3yFGwt$G$OW-UI4({Z|p;rXfE;^tJ)t3_&@+UGa;jTMZI&jXm3qKiU zxPz^NGgo)rqa>ATyuT6ig{p3Q39-W`1K5*QB5*t_K_xlcEaHkNoca>Q7|EnP)#OA2%9j%#_jpF!h!1zugd07BFQGeboaHh<|WNB4B(km*VH*fAq)tO+A#vs}AtzFaO- zGeNVAYA_{5OdpF*(2`}wFNQ?5Vv#_Zh-De3B&V#WD4!GxmNl5>^fVO=vuIv{8OXU- z&vLz7(V`qPPg9@#q}tl*eh+O@I5rxHQkEVdX7v_}zBeifThQOF+shndo?wY`2_11yE+Q9pwfmvc zX1QR7wblHoeF1@^i|`mHiZV`lPMhdWz7*?o*$<`yBRJU|0%#p#)3m@15W5MnX=_xS<4aczba zaq?DNEEA46wwlP%MOZDCU6r>*6oIxPJlLYRst z^NTJ9>@lNTGfj)+;5jPn`Vsr1l+7D1NpfEZ;8D!%i?gk_2 z?%5y@6DF)Jklb6}JWZW_z`%BG?~u@p77UzH%JA-<4F>Eqr1gM7Lwj}HT?JnQ2Cgth zIyyC!_Js!Q)H7*#F9+rk2P}KrT1EzIc3_VHlo=z(+#W30nFtpHru82KG;lyL#R_J& znK7u3PZ-$4S;54(vaQGPF#!MzMke*b28g`koeaUK76pWm7clc$eDFx{K*;T%d(0Uz zpnW9Z87Mthj2;-R1mA~rVl>B{DJTjY7RJiPtIT!+>|6{HVs%bPbyZ85=Co%@kbBy! zsa-_fZvk7ZP}8QTm05{a$B`(DvSp1OtFR(t@un==h)q7Qp1FJ+teE7)gtMpz7qxvM zU~ipAWof(fFOL>uW$0>w7)@Kf=(?V9>)Wwy4b*b~RRUUcHKIDgCG0)6l-ujFcJ(- z&?~ym!nH>O?l?YFBNZQggjdpjbfTAr9Xhmk@?qaC+VLu|5GDTXhTZEk1Yg@DG8dIS z`na;YEPeHE{!|X?5ZO;~MB}XU6BR+R)tOVQ$F`%S*f$*Bv=>{wY$c@oKqVu0Fot(W zNOetNw#?F}&HD6%90vHW8>pCXJZ9Atc0?%A*C*_d(3`7Z62D;8+yE_TH}xH8CA{z1 zGH*v+c}8<`9ccpY}x(KD6eg&rQsEiz;wMScvu*MI89yD=X%< zFWqdxdzr=yv%~tGWv1NQN1xy%8`s2gr~9j;aaLDiC^<`d7M7Yz*IGwfS^&P9Hn{&XwJLk0CI|^kT*$Hl{%TpR zbCs{qd=nSVrWxmx|VXqkdoV=&(35ul_z!bS>EcAh?+tL8t0eERHhIeZ=xY8_W z9gS^x!KW)>C|F|9eDvMvf(zxdFCkUWF$Mqx18;zfRlxeQf@UAJaSj3Wb%atj>0@(9 z2!Y^Gy|99=59~G8oF;|2V$S0X5P6@u%{}R=@WKk(4bD(PgJtQZU9(eX=f%wAh5+^c z=GM?Rv$jL<4dR%f;JvG?3S?>KBA5*PUZeDR;O&)>9Xgqf?Q+SJivbt;gw#@81g631&&NyP`IWQx{vpg94(G4B9V-CQ^i!j1zfN3>b$p}fYL#JXQv_So{ z6U3Yjn!l3K2QGnIRe>7<0)wt9Is~F+Rr1EKuf%Xeu|pR>a*^#*pfq4Qk!{$lY^nE_ zo$ML0%{Fgg*sO<9zm54j%tn5zMe#>nfz}HPf9`$=>ph6ozkcVYD@K0dLQn<=qQnlk z12=C9S1^B#szj=t7)Yr^s2y`j7$Rq5Rw@YDA~tD*-0G4+{`T!ZGkU<@qxB&}YAlt0 zclLAF;(oV*d!B74Cfzvi;wDGGEcw}NLkVg-Y3$pA#J0$;09Buy2#da1>GDApNC@4=(6Vy0+zaB$(&u>zb7@* zW399bl!Rm59jV6mfD|w?@e?yY!{B;8vJ%lr%Z50ZpuQ3UOSEb1yk$$Fo9?RV|aGcAPF*jYKF@ z5}A5Qa*rhoVMBRT`bL9eUP>losCvIRi9HC6 zt_C)NsU2ST7qB*`ZgoI2EW=;n$iCok2k=gg0dagZhS|Si<{miu-Ghg)emR-O=q_@9 zk$gQvvA#`N7PinmZbA(CJr(7^6`$J&eH_kpD@epc#%nW^n+?4IR~QJzr%fZ0yyK`- z#`iZa|1c~L$uKpr%_N{(B8ypNg8rmC^Zpi(y(}VQ`;B#TW^Bu_%1m*`iRjl}tFxc% z+zE76rp|>6`xqFZ_`ooRk&}W{*QtrAi1W80G(gl<=R9>WJUB*&V7Z%be_bGrlWw*) zPTR+F@RM^gTchK(V)Ccq){JUdE(1n$#UNp!o_LB^2c@f=$^I?~$lC%wRF<)%2I*o;aNi{Kk#VtwdlRZuDZg>lglk2}hG z2CEsLkkDH74;_+cHU5AJk#x(onAZi=QSwm;QOYLGof0y6)tcB~Uh_?(c+v9>Svy=4 z;RnD&zKuAMx47J=iwVCV#9DbAf9UWWU%w0F7BA#k$3A01+)Exqb!JV&0fTNf90Hr+ zho%ZV(g#iDDVFTMms~S%_~mB(*c|bD`ME)(3d$|yz)SP9g0rp<*tj#V;c5#&{`k~D zKVgS-uV6VblwAYQ_yA0rIhIUO2YjmVpY(EjqbHo)K9Mh~S*1=TTV6pI+qK|JnW|X> zFNFdbnYos1u_uvkxE1y0NKciuMwH5rWSmNCOQe;zp*W3XmcWbZvA8?UCYs7WYOLNw zicl-cnw7fpS}>t)MKqoj5F>2xkPVQj>b6CaIW2sD!i&Vvk!|d`efVMhT-jRNcvt%k zyrI|OGJF6jzHnOMA6Frb-oSZ&A~Jgsy7MxCizD5_Gl<#tpZHETB*mA&DV$`g-1c}jxvljqh`cK~n z0e$MW$cSO1ggw&#f|!^t>riUK-b(-uxAFU(=Ysvd3473i}b~7+m)l$ zf*iBvwxofLB1NH0f%sB|Ww8(z>eZyd2Js}V2)s^Tpd9U9x92~X?^_B^vl-p7ax~w2 zS-Cru5iy@@f_F6nF?J=1C_RG7NzFK*r zO7UPoFE*Yzu5{hGR9H!=NFbXheT@5^r1`QcVlq3m#0sd{NkloKms^WIuC(&bf4s7I zNCp#f`-RGfWhs0I3_P}@O8iv6Sa}Y6EHGe6WJ@R|f&aIc*%*Qhdc&mzb`NvTn=jtH z(79mNY!Q?D*BGZZ_`6J6AVc;crZo5ix3YagTKt$>AMGR>;7NmRw39h?8hzf6Y|pQ8 z=Ha~JZRkNIln4|O>MHC`@@y3YcYeNw(xGPV_f{YHR9aQ3<#i%$-@=;py|F&}b#I0D zbXFiRAue7&bFjW?emMm6&8P7A-eEkPsDzG`Gj8Y-M<1llJO;jI^6un)kHt-l_yQAL z(TxB@x#J?IfOaTQN)WZ$$_ALaQU)K9*6RV9k1Ce?Nw*n`pq`n+GqgAuHUwZs&K>eJ zigyNO7S?Y8<$TW&oN9rz&O&z$G)Q!K0h2(5?oeFrB*8rH15rTkR#=naIoC+)aj`)~ z3E67%qrzXP!b-9abw@oxzw`&%Q8%M?OtcXt40us`&L1DZR6g6j!ka!EJcPp?={1#N zrX8iQnNoWKkZ^hqXN`+hO-4OZ@D>XoS9SI*rOpno!7_$H_4*~rUxn)>`b&}JDyApy zn~vZXLeYK#E3_jN`hky5x{2b)MWU3^nGq_tfu?o{rN%zgyIRDx(^siSAC+R}tOq8M zi@pu6R95rCjFt-$Bb~{@^Q8q9$0C&$BPwFc%zP=V$*{vAm|Li@ho^yV=;etQ}4z zfz<~;qXQ`hixt40jjbA031I9Ryy?A=w&eQsi*;?lufc}@PL@@7uOT*E(xDs*iO)D# zc>vKn)*01+8hzrI$|(2Xl$do%u zt>Z&3i>>E<;y=slWOg3{)JEksT&=Bbz%9x6Lg>sbGrs;oqEVWal9~c^@|-}Q*9p?e zeNo81PB2$P2sfoOc9S-TF>7>#w)?fu)+QwSsrYiKz4MoEkwHDmq(89MHy3d~X22=# zFq;A4Hze?l(6WB44~E3KROU|~C_5qRIw`IqMwxp`?Q+<_H1yJvLM!_uQzre?ti_Lh zf@C_z-TETv{vT3UpXJ-Ee?^=A&k;)hMGDK#@IPU%G}Ave00tP40X3DNZ&QS3!3)Q~ zw*48=Y})Q(qgPn?@-C=Yp>LPi^fo;A0(^fm=X_3(^InI^`R*rdoL z5C$NTuq#n2@#L7b%W)!7ePc*Z*$ET~SW%F;~ri4;u`oewj`)<$S_22aW&35%5++*)1|8yho##t8p@^re= zxm(Bf<4%lMXJ5NIc*gb7ouQ}GMP2&p)#G82c5~y zH4xP;)tGzGCfj4}NH)|5iqTc6TEAN@3P?32qhd-8&=szixaCXio71!@YK&95tB z!}RwhbUaDuMBo?wjAYi4R0p_YR;aWm?l6&->;aLM zoKdbQtWVhjFdVKN_LP{u-B16OBB*;90=+etAx-ftpffY zaOPn2T_Fj;tk(@HJk%I!Yd_XT2Ns9*MWsF=h;Z8%c2Pkb}DwL2{uoTPXxkuX*O+#?|XL zTGg*b5CHPUv;7>U@qRj#%alRheQE%#^iDLmqH_Y%1#1HUa7svGgv~T9Eks*4j&$vh z*$An8)YAiwR)}bFlWIzu z8F7Wx1t-O)w^xlqIBL}@vJIzeqBSJltjM_Zh4))>EOcL`ni4&+w!^4kT9Hz;y_S`p zFyzuH+pp1ZX$eSc1{2Vvq3cS>%SHOTOxZosgG1UY+_hvNwK^!RhN!zfX9@$r?1P1H z$mWGPhp;P;d*VjPC}y(Gd;u4%m96Bx{$_p~H6q+`Bl754OR^vqEoL;c5?v#)K+_&2#p(=_V9 zV@8MToG0ktFEP{I<{T%&MI%FU+*98|aWrBByi-7xgR1+}%>d=;#|zCdswv>#MJl$<+m10Rg% zLEbRN9+s#9*B*u-Xa0>=@Wct>C-x;#GOx2H`YPh0c z)_|(QFYnVUGV{?6o?JRyU5m9=D-}F2mMfah9HmOsaIA*`Dqh(=VEC6a9tqz-D6z#= z`yCKg-bJwBkw6d)(Gba8a71;apva8}Jfh~3P{h@mmA15UaKx2)Yp4whIKt1(23fxk zL*T@qFj!g|Dl~3MFNfP9&e{OMplvT~a+sjs%F5M$Qg)!amm9^l7van5~=ED>9f z&oZ#;_B44=W|$sm|HB`I`cIGEr+!`Yuh!C z;xKnue&;Th5ys2 zGPyB>oX7kR_U4-r=1QIex970p1_M?qrQ_bUL?IFHwFyuHRM}LtgzJU*me-EetWuSx z_PX3&sLKu=@0p1>j#b?R>tBZ0`1f3=cRIINz%zb=H}j6Z)`r6V&kc?Sp+&EX zH%TN*HoeL5$`S*+r#CN2rL~`by@XB&J8a7?HBW#OqX@OMk`yrXYo4R}<<3-gKPFdT z87bvm(emz*g@?En9CBUyG;dgV34JBqI<~vuwY%2^%HHo$XY`$UBWBn%a+C=udz*z9 z@G^{w-Q4?tGnIr`Iw4$a{h=HD7)kGE73F)sF?BWyZI@}~Cig_k`afRKZnVh}THzar zFp@QQtf`j=M*6m@1!A>TmQ7|nh;o3QDZ|AhlF4@adN0qvkR;76i3=JbBjo@euPiba zQ7)!V_7?;s7tS?o&d6p$yt`$a=U&qv#F4T{LHD!IDUubE#;q&{GQ8?p%xEZHwO(@H zb3C}tp>4&2jg|L9kTR&-PnTy}zc!oAEDryg^=PFXwx*u(=`%{t8hJnh`IwqZI~_?~ zMcJx)CWcBm7E7qc9Z^fS9rX3>zy<(}+rfopg^DrTu)Xh`?m_v~#46YC{+1c+-i7ex z)b=_yAX^Fa>rD1S(Yx}Q1m^PT8S;O`KDBlZFEW*lq!zu^rhLN=wIlYvTP%hSjj^&< z(Jq3wcyx^aJ#+tQC$(~`9dZadJ1Z30JX0xx&Fz4V?2L`7d#of5Zc_NXD}+8PNDRm( ztp9u?;yk#q22X?^nfT#5Ry8IGv_C?7ULT0%|E|2VdieWL?Fig}1$e z1}@dqM0-FlVE4|!WWjd-U-Mz^wk!w0?r-whaWb1(iitIMHwIv{W^yCv8x z|IZT1W$&tGep&w2`kJ zMD-R>di0iQL&Hw`a2z<{;TWCjC?-Qxkqr40K)HeEegkkr?Oz# zUTD!=fP(-E`>FN|OBwT{$k9(y#+N2wH|mTXg7D36h;m)k@utYsiy;b=y0hw%$e{eYyLC6${x zf5YT!{(bJ4;2XyN`{1HIfd2GRV#&MsVAmSz-4!d2(CcZFPVF_{{PE$9I*i8@Y%<1l zr}Ob0;Mmm~15GsS`mB1Y4s06_=_5nta-hI1h1PA z;L_qI%vX_o0pe`_QC`54{sH=jz|V^*q`|B8EB8KyVkz?<5)4&xzHvzlPAd~@IF#WbFZ`78k6X+ zpuanOQ0*OBLhs8zEZo;B+eHw2rwUx{IAFAdftwXk9!fDV>dCss04@#kKrd;ynKW+W zYpXcw$(rbaFU3e{2Ujrp(;c~%;K3j&@y3YA0i%GevR~4HT~h)<^Jg2$fL$SyfG?iH z-Ou-N6zcf40bRv>t-#7_Hi!XSRq}=cxTb(1%R|d~6|!rutN_bvWI(LcR*?OZBn4)y z;(Rhgh!mPSiPM|RK=V&jaWNaSfE1)PA9T+h7govs#lUsu0y^#PpCC4Adg}x)vW&Q? zl-An2vY!EpXXC^}E*N1v)iJ;-YxWU^L7O-=aB&P8xaJaIRx$5caWu5M07=zZpbA`< z$#iO7id<`nnRLvH^N;s7OLhb|Fsl_=fjo<$gJ zqx#;&dhY^;@E0P#pBi#AkMj2L3f1o&Or9BO1r=Kd+8%gNK~UCCxzk*J9X$b&6B@B6 z=n3t+9E0tB#liRa!p#^w0qa|Qi_cb`A6%P<_xg*K?yUO>KjHwA>-ELq<$BoXL)i9N zkSx<$g6EYUqOjdmtePK4#-lZa@RLXLODk11uhzE}R_RDlY)NwmKv`ggzkyD8q)+g! zZA;f)8TfDBKA>bS#paARM9)A7wd!Y6=%e1;hGT1bAaWHPxTA|C$YJjO@NAB*X$K`E zvRt(zuqfqEA$boT+zx>_1&90YCnS9nmIZ79rUT}>x9P%;@+VvpzBla(X3;lXz5QEC z)|2}PPJ`SoI&wX5R0w^&-&!Xfs@?DHMA#l@4kJl7{>R(tpOpmRpOvKKEHJ3u+`^?+ z9MMzFGs6P$g{0k>m} zxUZkiv(q`dG}s)NCKtRtp6B5CflY0M?6G43mXck#LJL2KYFAOCZax|LBrL(wL#qDz z=TET{?o*DwcQnWx^%TTw7(-iOO!U|@uooKoBW!FxbBc;3_vW98h$>HqkY9a*({u>F zN|V@Eh2i2MxBpf}W%7`A}1fR{CE=; zNUFPQ@u98N@=Jm$XZlu9#>wkCD4I7~R7p29mJcg&UL%m&?@&3;1Bzb(t(b+y$sZ=I_=@aJI7ODP@he(>r%C`EzhS(g|~) zQ`)P2=+B^Aon9yUBpusS${$Y-h@K~`KlzpxYC6TB`~3>mZwsw3EK_l|yq?aI4rs&) z>K0Rtq!ybpk=8Ehb`M@dNvPYdMBuVh^V!f~SVlmrGqz+@ee|(Y@r_N_rtVsEm!hP& z{mHmI#dFNb!jMK?+X%>Vi>b~4SlBs76DTP4$;rn=z6XwRY`|}&L~YRt*X3QP1xrDC zMyB#x?4quM1e(PZs6Xe#Ba+BaM49Yv`aJGv zC+nI_DSoHA(l}DhxtJ1J2fdL+4w)DC64~kJB;$Eh#H%E98hBK=XQYb}S5{h$itG(8 z<3vWD!DYcqelm7M7-LnnOSsuEp+_1avF8E3<-}}C2K6v zpOQ%4I>6TFr5<$>H6?ZPSj1(tBjE&QkYsA)lX;?{mrLT%ipw#K(%jDdZdzGo9t4XlvR!~&N#qUv5g4NQNR0Ih%y`{(7uR5cS* zv0Q3{%%DD`RjSm6v8U)6B@|HkF{Qsp=$PXE%5s;|8HPFN&QX|zizTeWrOIZDvN-r{ zjhQ7>3hasib2^{02Pm!d05d^CO5Pt=L}9{F2ui&vmK`VrVLP;vOA%G$4}NS;AyrF_ zBPz+OAt5w(kDUF5sIQ)~fnh2SCF$`u?|K%6hI^uEtT8zbO+v01IQVq+4nooz$khW6 zl&Z)xw*!;~-jPWo*V79u4V;-KsSbXKutT919x#jwa;!#X*insD$A=1YOmpU}Re^So zqYArWMQYSh&(%aGHRez$?UTo|KZJ*r(kPShN6UCz=u!?V1Ck z7OT92d~0mb!F@$%&_PyvmjNReHz#IaQQ=`#IZDux$qxl3-q*m9XD%h?&{J>~Cn@>7 zh`@7^PY3>L>_Nv|gl0}Uc)$!5Tn1^v0tJ(FHrY(IG*exUdj zg+?9Vb-f*KK{x6@Zn@Q!H(HoO{1oZB7dz4l+BfFXF> zBLHg|O@K>amH@X#06x>_0X>E1X6jdgiQHG^D7mf(^kD51r&aikc*?d7K$%#aV^cJA z6%AN-V!)I|o5F!u6$o{l2~1hR*Oj9eW0!EnF*gS36^R#QD&CZ3o3Ytg6}c3TV5pDs#Tv{35m)7z=JQ^%%WC1AT$Riaz`L0hGGc?>_?e?q12 zC-nLnG@b9rx}t<$;gXaY4fAV)|Hj_ z#KPV*kKA9x>Jr}A)xo9Se^=$hQEcj9ub7171ZiY zHjZ(}pl#{Kg|PZ0_YrW)icHM}ONOll#1j;@p8Vyjd$4cbjd$?bjI!-ew5~c}3#qwj z<_2bMUSGNbOQI#(MYs%OdiC()VOWwo(OY2DeBf=>)v%GQtGLz+?xZL|<;Z1wPvmI1 zb)7O|p)cib{dN8-WTFm_NvN2{t7E%s0YHSjO~gNz<`eD~cdt(BYD>YjX3M0Yg;i3V zfL3<@4NMn=-z1tJ;w&HAE`G0SC(FRsTB8mR;<+a4UM1Kb$NR*Ao8MF|qIfA|)yYN3 z=g%-yyp6{2cg*|vn)I}1zS+MR3@GC~3qxY%iKUL@;UvVBq{PFGBCHyT)0`MEmoRnk z;if)D!5B9I4RQPAK(V2Tdzpx5=9A#VH#G1|}U3&ADL#}}OFyivHOo~*b-*U)}@iAIlSfw@Wb4zi{L`_nzT>wr%{ zYo2aIB7dQ;8pFV7VTxPWGxqVy9k z?&_3clm8X2FpEn^LO(?$5qimf*>~YY8f)(olx&&k8qx2PC_L;uOG|Z5TOEiipuRi= z)~n7ihbI0AI79G=&r2DDT1z7O{ld<+>{)V~x>7hFr(m`-qhmB2tQ1-uR%4Ra{gKZAs(%MnR$Zo^E ze4OAc%SA~`T7{6>gS&jb^5wTLB1Di_IdumlIatq-s@?=#j@cJ5%cVxPjbfA7&qFgy zFiKHWi8B0HV1WXifu|cYZeiP6A2;5bE|GC-<4S~`VJYipBH!F2t|7HJ;i=n8PMBMj zs56%KRJt;{Vk0)22EA33*7T~<{fzVXg<5eJw%jThYEQVZL&$o;DVcxvV?s!G_(x0o zs%}Cw#3c0yFj!s0l|7%y-yPz1yQ3#N(_Q<1<*Y!h8&=kvM?ge?urkg83BvP7JjFyC zIBQ`ZM641a9ImP}Ij#x>rDoEegi5tQuzX5(RaMX5P|=S9DpWMNadmHKKuOAR*-*{r z6+`{pEiA3*{!S_qKY8pWWxdsO!ITZM%2GDWi743?%0`#2QaH50ocRJRWRrI7q4^Yr zVzgiHwm4(amyG)K~(f=;3I z-Jwm7gW5j3{BIsG_TNDMuV0AA9tRQoQp$&zaJki+h0lgD)hZ(jWX5ZSK8rRo6UT7I zN-E-~016g|1G{D!XDQ_c-%ewuj{og<*WjoNpUtJqNZC^?4w%D*s6 z1)()%ri4Zk3DW(Z7OAXTl0%@oVRP^tmkqL5=_aMb6=#FRxB?!?l<43pyVG3EgaWO? z)-}3TUhj;3Mdl7fm;t`Hs1N{pf})}CVvr&`0lq!rQi^x>oj*Jh27URin(%y@g@5bX zv^A2n{xSc!#G61`pUrc5K|m_4|HXfqVQ-%{^gOIMRXZ(-x0P(^*y2bVVMW6 zw<(^Luxa37dCNc_o7IB};9hd~>W~;y9h(MdZ~TU^cg?#u@16Av@i7<4U~HhR;gjpNR%DqPIciqmMMWIL;wQ~90g|&^ z3a(rCSs!{=5J~K3_YM}X4-U>(ztCs0)!F@*sg|{5a>Pwbh`rT2KmZnNXA< zav=Uc03=EXb|#h(=5z1fffB+T-b@5bMj(S5O@MRAmJp}$T)8BR2)RCbR~7Rz^z0R~%WI)?<>e-syRVEaff9;&zS`^v;p!tK!}CXN(f zgR#fL0_`sW!FQH!Vgx=?0a?Kg#&1suc#o4Ih0F+iD~T*2@)Lq1g6&h^)`UC~gAgI| z%;{kc_~hXGb@RK!o175-9nW6xB&#qX+C$(0cli<@g0C#-N(i|SpVPnz%a9Q4jVvKe z=3mDECX^87@?gbSy-5hOxv=HAj8j4!{>zA@JedJ^Dp@+#H@oh?WXj?jh@g4AXzsfw z1h|&3(0k?5L)Wk+!o7l7E5)Nmt>KD=X9mtLT_lmCdQudxMy1N2XD?xkg?oyGa#kGH zSiWla$uOGdA%@(+^Q2H(;KVRa(NA9ee@u5S9&G^sf~R5s2RrlMVQXDhUN{kJJeY1^6A5ryv?nlt|pE}}yl1b## z&}RPCf&HgslK%jG{;}>S@Z&!~pZ}Yw@&Aqaj{RSPKK~fxe?LS2X1?QKpJp%;oXhN4GX>$EnAt@7v4Qdx%c2Cpi9cV#IW9c*SmQ`Uc&ZttY3NJ%3@nT7@i? zLbJkz%yYn{q^S_4dfBzqE9^H-)|(M_o0li^?8a5%aSZRQ8hU!NY5V{2OvS82NzQ7dGEKCk)hJ+IkiNKl@^YuCsMynl z+*mSIvwR3qKZ}}yre~f}%606yJ@$6F-I>|ahUiBfHyM-_MaZ87tV9U!A==k6+w+@n zW`U28NtP6@_z2SU5l}(r6ULKIM0K?lOm59^3OBNXE2g;?+)+z+*tJ+*>|l}o7Imwg zKk|tyNx8ZU@e|amGFjO>1^$KyCCT>@VKT$PnMt-uxe&?E*~tg%Z0XQ90JN)s8k5%} z$K@A#YT*7%Z_X~0$*6@}k|W&hJ?AIb1P4LYNTsaMp*cik8GYv74QO7%%{+F+zJy)Z zvvDs?HE}xio5t#HWX4?<$N2q9Bh-J;@VHht@G(Y>;F= zf3!6+Lf%?H_cvPxb^3 zI^p9J4rN_de?y7lD#y_;5x6girN7_*1@2IrjEVN@8=O(s8x(txFQ76iy+Iu-OhKiO zrL>x<02c`r7QHtoh_bB^pJ_v#ED9zi{wy7&Q!l3Ir-klPKbU^tG5euyf4K)dz3rlJ=j0H({Fpp-kLF5RM{ z-VMn804ZqpBwB9Er3FE>7%e7Z84Nc$>x@Y0(pF^C_WB^88--J^r5hlk^2oLC*@<6j zDlO${1Nsqfx}^}o7FsT#@H5UH)i_@_&)zVhH<155klf`R^4Q+6Q|=CYqHgaHRfm)l ze*inS{O->mSROanpGP>%l2;BsT7_!pPZPjZW{%FGS){uxMc1IHxhbapfEdFic=4-e z&93z#fuzO@UVtPU9gZCZ0s)VbK`cxp!1L`dTex7*BAV%FQ0nE)UNg{*;GoS^&% zs4Y_n2-k?+=DVq;Kh?zyiXIrF<2S!`Le>5X1Q#f{?zS24lFTjplT<+Id4Gg~xSYL} z0u`W))bFBr225#my=P`Ru$ZU0@+3DUUxl`M>)pf#=uOHg`;!=FoqD@HGd(W_LKyho z$N1h^qf!`kIeMn2Y8a+x+y%E_w_oc^3Z6ki@oP*5cKvcEOK^+$S>6oX!fmyaxsCji zgV(i-B;2))gyvs=HSFJfCd!mg6Q?F}3lJHmJIGicMM30lBR;S@d;l&jBtZYA3g?(N zq0AyaL_ey`g>qz*%f!%1A#EufB`<_%Kxxu?42M`H%n{Xwe?SpDjblgwT$oH4CgjFT z%TZ($k>_$@DsTY&X_Alc=*9#9fNDez=izDRhs%C0;1H4x!{i>4`N|+XZIbZ}*gc#L z`6sDqh@sj1Ev0tE{aAjlh&XWvS(X%u<-IxGJJ)ztJ@e2${N9JN` zUg%W-(EuO#5UL@*5MSQsC>8Ks&`nzpfss4J&*OveopF2`@tu*NlS+htyjK6iJS94W zsK)Dx0IJ%Z17b}&_ep9)Mg5- zZZq82hBe017EDy{f##TvSRw&}g8Yd*6pT)qLH-X?O6P8f;LYg+$aBpkja%Y#%>~TW z$m;VeFC%Xqsf=^#x*Et~G`(dTo?A`|kn@a-?!n(rPeSLO-+9&Np8U?tO*LazQdQ@& z?Qo-B`Z@>?q`zw*3G)Qc*NDF6&wmH@#=G#2ZhXHGO5>?74`0B3cTKweKDHL`^1Z^U z^Y0Z_S=ouGo@sO6y5Nl^X)PmX@V3-M({|Scy5ZeYXjtn&+OhUrq;3`EiPk9nE8Hla~U&LpTM<5ceg~hj8g#Ag~EH8lSbbnLe}gL&_`RFmNiD@ zYJa7*p1nmFB|9dX<)j9T$E>xZ6c|;q#65WECn)?7cwzp@B?a=2bPIAo+N?cSkGT8Q zWi0#Dc&6<(ZFJM7;#1c3lw#e0`06$BtRUf3mml@eKZ5r=UkKch+V9aT|&hUy<~=ipYdaQFi|;g4wPSYAPEi z+5HW;b{5iN$b~(0G;m%Ljoxhgb|DoYh2CgrGJMCr1PAo zRM~o|l9Nj7mq~3Nylq{K9Z>=M&9S6-{t~f8qX>p3t^C$4SRe-1{KthvnTLoUH43i= zyLt^=jNGUBc6vR9l1d3(fP`z4n~G17o1E=Rg^fDCh5C?hi$VRHJYVUwcFnGph%7h= z5N4Io6sU&P$MospQ~Iz+@Em9-S#PFc!*1YI3e+G3QTufhQu71~eq5HOg@NZNzbmHs z`6kQhBpNOyhujVwLPsqzA-tv>2y5cz5`ct+gU;QSh8H4n_`}e_@K;tDn5B2+VM#aU z4A?1W0M2Ux9i#2SipAoSi|BY+_hc`VKAkmtHUzV}(GT8+m0Gzw$BfBx_I2B)DPy0; z^8BNEbPGK<2D{;`DSRv&@nL+kO4u6aDIIA7ri|qi{Kq16l}lF8+D)tAClM6(hp8`* z+LSGh7+mjdJ*JWLK+_K0k5QTU3rcZo;TC^fbl5(JA{Rg5r|~D^&u6KIEuE^IvzCj2 z$p_R-<6DLIZqHSXd1Kf3JPN|<-Ly>naQME5p4fYOcB8dJIb8>)ogsOmMzFJ{dESIO zcuL-Q$TiPt!Y0`}ru8OqdT|dmX)v_G_gjf562a6tWDPQ%MBT2EKVL^7T@CxrTYv9X zEmB=zvjCzp6b@72Vnn=)l3hK+i=`9ji^VH@*9B?wf!Hi+UxOs!7XyEo2*Uy

>kkbbmazsxik7614q|^F8K=Yu{ZC|MLAQXixOoj*Sxtq_uoOmjjZ+eG z>mqWgCUv6fnm9Znm2B$^ZQ1*Ai>+QMICQ>3q~&@M_1n6B*~DkRjCGRu?IKvZ<{XbP zGP8vS4B#RosL}n%?#_x>8}@QS^>CXVzP6Y_czH}WMq%$n}i;(%#Rf^qwF>$j9=ZeuQ*UuW(WIbQn z3}jEpH6fk_K7|_|J?1K|9f+XU;^4WJk0yCO9}P`J4*R%mB4Xudp?8*%CHywAHGM~+1<8H#eAB`YV#7MxekwB)+byy3AnpD~dIhxfq>|Rev(uI0-yA>V z%Ij<|?H!mg=|#x}FM29UY-A|_W$CuinPoMSctvFjFDQ^>%e?hujWGj0D{=~Muz~Em*!t~IeKBjZ3XI+$KfS5r*EpMiL%@3P2Qqn)zXPlTw5jb$9h zS!74>s(y~(ox!JDDQyHQC>PSox_I;^;FOiGZSMr?Ant!2?J@EnhT_gGT~fq&GfibL zqvT<78Q~!Z+0ryF;rUyj^UaT0y^HLdX5qB9HZU+puy3R*+#ZTXcM&$B3IeM7+Y8fs zw4rx7lQ2!2Ko*Xop%|-oQh>#`i5k&8d%XOM^s|sESTmvUqqQSmxc1<2FM%Y8gIy@3 z^>t9&4w6LZ;g^|TZ+q!sHVm~#yg;Eynj%4#FgHhe;?;!hF~Bbz#{KyV9R1uB@`!V5 z83`mH93oQ|DI2cAoOJQTywddk z{R=k+F6|VKK*DpQZ;M{n$+`!16v&ss7hcP!?g>Bc#g2|h#csM{~|MJ z2?;S;%?+6a`Ium$yGigRD&66;6F_I))KYGCT5b-;< zp4_!wdC+Gb&Ee0YkeWU~8JZ10Ub40akHee|*|(dIMJjC(g5XtVb_3Jif0C&4Y|^j9 zAa3;)7PFnG2R|(lEEnX6trVP|eUK7MyJd?QQ+0#2*AdweVgea>^?|~m-GKs}njNeL zQ@Q1e=iQpJ8uF0zii?ungNBhaWLW=BG7&zZPy!-hIio3TLmp^~u)*nQ&ORTc6o|9D zmy6$aT`*=O*}RrybQGI7u_|UvAX&sDLDeDM1?O)~l@Xv4iz-9A*G!ky9#3QBL^GxT z?z4_M|X^7fuAM^nDo2)0M5b>32V z)7|5H;ev}?XP;ZHX=&#<&I!L$82AK*v9u=DrQAr7fFyWRc~Ui+h_9^Jn!{1g5lSj z!2B_S-)?MM1j2Qk&&I%+mG=vZs%tGgFJm|U4(u@5g)Ru2-vv}`@5+va8}O3+U*67J zbiP&uZtuTJmhVrXgr8UN1Y;uuSI{fMQ&KP52Gmuzsm43LFYqgU7`6ocPgm~)IBU7h zJaSuUq%e&hzOwWKOgsosQEpAsQ5S$?D&%d;WM zWAP7HE;39ljA)n;lCc!37T=7a`SXA{9*~#zPyoWCA@L;WnKH+hj+df9L?GYQTWAhV zmD87tfO12>L(IOiEoUE|HgUMbF+TL- zUfVf`SUP>Ibn=|({mR+pjm{U_3zQ@pe=SnNJ+GgpJ^!v3i!$n~EHf60^FYg-(fCRI z>{^Bk7Ev+0n7+QkPx5XUCHop`aXKc$g_D9jKlLm11r|R=af7xdkb3Os;XxO?N9U|J zW%t@Jj#gUHmKlYIUtnvt)HSPnebHOa^&rYKvqUCDO}?!P?E;62D=g-ZWs6SPe}}Uf zq=mQWUSOH-X3gzvtD}ek%c7b@cj&J>3)ylrOkih9xiICWbflx1t{)X^a?visle|#{ z&m{S&D+uyCzQ9EgqpXFC_KW{?G(Ob+9nN1M&gVFJ_@F(XK$(xviQ5>#R<6o~7>ip* z(+-W2sVYQeXj$<6l@t7|y(p{_*P-lOuuDrV3ALw81MiLT!8HZ`d)))x80QE0zGTi% z3VJ{kV42QgdQQ^{COt~9J5Kgfti|wOLKm?c)#VhsN z^9<2qyD2h8&ihB!7?lM-J|8X+hj+-*Wn@j2i$q1`qsySawsITUcuNZlz%UpZlgdm-6qp*qd*aRh7NA+s2x1KVuP5{aU&pz)wZwwq4fvq zVjFnrF*H*|#YTXNb5JxY%|<~&CS#-32C<}uZ6D1WC5<8m48tSNQSmBO5_r4(z}y2> ze}n~1hy1)l+_s+%R4?|a259v=ziAHe!mJ)U2r{}NbL3JRNpo5mlTi}vfULtqzh{(q z57AtX9%(w?5Ce7>I#?l@^b{l9IYZxL#DGb}+&cCUBOrz-PG>ZpV;BLFG!+!J=K!cLBga~WNz;EUU+R_@X3HslAU&CO|l({3ij4SSdpp+d?i0bSPKYpzOC;RLm zwa{}>UvS-#7tBsumNk%-xmd97jzuN^mE=2Z$cyrwW?g)Um=R8OX)Qa`7X=+dPDn;~Ax@WBuQjGw1)D>Eb`PhIdWS|`_ zeW!r-$dxL7o;}|weJ868aJEoN4AJ#bgT}`f%9SkRS5!83PrABxvdQp1)AMVGI!may)~5t|)2TPqj^rnT!8K4@m*&DqMvNp0^D zCPCm-w5V1c=c_{D)mk)%fr$&)iQZ8%(RHKV_|0>4sff77VLS_Fx5BuG2v^4D>v zWyboj!HM3k!df|6d_qr*%zE~wmRV*x{su2ci=h{Gq^_9knr}}pXpi+ojT-<(gBTid z@EhK*K&D!ZEt$Dgi?Ok#A1a(KnVHneaXj#wX!bv9C|o?N?F`$eqXi{MIEmExPOG3h z{RnESJ>5I-e`&~(*SPGqiIa3tZe>i{&noC2Ho;=YgL+}(5v(5YzfWBBL+ zq~6RYEX~Fsh!XN+F=x{1s86G1h0*M~jkfyl+y#;N-oQmKXjDcIE@XK()g4uf4X)1NM5_*Dt8Z7m3 zX#E?2v@+h;X!}BNV$KB@;W&QGB0q-bsMgG2BBVRIk7;OSN>|s9+{}|ZP#J?_`9d{~sUaJm zj-Cftf{<_fz=5jOxWHCs8eY8y1Y}o~aC!yzriS^^nYm)U%q`j)iDakgGVES^jKwtN@yn)e28$$VF(_@gmR+H9JUyLRNler2H);;jj@C=@Nj7<E1B7Pr|i3S zC2~XM&AUUGK5pIRYEK!|TU@G`Cu<7yIK7TbA6^Q#Fid$4&m~QQP4f5i3=X6k3Ir;6 z?2IXmq``~Z5a*gs7{gsG&Gs3d*9jdXyUy5eby62vF>PNEZ(G-3X^Nsto#E(VzXMnDY~aLC$0*BFCmiNT;bD9Xp+2_$Y_ydQ!cQW z-&P=Oz>^9>kWR-2L3ggH z@H0}mbnz^JaKPGbjE_Mvz3k3xDDz7;s|JgnKCPs`=~8S?Eqe8o!*}Eadz+hB!!n|m z0+G)3Y4H;01^k!*(R@)%Vq--3Gq92Sz*wlKqx!(8tIEfSv~?9KbOBTD>NiJ(eilDi zW+(Skk@7u0^bh0+#ew%)JG+F~6+Jqw|B?4+l$RKBbr;L;wTpSkI@eCXNx_p_NUY$A z(f%rq=&uRyXPvs!V7q2wp}_)j05aF7a&B6>e^TRknxIrV{sdbm2tFUO!>$wdJs%_O zBNVc%jeWI^j?2+6XBFifi=M-pW!{1<-Ru7Lo8rUPWqSe5@nZAP--K%U6dUpx!P-v? zN^$)cnQ`L15HJThE3|uzJve%lh!t*nuJ>#q6;z4xkI5l$_@NElrt_u9ok}WJ6-Z4r zIjAmEx0$CB=7;mRjITV9hQ_3;^5+&z6%zZiR^G>!80vb+MB0DW&|>x`)*A`;As{g_ zLKKiPmh7)LD^5_l13I_oMa;WTUcjz9Y`>$?rC2*UT6N4?&#pV(yxxSS$W`(wjIAY9 zoH!Is#g~;_Ly210n$9M6>)+a~7B;V!T&Hw5d3Vn5y_+qLH$%)zt{vykF-)Nge2-CF z_6fWQxj*fN!G9?#i<^~EcD=LywG-&8kebTYfD7B2ME`)`}Zlmbl~Un-O-C_ExZyKMi^q^wvg&aYcjzGDS_> zveAZ6KSjQb@(O=`_`|FEFV6Y$F?LHkmMQm^itdMQ!*IVXxYg-2Gu#~Rrl?J}YZDcb z&m^7EZVJ40ml^&_e*+$rF#*R>@_9g&;&!n1YxW)hX9hso{m6;iEXm~k$P(8 zQ92q!My0(!ufLOii3lsYX3faw(+N*6!+THP>KL;rzrb)5K;O+BuJViX+tPo-1>BgH zGrP_~S1&{z*s!+Bp1w*4;O0R-8tOAc#!%mf4Q@==0u%&vjL6gh!4yf(2&gXQpeSJX z%;N}$psq+7ut3?PywLlWd#LoB7ydg{#rMX$|Baa7f028zu>S8HUYMEL75@=z$leO1 zC;)SGbR@q9Cz&Z|fkJ5mR~((!L=+YS4#fy`q5os_-_7J-A{PHk-|?S%j{nkk{6`$& z|E>BC0MmcWS6P{v@Ly660fYdie=JV%f17g1@o%*r{}T83-=`e10GOEn)zqa*Z8c_H z6#dtAZJC(kY}HXWkN}X|dN^+#q`#IQ0V;{SRMKF*Ftud`-tqY{eRF)lfw8H6nhaW{ zt;*(MmeaK^-kWKc`qN%0!PV7u$In5Y@W@WocQ^=NzHQ!|oL9S0&5d80504H#96m^H z0PGi&dW$uYZRhHj+mW|rguSCFNcW8y+p7&a-Nz5Z_G&dlANF&2{rG2ErSu{Pq6n6F ziKNJ?Nn_o?f>G*pcs3s`2Gz&gyBary*nD-qNcpg16E7$B%wS^V#zz^lN^LMA@4Qy( zpeY!p^=bYMS(A9<8 zz!j_ayc<;?#A4kaGqdZvKvAN}%X?t>zn%h7r6CnAs<|_M6%iOh_!3`t5qQM<#0@0P zH2U5>#jo)dT^?_7r)ju*{iN;!y{Z?Qz=Au#?`h^(V|XXL?Ui{cd<3ju(S;bp{Sf*7 z>wTJPa6U*=?)&3V)}#h_Za@BNZFP__RxTkPqpXlfn4pam$r*D;T*&=h(}TW17-I}P zHvywz>YPy4Us%>A96TwEI7W$+dxycZ6&mVRy9+Xo&c!es33+x&qsu#yblBbsw(a5r z=MzxpSDTR>3p`z8XOH(nGmnk{H)`wvS z9>w@2sfbzpT8Y@-L)$bFRJ@06ON5?79s;Y%(KW@?wILnHXIB7@dsUq$sM84FMVUFs zv@yfjz_jI^qd!_@+9eWRbMvU)KPdqmjT%bCf?+#Gwd{yqj@FH0cGp7~*dg3^EKPIo z66_=G>OAH+a>3-^jxr+_uT2b|N#yCPNk62TnyjQx!^!foXTTr&)&_|><>XRq>X7lO zz`D@Sj6Hq{J*K*g)vNk?(Kgd{FOzeF?0f9Lv_Jcy=<)7PMwW9vkE%Em>oCK1hD1w8 zvb*wdc~GefuwnlYKGyJ3=a06(v=3gQN`vhs7k+06dWv zKFQkLbzcZJkHgppG@;wm#Jd>DiLl7!_YC^G!m7N}+&Z|iet1|ZJ*Ge%WHA`_Pq_S(ZzFFVoY%43=UJ)28A43S;7bPsKG+R(z5PGmqse@VG zVya+Fwc3~x&6C0lokg4hs^d7O&xlbQM_(BU%2Cs>v^A5+xfI?JfqI8ItP-U#y@r)y zJuBuF?Wva&l3IQ`_!?LDuT4a-=5p{w@fFCA@(yjDc2pE`JgTA`17dh}SC74dgUQJ; zb$H0|yGF^(4^Zf%CVR!!OBt}Lb8Xq?hhWEmI}j6Worhld)i}uJAcgGzBFM5x>Rv(6ltOJ%FYg@h&kUcL|EN* z%;}$qN(y`&*nYNcd8IUmI}TFkc(-Ef3-X#-s1JdF$6#JzBiJ>;DhYSLr;wg;>R%<` zF)CY{v?sb&#j2j`@==cOor0WMsX}emlZV+>H+^GXzgrxC!>{?fPRB`h6~dMQbW}U| zRfgKv@DhbA$B0rq49EI1*G~z~U4B)qo7kk7$nIl0?1w#wO&LMSl=EGP4aR+M&<$Js zLppVuWc>7lr9C!O#474&?d0RaQRwVJTLIh9H?cT4s{S^^E4N+=g$9&C3jAl%sT{hC z_3QPABhXwIu*7@tc@Jft;-q&hg6Er;yqDC`>XO>emLy>WgY>B!8>f;VQ4$-T6W;G{ zWByK0SJu?DD%1nWSt2^d856j|fs+-IxaXOVDyecn3o z;XSH7m(jEX0)6qpQVOX~EY^z-=Swxy;aMJVOwgfV64PiOx~IzXe+{F?X@248j0?EL zC8r<1EQ_a9?TZ4$KIN%4mt=lwLdlN+3KN3VUawVKogk}t{TPQ6oZhB6UAiekdTsyIHF3m`lBfA zOrzWz!zpqHWl&z1D5NZIF%7FX8Nr9iFocLsIYt5_4|QW3eu2ajatD$zjlSd|e@t1a0h(qv&*0{1HE8`CrTl(pG}nY`gx;!XgGC*}rJXFFs`;Ds$ye z{6m%9MSE4s%`^hMbV&=i{$ivm&YgbTVS~aG$#B>Ysu>Y8*l0R&LDv+#U}ejH1YfNq zZOwdK1IXq392UzH_|h7@^Q)Q`s6;8|QJvD|h{+yUauJOlUbHJ@AvJ^y0i-pzf47hD z@Sc0iTg`4}TLRa!T=`ziSoa8UaDN;h2WZ)63K787G~;e?q$s@lMxR!nNTZC;{TUoS zaXMs_|5P}{Cl_ZiA6`oqy1d`W4*TudZaaZugM}8Uuzx z)FKVUgtg^uH~<+Ue|Eizf|qvC57ycj{D1Z1nS5}gOj77oTHL=gXo}gsAO7^@|2+UWn?Z+Y{!&VFr#so9A zWtx;u$)0i9pNPK)@z{DuW9q*-1)3h&8c*ZWx?|Y_DRL*INJ*(a2VCN4kd9&2>)R78 zlK~VzkppbF=B=2>lwcyof;7Ecc~pR-L%sUshe7Aqt`teuO9F(9hVWpf4Y9a>o#TQ6 z$EYJtgHBS!fvc*51m78Y3%sW*nnx{4S|J5ZO=p^&ucp3=33z*zmMrFEjg9jzUUdG}4llTh7r zNa*w%tp9DK7(jPw56P2){&ze$-Hmxq>;4nd*-?i}aWUrHEYy8HW}r8(f4QeDV8>iS z){s(4{wgT0ck#J+8c`NSJu}2$Ff78vi6f5Rz5d0mIIbvhD13S^NGnD=b*6)LG4WO` z%w$)2sc1k0rM`A`>f#OqTfrK`Tu`k#4WOI5{G*XjM&rE0T6P0ga3iWl0i9x8=$Av` z^RGNt`&ZY6gCFvN;VG|UnC+xtPJzhDQ8XB3&n!;)M?6Rc3ua+D(TF%Rzc1lBn$@_p z6w4UEHk$X%ma}CejVNQaI|;;tRl`L|guAclY>jNJs~3fi?VVc5{Rf;j7FdlZ$LZ+H z5bX0mq>bzLHf(xfBqIuQJ$_I{@Saca9x7=h1;Rm0p_+6R`K&j)?Q>DqQg zuk&O2py{lM(5e?544BkM4xyVzg8w#W=wxp=Tp8v{i66{-y5zn`;4Gr#WLwKrpYuP+ z`usZ_>`l2gP8ez z6U8TL;PIx3iN(B*B*(Ib#E~5Q?7;DyRj2~*?`!n02p-UZZbpEMTXs@I^)%oI#i2h2 z_YGg|JL>vd=ahZ&?LebenpxQZiM`--z~b~3sLwu9k1fD%NwmpziN*&Ec6Ifo_b8qA zb)v1Vh*ohS!}g7oy;%@V8(*6kGVZEm4`)6ANST5?-nk*O#GzFbW?A0ra2JSL@><-b zEk2wPj%K2Ix(x9qWtF*Aq;~hOD-~roHgC&@e2$Wu+`+lHrv9y0p_=y1X3A0!h4=fT zLPHh-y;CL|?A^>}b~S}mylIm_0b;>L3pjgrr4WMNpZGXq1vk7jSM|N)2R@^#(1t>h zaBArltoZNjKm=`1X6>c!*VqxE&4I2M3OmGyJ-mRNGx5E&eH0AR-hpY@H++1C>%M*Q zk4aRSKg#poM6}*y3Z?CjN5626sa8}Xg)<)ROPa8Cm8> z#h&@A>vuG=ai%t%NW3%2yWk=>i=NSf%}xT3t8qw`Gq|1N4BqH{0-s}9L~;R!{W@HLjdlAXte zw_tt<{VcIZ?F?S{NfxQ`DZG>s)@ps)^~mN5FCdqIRdE`nY-Zsl=JIV^R65-WF@$17 zB}Jmqs~iw-PqRfYA1&6p>y6HaeO_m3+0x@J+70iKn;C?B!*aU8=;`7%AwA^^D#k zNB>;U`2{Pfg$i|QIC3>*t%ugCnKI37lBW1omt4%)l=s77ta75L2q$>Hbj<}|A{ruR zPHmb8CEQAXfJV>OVUt7+Fi{z!uN$q@L-N>va12}GaNH(Q)*5X#G2LI$a(b2!3pk1` z7whEq1DI^AAqgVd%hN5jvm_`uawl>cGfdUzi8e@EWU{1XJms3RSq`vK-vk?tQHQP*~LdS!|pK z@3A@*S3vr6=z%6?9twZG{Rb3WHE-c7yFd)H!ZSfBi|gX z9@QRMhNldc*QvG`JPgv@3rF5rJ47Wh32xB^>knh+4Ny}ET7;yeYLERVZjeVrKktEJ zm+;2`OYID?hk6S6qB1|M6t6#1f}s7Yj%xe#Jzsk|MW}-F5m}p9pkLqSzYeA+r2mc? zvH!oJR{-|^1HDrH2fb>o`zO6Z|1WxlhzEx7iHytcKMHil$HP4Uw$D@2xE9qy`KUWR z8z{LRocY=F>q}PK`vr~O4g({#JG^or-*)Lzl9mDE176iQ(}X%v8KXcPCB01B*o6FE zFp{{^n2Y(nyjtz@vvYC6t+n1fN0@d{Xj2c4$j^U&xqEDR&eLa#t zDDq4DL>XNI!VZH5fILAv^WjgK;-DQNLb4;;SFkNiKOuRaWpeY(SP@`^d8&^hDpChS zk_7ZZZN()^mC7z6vM(IeNM9-v!*k8WxRORui4Sp9U!L)!o;gOSw zu}ErTa$+(Sw#Pe&&9}$RN3Rt>9hwlj43m^JlxPom|C3ixzA!H7z!;%!xEevc3)eSB z=+|R(-751e8CuY+*DwO!2M!aV)k2jeti`x+>^cw$C&APY{AqY4ieCsVzpHukS z$!q&+y~aWZHCfFxVgha=Qj#tNxuuxDqaEG_c^2McfI52Yz@;p~ufNfY^xXQwRn2WW zhEHkQukerlEsfFq+Jn*`VC1f3xz_m3wrCF7dPZWWXBqRL%GK3N87#s4VWQ7rks_0t zC_uaeJ<|;Jn6~;)TJlVpYW5Y{nP!GsK~}ag?E@8ZCF*$#O48+!@KHM;gAg{dT%>RV z!!`gWEmv~rH2a?TY{85tSWcc>u6`fQT8Cuu_;G6|TvDv*`|r{R0wV6LtRd}}N@c;= zsYxk~&3N!_a%I=)VqEne&z zLnXX@gPn{8#~*0^%Or%>VqpEi*&6#g>+Q!sbTd5Lu5Z!0QiEuNH2ZUHq}`bV^1G@t zOE%jd&$v6m)zC=At8VhP=<)g!?TywGy z-a<%3CfqgpIF-gzi_a}-cKIU^J#0C7fY3#mIBZYjpJGtH@;dPR%P3H9!D8=j_H)SL zVqa1fLnK^3-37IXO6T;3z^9Y@cr1VGUsiOr z_WB#tdS!SVM;M21Ha4B6%WyaGmPW~?Q#nl@crI5-&d)-#vWb6Z76D1dS>|0K^&Pm* zApy*jl*zw@7e$e>&QN*(Fo|MSQ(9rtjL+-?0RaqbP>KUshaOf~Q(UY+r<8wI0N!C6 zG)mHQ_dw-mxrAMGAoh*`P=~R)vQ2ND2SwKQByuaRV9;lBYKeyWPva&AazGg%$%jVN zA7*8xdDtgS_kk>AlH&7NHh#jK3rV{~Z5N{c84IzX2NKJ^;VJ65t^+U!u0jq?tRj=k zKe=PKR;X!;YTi*f6b(%+)2enD1uq7-o)NqPWAPq1FiYHWsu&f4dTTH*q+E?;vYXk? zGpSN0j4WofRUfeYWw~|Q$@|QmX|G-(Vrj|>I`VC@?Hi-_9qf$4?pf>vVEidS?Y@p* zOXX(20LP-8Y7A#GR-EHasb2H4PDzaB5#jz=#>gAM*FtGCBAlszg*B7_*SNOQQ&%x= z11V4@6xb#e9PQ19MYrrhmPvreFL7-4HZ^85m^W)#N@gD-=@qvS@64sJ5vOt-v*^1O z1?V&rU&iwqe|S9kZ_QL5n@-q!Cp=S2-5?1ONNdboZ*FA5j$3SXIHHd;B1{Rjv2S$#@4$&8{)tsqFuq-O%vBP& zJxv_=zt81d6DArjePAFC);4S5yO?#Oq=}<@xI^aIFtMa%?qz1K2tps6{qRe;igyFXi1W2@~A5{ep8#%sw`0);8yRz6GjS^)t6?xWToCC;W0k)*{@ zv4M;9#r}hPJQ-8y>s(}Yo$2K=z2Q#6bYIhsHv#*c>rw_b<>~{<{KoGG*TiV-I#C)< zEQCGl8CihiI3{y`$(Ij>4`@|xJ+_liN-rw(OOyza7j?*WI>EQP*6P5BgIi$zE$%WS z9UCfWWlXU%Vr>)}OPQ`qW)1=Z_!eDwp>4rnO+P^uXlmt^V)a$ep$R<$-t6%r?w_bn?~H284@@tK zIKJqVjW%5}w-(>=_Ghia-J9{B*SME_9EYpz+;3=BjR?+Y`Mc zCf$cuJ6zx_5VzJc87U3e3AQEkj08aEC8wu=aUMC796M>JSJNhCd^kj8DLxKERldm_ zY^h8_9_8*1l>!fBXL;1U@>5-IsSF!HVC`Re; zazV&sMuOfqT!K+56Y}+8=iHN!}gK zZ{czUfzp?yf?Ww7pVYP1kmUJNANo4Nlyhtl2V+f~rheM6hrUP116k%$7EV zrZg(V_{EM#MUWlZcx`y|kcdkUEEJ15+w(t=h(W^RAyo5Y3Ug5j&1~f4>KOgpQ?@vB zhex3hv^coIVqy{@!0m!Lr|}A!6>u4%=;t7C`>oU^q8Lfwf9#z;^oMco6GnI95x%H) z+!Cv@J;uu!#>ItAUIBz6c2cbqvPK9FGF5?np%OZ!(wa5#5vUF$pqTK|3HdJTuzMc-4aK^e2$S(iQ$<8=*mej#$MDSOkD_@KJ?IDHE8JaWuhr zdX4V%m$?fxjlzUKMT&bVHnp8I7Y}SB>I3z<4bHdCbH5mtIcDO*>k2BJ zCLsW`$pi4Zvr=Qz+oG%`JKvMrVV_9LU;(NgPnqz$bE5rNIETA;P$gQ>6!gPlYogAl z5PO{Qb4NqTyxWPJvpPWosnjNcc?tdRdL2f-nx6lby`OdkK5saHcyK&t5`TZFV{O^t zr+7Op*Z3`|RFm>wi-ahXB`RIfyBV$~Uo?UC@UDE`GY`ufg+5n`)~2%uV20t0-7|t_(L^6^VI%=6?0{ zFE$pI>|9U6y`QfLFR>E&GfA=zM9*R7kR(BV=)j%_4$3!h_3n?P21qY`Yx@S1SRgjo zy_kypN&J9Rek;8?r->jB4Q6ODK9wBmrdy@ze8-t+@-a1Qx7=BNMTWl|1pfXWYHO}# zkWIHZ5BdixMmvFcBR(UVaeE+y2=m=SGe%U78L~36Htpbo{B;4|J$$do+OO>RKBMPi zl57LLP7*bz4GCyjw5;O3iX3_#na1a2I4pCW&MxSlkz!*0scNx-+@1zQ--!^q2a6b|+u?+rjlSv*Op4-%PaCu6d{TF7hJr7(zSvOd4r1b8RQgnd>39?WP| zz*4;&IHuKR`uSDfNu5C0Wg!=$ikZ~Ww^0KLnCX%zmepD??dxsp-P?s-PEVTW>#ki! z#$tic4-6BD+(NX>XK;3XSUBUHPITB=mtCc~R8T1$hLps)a>P?iI}>{#4bHQx%D_eu zP=cDNtg1XUSyPym`y$mYXCb1Mr-QS zF+CTRTn|-=Gx#?x*@2y+X83}a*TOuokbkmhf4s+EE7>}XUUs%7x7Mr1=A8(D~i8i6Ku zQ+`f^*l0#j{hN*-?mQKNj>NFx*_5kf;YohsK(Ij@=cUP{K3>okKnBzo)m>zp!z)YM zh7dOo9oi*G@*^nM%^0%()wBhL%?Nm9^#jHK^*u;OCN)Z<3858gxE6(X&&UC?t=h1N zl%P7RtfS0(JATGiiSw*t(m#f#CX&L%RI9#{YUS9qmZwZN?14#GP5RBjZ@6+K&kL2~ zmGf*6wVm#-Cd+}ZEg{2nDE$iVNSThXl*cdqHe4RvXoJz+0ZjcZXPZz12#tP%`ZC}T zl4#Jg26@iuDM)891@I8@%~&fR@=Kgr?x>yRfS{ zPQxCzm<5X8<8Df&lvt2H(X@=#?;?yPh|SDnOmPy9BKqk8{+7o(6cAWT*rfL5$*WEN zkz&Cx1Mzh^9pXxJlkf~?!dV{aQVpSf(-v8ylCI$H%5N15$`1!glwy24sch#%P)aCa z%g0D1><23)A!7AIZv*0Ub}-Kq@g>j5+jsTdQOd)S)q?y;DP(Qk^^MP%WZIumIonkp zG4ikD%rx!wc11Ao(W!Z=~Z45`OK%m=fCxgEoIQ>57+30C-DCe;}R3e)i z681~IAHI0v>-{AHg>%s@lohQqvkES#n}gRY$RD)fj2fas z5wu}Goe7wC(O#s+dpK@8VW6L#Yrlo{B6_2(@mkNm5*)kd=L~YqJcvA&NGqzvP`yQ2 zsS~v5tS~7pklkgUdn+nB(Ia@+1sdXwLasH^s%h&_zJ>ekzDHdOh5RG+GEOwhf}%qJ zRW6mo`>=ygx{ES`MqsK59yWijQ$tLCHuH5MbZ;?RoGqh-g(VTj(hs}=_hd&kaTJS9 zVLi?v`JCXuU;IeJ*({Wi1nBiZbZOD_V9ol!kK_5X2i4a0DBx!)73EKU*wm7%5OhJt zroS11)KZbC%S`HJCtf_0jye$DG7K46o~keFV+_&`%CBM?Ld~J68HPn%e01x4rA=${ z?7g*QPpd|@r)~UPu+g#`dVUKHeN~Z`^ov7=XM+d^0g*8)p5%X}`k|;aP-QoA{N&b% z_?&z8Tjyfv1BLg4U8lxP$?smL${IDCal@A|%9jfEFrub<>GuLj6MeNKYve z5HEiMF>t6MEvRBSpx`ZTRwbZNNi~&mBMbu-Pf5GZpIuCstM>HgG(|sfCX63MGz-k| zag!Szqd|_x5L~Tav`D~D>_Va}C}nuI6=>!wB!+Duda}JEj5KK_bLRAV6j!N@Y_U8w zjBxQcXm7U)Zb@b1yqeR1~=_fQkM7Xz{7Xq2=Q%E9N15G_WBA*&~K z%^uP50!Yw4!3#WE^G|GEK=;k^aTBfw;kq`1+;D@RI|*$Oc&KwQleS?JN6i}w zb6mTw!?J-^w~K5VLl-|wIQV`a^UBCwAw({=d%Fe+bcQB`OXxes-u8`g@Bzk^=I`k^ zsoBjVAcu=aGD?x`Ttx`VsxNR%pb_-ppZ|u1a{L1w{`ba2|BIH874Uzcu?pWbwuO=9 zA2b#nB1L`;L^4yz0zhd4Q!JC$#FWGq@Fzqb+gg{F|2w}%&h*4&i+%1 z_dh(NIR4W^>i<$Y`~O8#2w?noME0Kx{_lUL|MZUna01x>XHB7&-8u)l&rI!3VP_fE zKZ$IHvuSChYpZ$JNuU;LjLAsOTEa+j;?wqZ`#*@Rq^T)E!6>SS`?QoR{x!-XFWp%kLajgO4L3ja*k4Bei>;qOjNK&+C?R*5{J% z@Ap%cS`LpBTJ9T@H8meJx@=R1?Hb93KD?C(gz?vtMp#2!C>pq9;>E;cDCwq}#u7yP z(?dPg=!a+Sf0xeJ5%Sf2Q1TJCQx+E=t&Xe2uiq4Pw ztX9&&-@?ZOLAUCK#-M47fO}Nh)@aG+DS^^%#8@SEOp`DZ7*UH@gh0O%Nw0}fBscy2 z-#y35*1cP9xX%Nm}1v7!FCOh$;pHYfb{`hJ!KFOEw~bn$_z< zZN8H1ARnOx_HrnUqc_xGZ>B4!jH-Ny&dy+P5AunPLdI1_<3uPuE#PykEQgDFQbDBT z8@YM3U<-e_q^2f7Y6SgrSSH%oa?vLiJ>LP1@9JFjPykdNSwExM)yk+9o5_1+V=^7~ zZ*D%Cg&{AH#FJ?O6)U0FC{h_w*X%$T#t9{}U&1974cG?-@ZN6I0D&>5WM7vX_qmP0 zh^2pJvb`gA3e4Y#VILEZWcQ%|hr4%b5-nPgb<4JGud;31wryjTZQHhO+qP}jDxTH* z?C#icVt1doZ};9`FvoluF=u9enR#pMr*?05AxGMrmbIuwZ&-;fBfB4}LflmE>S;x; zyt#>O9YkCyK3)p-)t%ji>PCPATDP#?dAQZBafhqm9}fc~jQm z`Cw*Yws03q5RN8z>Km!B0S&`qA2L<2zeO+1>Uh%TEHb7(pqRwUZg2p ztvy8kO|%lsp4@_s`;JZQkg8&qsPIj~sZblf#`uK1)M>7dY)(0apw=tqpG#c^O8#UC z0h@s1rk#3yUV+fp{!6&x_xu1UDdbGLPZK9NG zS?BV-ZQJ(4xVARIIKDm23qKRYD04EWL2ALxpgc)aUFYep;k?66Ab2$~a`S6nHbZ~_ zTf@1g!ts$|YpdBgrAt%qUQt{3IH8cV9AB{p5qwweO>c2Yx9LLgA~u@=m!K0p2pCwB z9Ofa)oa6TF{qKzR`e1iA6PDvgr=#cT?wLX+SDs@QS|S3wZ}Mq0bQyRQ$_wSR3u!a- z^|5N)yzjg7Oz*id-ohH;;5Q~b?i9`duB_WYCd<8AbNRLaQX8ON}KIC5{zi z9|pG&OCDWWj~DU63qnS&2QG28h|YNRgEBDlb4^+%fUdxt8dvK^i%O5U+1K&}e_C)# zTmrFP0^#@0XZGJl$0xG@J;m&Z^>gC=i~kTWPXs5LIsu3y5ga#?oWO+tr4H{rE)!lW zpD)@fQhuFg3}DNk#+HPkN0$;tb>j|v{YqlbAWfi_J*P$bn7q1pA2d+l$fs&jv@=aL z)3vKoatI?xTflgC&j6V}BX%AEK4k5U1>(Fe>D2rqZ zr8rLiE(#H$Kq@wtO(d85<>{@78RlZ=*DmtG-k?>{%qjtaJAp(i8B$WLPYz>06Z}eY z0I!|5j0z_2xZ;b_R*x|$TP(K7lpwNSdMZZi=uGS+ncE7BEF7ggU0L)oa$%)`AFt5g zW#3*94KHoPwU91XQ${_97^swctVt50Uubig67?R03^+Fd;76yf9=8(Gk9`*;E8a9x z!?=ujzlMXlEN$jc49zQC_)?F*$CtlEOO;X-Nk}^F3Oh^F06oak(J;=CV~vEVaVz4p zerXh4-f4Gv7S~xQZaQ(=#4Bp11m5Tk6(-w#LzOW$aDRpVyVq&CN($>&NpcBF$8D)& zotYlv@gELR%g=FPrupSu$N)LO4#7~ynd&J1(_E&~%L(CvhT z$k7D0aC1!4p4FVZ;5CDH1UNXYR`FKvfN_Nh>&PnU~PmY3I}e>t{&XM!O;C-<&<46hv>GBSVA+mu>BUnRD^Mf%=?(1$J!!B*4(u$ozmMT)=5t+(qfPv-{g=UK{h0-3gO64#WBeC z9iXh01`&geaWvdj^}PX=2#LO@to&_82^GmZ+`#(l^Cc&yf5uNnHn;jj`KU_7 z?vVsVncVT`bZ$3pnACA}Ot|!$GC*#c2c$>Bb3OP6Ij4c@u(QOGPQRh^ggpq2>_Ec5 zXCMS29d~rTWoWGXvpeMM&H{wVR7Wl$5e6zqmQs^Qy7N;&w9~j0+1yYR{}7L8DIYaC z1=6(IpEBO_L$77EYjD*%{ur=-ZzA_JHlw3aXv9~ROwofT(|5j6X3>=5sc68j)8 z@s6%M17H=aI;@@PzE5`(Q>xFjdOj4+O?)+Yuw$sjyaleIqe<3U%p2bE8ba7%e`ZB* z%Z7$Hy=J9S=a^+bu=7*qzyWOe1<0$5-gFb=xpU$49@lcvRZ=t#3D*St!;Y0RHD z=J1_V8v)|rTlbQ)K#dKDPdoWGz_-cU|1@|=W&`^su!emt@ocAIi8_o=ZZZ zF`fX9<+D5+H(Tln42FHk?K3P{(^=rTK$GerMZfD$d%am~CQ=}67-cfCvW?Md@Ck$} z_ZE+Qx6~#bH~0}gQDf}&Be#>OSzAetu|t*jGvy?ut*QK!!L;vp&G0JMLrL*Yl1xrr&aN@kx!DH3b2!PKoednJ^# zlnT=rH~9A+Isj*%-m{B#ii=AeTl6WFN=u&HBAyMxfT95j)6o3zedwZvqkQOmc4zMC zFby`Po{7J)=$2m(vw`!$9jML~y@CFYSsgF;I`~jEKIh;1ea^$X_Q=>*d&?1SMcH_s zCBB7KoM>Ek3sEP7H5v@sMNgcd0}}ah`Ov2ZXBJt#I4-GFE#Z9hH6lH2&jwqKM;`huT2J99{;yKhcbVl05zWk{pzR}>#bz5wG z*K>PWKp0S1cju*RY^%O~6I0j6XgMiszi;qXdX`vneR%8K1WJcgzgk)AH6#V@My^oz z<;AH%F#O6>rIqWA?_uL2SlfsFR)Af8Dv8JjRpn$PSV`QWRK*yT^C=6b6lzWrBZe90 zg&BKhrfeJ@`rzt5TDdSya+we5jTKIum3(moiL=LO z>0(N(`T17MwAE?m0V|wJ!T2Zi;}R3zq>LIjq?7&S2nyH2`h;- z+jE@T1ZhUc@qXQ2EYpq2txLTzXH_^%8|KDSW>Jtx8vJ4$rwZzQN`D6g{@cv?NLq6< zx_%e$-~j4&b5k?AtDJmUWDF~x$&439X)%TkJ%K!XzDs0}o{vS&&LDe~vN+p-D8L`j?MjPZ0#bJ)j_&aK@B)SI%>rNY@EQ!W>^Cw^t)YVPJBSn~U`HL- zb5bTDP!)O{-U$x)UreMoTm;RPt|0(dLKz}S(3bir?y`iF4A5vA1dYnFqt8oxn4?%u z!V+oXTnjy6#YYNH$_0Si82ni~{6EJ2*T7ux{RNADuF265@AyA5f%Ao@_-oSMJwTI znul*FZT&;yz#=T1z3Hh$DBB=FKybxB30y}kx|LhxE@?(moks6S7sxV7U#;B{%`+(R z{Zejvghvm#rjvSz2?DI%iTeOz>`PSsop|vdiL3wqKmGrZQ<(m5Xr{_P#=l=^D`d{M7KLO1D1TxwGDW?4YQ9NPzx8ljaP^$lZoQ;`@ zjrCu}lOr`LyG>5Col`Y?N_Z;KqMg2B(2S<+9RX;RX#Y<74tZnJdu^=;cE^nG4-WVU zB$IUqe5S^_h4;5vrhMtr5x6Ux1I6Z`+E1PawoZyfI6Yk9)CO~>h3Xly7?TZR2 z{W#;4x~q5gb8Br3@{{kyUXkwXiR)he~C=_ zS;I9U=PZ>4{TQLt0i4-%U^(0W?Y5m!+(D*Hc%%}TvFM4Ln1NwN_ru8f7 z4bV8^KzbkaeTr~YEc=l7AvLPAY<9jG9%EcGs;i^#P6Wg4dV6)4`a!d6`_`0D7+mnj zvxI?V&nD1_IlhG3`VQ((J_V-Z7-+RSi@zRkYnDj7+9Jt2(x(WR-*EnMS*D`3>XV?SypK+hU$OHNa|*ZL9+z#FL;uJwV8Aa%XE$@V&_PJ#D|xS4VqhNC1MPzXz;Z+OV-Kd1P$2!l@Pv@2IcmvE{Fj`%khkK!*bhM3}?`*+~VND=c zRUl(QKQ%K1?45+pMFQJEPW<~?XYcqIEt#O$0d28_go$w7uNtlU!zEkLlV&azWX}W0WZHseer<$e{yy5sns9Er9^lgDI?B&r3c8FDP$OrQ3^`|ETqbJ7UYjd< zNKm{=+56g*7NFN0dIC2E=k~Z#3H#VVhM(ub0&-$wcZvK}bzf6(S}_8sS)6wXMF$Iq z2O25$~Xlcj@uizCX4n<>DoP%c4L)PV`G^Jc+@LwMU0z5aF3;g zw}?oZ3(OiDj5_V!rUdw|G?Nvj8f5U@@evtg!p#a=+ZZ+tIhie#04y?Ft;4#f z1$;!Kp9lPMWM)y)RN&R+(u8f2h$dNk=kA23Mdyw5R$EKQ9yFmgYxmQLNyeRpdcN14qeQ_DfaLQCS+xYx8s@N|Nk00K>!!XTV^_^D83J4E>E(d08+mu3THMSHlWODq%Odehh+trnj0wVasqJ-H%eIn!y0KObm)B;6DAqZT|Ei~-a(K17Q=sU(6c$lfq{>x81GPQ zBmgmmnvdk&89G|K7Ln~*tB=r14qDv1g{G@K$iT4TmLbIy$2&hVbHroJ8~oAspo>jy zcfGKZ_b|{yUQ=F|2nyA(YlkeuOH@)u|5(3p*X3bctynTR3tHSacAq+|l->&|gCezF@XESA z1dpjU+yjMcu@??}8{@tTFF;0fR#-Pa4C~}q09uPnXng{WAB%$23FjJ5O#QrKkgPCu z=HMJkJX&gXYGYT))6bqbF9p<@x=He?D9>Xiaj6wf(z^hP7~jBIDSw?`?U4Jh1*Ea@ zCsM)1{#j&Aj8I{LKeKZh7$l4$jzPF#Gj((-?**`0iVRt%&9EnQI46utC(RBwaEw}^|D?Sbg^W5B;(%x3-D26* z?N@XHk&yF~7=*(4p^fW+Y#};R+xrXzH>^A(GB9v9>;jMgV z=2iHULgyouhTyu5d->Q2ykFeO%`0|)afeQ^BaO^Ils=!xSLQy50YDgmjG0a9E@14N zyB9qGvvLV)=dO9D=_18p4)v#}MWbspD6h+dXHnbvHChH~<{CDY=pFwh28TS#xH?8| zLNi)s_(yVOrXKl=eldc_U*iSX9F!TUW0}2qSAl@NTs+nSigv+EDfDT5U?@a(g?Q)w zPE$G?f{wP#G2P`^ye1Tl#BB)Q)b(HtRsCUoz;YWKpl<%!3KhKD-I2evF3m%^IzXF8 zs$5jD{E$tU7A($oS7x7c$N=K1%M}Wv;O;fT_W4(X)z~0^r4Oo9?X?l+L%YI+bAj1#G^B(SMuNH|1e*4Y6}&}>{8| zDCxA&^x9-8&@qquxpMq~CEK4MR2`hdbpFxxwUD%Ljeng}7e z5rFrF8H;KizC9KbgcGON^Im;-zO_=^$G%THI{nKB@jriOsBqNm+*R-e0H>IH*b%a+ z_@ZtMhC^G}3YCL5F3h+{aW-_r(6gAY(M%WtDn*x!@>b`HfxPqyZtkxMu*|>x@Ps29 zK|@Z0a%YW?DKGI9_@fLXwG6Q^ykc+m8^yro^&GkQwQBDi%?z^$)C)sO)ljdXChg|g z-}CxA$qSE>>?BkdRs24jLB5qB%ZZM2##^bjBJ4Slk%Asv<-!J0@f_UBq~W}>>oYv- ze8gnjq*N8z@6erYa*vE9PaiYLINNP9l#OpjSI)IhDu6pDP;8hOBaeZ{h^NpxA z9a-DEso(AA`DM(=h0bAncMmB>qfNK#*N^gg=8*@hxH4c7h8D&_JSRW!imuBMjP!Fk z%Tu2UI^f@o5oTomLi+Mi5j#C<;G!v3jWAgi1sqSR)YYNWgjQUA$fATlY{x1Kn{BYc zKM^r-2*?Fx&8?VCeK;j&d5JkuIL4EO9S$Vu^(EQ9yRC6@C^VEegfTP_Q^m}tab@Ayhf;xG08avz8<+QAQEz%8t|{u4C+6EXedA8)fk6U_L#Untjr(GA0o zUPXo{7_GSg@+0JipZOXEsBpd%+_N|2l^li;vHu0>W&f~+<9>h-zJ|3W#y$7f=}vlw z+C{ve=M}tO>QvA?yz>vE`}Ule3C^`e>Sb6<8|L#gQEFl>M)9~p!7)b-aW9CU4fl;p zq#Om+66~mc9&XSf1}UvgaSJpB7=f|2(bfL`B8#Bc`@Vl3wh0!HW+W%xGS2~4;f{ng z<~krFU=FH%fgT=GkoDdyxWsN^d?kcj^SVSTq4U(7BJXhwf~Kh%6%F%!=!_pC1aXg` zydHhE$<&|2qJAzD8fiVquWF>B5^v#>;{0fq#6{83V7_m_SJa=x59TD7t&jDDaD+{x zrq=6o5;3Mav;n2!psKD1(eTKKf^wXpiHE#89OxyKnadp|t1j&1#1>-{18{U!8#A_K zNL#NT-m9kz62}uy4VuR(v8=?x<$kt8mKWs;!}v_bGMdm0TDYqcR|4Ke6L~uwl{9T1 zHYb@)k<{SB%H;7_?uy!V*QgmYlJ^+QREKPQzAg}rIZaa86ZZ`_)vKkQH&3T;VL?3W zQi1DCeem<>K(Nn5%2P=4I`-VcUo`@8?#1ck{CTi6U^y7{cQ)kh9wjt+1|je+!dk1 zh%AvPN!(}OQ<$yy;ZZH#TnU#NRErzi7$z`JCi~=n0_ZHTv`$CMFk=dI-^GIxjt~le z*MrH)r~bzOG=FvComDDiYSOG}>-LZX3+N0W3h->rlJw?8bhCCR?C4pn45{k~MM2Di z@~)aM&!k^9nwyp zde56)Jgs%L{oP;uGf?r1_8Mc3tLsfv4wXu3!h9H}?{e3HwJ$xd`YfuDhd>q%rCX>6 zHE-~?)tyMV=Q(KryK8?Ij}>EP$kPAh=m|4f;yopAMERC~T5iSEbls+a*Y_$D->6|> zp5wS;LiDm}-g5h4+P+0KJuNtc2a+D(l~f+)-)T<&bKlkfp?NX0|6d|RO8?Njz!d%_h(=1}H$Wt@g)E3E zY#@qZ37XjD0DqiIgSGyJ>5{0AfXPXX?~1hD_jP5#&79Q%LppZ}j>JO8&@ z_rKZBe{i1vQzID@{ZeUa_{XzK2= z{Ec#`eyTzE#&P!biLrBi>(;vxjq$B}oiW|8!5y(5hGttC`ln)Jdh_p|$;-u8bKBb; zD4tqjWPa_zwyoN7qbbqIph72DcvYjgan_mkkp#6-a(I)tNs3i`X|c3t1Ru+C(R$V@ zXID?8a5ri^!ltJS15Pwi{*bw+!UQ97AO!E67N&oLc2o+wKZKaC?wRn31C*W^awF0> z^W$1MAs96Lpz5Bpp{H_lN%2~HV_`5q=Hw6>p$3fQl| zxuH$_IKMOhh5n}EZR~eq(YwNc*?g#*0Rb?r-%;O_7s(3v@$U}yz+8{|m=#3og)yL# zvId0EZ#Kb|om4nOfNhXJRRbu@g+pOM`2@x5EO(hDBD+lSOFV4y+Owd~72DSvCg`i` z6-0;*k_yp>(k10+8dNTHW%!$}uZr^En&UVHsV5EM<4CxLlw`D2spS|H_5a93g%+LV9?ElP<|J; zb`&g&dKbq1E@l@He&lE7%SJ(i@Q8aTNBddGC=mf(LP>Hi#*cbk$sr5JY&q*#p{l_=C;j#*Q6qf;l0#mrN5IS&;&W_Wp6Jt zo-WW(8keZ#K4ZnL*S}H2QAeI)V$pEB0R4wjiURHG1#=az8ln>jYpq#KZRel|thJh~ z8#}jcBJB#LRvTz=E<}E!Q=`nHke;-5tDgK3baxo!8^9Ii$*w2B7v9~wJ@ga~pTeE+ zPC`nR`eti-9&3SHthkNr{;6LC{771jBJF}IZCb{VvI|8QfKh&n?T$ESj2v4sMK3=F zsGhAxl3@8zzqhZ^Frs+QR$qYH!9cFOQB0x>aM#@-M67$M0jm?W%=2N%$sFwsXMLrn zrDVt1@8XVR6na=zA6{4e6s~^6x-W}i)sl=bVc8pq<=Sa1_uA)hPq9vOmO~qedh3xe z=cC;Tss#-2j41S*|=!a%|h_1M{7IeqQ^pKrWo|M}k#<4Wq4DwR07gP^^d- zIK<;VeXx-p(r{@w$B|1wvcHj*tEK6C2zrjmN`5*ds8ZGh4_jV|!dKh_)zHx|m;-4C z_x_KUOBRFJUpA;X=(h^VIg^p;CMJnZhb#omdI}*Izvy9zonVhPa8`EMr(JcN}W9#q_XhdiBo&bvZc##N}I7O65#sqT|>@!mgo^~+H9@Mx~; zm7j@ZNa^~Lirpt1Q}NtecLWhkX+n$;iJaZOXRCY}x5|}5!4LJ98M83{m_|H`qG6)f zzJKfhOXh-$G)TvAs=|wxiA}E3-y)mj7M?~2sv(TlUu4-zr8x``ObD&oU`BfwL>W&NLU`dKb@3yC+LYMt;!BWb7D)v2#iHo z&6aPnOc|6&3mXWeg~x@Z!paDf{5w;nh2vX zmCw5!4Sle7x@n!cvIEg>2c}nhw<|QQ%g5^(uUBW6Qo&Au4@}bf64luLY6Nzy9V3k& zP5V#^%>;9tydGZ^eX!G|*-@|=)sscA9dVL0U5AJQHraooNggupLb^zTI=_^bE&v}! zRF&rr?Y$P6&w)2e7E_0ek%~VlL4z;>CO2MsQqejil5#Ur)n{R5W6i1GzdS>xI7ptA z5G9|WOkxW3tT{@!Lf=~a4(O1eL;yW78*d1Wqt7Fz(lLjjVJbf25)`S9gBJqZzRy3) zFP|9t`M1MplrQe5CqO^q|XuO3{9@2xO%2Pf9Oxd87WEf3Ow6 z^@YF^p_=rJwCi;6pE`k((C}_D;q>$x03yrD$kQ5VrtFdJj~Y!ior}Zxi#m*xB`%Dc zDj(nL(-l@==bEGB=sHF1Kw=`c-25%&cTQ&K?&tBR!=6qTN@6Z{TbbXl?`Pp5fRwrG z%m9+WhQ7EaN;SG7i`{|wC8ImDa;^|f_3%x^Q7fnNqy4zShPalNiZm}Y-ujsXyTR<% zmfVs3yFA~`uyH*u(>5_?4l;X_8N3~&iUd~}f6zbIu_-qZ5@hm%h?{F)7Y04o@_}9a zS*Gx8X4{?nI_LP>Erby~s@ssOaIXtu?)EfAMf6N#4$~QuF*XClq1=#kceogsq}piy zT+Rx9o4W*j}d1S_5qB}xz_a3_ce0MryJtra{P z+22!N>Wc!86k+zc*@8mlcK8lOGBlHQDDAtH&)1@(F>-c|;OA^pNeDVbU z=S@8T$zT-f=+coL5)|1AK8>L?1}jXoL0=y!e7I%-g5L!c;6)Z)m9(!7wyRu@68%9EPVh4YbLF)d!zca>>xVJv{QX&Bm-ETi6J8umk*BZ!u+RiJ+|T-kcuFC&^|jKkBs^J1x;< z-m|r9g--*fZ6qS`qa-%*KpY^=3MlrTd6XMSM3E7~l-Iq!W;yG0iUmha&jPv$lgC$A z-1tK0MX0?yi`9JL_U?AF2vq*L@a>&g_-Tb@@o*Tbu-;0@vLPCxo?f z0nNrVnCMp44fJvKzzo2qdS^543+8ABtA;$Y%o@en-u;-Ga%=G9(G%-d1M~GCAfbs-&R&`yb#}_| z(wm|iO9(NeBY7Ln{0%a$5X#VZeAtBgoeBQ2gOTQBxzF0F2==YBGm`;g&nx;U}A?KO|s2^UAguC zG~y~;`Ok?i^hTXx>Hgt1)8n|C$}xsWMlnzXG&UlU0sYWZ_T$r7G+*9<2dQ``l|XX% zvW87weG)Y&l;cUBu2Rk+L@dnN(AX@z593y~#!%ZsiyQYG{^>B6$MZ-!DP=sA$)B;Y zwPi@Y%Ld*(0+=nOigC0_X4?XglW9ty0G^?8%ep^8#`R##1VOV}ySkD|EGO?Qymk17 z6`Cn*ScQl;a&v9j=GKt(1mj|;=fgT3n>r^b z0AJx@uUwl$XB1LeICmxAHs|iGP~4?iH-;(9x0p<*gjS{!CdxbCFSEqX$lkv;vw7z` zPS?g{yM}c>WU0r@V0UCr2OK;KIs7odE-PfbOtqLRjcU=2TxpfeRJ}3Sr8lQ{vu8&C zglq(5%c+d{^%5_7X^NqRHQo4&zD>q#z6#4a|geQA*$i|C~p#YSn)*4k_6Bg~}}|m2$by-!EFo4}%Bn z)G0cY7jV3(1Vc%SsILvaUHB-?1JCW(*DiPJNFxJ1OF*&(XS``gASFbTfJnY}z@w9Z z*sE2G1lb}xZEEjQ!wzWTt`TH9>_DyT*F-~`n|jp6P+rTmjQ5ZQ9?l@xck-KT z9l+7}<0kBBIH~IlHn|g!o$D~s%8o5T{j{lY+%R_pbcTzY`_=aj>%}#l%_a-i^9!C@ZsqB6NV(NxU^zMG!ebOs zag*HAuODF|FLZ*@4>t9f1UW>rbpl|Vc(WN2$TX(sBo;S{EGLgtB#UtVN9GB6f<%%N z_?%;!2OdhgI3#M(F)Lay_Ii*}ZthSw!pS^dKV#Pzf7tdUz}7Rzg-S<5V zg!7dpBBWXzO8t4eBsQOSi-;EnhJ6O#axCYON7g#AiOJtU>#K8LTNAuutw-r?s&u9sIKJr%QJLjAP6y*EwP$9tQH8oC zOV`YwH#x5L==lYeW?M?3Jc`zMzjPmnVRv95Jx4@-X*+W%M#6V7RSy=5<%_F`$7`h5 z78%M5a$6jFf~9$4-3pUxX^$BVucP><)Cfj&CzL{eCg_rafh?)mr^l$#*JP7*-d1X! z?1@Az6FNVx}_{64NE)!U_Cb05O;FNpay<93!COieC5bhRFyHT#mF@@%I&W1JZ z(F~t;9Uw!eUovwKFbRuDaSkt?Cjpe~>MV?1u?YK=DB}=bq=%VmihArTGcg4POl2fs za#hh1bO%xbo)Do#;;bwZAcOi^nM2{Skdk+Kj^e+w2Mx88U@aD+&MR|UxS?cf(xc^a zq;l&!1NmN3nm1;(jbd+|LW2KRxCu)q1(gt_)3VL(-h)a4Cp{{MT%2kM4g+LYIdYT z>QNr!JS&P?(7p)z(-v%6rABbQlF%~H40LNW($A}LHw&v2q~K>-hEEDP0Db$=7weHN z+fHa&%UK17=|_tjZ(T{P*Iz0OR-lLrB>#ysxi)KwtiXmqrjZ?LH3WY_Ib~S5fnr@! zYAS_@yHoofUITBdNq$E$TXvepI4qm^I{?)sVM)*^36-Lhf_qpEf$Hk0I@7|GMCw9E zSGIwP4!(4yrBAITjP3Jj1g)-5E~428h9C9M2~ga+s_QYFmaJ)=7T8nLefuBL;h=`~ zrcX3qLIjyc?viGJ4A?MxaW(9{)l!h!R-i)p^zetj#7Q^K8d=vIde_~!s_RRu zP%a^wK#&*5c_XA!LYdU)2%UADa?o##xFl;|Vi*-KSHqq4yN(#;C9WFX@#_23q#)P* zv0(7#Gw0Z&BaqEKg8Jh8PRiptZmR;U7FuNxa_TUi@q2>URMmYgbqPh6lI?7-pd%oJ zu{eqaqdciy`jijYQWEBAy0-(+#(*rJ(>T=`uyFJQ)YX}|J&u(|UEgTmgX`g55W;u# z;U$bw70MUxi?0dCHt?+$p0KXNn-t{!=P&q&-HH( z!|^Y~^nW=F|8x}oGh)l}&oamVcSLN381?80Sm^)LNcf+tB>zzv`L{muzp5nvN4tUP z-`NfSc>Di81pQA~nVE%wcA+O)0q<@2=; z(M7j-(AsKd<>fjxqT&=@P04)|f&viTnQew&LwH(sEfC_&cblRHw}g{Bm)2P_$xP&c zURVOVgd{^H=$1IbJ5fYi7S#?D{5PKH;vRx1m3qiLiF#1{Q~+3wMBM; zz9?o`Oc4~G=0DH{C~$gyFIi@URPY8Jo>&vlaR(bBYPs zLRLXWiAdI|B#K9sr-09H%XWL7sN@K8m@aSlWEQmoW=6pw5xD~`5$5FQ+627$PHih+ zZy+#+###4N8^hq@;QXVuR1P?7)-VVe?1jUsE;7>y=9QpGvZFjP1999`>_f zeApZtCBaNZ8thWuU!mQ+(NL_hx(zQZAgJd#;H_!FW?K;xBppx0HY9;#OvE*YUi{I5 zKJh()br0<}hEw$g3;OM$9T!8T02g~`&A*wKQ^0Zs)lqE9@{smVS`l3Qgi_sY^2$Jb z=JRyf`Qcce0kBh9x+?Zo=X1E8gvDmT-{79Z;oCw472HG7Kf!*~tf4N5_Mcd6$gK?5 zh-7`fv8i_rRd(}f@W&8Y!rU$Q0@(CC@9o2#ICKSl7W)j!5 z9tl5elcN)8k89^Q>_*&9P%qK`xQB(PP z$BuUMIOsFL=F>+Goqs`kE^oJNs8|2>ifLI_)VR--p*qm}v?vdO?w#ZC7GGLDd1hNl4X9A^uu zJeQ%NEtu+^C(p@sd_wLaUr{b8tt!vWVZECpRe@grm#pNKfrQ3_)((-HF}a zifzFsa?OhDB?c=e1fwj(YuII8i&hNvpcgL}eUF=PFrCAzhsD~=zMfliuelO`1&0RMgJ@(6HH_}oFaxIYAOtrd~@bN|?^Mnh%OyYM) zCO?o#N?2a@?YuFk00Yk?kMVu?5iyNXn(Sl2(CwP9LmRnJ`Th&(ettqjvrY4=u<_=K)W0> z2Mxx|P2$7a74BOfLVBP}`gOnuVUgO|d>kc4_zSQ;XoSo0DJ+&s3Ugb2g2?wmQT&>s zsM6tK%G=;i3o)vOcEl$!j7j2^{+`436gv+==D?buhokyHhxTtltLcL^TGU^GwhZLC zR55Rd0Jup?T57}~^m0y9?oiP(v!`f_^Cg)jwH-;}w}u%tUKqBlu(Fb7Vx)I}j5(R9 znqOOF>MUn$LPY|{h-353x(Ig+PAx4X&_sd;+Bsf)LV(95%s50COv=UMMiA&*Tlo;z z1)_w`pzhS78oaDJ zSn(F3gdqHb<{oq5sYL2D53a#dI+WthjbOa%g#(ze(#!VzB`?ff%{OoS7z*O%nMQ2< zr4E8FC~0IkVu1^CIJO9th1XNee~3SaFo&Mt8?A*zLcvxoU-v=}H2A7hpc{Kg48^?^ zkSetE)gM4~TQrHG%vj!ncA*<4-@anJbd%;~jRbqe7fEs^c*9UUaSZA1!7dN=f43wo zGI>Z&CVkx`;gNc*+PwLd z%bKa8y=D6mxX%#BG+-GO>$y1AAFpnThKGs?uBCKh1PqR=(wQ6(Ezwva%s$ZtcPm`X zG+J_*BI$th`T2Lun12z*%Ru!c0|5009SlT=HM1xM0MU`f7Xi%S@_ZlLqwQPNI}(zA zn1*Z^nJF}bJy}Rjh7OB|Dq7+Ttd9f|&l2den7jjr-O#=1OTnVzDXdak-pi# z(w&Z5yk)I{u`q!%__*FUYK;WmzIS=w+Z&xu7tQ-;3bE^yzpj@84DZKj#%BgV+P;3cMd*`y@jal1o~hb5vxi4N`-2agSydD{wT7 z5BYGyjkOow28~m5N;IdAhMWFk+5^n~1Dgowy{{=&r-c+ekr_#WZ3cU4VEwh(DcYe< zkbtYe&4U{^?FNjYY6CGHlu_xD(?n}cyMdM$30}Q`7TJa1m0Xyo!NqX$2tvgV)D%nQ zd)e@f^I_I~vlOnofKvcQr&h*70lK7hY1ILejk<24&Ozz$S}Bcq#{XQX8c2saCMz%e z$e`1P1@4@36<_A>-T7MzX|Cx#8(dAMr^EEQZwQ}nU_TNI^8);}O}2UytGkY#WWBB0 z=C|Ad=501^UfgvXG2%u5j0D7DejKgnd>(jx$ ztN2MSPB$a~=XC=UsGQ3!T&LRk*j-xb{#Dbb2VD&?hh3!8zfFMGdu~AVL|A5y_U$`Z zWNj_-*D&})_HJ?OdQFY+v`(x#?lI$CNZT0p0$*z&^Ixw#(?QazZ5zLUbQKVbSfc8s z226ZI-s=BIkkwDozKA}JEU=1O28Z-1RXl+s_cjzV6<61}BN+hI5 z!tJd@SlZAT#5b4LHD*iH<(&i-M&pZKkd1zs-3gC=5vtT*z|wLjx>x7ZR~v+l-<|F~ z33B;_P^+CXVoRbiu+8snwh~0D(1GfZ>CNc52w#JBp73Zb8Zk&)Yc9@y^>J@b?NMFL zA8yPq=)R>t!Nj!@2Yf{QNggHpo#y>`!xWT`e9@0*eM&}h`c)R@)N8IQ$Rtmg!t)2V z;l_n5)Vsj+_wN!qP35GT_&++cnR_w+i@J9TwzSRGHkWPNwr$(WW!tvRT()i7xoq3E z&FuWDcI~dcqiX+uM|5{|pU#8%KJPr4F|Owt_l>-%9yF~s3xWh?&+3pnF%e}>fAgSxJ>UH zd2fj86l_jrQ8MNw_0NZM@CO^?RX|ipIhQ*E^E~{{V*;dt z@pG0|8t(CTJPOaQW;dkTZ+guKd}DB5{L*xQm>g?CObuNxl@;JY`o66eRvhYV(PAXf z6H1h%Ez(oipCia-#Q4LrSVihDXTJ`%{hlMq7DW&eAx`zN(ZYV;d$GN*G)i~z z+!+kut^FcY9<9l$jyvU1z;wPV((Lv;j^=QEmk)O%&;b>y;~aKi*hddd`C~Nn#^Mdl z|H5#47=YtrchUZ(I3DxHzUersqEyK7b1$W+fe6aPS+uY{LqTTtwZP8CoX~kjA??gH zv|DEEh_p0y+I#|7jvhzmxObc-;OAXq;TLpwFHy$&@7z%#-{PTEXjXDr+LQss;Y7oV zdmrgnSPRE1|EFm6-~A8slk=5o4JOMSWky~PXbm3Xt!`wu;m|m;K|O=hU{-DE!%_MN z1`qmA{2;}LJX-4LaP|3xUY9>cCNp?0E&|aJJh@q)FO%}0rHZ@iOVm0^Ta)8rF&r%K z*Or`+=uT-~g$6}cNO^qFyKEuCor3}`9Z58F1o07J1R{wCRl{K5g7Vqe6XzEo#X(*Z zI#Ll*oFx6q#w$@qJCh!;eeKA1irVWNU$s@`H>OI4e>mzKcgDckG0@H`=CsML>^;|c zAhe%h;MA@N7VVV22p+2ho|>GEX49OM=nuWrh;qBu5Ntm4Y92zii9pj5(3g2EqBzP5 zd^O>y%JTgUDrZB zEzO3ji8&MH$cY3~!#sp5=j5s^#C1C;gT`CMb=Kp6pD&ZRI6Y>sQtu<+C!Wia(?$~8 z+rb%eK^8#l1HZd~a3f4YK3+`EJ1Uz;Vm%8GGLWgZ#q!)Jq4Vtekgvnz2(El?W#-T6 zrd)8o&kJ7#^Zmlc-^ZMjZz3T0_<^bh0_w#tPR2{qrNb5E(q@a1Ri|gCAL|JTD#8vo zucyDOit3J$qQsRTQT0>p#`hP&u>23y z&|i5UeO41@I@1+u7u4W^fg<2c@W*NB$US`!6CQ2DBJQ>fUXJog;hn32eSTpMDfwVL zKTAoo1t(e;V692|j(iLnL`f2@HGS1Unwg5G#DWa=F3Khpx;W%~eO8)*r*-{{q-a+* zb`QtaJYA1P@5CM<#Gdr^YTmoatGPykB7tRw$mez69~0JQY?m%uwRtU9pxnwP&?8ZZ zy71TL<;9ZSfT;XFnN+f&!H_J@Z4LqTMU|PUKYDJfST_*L$u`6yQoikC$!`)#?=|e4 z3W3tPIL!D8r*zgQY7afr^;`EvXN_d*SSRv2bMIVOEZSU(Z86^f2;M}?RucQ?d=u& zb92hpc7S%s_g3eU#v-0K5Huo7`8ae3P6?YWq=etuM+Sk*^_I4F&KmuLEI-SjKubzi(MDC&_pGPOkW zD6gtHM>zP%@LDYIb=rMJEI=9eg+b2CXf$M|(9k4P^Ig9j`S(#0r5F9YuADy?6@M-Z zhfi3OEfXSGN~=mN2p_bgh*CRJ7xmU#I+GUT)f{2gCPo|+63My5)n#-SHI5C7ipIlq zeFa~9JAXd`gXmimx?=(3r&qGW@Xo{>aSFk4zH%>P=_@I!1Eyft6AUW-C}P{1&=Ak; z2-wM3z5-YQt-8{5ZzBGwN^*NliFXuVzsra}aah*XG*!#?79u5)c*^`*^iT(xeZZ-mHQyw}+;Pfzg8d08atl^O`$A#E?$&h-)4o4=`?cmORWw6Fb@km80rFBkh`UwbSW`eLf`GFqys@Aw z@d|~@1WU;47nUbKg>7!;g-OnsAyx_}3oU3EKywUqj(`Ur1I^FmV)M*q+%iey1q(5_ zQV7}EoUq~%tLe8N&2)Nfl0N}5No97+StOYZ!g&Q7IQMJi%L~Ddng*;(IMwf*1b`!`w zn5@=Yc?AQ<@~8if@#gQ(XN(yX{=$v9)y9)%or*=g-7EtJkm&#x^mib_HArG!{TbDf zyX-_7O9M$N8e}3KR7*9mXCIlthUy!**$~Nk7qzDWVeS+Fq$pIJ@+kM-fbi9$P{TM% zj#XX2QmxwKPLv^The89tp??i#gUY5n`$>;_hceK_9;7b5W3lfT6{ooI7+k9Arm`VT z9WUkkn4qCVL`EeEh>xA|!D0!P~|omfLE7z5YFZzoKUj z`q|(B!8~oyE0}MQm7v`7vYnTw#!D$wYDx_Yc#S%@aj6g>+gZVX#70s|-FY>U0$37$ zA7uae@_SKJYsyQBim1`O+MS5io(8%c?r6wsKTPoGi98aUBG}ZVBh@8Ie%}<}WRdh* zs2GHmAq}1f@12Y}{eqf6s1r6}bq#Ul%>f_C8&K4eN$t!^Y2RhY5C+NLuGc`#+Vj>T zMw&80T}jgQj^y^#ItpDA`uR-TSo3#9P!nQKA{hOsWbbc)u|c9oz}q&GBdLVIJn*vP z)+*YZ`r-P+R_YzB!&-D$wPx0tP4IlE!}=@oC#M6Jvwq(dtJ>r-ob854L$_RdwrvS) zQ4f0{v$2Y!Z2mPau(okeV2BwtR-;VDmc6d_B8<4QDabZjc}3ze$**~2@>xppXCHAK zF$Rw6RqkNKmJPNns~nzI^8(s*4%kILSpc^*6r^BOCn>Ou$kgDDTjPm^zCFc#@*cpE zp9|{=AqSBm_|j%3vb`ns$n{ob?{@{PTdoY<>yVepE@m6hu4(oki?uCQp^d<#NYl78 zVN%a7J3Quq!xjcijhl%PGGwlyG10;H#BX!Ut(41d`|$vrbufDE_vo?{aXEMKy!ms8 zXH`sI%$W2TVk!7?v3;>oM?Wu_Ios*%03IYc?4-x3j#A$Ok+kQhvwrl$S?l=jLB|-$ zugOP`VwKkv`FXyLQZO3Apiw{VRF+z*vzi>Ccv+%$u0-DC zW3^~ySV}Y9$$CRr^H`(DeDFBsA}2-EBA;dRr&D;n^59{f|8^M1(IYjH?p5)knHykq z6^q3yFON{M(7?!dH7wsc0B3Iu=B`?H)o1<2@vMUh)GWRzXJ$V*mD~u=!)xgVJU2bD zkW)}DA89E=Yg)JuZ?EcC_afx+uw7YxC4&*PXjyhjFuSJSdao$fkY#V=227Zyh|$Ml z=2JNa86ccU$+oGDJ<2M{qEbonVetd>T+#<9s#T1!K2(eS#OZw8z>%HtK+Zb7(eFQoD10 zbABYW>CMA^y)+X6Ia-upzuj0ZCA~?rwW##>n~}!i&$VR!CZym$uAI_0&4SZskTCr% zfashr`M$M_I=+;Gpeto8C#7Ins8_)p1H`EwYnRePFC!+o$+K_}daJ+9d}QC^236k7 z>RBDdGOt)0(bWZe!HLtVJK)p0F2{qK4U&|$v|XoA2~{{_=L4318thYR_3GNR&FB~R zye-~@GGyogu}4XWT6q>4(S#F+TLIW=-?CnLfPAi+-%m=t&!plO`1iPFU$Y_lFh`j& zB3S0%4z5tr$-*JVyDn7FivD~d+w95dv}dIGjxfZS;`M<7!>_C52NFYRwA*W}cqm!w`Q|IPn^DMfa2pTrTMd6pcb( zdBn*(z3|e#A1^FHO6e?4_bqCFRqk9R^iGx|)?&^yCJ0c&w9qeIzLw^KCJr!YGmp`V zs-6Ii)R@KreghXowTDDjCpW3fmcQRvGZEexerAO0E1GHko7U4kpvdL756MR{Kp z2+o|2;P(@onp%+Vy!m&6u1%*6w&3HIUVR*zzM}3nNwc6ohumC&1R{Z_^1VP*gQ!0d zXnMm!?~S&r>li5|bvZuC_-1x?_C&f9jgHj<_qt#CaG{kuWj}`^1T5_+?G~6C*I!ef zf63R52D+at3kYZJix|yNXfn^j7z5zJs=tMF4J@0tDHQL`bGNzyijfI6x3D5I*Dd{kU-9;L;)G&U?R zV5HH0cq^o(9;sBP$!t(bZeQV`)jtMq&R`_5ZUK)f*Ammo5~_~}R@z!mVIQ&s==9;j z758z*DYXW<3BJbeNhXE$^{Ng73+%Q|%`Xm!x8V3GwC^xIw2?P44W6HAvX#2BBHNXUm+83@(RgQ;#0ih57M ztqo=|ANl}I#=HhpUht#h(Lcr2Y%#vLinW0Z)_FblX&wCA6vFFE(J`K_4DrXh^6Ak2 zt0F_GDK+k7UVaaR5MqEftzgsTvV^%LJzR^9jrgQi!QtS8w(-I+3R86&TrtAR~FVb$y8E`ajfBz)y-}aNl6V) z=0X2%h4z9|m&`Us7hag73}=otW|Y^O&;bKEqOD6d1HA)YjX(u;R|5X*V5F1raDh@~ zM_aDuv?Qo>laS(;8YW~Z>2(Hv5YBcAoK9osdi`3w|&fbI~eS@9**~A-Er0 z1_|GW=TEDIq@taszu`%Q?}(zUR<9gw z-rp>-iv_3I!~E2tsxH*8S1`!hU9t3Y!L{rH$o$^FB%|%@{$3>92(rT!)2BtT0t7A* zoNUJx8}y)7?XZ0YXD02AV0 zmW_u#1C?>}*&^c$I#)(4Q1kdm=@!MYKu1=!iCLb|k}L}w7+@eqY`vYAd!pEwW4v!t zv&D;fCc?UwW;^7Sh^7VB=W5DF5|z^lw}$3Hs)Ip8YITUXg-5GpsBVdNiY7+iwS_{2 zyH=Rh>Z(FKM90@cBe0j|hIMB`_>dYtjaH?6v4-!mWgTbPQlO{TP+Bk!07PW~=vFx$+;rd)p(u#Xx8&*OfFX9j>Gps#=)?4hT4t)%|!!k7-aMEw z02#Oo7Mb!EZDsbY`pK?NKTB@Uc<8`DVZqL&tu^W zWGqSttr|Rc;w|!%306~b+Udb5Y&5Z5Gan`|2j?5}fKKA5$}f?ym1)w)m%s}Op5!14 zu^XI(;I9-Yl>854Z+=LFL$yOxOa)%~iuL2-BLMg9JAX76UIj-u9E0h*541(;b-PX1 zuTZ7lg%h&;izeNg@9! ziQ(T&VRN5v(#nn{**{}pQztb05TGvYxU{-OENWP53#t;GS-2+;iE{$cGr`( z(z=oT_N|P}O`suPKu<3zGps*fsvODOf!i`wrQ?2{pl5Gwz1p!x4Ed44{Fe3P+q1=! zlhe`aGiCd&<@#ixj;Fgil|}YVmCG)>X0smp^?q^nh1>c5#mr-SqRxJe*6X6vuDyb) zwK=)LtC8e&%ow-NVDtm$PNIlr3^~V?a3Udaq&tRx$wU>u#I3vC3ug6x6lgeh;`rQ@ z4?a!*VM(<=DrpMHU9gbIe~O81N5^9__xBA|v+5BJ~qW+86(X8EmmX6fp0%!ppBJivBQ$PCm_wf&%u&!;nn47|wst(F!~ z0pE3;pAa|ldhT)GG$Eur$G=lHpqqUJHt4&+HPSt}^$xf3URDc^K**HjWB*jbN%^69 zAdr17yh8o~kidTcqzHvDaIa*-3o-I-9Xi;`f_7m~2o7UeE zNhlnZ@vM*;Gs4>ePAQS~EGI7z@k?<7Eq2}*F_3gOU^b=QQ-TjzzD@RtvtG**925Wz zBp#v%!hhM{gl(Sz_}U^&O)+2Gy6FsD%gD_&Di0xel6I}_`&T=xsyG{C_0FoxzZN~! zDvP5d=85BX*I-xVz(E_w3}h%O(zbFm_J?T;QERLz1gJjFAqwjl)qC)tDr4!!!Qhn# zZ-z91ewod%5Khe_?Ik}t$R7VFZ^Aw@=IgyFc1sr!)3OWsGE%zpg zLE#H7vsF|0h5hN}5BPDOe+Q1Q-93Wa^Io`F8lU+dLhjV}{Az^&VXopAQi>$iUI=;_ zCb<`ZBnGX?eka|nPfQ8dlGl2l1znpVG|4HkOKt$cT8wit{-6cowSg#K(+^R7Aex^N zAXrdEgqLiHF9UtU1F=fr+9;97NRCeBnpj$FGa1#hT_)~VjL>DJN%3BLVhv9^$FfC(we6=A!=<8SBUe1>=ZN+-_v%| z3D-2VrD>YDrvu?Qw@6V%R8RqD+G<9@B5QjQx))BZjcmCl?;K3^&k`{V^jX3JHz(a1 z6~}q#vuV$>VV+j*=uzKFW*ZM!PnAxg9#_MX-ECpkyHH>o`&Ri8vn`$S$B{_KmLwSV zjLTp<(j?9yL=6TQe^{uuCvI=D7zKS&tL}=z;_YRgQ~KibH6@bvA-IkR#aP31+pz>V zz-J?z^?U8KW$}C(7i;njd&`#QtfwNUN}nmha$q5kH)&^M}nOf3_H8WP2(F?6VGm6GEiy4+Q5y z$hykU!$Ukb^JO{j8h5u6@aq|6FICAIgm8kNyhkjc8c{Z?WJ;k#OIX+lKH?9cxs;#=Wm@5H0qv&)g>VLffrMY;lX=QP^Rj}0X+4}i4Yb7NE=&)4 zVbHdRcHr0fIn-p;s3sJIo9!zd;$l#j=F0xW0=7}k8sJND}1=`wY z>H^h;+6iIL?djH7!(QhW8*RiHNUC6%s=w)Z>nri%>Pz5SX`=&&5wHS>9XPDd*T5|6 zj2R4F90mJ)um;k)_{weZ(IBwg+?8H6*H`R&SwDtv-#9DFyij?}_dm|#)P_8Tk={ut z5?byK#a|c3<4kw@$!~Ksi;L>oi60~lDvaJVJJ#iT?PaulH-3NsHuquKXM?L$y6U@E z0lzkr($Lv<$#x8F$7~N`Tjh~!t2e5-K=did%c2Fvv3H3dVL9fznqC>f)UzCAoGiD^ z28GAj8Z(EuyPWY9Cy;S!yrLL_A~qPAiqX=qHC3`fFk!nlc)OW40XMLQ z9J}1jjX+1Nx!r^z0hul!{AA6Gxhs5B>N#v9=d;7v=ReC0!Nf&49dVgHS0AN=ePlVPK1&KF4CQ3&`jL1ja zZ)sKnu;(?jbi!_8mg2Hb%kF8`nZPoOxF8{8(C$w3RX9 zQ0-Wde}RGmY+7h@YaZ&BU$z=qA+6W##p!t$i*8kt*WfSrrIPz-U?@a_(?qv@)*td z-I#r-l7#d+ie~9^Px7yMfun4%{Bo}R^)Op0Xn80+rpIGp8%D{6BjhKk^i|(r@??~> zn5G|VNT-T)de+r=V8cXo^U|N*CHj(eq#Emps?a&Q?qR61IIeO=4mIXrLsCxEIev=l zOxSE?`0pC!nYiT*U&hg-i)P4(=@B?HCyQ7is;<2q4P8z49F+-DD4i56=M9V~YIY{l zH@Q=aDVvc8A#T*f$QvKy5NZv_)mgn{m=%J^6@?19Q|fD^}@_3~Fr_T(0H$QMQSPdQUREe9l>! zMmRT`WPh1W+w4P}XtiQoSHp|fg4uX$h z82$#pbPEfjh=N{^n*8SL?eN%4jbY5NENpdZ!j|YXUVvmPB4vggFXPwvMs@IqB@Mk0 zlEJz3!6r%Lqjr`=r|Q>8b5{k$v61|MDb6V5_h9;&S!n&y7wOI_;M#=wJ-TGKb8Qk2 ztJTE9hL)2I9f9uiq*ASI+4%(Od;cz{yO)THHFR;z~2 zyl5Au82_NsJP@ZajM0l#41GnN56+8mz|`qK(TEo+7@#@$q-1~=E)v)hUI?8x#Sf(E zHbAoXV|C*ed%3g5An}zsKtTISrqSympmHq{v4{>xb+YQYHua4UsW4+8>aClnPD~2&n^$NH=9yaZh({8*ODJ~G;4eB+2dwIyQDcg4nmd6Hc z4?^1Z1WUiyReaww@>2fF`fJ*s*V)Bzj39|pnhP%uXtWGFy)Ia{5Pzp<_^Nhj7i+f2 z-)Iso?P8OR%R@7sn1_*y@iBsHaUj|0JYlL%DiA&nB}{lKz(_11vBAP_NRmN~A9MZf zw_I7&YC{aQ^FV!Uj>=YznLxCZm4E^Wg<|86b=rR#K-S!!P_|w$|NYVP(!D03=(@?` zk80c4eWZm0C0t@%MF-VMobLafgi~s4@LoeCR6i;?^}8`qsxfKKbGK-`m7|<8?)Q4D zN^idc?JIgs#Xfb5?6SP7c9T|*TJBGPW%T!ovlxJ@S3=?XN{a*0yM&|p1*aKSFm0bx zhFikr&^bJi!6X%1SZrXMZBNEnbt!A4oSXteaETYuZa^YBLZm^REkW*2Cd#rZaB*hI zvw2@)HFGMg&tYAMHszbT97<?=}mEFpim;mdrvI zpQ0mV&q8x2Z^sJh94#FzyI=N3VbqxYH=DMve!ioA$+C6RM-NuIHum07&;w#Hc-abC zML(Xac@j#D@()EVXy}4lAQ#teuF;#yuTeUN=+%J;t*4z zE5XRE#3Y>KlYKvKoTC#8brJ0@X~SLWL@G!d^qer}aF!|Jl**tDGAy~3WwDBZ8yb0e zMiQE8esG4Fv3`_O9~&@3*Ck`{(iTe*lvrIBdNebbvR zW5vmP@%{%&-knbGxXrR~mcvFAugkX5P7qwaOs~kccBCv^7!Hb^0s~qhj<4W4gPX#5$_PB&4iVwnO#dXu)<&M9RVz zvZ->3af&d6j#R@}@IX||It;DsjQ*&^zLGW;Y!R64_qrcSry;(hYoS$g^0$7&UHtMg zR995%I%dkH)6H;?)FkD&mjJ0A^dSO=f~#If!4C#k5xQBj8_rsQQvUPNMFs%Hqf&W1 zTEn7%H?_zCj)+%0Z1POF>Zgm`ZC+OFD*6G5a4I`xAlv!1In|-8XlJ;v^9`MO^BDw|hI9!< zur3L^udMhSrdxE@6d4xQKqkt{cXXB$0nj77LIM(SpII_sp2@!+s9=gh+5;Ci->7Xl zw$HZUn*yVwA{RFrlf1a_mvifn%!=MmtI7lT3x7-}CpRg%KB=3T(485sXfFKF z(|jU{ozkYS?tV|A>Q~Ur@r^QQEb>S=-hmVn6YO~t5rfLjluY^#)zwcmDV-{~Nq@G0@`W$316uc# zHc!Ic_TKl1r%iL(I$YH)e31Zu!QE7B&s1{Hd#oOps^u4J{}kwKagPKpEK7ebjmj*l zP@PZ_cTcp2(-|q(1?nKvYEF!ef-~tCMBN0?ljbR=6RE~<$?0eCWItEUcPC^jpvwZY zwC7tjISk6%(sA3~MC5l%B#;myPk!2!mG8VP#_KMVX&Z+O$IxbJlfj=P&XmXQ{Z3GC zlJ0|m28d!)%X%^;R?!44-|L_rCzBR*ZjcJh{L(OmWnsr)>+*B)K`L_$5CtBSqD>7B5{-F|A|*~?>lKj7Wy5B?I+p;ywbl{DIrzmVysM<$mma}HjtT>OQ~8UH zh%lZC0fbn83%D{Y(4)iIbJpi4kn&%~XpZPeQIAq}l8m#Eh~Ojy?tHSt;w5D&abUR1$iTxVO@wdeXiDU($j=PTN!tK)Ghlh_Im;iy+Tm^0jG?IG`;= zpD&{LwALm`3n&geJ|~4J=BGk}ePA*Zp{#ig+@W>f+8R96ethJG-p~#;#PBP%@3dnO z9+9sD)4f}<+N>ZYMlIjVveZabr(G(HSQ-8ZX$su>G4j!rI2Z3{jE2DvD9hm+oXNyD z5D?bv$bh9aLYTKdh;|DpPbypoL2dd5C0y~%k)@JHAxm)@&C7gpf`Fx)Ca1$B{{9EU{(!!I_5wQcL+m#t^fz0xo1q5GF9jIcatyZhk0 z7^iepw)A=5zGqgtbQI@FK(sgZvV3fsU%fXTrK&pO<*F{9{fe8YsO>zUHv|;$ZM8)> zSeq2=Oh>N~m`+;Mx@+l~fu$*oOw7w@W7k9t`nh^oAA+O(SYQ-{cDTP3PiC*CRls@@)DhXr$*G!by7% ze);|Bte%opVE$LQ^?!~t{qNT4|BQPZ82)aZw*C$G9{+tE{6D1AG+6NdfA#!-T1Wp+ z-1{FJ-~ZSEXJq)Byh$(o|3BXQw+-uyG6;_D3@*m^o$;N5-Nk4BlOMy_2otp$qkF~gc5oQsec z5CEe_QN$Q$na7Ky!+3%BR^2zO>+n52`|d{>(t8s=i~2ZmXTJ+YoPCld{-7D=lbO{z z9x#YVv$f@6%=kVL3uA&LO3O|$9ZXfk=>#^?d-tU(3Gqh<72=Q7C`mtNWxmlXbU4Np zH@d18I~b%EH$wG9A;T>M&MOj&tz}%6vsD&@*DSJRoXUYi8UDe zIO~0LH;H{Ydrf~qu9E4Rk|^Fro6#sdga9*)+p)yqJuGzC4M@Kpk=l6sWHfKtkk z1^%s=^h|poy~Pat_LQJ}lQEFL=IjewBqfX}a=1hgZh~TgD6aI9^*&R=$#b+=uf_y; zBN`z?JTF52S4Xv!G0Cu0g?ZT#w#v=ZmpuJ0P&EyHjrrhYmp6*%#+5CKJ1npIDJ~9A zU1lG@mm^pY+Aa6GYMYQ(uGsp^;4lYkX!mU&Z5LAO+zNAm(uLB`-QwiQPWvLNF*2G4Gsj;?G_y_@u9mA6a?-^`e|OavdV7oGw5TIdX7fx+J}IW^C4!A#d9hd zXN<%ie`GDt6pBQa;t!NEqVamK9{{wro1Hbd15{hp{1`*PmF$=fc^p=2y;t^g%z{qW z`BmxlcCD`49d%|T>-8kaN6Sd8KA98zD#s;Hn9wfjul|0;hpYiIj~35ei);+ZqBUnK zj4Ih<&ZtuSESyrc{(255qa1@G*&cwF)Zz!D6QSj_K`dMW^*zpd!w&PmR)Y&1!y53! zDf`34_26JauAUKEflbW>JHKo~G?aG>*9iJ-0PLt70zHIiSVGB;rUE%N_IcA~&H_J*0v?`)!iTlg&QN)FgPr1y!D0Lof7)}Z^L$9ObuevTyT!{ds5#Jl1L`^1)n}e0FJ9ntfJr) zE(IT{qYuoW1US_&3{VDKn4QTDmD|XCxnw|FC>nkGLlJbPbarlZz(o6f`qldToX$7Ye6r$4n*uDAij^RbHKrm%|^Eoa{5J?WBtce$oz z@q6?o{o)>J;yf~ib>hW9?X$GQm(17Iof>DE9-Su8C)@VQK@fQqIdfn)Vw z$r5>)uuI+Q8$ve<9F~oRMt&L#T8S?ORdPz@*`+i$*Rp8f(8lwlQie5i_D^DwjP|iR zSA*E0Dp)ML)3V?+VA0I2_e3q3Lg!k2ZtoU`((isV{*i#>Hz&wK6jpG~V}+zlZHnUk zQp&uxL)!8t0PvZRWEubvIgc;~h;9XvEi~9i6w-;0I^xgTNH=hJM?AO4W95oysU4BX zkw)bD9YC>TNsB9h->YYT<5^UbPLUox)Z?iENJFl;{*&fYo#;c3~*O8jT3ijH?>&Jb>Le>zTF@Qp)t_osP7O4 z;~OzFkkg-B-}X%tWwoQplT@Dx)X#2kyGq1zIdNAcPab1fz(*V%kyTOYFe&ylR<nwpU_}KoPfc=prna!SkB-+vU?iotg(f5%#5buoAraSSYPI7SFJ8CV>g}_Dj95+8^xE@8g?Dt52o5g++w}V#)T@RI~ z9KLoi<9HUD&W0SbTF2Y}@xt|?I|H7YE^tNG3<2+hg8wv8Z;0_AXMJzhsE{4Fv57%h zg3wVQJD*97M*~v~mScl&DeC1dED1np1SD&{Np{{?p*D*9cpRNX{;JH4q zBvEW5!h`6K`vMZWGo<${`}3W@%z)PklYqP}*CKl~f)0*YUyRhMvsg%|sM&emXSgev zXB;$KI?w&Iiw>==(v~`*m?GNv@Re2+BM`tSdj(?!=$YpPO56PsZT5I$AtBiu)k7$Yo}S@;H)Pif}_8c7UDsAVttZRSdmU(U{!e530cu3_lb zHqz|P8!h`_77X^zAIe|&Km`O~40CY!c-YIXB1kd3lTz=z4t-<6X+>dH{*6K=iUP=E zc4rI%t4ha(*~~_<*jG%X$0)V3!?Xv&TV+_isOrA<&y0>#cj2J`f_kw0>_-;0u#!Qz z@tHQByFmUTu&-1hOlkw+ym4V&R`0Bj<}@VW7>yFhJ-z;EUw68-6mq{QiP~NsHT_27Y8wfKl?a9WQdLl|&Hx&f zJn=(H(dNovd1JgIQ)>SNCnO3OvpHSCL2Bdm(ZsRZR#ayz(s-YV-2QN<%vlf)iBP|U z*N$^&e|0$#e1off^DsC|dve(e?gX#+yq@x|Mdwc96LwPl=l2&0=}Iq_g#tp1JqFss z0*LoXfafB@J|v*6+IADz}jBlyJ9ybxV_pA&{2afq03t z9S3RNa3kQ@qH_TOs-Nx0a*aYlijDAn-$%;56g@&Qx+fropIg#~efv_-;Mwpy#ftlj6 zSkQtvpsCc-kbIo!P)ci)E6bi4cIbhjQM!>0aj$oQ+1aG@ar^r8@Ln+jTn|-nX_{^c z+jrcfZaR-d-{2V4&IIX}nK>t9p?TyDp@!U>P=cY;}R3-o2wP)|gt5Ez;Pm z1P+|XO+JB#!Zb&XGzocSIJ}R8lq6q;ME3dFU0#G} z7sFH|SIe)(w&z6mCbQ)24Sq0yHcsxE+jtuiKEODy_4?5PBR$|=4a&aiNedZ^Tln3K z5gy0{#A+8I;AiM+Geplg#k}wOM3od1Jy{_gfe$J25u`ig4T7I2zK7n=@b#KJi=a)@ z%aAD^#LsN=Ixngqoe=Y@)-tpNAC4f{wIm!|dRF4_QQVxYRpEyxyiz*J$8-M4@PwDr z$6np`PD(nJc~Qw*(M;q@N3PoY(iv%s6;d9Rz2;V!VcGY&<2q*)ES94(A^o=-)N>Hs zN@}FKZrDu3DG!lXQt?Ubkd`@0EAxs|&!tfZaGG5*6PwlF)6{fbEz%rI-Y)4rN5D0! zkBgt~)-|*{@c}Hx$hy!0lAj+vyqk3h_-yd-A`hAJhL|J{&K)Z0rj)`pmY(G*WQmhw z;pi)?H-q;x{ikj0&18s#RL6cu-7M9v`|O{&u;Zjeg3S{zt*CSE(ylgxxmy3Jkidx>}e-1L-IN-g!}(@$IKt z(G}^J!F<;$gq4$at(r_NnBYIa;7 zZI~TF6KhUcHwTICX`-O-pc@Ef^lrfgMKR*FgHV#{3h{` zBYE4}Z;yflCX8zhHbA2R6G{aK^C!1;FseJXaR5)QpqKX$qkzXaINR=oO#S-5Uzu2p` z^sS;4ra1=FkZQ~%KZ=}PNKHmhl^B6RRi4n2if>mjgzkKIvH|Z#RB_*^C#yl`+*V6HO7Mmq=*?3O2*(;h&svmeHs&_Fe64YQ`pKGND@$a zi2x36BLM6*O+6Z)?#JaGNg8UM`sAKsvb4Kf(5NPE&MxtH1A>=!*(^OOj%z2BUp_0y zD#UFuq`v-Pi1tvcJ$$#oG6ATRiu6x82P{HF0 zxUaB)$Ust}4El}|`=2*Yq8Tga5Fpjzs!N+1e2y{0g6*Z9l@i0v;=(l%6ct}(Qf0Q) zLG-}586pSC&9b$}k8=5BZa+0XH*AkF+_ZmHEGi_OMnl!?_O4#W-6~-#v#Rvj1#Y3h z!=DmLX_q~Z^Z|}irCafrknaK`ntU=`ndj}V9@!vu0jX_;7wAtYFx7b0gP>>yl}BU^ zr6)X{KWqem0E@D(Vuykkd;_ng+Y_00rr~bNN`D6QGyq25p`=vhJCfdh#7^(n#Qc!O zz76ZvDVJh$Ak6{}WZ)77$8gV*8@M1-b3+KI-3w#(=pqOiJMvcW=H@d$K;Su=bs$?u zSJS?Xq82RY@YPZ*7za%-=w#`PtpDLpl`6TY>Xr4%g)jXM|27DkT7<^Fw&?D5KvSj= z`{+<;OKQ9*A)!J}QV=Yn|4V-CUPMZO%(%1W^ zd?!(<5C#IaZ-3nG6J!=Keo7i{{aIVr+4><+azNiP}Ky* zTz|4C1mro2;3PcMnn_4HFk0w>99-U9>J;`{(@=MN)bEt{{6ReMy{=N&WiFE@6t80f z|HNt{$sZ!zKPUU;bg}rfQaL17U^;g6=MTub5hY_U1sEY8TJevNl#Mzyb!KJ%k0uG@v46r?C+qhd-&vV%VH!|MY#XgupBGLq>fD)okz6 zTw_pFN8R?}4n)JnU&@HvN3B&&N(&)R1M+gi;%QqZU*_0 z)d!iAJqtTiP;xh3!ZE$CBVk=BFu0!{+1ExxO9F;&q(YMk$`O{-0+OfBw8Ryinv7-q zfNustQ_R9tJGkMl@hvsIcSW2}zrysQCtDLse@nVNLzZ{dhz!6w0>X_y4Qy$A72?8NS+&#-)$@Uf## zXp9Mt)sr1rL$LScfU|kKixMlbwJytkcr94EXltoOBvs0byn5!=YpoHF1%9}nq|eJ9 z4_mdX(=R_^VA3MI#<*@}-11gcdXic)CvI%1P|+lHxg1FHinM`<^{!j4?36%h3UJd2 z%z)sKq4_xAPuI#oOwRgEwt2Gb`lu)EB4XRPsZxBT{BED3hHPhT6?b}`kT0|Js(6)e zGiBR&!D0ccd1x#$^_@r^o0dGL4*Ft-Rd`?+X)5#~C7~JSomqfVCwsEt-yt0uXrT>*thvlb*XA3$yUf`@TO+gER=Jb;eNN#m1uh-ylH#+J=X zX)B($K&RToTeX+W7G_C7sjrq!<+hi|=WWt*ddt>A&ILFv{OC*b;hoHBXA;(?M2!>tbaB3= zOeI*BbDJ^^ohoKTWz+h5OY@T!#Dr^j6zFSF?)s~8)e@GY)Xk=ti@PVG=uG0%g?$#G zQ7a9(6Gm&lO5}BGKLr|$ijhU(6mHA)W%P_?ok;8^Ma@a=Oy(_w0|M*B2BGxtx7XJM z!F@MA1dVW$9e7||fma(4bZED@zGu{?Gu+^X7=O7ko4ep@?7YsTL_M|6PY-=Zh4(<& zskC4>HQ-nx5yK?Wl};hIJHyi%uh>P{aFaCZG`%#}W7${q$BX2PA9k5qqKX%!PI0TA zbuu9h7WLB6?=}{$bLQh_@<7;58fo&~jVxls$-XRY#AIV6ws8}7cKE5XF4wQaKiKH9 zm%2$qZo%f9Cg{*i_v6j zOvDtWzPlbhnJL$xm6{k0U*c2BvA#cBg?rd)PcPiy{lq+n_U<5@nx+hdZv4ujsNAI_ zTkIyS1YF$Zitrw^nm$E0VPUdtpX%oLSJ!(cg=5-A}3DV8QL zxh!x)G(CxEW(D#Nwz%jKjJeX^r_vX|FI-oH5!wv*d47foj9G;K?7cQ@LMb-J;gFb{ zl~wR-PBCyqmsNq$8#Z$=`}~+?>IgM}G+})$E=Mku1qel=qq^xxp>HT6w)n$rAFTP*BEDv`q<##Z|2w~yU;uf#16|Y$ z?5l~rTx4RXVmJbeL|79Jn^4u;HQ7wD7y}nGTV|Y;D`MnX-vDUqUESp&WCwlpGo3CK zugQQM>gIz=T~LKOfgf*6MWv&rBECIMMI1pscooZ}cp1>6XR~>pl;S}djtg1+K9c@~%}H}!bvSi10)WGy zp9Rh$ag@ccbrc(42|?Snd9X$RFgX=^K>cE8eUnhy<{#Z6x`zc$MY=mOi!Hz|IXZyn zuadNf9zV(o1&PO!m?Pbtp|rz0cjs{yN}e|*Y<~N(0KnGzU9P2b!M0Qr1EjcfqoC?z z0bEV5%WCTzx#7~z-O#-30oOjS8}N0m=!F??s)zyM*?bNN-F)6?Xdo6|D$|Ril^#y; zL1LAmHt($ny1jP7z+RF&&6+ zjVWKz8fb=zOk2%vun~WJ8CxOHy%}deC`g%IZ<&sqxQXei*Y|iN7i)hNd-@TXAeZH- zJt2Z##N)^6AU@i?07w(64iWd!pI)}~?DUyrMUd|e|5+Qv2OS#QezY@}X&CJq^~91A zVZ}U}8^{=FBJ-Y@QAiN`AY~RAkO)ab}jh+=9 zMK&Q_w7ENh_){)YE$$a|U< zag%-HMhExpA8wIM>UtaXk1L-(*W>Qhb0p3kL}jAXpr3fFjS(|G&rEXYRf{&L-sP7e z5B}YSiFj(fh>z2hug_zt1c(%iH=OtfjN4&JUbvZ6F-0VW6~iIs!s>``^5?0q?0<@k z^eFtEpaH|rXj2rH&{uB}9~2wdI1EioN`pmlrB%q7=(49I!o%t%YUg*D_EqLIi!ha( zpIA+o{`_hRTM4Z8WLikuXPGGdN$yd5jc+YlX3y49tPwN*lcq||g18cgtunCG=3VMW z-ihzUn;3iE0zEf-H#O9)=R5Q8M3q+ex3J*9mOB04(h9}DI&Qz10so>EDZ}#X5R%yc zk7;WLyyI^V@Gm91f2S4ygcJWe_3&2{?tf^t?*9v}t^aFu!T$H?;$QlH|9Q2IiJ6J< zU#o53Ne8=?eY%2?EqMy$jcV`w)LehfSr0KhQJx@0mcSFyX53RI= zcRvPf{`h+OlE&!%49Z=&o2&C08Z%|eripAz!-=hA z5kKT=5D$r$6~PK-R1OboP$w-%v&fn#bGhp<ZLVCSITqsXg%MLKw=Nm3dW+ zF>J*o0sD5`Q%rq3?j5q4c#y@n9KRj+4hrQ_!L>yZ9h$y^PrNx@eBi)Q)VDdTwu?9-^zCTe zcfBtLyFRU_p5i6tkdH@Z&L^OvH^xO>6MVf3kU@f>_L?3y_B2HhxdR7Qa7KqZ`-vKD z+(MY8uR?OEji9kPvs08QZ#65}UP(>hRdVyg-u3w4x8_b)$c-c>a+>3m*|>`g@#Whp z_X8FzKN};4qYF4}?e>{d-N6`@9YX4(NR5)2-`Jba&@n&7Fp~I5$3aZO+fjiWDW9Px ziEjK=N0~iOn1S9hjdB@Y^Xz#{nPAxon^4$hiLRu9Ycf8yr zp0hfKQ{UCeO8qope4VrC*FXf!jgTCrY5VpY7MMQW1QPuFWI5}RFmGg1^>h1R4u!xH z3Czr&-ncxkYc$QueC^EX26LTOh(!B6h~k5?k-Qd&Xy%{YbSlB4nhp^#3a7o?0iCk% zu{h4M_llP-2e6VveHQPmmjP&Jo|Q6PJ$^i2?=|~kgJHa#>ag{3*~T)Je<^+OU?trZUR)qAyXRgY41)pmI)Op+UbopI08!(zX}xs3cwRVmS= zr=;A2Yd(wMN4Nv7>%4NX{K6i{oCB4BATkl9itj1$$WzXtAD$OJM~1`xA0iH4LF8ph z$G|U)*E>+ywVCq~pe&9TfuD9lVA)PdpFFS{v7E@=*EB3-KgyRc&*b~5Ek&DZ)(jR> z?zU!VlY1I<&d5N-zIs~bEE(36$|mQuU(By3HLk*hSXvq%A2RoS{GyCA8UZ1_Li8;V z!Wjw6oeJ+K_AuKAtP+01J3E2{V`CZ9r(h2u4Zd;ZK%m8AZt+aeDp&<&iwL17)%Ig)**NR`?xlTDDmWhfNcA%h-X<<;l9 zRi4o=G-MnhFH+9r-Fw7xmDazoG@g{|ipzN9@(ZC0N$Fv<;?+1Q@U(4@ib&kG7*w9~LzK*jd9RInu#SI$)NYPoM_$i+b|siD9>v^o@-E-9Zlmoi~UjCOfp z{yN4Q^UjzpbK_T}wdZTxEZl919AFM3X(jD#%I1~xY9*8XlXFxMI7sRJH6B72DCy;v zY^t#d9v&%S`iM=H>QTh$i$f7`U>u9bqR3vuI+kr*Yrc@c>JJ)=9}yZ6$6@NMtpj9t zemzATHW>>;Xua4UeWWP9BTU$g0S`4`_MKcWtI`I&86`FtElMR&0ha*;k=-OXc6agY zMrPL;?pBtRmEf#}E^~wAFwYx7O}@J>#f`0&pM!W6HPsu?gnnV^gV&XX(vL}C7ft>_m|Kgyo=^U4MCg}7))$$V7>3K_cAPyW&_EF? zuzs3{Sm0iy+mPGXSp8V-OsXAjvSkYTY)MYde?r-@3tm@QtDs7zIkUh=d1apo^eoPp zowsxOe!SYzizXK%2=rAT?=d-ogWd(Lr6v}HpRjb#FGRJKZeEXa74n7Z7Il<0q;znz`4;UniOx`%e=4%j zH$SvIdfy#_N3Y)&7~Ea+<|~}m+YTM39u7z9dfUL-R89NXeTShoCt_4_g(_UQSkPG0 z618Jn66DD58 zp~S!-Wn8+D?hc%DZ8fIEQtK}5PJgmll%;?UY;sviK5~-ClJ|!650->XD2|;4Bb~mw zvga@^y|Y&xb4cIqp|?5B1Kwt*Yizi#b2F>Kpl_P+~Y&#F@Zp&4KvmFP)R zcx|?8+oX`r8}2^mhG9YJJxn% zv<}GyVq1+CqD{6iv9QR(mVKjxGj_^7huEHKx5wE+OV1$FB2^*a#)t1xhAG$4oUne%LVaSt!BWa@?L=QluuW|c zU1CP-bPVOy6nCKd^Xj*VpS9p~b5QrTe$I&hki77m$9`$kD%KQ@pEV#2lpd^`G*C5T<$vvkI2Fm*mI4+UlgNLvwjC-?ec&0ilv8#SuS{4dZqeWp9z<7 z0}BH+cixl8Jme+g_;mw`zIu70empVDLD}4;f7q4S6x>GOc?eS2Fx9~Hxn)M8jXEF4 zQEb;Vdk*US$Wl)gG1cjB)z5XX?a*$caey@ATN1Om-guL`k zVR(`>n)r`BGm0jiUuili7MC%{*$P2ejeIEtwIhVKIDqIhXHnx1(glj8i9O3_EWPh1 z@CEK133|!NR%H^^+@J2t%bSOwyIW{E))`ss6dg*o@}6{2 zCdhNWxV@^?#A;ia2O%GL2Wzco@4q!%fWcc^1~=YU3Qvtncf5jj6H)^U~kAJ4Fc zX7=oLdLR;MYU_3L_;vlon4A7(gsQ#+tk+%ifCon&;sQ~NQ)n^EQfrY~VkIgYZ^j#q zHDm|bFzhu3>*?#F^{Mo3>|fjdu_GR|(7(rBR&99$OY!SXz~h&GlbZkRqb#0>B1H0< z?}D87dDl->uAo%9Uv|Y6$-ASdUq=5VQ2^t5>b?1`8=QOS~)&2CijH; zES-7;(12cPxtB91S-o!F#SNL1mHkvb+S-~5zCp@ai7cq!S#H05+B!M5mv>%*TR**2 z(L+ZrZ}DkUoez{;TWA_QwUYN&^lG=YBO_aW>>XHoIIL~&yXcs``P89RPM9=>qI%3Q zCkd1x#|QbtC{q_OCpZ*{Aki4FuD!MpYN3ao>Cn~MdB^iX>V=hdcD8EM`s5EfI@QHR zs!#g7`q!ZGTBn2^9R!n^PW;q8l_wPd>7iXYj-e}Lx7|tmn14~Kn*`M+SAWb5Gijgw zoE-m{uw>5phzeCD<3x!)d041Zx)DsLI#acy-3VKa=amF(0qYw*|=$hei_ zh}1XJo+}-)Cn)A%x#f$Pw>9-H=d#FamqmT5sJJ+vTVjc$gcU(hPzl6OQ796@k=UX* z@2IGdLv^W1md^83AY2F|g!G0PS`;B+L~w~ajPm; ztRh4F>sZK!mEV1!>2BS#eXdGsLPX9)${%#ioAH?}nll$9{eY z*}pm4ixU;yPcYPm+3oYWN<^1dyHpQky8==}TA2$#)RE+xK2L54mC%7x9R-|7IyxF&U10f; z6TVP6ZjSApN+vx^!YX^Nt5^Fpo?G1#!ptKxXj1t7GYEf{TltHEK=B4G83 z89?#=-wS@e5$1QLu(=iA9t9(S@R<_b6>yTn z9a?%XP6~Xd14mypjVb1JTg^MPuGAZoS{4kMQ#8*^t8cfYvEntzKt&tEw$jZR+|A3G zWB}JFy@-sh5ykj{s|7M=VIwBVwwYL!wWS+mkHN-CO*3n@9MWhsJ*j{zVoKmDr~k0X zZ#OH7-d3#4CMbhY+61RL@ewh>c~Ix#3hxhU-n=?AKcqG;f2x4eo>S()3e0ay&;1a> z!kgD;{th3^Sidr<7VB~>;_Q@chf9*KSKoF-U?q3>>2i=IX>n7yEYOeu0kh=?Y2+Mcq< zJ75pyVjXaF2cZL$$gD?0WLXK46LzlOrizc}T1E+mC1uZNH>H+k76vlPY^x^bQBfv&5?2I5OM(>O`E`vCocG zKU^{Xtbv>%D&P(8KfC%VJfb2snjLDbF@P@B8dJioEsESAUm1aTx0~+s>6E?dalUid=0ma zPx3$Zu4!nIC9{>5h}iy22=qDxH)b=;51qsz&K{#m+db&Lt3#3XANC}0MyXI?q7FhC z+j^*$gg*F)m_OQe?XYt60a60lJKn&6gkwLp5EUPY0GxI$2yL9RJiJtT0H5clgxbL@ zmM7;po7Hzh9W$T9>5_3OENbdu3k}@D%T(TFK(tK4uH)P0q{OHDBaeY>7BbClJpfFk zz)EU@R6i8;8yi>g5BIpF>cFK~qHmZ552T*5d=FEW&29B~hIik0yCGd+<(hi-41K)j z!Nl#WvjKB-lX57ZfP`H{Hr!}q_63?gy1dMbbm1Z9$`2kOK8O`8=jw?S;h_Y^hwylR zuaqK_z;CQQKfR%CG1|gBWaJs+*FD}SM{fpJ1-AyENi?J%o0Ucjs+xgNgoKL!qzeC;yeN*)vx`K>ImiSJ4p-e}LE%RnQq=EN9 zC=N)sdRF2sy~P4r*6^8&b4c?r);hylZ+oX`Zm`-5)79F>beA>N>o(bAI#d*w3MjC_e3DK#*_yUq<@H)?%Mba*E{u^Zk_($2 z%y^Dg?7EAX8k>oBp|s|P=m8IssB`3>VDmR8;w{F6HgHMnz%=9pyR zT=2XYW2e#|`GMh~%@|wAAN#gw2EQ%&S#rrk=mPV~>FyX5sgzqz;UP88htf}HAuft8 zTJ~kF^pj@HVR=q(mBB3BULG2B52d+I4N5s||JW(}E|)bXNn=7AI+q*>8UmjZfm zxM)rv3m74F00rvk`|RZbe`I+ZVnmzF@NaAKnU_i~;Zn)SVaz(74SROmihnso!kV4p zRM?a&&SJ(-PT~6=B7rCi1M{e4%qvQ&PYK!7#U2LAAYw+sx#4dtBXnD$2!aE0x&G7RRqyyo&dTH52rU0ooYI#X&__2vMk)WXu%Q}d$~APqyfdvUw*%oo5{csu^Dvc zHZ~=4BV`=OCB@^eO3KbkCN8i^?#Whx;t6u@k$xerU%y5HJ;R-QMWe^dn#CFHRDZd8 zICwP7NVe3ow5myIZ=MWU?Ac0@qS->a_=nJ81fg-t9ulJYFFOLtdw1S;%SX0=a-6H* zv`o|mn)B;GOpqf6L=CP6s@%=B3dG(pVSz>wshxJ zi+=L`@4b55(8Y7*!*_SGfIIp}UXP^1hDhL1%{|F43?UOG)yXdl=vxVVJSk8|tYsu| z$ev(!|FZB@QOVX!P6h}xIfOQLUfq69qmw!QGQnvNgxvvF`nj*YbGAqMJiWYv_?u=T z|Mbr+e~g0tR_5V-4Jq&6wha z8Yf#yK(P~MXZym89h4i{CNP0-zQb%_$n4!|CP0jCjH?J&-xfxUn>Br;rZ&RbNV0@k zWtUwrRb;&QE^FJG4G@l?_ieVEVa_5oA*>KFGrXdXHzMN!bcIl^L@G4Dlx)V|XZFn< z8XCg#4dUud#uI5lALxs{x4-b$oRWMj-#qBI^ETNMSjdwlB*sPkLoK zQ)&q^I(MDaM|MXS0}HFtqWO)p0i6m_+Uu;`jUkBAb46T3dnL~`%=a$UTKSo+OSw&Y z*>=qQ_Q#KYR&Eo`Z%^w=!Kvdc=9+N$Gf3;$L$LYQvdnTWT<^<`Q&SO{mXN5n`_M9c z_a{HT+F}Ur!ATbd@1VEIDWjSvY{PX~rzTBwLWHH_I&<{l99bvh$g-*jL%vSN@(L}h z4qjJn1Sgq)J*7yx^k{5><%3COAnI9cJIS!w69Y-KH$y&!sEU-P0-X!nLt>9xSH~H} zKx!ev03;%ep6gFF5V=b0B@(y1HH=|n`=vL!zs~7Cmjv^ z(jpaM!6>Uw(t!OfzJGFQkRs$u8>+vjkNO-!1d*|x? zutDVRVnhu=Z7y|=RlqWli7_Dft|!GX#T*dO1w3v*o`A<&&$i_07Qibh$kZYg>+Qku zftikWHcuDV)r*4V;}JDenR<+N>mcWl5jE3VyJV;XGQca%Y_QrOb|PAkDSW?YcCT2P zsa1sun!JR4hpn(in**{llhtD7qomo5s@uvmtm~&6J()AEuwx3xwONia>`pPjDpgXg zuAe!_A@FjaJR9()l#XmN(?~%hWx3=vt$f_0p8=z!f>NZx!nf4GZmgp(HcYQ6TYfH_ zK!2?{L5&_;3*d9-V46fD9AoI-80$iSLhymSUXk}l{y_tbpFXZnM)D@Y7)TvcbB?A# zs~GFxsiwt}WHl3#qf`}3IbP`C<$eneZu3v6c67` z>!%q#aPd3$#N%hdAovK3fCGDIK;{!jY46o6`kwfjoVJ|Bgm^swW1!f+s5v@Q73q2_T5SD)%ibfoeIwRop{8*XEM13jZF!`n{EuDvTch z3JCT7W&D`ms~ANq7j`75!1;)uiwEM!SSHSeuz4$~%?RaqqfFGSDTySdEnCzAE_tXL z>%RT;`0y5HeU6@4W=V;~WXR(asCtXJo<(SJ=DvuDWyc;S8*Oc@ow&wN^nuP@5RGX` zPTODvD7+f>Q2uHTn0KP{REV`=dCLa~25`YW*Z+5b=il~q{pXE_|A?8`{~OGt@Nbw2 zNI?K9Mf4vD4M7WF2peA?2^#X#aL4|5fee8{;W^*qhMF8u}U)Wv0~@}**Chr4;nMz?XlGUuDcCFirmm3ZBU=& zaTjn)2ahB%tV`$U*ZFii{!3Hg-l6V7loPeTikId!|Dae$4zfv3|1LAgIOUzw;Qae+ zn#t^B+Nad!~;$t%)jG~2NH=mM9|%Yz`Aus|4Migbq&oxvx(cU!C*No0RW zlE!tB$6E;JfI#V--4`HXhPNe9VtKvo6=?W~q4VkLcA@4shXOu2JUm=qXND19azr$K zNfsdT#~MC9<^!pNxUzc&#n;J`ksppp6z$z@XP3&h^=GA~nUU!!Iqu-&Uz@j*BA}qg zr255_N%2(e#eS(1dM7!>{wN^%!M{Ak687S=u~6JlC?|<7zp{`1;{4$!9-e(ubHfo} zs{DEne-A{qd1bC241WQ))2(zsB!j*o#%7;!;=3$)i&96NOOAQt2gRG(a%7}|Ui^Io zrvPaOkLp%e2k^r8CVVW?@F{Kn+s7 zi8UG;&ZYp#q%fvlkYs!zCVyD3Hkd4!57g9YHA8?&#Y<*k zWKffKYP{2M-P;h(Mexv zJ^nC?oREjmU~prPh@2~PfSo~#R>8iX7%KbF6|xda&d|i&w6cvpv|T5>%#WXH={~w#=3^j)A;r*^ylMXA+z@kH(g_U z&pfT})<3_@JYr7!7>DY6kOFVFh3{>GIN)s*wL9~E1rW4i5s6ybxEoeO)Em%ux9ti& z2-}mYnJ;a@exWuCA+YkB_JsRS7)`IU(p;FY4pqutpdZ@6EM!$2nkV>aFA z+$iicprEQQ^D^m#w=(}Ncr}Gw^xExzc(shMw^UYB{fb9>Io8gK0ThHF55gCI@7Hgk6w06 z>nJ1anv3|8%BGfTY}zrrtv7rYvcHLvJoXew=DUI%q&iFe+uqxoZy8@6B`y+~{dtK2 z=ObO(2zvBF4Zx$nhOD7z1vM#|mfDjGBhn1(638jf`%lC=xm<;@yY!k9N?e7`z~WuO zp{M{rpY<7t9d#?ri~m+dWs5HKOVUW>U$`QRa%M3xP+N0tZK_N#ocKY{^1v}`ZC^OD@2@3VF-z%A z<8Gl1KgLxAPA)1iT?{q1Y1m$l?c?6_AUWaV;neS}N?uwj9?5;>Pq|k$2xqGBj^dcz zIr;4%!G!w7Shx4GYaFm@8sQpI6#-(RHP~@k9F>C6+}me+taGA0VR zB%eoSO3-ra;wCF~=<+_EzV)IZ;gTPQ*>$+^;9hdJ?ECKrz}1$lhp$sZ(z3vJ zrl2={(!!ZR5Tv&$b&m zkv3aTyD&a_)z#Nfx3NL`i!y$0?7(Q1sryC>-!F$bF_q~z$vo%8nJd6N;M>o|qx28gKSQ`t=;>_`H8jDBrcO=!|#ZP#y(O+)yoTQ z9vq$Jbw$HhCI;s(l&+$_szs9LanuT2w^%xgh|ipXg#*ln(yg9rVVyz zeAj?s6!;iTa8hQ$iBUH!UYkw#Qtu&&Uu~T-Y!out^J5?*!G6lx`r$FytvYcA@#E zjI$!>A&8kZGt(bb;7DTO>RFL#OP+T_0axYc!Ic=WKva=qk1kVm#QubkAPlJr!*TIi z0@IAB2z$V&kP10=a`yjHpr87cBV7VDzF!!4-}Y+0Y} zHivi+9n~$=A@bsx|4T5BRLmdigGU}YT8ZB}F1U{&uY?Y;B6=KT_fQ~!p+V||%2b4H zmNtj{ark*HSIiNHuVF-H#V#0lf50A~Z_!6h%T;H}vUhvI{KW_p=(*F+k}SpVSjjIo zFwrkJj}iNJb{#xDfg-=__2UuQ^k|_SVZ!_{BOb+aJkyn$7~BO!&Qwe*HoV@_S;xY_ zOD9mk4wIz2VDpC^iV^T14+P1MY#F_pmp_2xtClu+PO>!`lJ+ZFFWBmLZ1VgEvFgW3 zWDSRla0$EWmC!I*pFCPyD?(27)p-jC%UL{6b54r}ILrw67S3Gta0MPM#fG7=-eojH z<}_XEA}69a-G#9ZT8Q+xSvGGeI_tYCF*r)%c%_Wlk7jh{ko_wxr9Fy&isvLztt8fe zO))=8qgu!|2+f%s9fJKTDJ%zgQXnX6{=uG0kFr@r#m+rn1z)o-k`v8hzwmgAu>f*# z%%MdZ#igUZ@jI%wX{XBd*xcUK?ob}M{T2LW6tVQJc_9r$7Qaj;C_BMtj3Sll-ly_m zMuXJZ4FMUxQLC3z9LwZ`emTD&xjakh*Gq9}2}H{4FNS_~^K1Qxt*1}Loi#)YIC+%G z0J&XqkqmB>Et#E3xkm52dBr!^9^Go~PlSnHxApVo=&TCA5gcEeC4DOvAYWqOEr6Uo z(%2;T-+cF#C2S5DVcRciuMmpizkr!}RV$NKD|xCk8_{JgkTi*r5Q2aI&W8U9CRFBG z-k6F2zLD;J+~pC=kMSBh9Zf-jN-CTo0I`oBYog+ypaeN9qU?JezX6W}$g{DJ01fME zLn(_oRj4+5zPyz!plNoFGyfAO3;JxYs=)%UmxD)d?M;4QFDbVz_aJ%FF&+Gq=1M~{ zg*IJR{VAZulX9^@KeG<&Xj6EvU`uEFgEP6DyFzwTd*PU0re^p@rX_WG?1jQcuZ>!& z5hcBNKNwcFdLRV7mXmm=$auf&J#PwDdW+kpM@gi!aH~5ZG)U>N_SsyW z#n1o--VzFiF6-8D^Z=)P5W+HuS;wkhfyyS)3?nnNEZ=3$c#iK5#a`Qtf7aR#PuS{? zz^Vnc_(_JULI*6T&8pts#nL5X=MR7smy5UF4}uH~1~7`|S%y8nYzBIk?=J++93Fdt zDd#TeH4!LVyoSBwwksUcU+kzm^*J4f|RS2v#uVNgETGiHLUy$mt9V9zn7?XG=+qcbuinkvSR| z>WxMR+kGE8$Jmi^=7&m98|BI`am-1tqC3=I4opZYEAWn4;+H7FTr7Fu8e0!U4p`h+ zkv&%tJ??taXYL>{<>=C^@c6{k>rv)2T&I_a_mHvVOmLQN1EfO9Yxbt)eyG#4(yC#;O74M@pnKwJL&~xpBT4}avQyN+pqRZ$oY3*un{VM3`62O0v7<4>A z?081lTlQ>AoC5?mr+S%A^@QADqFruq2X`(Y6=R|_@?N%OY2hVG*T8{2ATADmxa1`W z@iLEBg1_@9Sp_LJ-iVv{shad;=eslE#7n)EtTEFA49i3%eMRgabFl zhU|!mM$$&VI2{2<3ENivsyAyn7Djo)=hGo51oVt!920tHR1jaG7FE7liQCU4VAjEe z%K3El0n4U?_q@;XI1A3ff4kHRPoN|#OH7b4W891I1gxx#zQz04__+X)2ondyMSwBT z3)t5}Dl&D-XB$SvYOQ&;U}(C#W&a-ehLTlzsr^YS27mMVC5;iubeWY1kTO^!6gWKo zkd%-^&9;`Mz*Gsb)rwNOLITHylV~E8DxSDD9BdnNRiGms(bB|PSGg8LqSrluK$oy# zwp%^VD1>ZJXQVwvYxp^GzwsBU5nic@`CB;hU&|-{BdX!}Z%~cOzo8n2zZTc0kcRxV zxE@Z6rz=+bFBWs27BdWgC(T<&eFWPC$LCtR(zW@j_3@;mzDkgYQnLaEzgqgypmtUg)q z36G><_KjZ_!^Qjg!6?xbu*TTu2*ZyVaVUN_O{SVWG%l}ARntWT-UhRyEGv_oi)D>tQR)!pb~3c%T?n{l0fKNv0j^cWsEr33p$b7krPpcz#vEX3%+Dq z={DH=I&`%;IrCguYtuQT)RYRA29@4eI{=w3ZI;^u&fsa&fz0L1Pgj&I!FGkg{7!o4 zQ<<*582WW6^^9GqmhG9lNSF2Ui_yXL8@=$HuNE9akQ#=Hp-wlqU4X*1Lh4bn0Zoz5 zkr|MSTe!l0iYahDN6|fv!lJB{26*bU*mN~G{6i3m^6!Gs*nsz%nvIiya|;sWFx{k4 zWNzD{ zHXygLQIIdN&P!`H6828VriF+VTL^r=x%DA-w}ng~QI1b~2(07b-*P56Iq&Y)K8Drc zT!O$s2rpjY%6q+_Z->&ViF5~V@(T?ok0uq90(OuYu{Fznct?%yjoH0KG<*CVuy{^F zdRPD$xTtdL249YB04Hw^6gh*2jmq_^`aq5$je`W*su zvPOg02xB5pW|2%t3S_xYi{W?Z583>QZ(bmdiW_JOO#qG`!4%D@*qn-lCd7RJ~3AjS!QbN z93UyBH#qf|3r8q;v8d3PN=>9qDRc#f2o#LHtAJF(Hr(tW5`(ymBU ziO(PctkznuCJy`wrW+OIu5OAx{bE!@=r@OXN}!=niR^<2VWIb|Oq@?wXAn#K?Eojh zL%HUg-2^N>O3@0-Eg|8IduUtB#5Rk>9+c3*XFd#wWB{QuCdV4$t2q)ll?=NOi4g{fBfRIJ0{X1WtEr65CESfc3gZqcfrXvHAy-Z zQPx`kJf&skJFE#4<;T{OVk@sbA1ctE|6oVq(5ws?9`!-YC2dWVY5hDx?1kF{*i zZ{_XZFO%NQ*n~0c`LQ+OXAnW{`O~BO=ItZ^L1~wGb?{dm7X~;_JSZ?4kWE^S>p7yQ zN6-q8i%ydTfev{QKMK$$h;qx@n0{qGv-{Y}M=H zzi6-_ypsH*U=f)((0j>B5x1xuc|u_<#kL4pJl0MtHPh$1%LZBogAV+P6Tmg9M}F97 zn-#C$1j=gplqxtOBQl4{$@Gl&ep7;asGDpulmUL{+n7M5ymHJv*F(F?Ff|qpXKtb# z6<#4))%`OdFul1dgia205~oWqZt}z~_G=3Tav>#-E(L(lD+m4WtO=$zQ(3SC z=cdoIsf8=uDtL+3FVNibC06<^M2=c=c(sx^Yt zGZX!9M`UE~cp`FV++NDxOz6Lrg5D6`YW$+Fa;#=c#UW24hs)*B+4*T{ta zg7x(C+wx}LX48W&X@Bi8pV+Tu_eEiI!gANMuJoBxe$@T=xc{Tpws)3yy>oo{aaQUz z6je%*3V$d`ZF}?x=z73(uP)j!EsQ3{s8}I>CtR9=roouxaJ|E3(m)xPCfC#Pi4b`x z5iFRU`d*dw%8ep9xuV9zAtiz^0PdL?Vy{km)I}xta{sM+s&1+Pq<2ikgfu}S(*-O* zK1h##U*ZoAn&pdDF=>{0Ox^AG+CRzl9`HoTH`bkkU%pDU%l6R94p3xybm;&Tm-|5w zS_53t<08`(_ds-XQ`XI;=Yju05JKTM?YFz1!ogd-kG`dq{*Ttn!!5v>)xBv*m|{G> zONVBP4}!ZNQ*h%mGCQUlj0MxizYI8J5?jyGCY_$a#>5z@C_r8pI_`d^Po2rLfE4o5@%Cs!SzrxOp zmDuq{uX>y!&)A1|jESZ=*8uS(o3xJ(3PRZY{Qv9>6UWKeF|o924Ovrp%XK@h=?kj8 zDDCGwEueGZe2!J&kEd15$2qigc6X_zOS1?-X(yX9HZc5EIjQ`~X4uYQ2W*$*mms!u z*8?UULiDviL5BHhd4jvR3Hyky621qB1Fad>gHZs}UqUQYLCzPlgVZD7bS=m?a^T=j z4VKlGBZaNQpX@yC?voB*g?=Hx;x6_bSQv)NdFy)!irz~pbig|ffsPZ!BZrn7LMd6D zO>7!JTrBdLsEJzLn#8jgBVjr=rWmSAhc&+}`*HTgjI7}90~pG((IblJp&nVjD9qZh z*=FB9@Nwhu)qxa6_uTMPEvHjyd6(tAHPykQv5mCscSEe6#irQ^>hHu;uzkAw>1EAV~+JL_T;DC@A9{mKJxJf6`5eiB?1Wspy1Qw1#hXf0~W}j zFb52fr)ERZd@YZ_bu)To@x}~PDP>S#>$2LOI+aZ9kh5R+J(tR`y2D4W?*`O{D@~cW z7v+w6xs6IFeCKnhD&KG|39Lfsyp%Y2Fy9@xbtY0oY}|=oam7v)O1~%er4E4V+-;Z= zlC^X(`P!7HW52JWHLl)j%=khBJV6B<&R$ut-9%^-^t%h%DcEuKvLv@~U%qVw#M9KN zHHSZww%3>w>Yo>L5TTKGK^qRB?J~;r_}xzHJ>a7?aNNJ?uLVJx$UU&! z{b+_K!?{Cni%6^3Etz35Y+UiZN?A<+0Vq>dl}o9Qw) zZUS=a?D}Xgt0NQ!A|p7YeOc+Nf~x8Zgwgu6kbfsTpqTw^>-&eab~3Om{9NYsa^0SE z$W5xTC6~nClBF*)^07mQPk`bmXDtb?C7D40A2t}ZyvSz1PZWSjwIY$DF zZ-QtV=7*-9fgAsT^g`&984oY4izW$58|a1vF5T-b0Rq}$IkdDvcXgmPhkhH1M+ckH zjss^&=fY$WipRjv-3QW7;UPu-_2+2A6r&<8$f6NI7NoK=?WJU#d6TqdU=q;9r=1!- zy<~MPwd6pa=|`d;ak-3f4WYquD^!wEqu$lO>N-KxtP>7noa%UttVl0purv>dzAF}E z&RH*BZV!3h0wE(i#3H^0q1JIi!P>GDRm_win`F=Iv4h9K*WUo`G2d%OLPVEhx^M)4 z5@lRDqxglOccS=C_SCgV0%MYzNsG#bWa}~n`s$tVO_$(nB}wVBRrc+*$Db-fDx$(C zCIio=`c327A_HEt3fC=ivRN=IAxaYtE)r~yG)`jYz?kTqL!}80#v=j~TiNhs9r5wT zc8TNmTI%=nuj3Ld^RXk!Dq=+1xG+*{#z*Rd>`aPQNQ|p1u>9&A!ZxFKkwp6e{vm0G z!+iCF9P{s5TD6Y+V-b)rlDk&RoyrbGYCNvO{*^V9q1L8!3{JCOgH(BxIy})%x31Om zTJr6p)o=!TyCJ_Gp4PCcqXc~v#jz?Cr#$A3pMXXMWsAxt<6EDP#GWlEBLxo z0%h9dRyS2Dz5Nw^b_NWM5B!~^2x}ct;sT?>(?**-?T-XlQN97|vzicZLrsA$dzJZl zqU5N}B@yy|802fQo1?=;an_Phc$#kjtM!Cooa87!0704I20S(d*z^#Ol}9!td`7|R zJav}&CGjLCiaGE{axt#ep}C`uvxAO{nl43HG8p7h0|*%k5-QzJ{v|_9$hHl$3g3O|vIhCHsqV)Y?rN&hfm!Kjih6=Ep=Ed~=B z{|99!ma_ZTreuFzlkk1pmSL=3x_;l9EbB#EEjM&)Ui<(zCMK02_QI2zIqKwFerl?lI3KJDQyQ1WqymHo^%h$|R)rnd z?rWIYMdfBZNm^xj42n#}ZoWOGp4!%u7+>Z&=;jGlY^wF%=om)j4|j>~k!xjXL?kaH zRIRj6=8^26SNGPC!Pt2cZAKuOGMGyTUX@&dR|YK8yY}(B8o20e zbwW=$Cs=Y{L?`0Rt&TTDkyQuTZ(PH+u4>XWsOA@YIIPL>FB~>yO56kBGNN(ELr@U! zv`JQzg0Ew#l$J9tCUALxzxyqoC&5p}E5W_BN2}zR;U0U>7n2e!`|6xEj+1d4=36M@ zw8=d!u3U*5k4|=0(VtP(9Oa>c(bfEDdEr=$hwyV7WNi;x=&AHScq0C^^+tAYuWkyQ zp_(3}PQ4F?iLFry*GcHbuNC?0Qr_R=W9UVQX?AKWn&@#!^cZvKaG=VtWQsUY4M`=d z>0jjJ8F>8ZX{Av7tTi8h>)32|LfPIPd;YEPQV@PPm#j;Tqdw%+Yn!cq$s= zw^^^GD>hr;$)?_%7-IdpHS3BrLSJr%J{$)~gRLmS7^w{vaZ;d6jM*1&%Y$g;FPT&! z?|h0z4IQ07RnOk{P@uq9vE6t+yYTR%8n(3e5!1|#4s0r^<_O5ZOVkxl4LdC~zO@^E zI%_f3IA)x6)vFy^qq)3qFt7Vm_Wmw(8yi^7 zc_U?rNTS8Y-A+rgyic2IhHKbCz85*gzz{|pZH6C zKY&vO)8|i@A8RcKGdfpTRNU5OxGdxmm%VOnYP$A%b>0fG3mc@iqT%Yk);%y+OsAC% z*EU`0@s80*U>pc;&o6NVDn%D*m%-K&v3CTaM){n@kcu2Cd;(uCHag9KL$N-5QsT&S zz~97r7{{Qu9D-iSJq*J`jXjTJ zw?Lb9*6Eo279x`i*G8;8UwD(J*t|vCL1SlorI>PmP z(0?jSzZz{3b^+PBoA01`3bbgYma(WguDWXOtqcj|sliF2RH&$oQvRC!A|+l!m$4Lz zfn}@9O9Iv)eIKM8>cMcvyn)V-O+)gdnnh;{mlee0lr}`74WKU!8SIDZI%I$C%m}@- zcLOaynS0n`cE$rcy;*Lu3C#CsAbm49$_!Wph+3F_Dy2`k6ERs>1Mf!$;jNVX@8QdwgT?7yggm7gvrvfX5FZF6t2 zCcSrNcl+aXlxwovLFmrZXW*Izg1M3`bTI|GT46B&a?@~a({rgwCjZtWaSgO0>x}I- z(hVM~{83TCCXj>4RA-VIRt&*MrQ&{m19Sil6j4G zU1H9HldZ9M;=!-t9VW+&1#6>fiYe?R*J~=doIDjNXW&VMP&T-k#|r+OdIB{8Y|R(=q82qe@GS+6g9-8kiI_HZQqcFT0}HpOHI*mJypc!&u*? z)_X&-7WAD#X`&R^KY#@T#-;Ju&fV4R8E~bw8E5`&iKF%`r$c2+R~P1hlRJE!wz4v) z=AdO&G279cs-iHkW~qTP>z=Qf1*5uoaW!JMF01{;4P*37M4R?-zOuC6sVDH{d4B)M zXneJ`23AX%023ZBTcsFU6V|)@S!0zfzi}3}eogW@7wx_;oJcif)G}|2xr^N}* zX_6dDZZKArC*he#7u*|<3*r#p?j(Y18O#LrIBQ__E|K~flVPrCrJm;HX-N0|ZeV}? zK!nv%^+1~qr4c4zdv8MTCl4U*<`D6p?$iEL9>6~_!T;YhNJi%WL4#EKR}Jzf(eGcd zaP)Uz>VJCI|75-Y_buMPE$#k&i^urCSYG)5Ym4{)SlltQv;OM`{iAi@|ILDq>7Op> z{^el*qw>hk&iG%H$7Z#4$Mv7mfoHXygpEjZAOe0y78aF=9P7rEu9hV5749)48mrM# zZX_PA+M3VjOIvM`gvgO`Uh%?kDYrac_Dy!5hEvs*CXZ3yx6_BiKaY7Aijj(HliqT! zzP>y??(JuI*VwLaF3T)gU28{duAC+4d8U6c<{w-+RGxQme!s^=v#sOKT+62W@!576}bv~J*n~89hi{_4B)9e?1;{o-ex*)-mp@^cNRa zOI9$IfGkiVb4mF0=(>d^aw2k55qeclQi|k`0qHb&?3$FW2AgR=_Iw0i_3(Ll!2!1S zAoL9MtLr}?bSk5>td&&W7(39@4IXCwgG zrt)SBk_&6XEUqY=W2x?vC8OU>1yj`;`w8^K&+PPvv<_L%+LRI3%RDz-Xucl_8-Wit z9n8=vkZThsMNgjbG0e~2VXI@hN}M`6 z*6Nk>#Kra zHhmKd*;U~JQZP=kaqO&bSSsoR&B#*IJ9sFg$v4KnUgKPtK+aUkFed{{{$Si_rfA~b z_Kqb!94C1(Buu$eF2sOD5SQP~2WV#eso+9*G6XygH-}p&)Rjk$E-4Gu)JaNV%T6() zx^6d0cMA)anz})txC7Aw{)EKv1}$~7+TdxBMqRW0m+A^Jk&+^9QWZ*l@g{z2sG~@u zzPWddt+3_aKRO|=v5stIz)gipLK3Io6pVzu0uZYu0xDrGwsex*JX11%Ey_==91|=~ z+qr881$GYzL#UX@)e$mV3w-)us%4}}pxz4vVC$JXGfJ4@=W*n{K{q&sY&gP-y8~vJ z$^w}0N3z!$llHJD5cfJs8aHW`1dj15TF-2EPGfz9>9$Ml#byo=E(@;#CpR?f z3#w_g@JE3|`Sr~2kHcnd|PTKzTIhiiz~k2SwV^p50@mi6so9m*JF9V&9Qm@X>lmC#DA+r|>S zVE}z>Iz}g>$s)f*%XRVx*ZV#tw$o`1;HS}Irrm^#;==b{Z|9F`N(KrFzuo`ZrQ~NJ z8`KtjMoM~Ryxp32FFoJfg$P5W5Ja#?MsE-^vA~ElL;l))g@Da8bqQxJZJj|Hz731o z46}e6+#v0`7pPs29zgiP*MtO>UUb*bpZWq$AJ+qw$m%)<{ARTU2kD8`>ZoaK+CPDb z0aU=P4Bqdwuh9f3{}m~mgvKnPB`eSl&r&Y#Yr*n6fKk_}cQASzS#53~OIrS?SA zvo7x>le59nvM3qxT#6dRqwMOU*tmv-g%lzV|sGad=7v z{7Qb-vZ|Bc-$U0#ao_fb(>pqNv-7f8w1sbns^~11QV97}x?Ilw@O59Ps~(2dZMPIr z(e_GMDRlr$j(~j-COxmNghAFVnrEZncY}V(gn~!(^Q{EtvErt?9YCbk)GtFzf>@mh zxWn``$WWPP+|cEQdOl!PNs`~y-kYt9+!?5FodOz>z5Tx~`%N(pt`B~{=ENCUX5 zF`+O^9G-U|KhueDM()i44*;F9Uo(B!DzD*-VR)tG3&=L1$sWzYyjgO@MUKDaOH%YF$I2$ zs65h~0>st*UP$_?<)ufx=MdnQqvb&>KH?QDi@(~yzNvy=&q}K&np3QuZ>lGkE@UUY zmSKah3zol>Kb;v0Rvugq!56!@mjO&SgF%_i9UtsgUl7E=#ICwg=xH)kpg6lc6kU^3 zGbuF2Ksl9?eNoE-u!RaX*FZS}DQeU{9UL5%msRey_QM^aaMyiNl#|i22otkKHU=|R zE`$!cmp~5XfXPm@^c%RZwfCHOTt<{+*6wF zqUBw^sZ|}MX(02HFe`}^7g-5&^uT#8^Rd$%Cuj+b1BUtofB^O zSlqC>(BU07q8>L!IsKX5O zq?YH~VGlCu0Ng&;AZ>ku!SU-@y_3zP`;czNbwpk5EwflAS?-wj;#T8Wa!-Qp&hTXY z@(n5%S4d8d_$qku1F*w0ZO)>*U5HqEPu;lK+lrc?K73_}oB0F36bHfhh&f5jem+jw z7$QA5%By%2aiF(wwWoMI&42*Y?_qyLlUTzKvZEDqWS8XM3q=y{gIA|%4^(K)aAujK zeD>7_Lm2m^!)&-btWsE;Fgh@?(Is@9+50mL5X(Y$b~RunxUG>S71~{JWlq|k#N^w9 zIroHz{>kJ30PNgqH+2#oIT+sh^1$5oWW!Xa*A*TuUUqL$u}@Ti2PAUDH!q0*(`x&_ z`lDmhg6lo=Nz(go6Y0BG-9Sek%q1{%FVD+0cr=kl22*nrifY_3@GOVJ#B$aLi>7N3 z2h&gbjs@9y<7S<2R3jAYoVjpQ?c;&g)@!X zKZsCSl)>|FkSMxci`vo3w)>*E_MBw{qih|-Ayc_ob=YNz2=y*Q%LS*HtSK!8#~Ld> zbZu5}uDQ2opNgzwA-dS;DWybMNG>8R^UM29_4(;3L+5p1D-x-1cZLGK5)B28Ney@p z*9j$fr=xl$R1{>^;6E(@(R5wGvutyonLI*RjuNH{Cc`&UNb~nwXcUPuh-CnQZAGy~ z`Ql!la-kOQ@k2!kJ-=cg=y5!1iGTf_(__Loz0O5gYw78qh!HvKDpP!yDvF$RsqXQ@ zYW>Yhsir+T(N$(YxGM?&P<|D|;F&r+jZ;4YZ9gH>B7 zx-P7yyoHUV#vTfUhjJ%B_)`L!tSY=)zL!l`AuXON;iy;gYy1w!I0NLFA%%lJJqxtm z@v?ZG8RuIfM3VhWM5uaT4k*{rTVJU7n=d#wt9=eO*7es2v_KIVWyKr=o}WeL+xGQs z>_daI8UgQtj*0kXw-=aufQQnbo2?*=0aDe>Z+3qj#{d?15)`HzT(~C{8I%my>F89r zk}yt9t9jPQFs4$UTLf^;zv0z-1nLC(u^`llDEXKG@(5`B278qq_(Z=d+WWdWU}lkf z&iP{nVmKaqDEs>{;ar1at{ZM2Le!vhI5fbG>yc-hn$@2AKqglk9RcD z8+Euya|dbH-C?vA`3pzyQ&GY2)>4{h|R>J$2k8WQyZ2;nvV zs**Jxh||nqjC{Orl+aeGSD)FoBn#jPH+WceN#!_Q4N>!&OgdgfYsjXqbCds7ow`+N zS)FcIw+QjIwlivKTQz{;5<>0jBsu6R97*w7zF(Btyob3Bg>ATK-!{yc<@BQE>4~RU ztK_&Z@cJf=UDf`bFNHg~On0&Xr+EUiQu7>vaRpllHMbFr?8F^-}F;O0}R9!C> zAgqR=X#}JCRWHFo^9Tw@RmS=_CezzdQB`24ZRZr+54??q&B_HS2sM&TbJx8uKFSok zjRwGKJXWanc`98HimYSQEmI^8{zO`1Lk;Y9FtT^IvjVvBwr zPq*U%!WU(f{I<_)chf-gXT2_?9nqMB6Vkfb6sT(`Zpf>O9J>^&Q<;%lmg_?9Hnxq_ zoy_WD#HFb_DH~g!W_s#Y_HAq$pO%O#@P_rg$CCwtdF!_?+(WVWD3vu7I< z>YEKrs&3UABk2|Sd)O5(Wj~KMdh2!N2;_E~x~F=_i&)87RFsrmOzKo7)EhCYO`Q6+ zx*NX~mbt`{_D_)w+_65UFXg*Pbq4AIN+MRJX?4+lgurt{4TDpUngBc2mD;pPZTN6CmXOMkwy=oetE!t zb8PrrFi?N!%nHRE7~t~BYTY4;uOyWQP3&2{Dai57j}u7t?N3<5*fz%!wSweZ){gXI z>_xtM$nbo1Xkm>Z@KEQR6+Uo3dpjtmPoF~*r{(hPyVUjiy#4_6<*6YQfnUHyPO+?| zXUra;6-2PZB=;j}m{VX;CuDT0S4wKwAG5UJsvER}R5vA>TT9L(!TNng8WcP4K6`=X z)RW&b!M=Sw2(!>C-7rp}`1Y^0D++ix(#lMKXA~n05|Ft9Z_V0oP?1xlN|IuIBDPf& zVlS!^M3FrY?}pSXXtntCsSv9m=EW;4^&l7stL$%#twv1+X!9I5EG{Z6+_vWC5T{B_ z;i5v$JZ|7P=uhiJgBl6r?lwNJtdp7EQW&KgnN2!2ZX6+2x`oX*UFKKRpz59I1UXL3&_a_dH&ViHmK2=s+3yln#Ev^%RtWrWMRx_BpApW?Q-pVbiKXu z?J`w&+c4xhhcnI1E3t~iaLl5q<{D1`6eEDwlQ_tOt}Lw86f{M^gY_e~-GSx@#>Oi| z2|x>>H5H5e%VFTyEd2Mlu!T&uvvYT(qyf->?e}p_?j#6Ngq0I@;1Zf4csK zG5X>e|AT|*q@Db$1S3g!N{*aGAz&6-ag>bqO}MN{k9|*6j7l)n`qq7?K?A|m2rc+? zg9D<826|w;0%*W?9S4H!7|igGE#Pi-7X~z(Gfmp!Nk!Msb2VJltTLV5I#I!hE%xli zj^Y=CiTX&Pidj(zO})G=xpJ;uhyg@c&9wsQ5_Ln3*(j0}L6lBXT zFf{Fj-#WQeTm$U4LK4y#DnvnWY?5riBGvgHihFXpj2M$~iI0Z2p!B1JT&q2#udfPi zAkVG&XIP)Sq$sST555RHx?34xR7aLRll^<`&lKzm;q{*Y7QIrDJMqO*O%gQE>QnBM z4TFd|HY7g~dF*8$ICsNMwJiEe$OvHnjrMtVrzIi9Bc_yvO8pi`ncwQ0{?T(E?rLJ7RAhQ1|OvwlLLOa+?&UvKq(A$q_^regU$Ie#6FN z7&|ZoFTop-GO%+9ha8Pg4Q<=%gx}1o2D7kgsOq#)KdJNx0BQC>+&T4@Y(Hs2 z?C+_w$H27?Vk$ktu=NM){bPlYe+AUGMNzG)rdzv=0~#BsHhBOaK_HibFQO+)ifmjYxgRQG$DG~gO257o3mugZ z+jxSn_)alyb7kZ#DGxsdXf+)e)@CzXbIXz=Kdl@(I_X?`@~i4c_CEXU)W2a)v>OOQ zq28WTO5mk+eV!lOi>vz2)&QA$Fj3N500S!^4Vzf_tyu=?*|=Jt3}NWCrU+;mH00N9 zE){+*=^3=_*QcJ^YfKR`9M;9hbgD=Q@pGP*XHT*vu{UcoTn4i$g1&^I_=T)Wej(M= zIOBzvif5`mmev{YRA>&&gD(z7myDKA+0vbU@>0tI?7$LM4{BKcMLO)!s`iBj_XA-?^byr))_t~?Ef)?*0m|L1Ym)7N5elT=ITb*8di^d!E!f zc?3VproE+dv8xhybWw?`#}cPa+ZGQvv#sTK8%%efhY7DZh#++jd>4MMjEgPWR7iDC z7QR*%GI^Yz(AnA4Qj)9e0QQ2Q5P65%le0$ccYu+e*R^W@;NSck%= zGJ}%zuBEswxRt6zil!c+S?&;73L5`!3U;f6sQ@(gf#$BA6O_D4dO7|#vuo!j*V>xs=!XCP9 z=4A(+-~sRT0=4N8-)9qz1AGEFLRw^z#j_%`+9uSuV9T`p2DZnbG<7PreY;q1LBUTri1Y^!K5)BMIKtAf<;^W>Tm>a{vh%w6a4kUBWQ%$`F z5;+V5@@Rbo)j!x&&?W}rOHu+dC8=FOF_*gA$5VO-J%kxYEzb^M z5B1$8Q09=wK z@tIMZowIB zP``{FrI?Cihh#&wHeND%q9MuOgmQW1U)3Mo@6K}YVEi*k+D#w)2vNCGZgS5YW~tD~ zn6QIJ2MU9K7|`mxVfGymk+NI!6>`mptKzhS?I{Yx>HMF6n|~Hi7Wn!BEs8XqSkOwQ zSK${7^z%HEUmi!h`6Wq3?<2CkOgDZ+xdPI~zKw1xp9Z_O`tu z*=Tz}Z_li&+(-Z+t9vgr)HOK|nlgC-!RVCLnl|LUi(R)v<7)PdqzYxpl5cLCtyyhG z4P@7-2aK%xjW$?z#}kC@ghr`X)o79XYAD ziN-@^)j<Vn+yNf9kJ& zQ;o(=TSg2O&O1@OUNx)&k^oXAZ_4n1tWI2(%yQik$Tc^|!)W2MpmFw)i08UbknyC? z2>1Nxc5xon9f99=iPVzA=lqIiqI`aivxFiAe&x$I-+M6FANE?il@H%tqfoOOa;N{@ z3vMd1^->CQyCnGVNUOHU5+*%1VT>+@~8^`9M zRoFU?K8SWf{q7o71M8?dCNnMqIfnjDwddWHFgmM*fZ|W_Oy5@zWu20o8Gb|6s@_Iz z{j0)E3sIBGl~nMTIG0--=3)R$E5O@f$ti7W7%KXccZ6Rl(b{CG465mv$vuE z)&$6Oi+{`@T9R)YFTuCsNDekq&cA4Yw3-N8c%d>9M}@ystS#$-q(~jVVV&b*5tn-o zwQ0C(lj$fWGO*CS!hq@k&Uj(W4uf9^@L4U8rPxH4cTsk0YHU>SzpE+~X(y0oq>O+zMoVwR|jk`2*Q6cRUD>d<~}Es>O~S(EU6wEV*5jdc1F;|rHf7?Mq#UsD~Ls*G+x z5^elc3x^nS$;Qe-w3{9*`QzDIEJW*-yz8oumGur=f2=n z!9;L2KFeV&0K?3&X*X#|Pa4(v152AS9Z@x)iWdWGiJXj z@jD4RZUc_{;LNm|Jv4bj4(C33gp9J@HDE~$j!ioZQ^7AtI$^vMSI>hwPbiL&BFL%= zd7;PnXttI2Om#?9k8LDAwd%y)iX;BA*8}^nzbDgCzRp&vw|HVT2*ca5!JDg(4{`vv zZ#@Cw*lk+1xZKBNH8ug4BPARFhogTFMiJoFOt1N)h9XG`b1~$iTJlJ;X}sC%pK|v& zM8n9y7T<-=^JojhR_hNF^N8L$pAB50QCQD!3XLHMuO819$75j8dWL0&$m2BW)P%#z z$e`vxptaX;XHCi)t18JPGseedR&Y!)p|vQ!PGXka0I+oH6$%s%JlsyOpTX}xFqXOT zldwE|jm&WOUn#0cdy>962H(RVb4|%dscNqF)k|NqH!A|CPSL1F&peHZ&=lIH@l8Xr zj=yB~1pF`}n{%VVs?w}`Xf+RdY^pd_ zR$sb)_fAZ3jjJQgDHkoR{F$-Gsahe%fkDB3hlJ|`@?*>_UQrq0oXV0ZEYzrz9E%qn zDy$d|0$rzs*)drXUY&QSEczjSKl1WDQT2;K=0?bmTFOFTP|m2LP)gIwB&QJ z^~X%ouoy6C9|+#jj#N{-*!E|m{#5)#K$3(VFgYWZ#JigJ#g)wgu*CppIe{|WF%1AW z^X(m)&NPFG_jYZ+8>oFk-1HI|r{+-$`>uk6*GM!5&1Jvt(dyO{_0~oDY9ahlycR|x zt&0LVI~QFDAu?=9y7ElAKtK31U`p+<%l=c8puIUNcXIw~d1ZboKCWm^M-^ zne-F0D3_g>*r#KEmD)AtobQS}9N46}=fQsk7qYc=GA~JbC2INwX_OnhTJIotx>Dm+ zNt(&(lZAU)q?%i$sU!?v^^l8f<}D~%XOCQdgFMl=hdsb%IR;H$swd14+`{5<`ahFxS$rx*I?s77Tx*+g36LVono9+TOb=d7Q>*p%eb!bORTywS*#t?crX zvB`P+d#crr~M~kSdSRr7`r)wCP(RtTs9`rPRGNmnn87O-*N_ zkRfFSp_4+3MiixPR268Bacw25mCCS;MoWJFMRL{LpMxZSuK(ft`a1_^j~~`)oM0QKfb@OE4_fY z0~v|^5Q4ERSu-56o?dMyhZ^1Mkp=k3ViHT-@q|+e#+>)-wk`nicp#FsDewi9)K#_% z-kH4*!P+cm4Nb@#R{UVPi1{nKX6 zH`qYkRp4BUk8IP4{cjO#P;I(|;RlH2MYj?P$P_ zLZ`O!OsMldeMzh4$zkW$=4);gS6tfVZxZiAr_f+@LC8PH&KfOBK95M>#dMq3gmlNO zbI?h(jzY>CQMo-_qoC_4|5^v(uP(iJm-D+28b6sz>0O_IXZ7(7e2I^7jQNkg{H93E~eY8?SH541x?~fKN`ka)J0>mlM9FlZ7g?w zFHf;Ebte}w&vul;J+8H$w_b-isg#&gmNMs5D!Np(7`MfRu76~lUn*ZWvzq8jVzL+BgGXgB|! zU8CJ@{Bjk}^b=a8zQicY;+?&HBZ>+aXHLdFN4S>A9$F10Ha<``wDVd(Jyo3me{(XO zWODV~ZYzrK_JZNFB%$vZ$*^CB5dMXuc?oTpsyZD#>OGF;0c!@dy`I{rqk@%;F`6QzPo;#2ZB@8bF07O>M7Hs*SH2;%r?@F;@Q#Bxm+YFHZ* z*a`U$Uf9h20-Upu-aKXEdVhN|l3JjH;Lpb7l*@4VFqQ8K*71Rgw}A?xj5}A*zM$p| z&s^a8+N%N|v@V<*cmeeMhVjnsVSL7QA~aiapZJU_F|T7JUWXE(@Vx@6{S7@2&@pFu z%fewCha2751=6$h_{`woaAg9F;?)_9!Fo3$>+a}G9_8pmx~w79ZS3jl%NT*`Xt?go zi$@O^FB@I(hEytH*}%V1oEI$|r*nP(5M}MFvI>{_p)cnl_&< zA4DNhgG;Kj!^G9e4!Py)opjGu)X(76w<{a~L)ZLhLs-!M)3KlFQoz4Wo;cEAWw5YF zLnUXe71!z5+A-U+^;1<{4eYq5zbWchfWBejk;3kTl}G$)FPwhK20m@n z%a>tJ;Tc78O8Q8*m2)zha8y*9_Z+kUu_dyWV%4$1h4b-`QNPHwE^xJU%lf1fg}Ffh zc_S}UDh(jSwkm6jhf$8eI9z5-Hz{CNrkNkzc34`XXiT+kpvKuq;&$(LoOX{`3IXc)7-rVw8E*wSf`L=*@ z-ovXsp)|biHR?}Ta|vV$ZB?xoosOt^x>ZDX%*{i;@RE9d-RK-Uum>lS zK~Tmb;*5!%0{qJ-^R^erro@tb_NFu~wMw-(RyGmld^w%4PNneDqNWl?ko-~m2X+$o zaC{sMx*!643%g33W<|i~iKw!3ycJ{QsFKQwS**sCu`5I7havao!vZ?|xbWCd&9q1I zT+rqTn!P*DlcvnhE1<4(LNXY(H7<7p=CAJzsT|0UiW`OfAm6hu1J8;N#}37mpnP7n z?{`C&5`=)Pq)sx=C3{slOb9r5abF^c*b}o(of_U*hzI{PYGa>+6`<6#W}IKI$T^ww z6y6!rLn?i|k|X8g3J?%U$e;CYeuZs~2g3N9SkZx!r5WsF#*?mgl?Sw-?NUUfjr3mAqE+K+ z#2oI&JI@gh1OYkv56wR_ZG~}lcvD&Z ze0#fkv+O+Sgl$4gZ$7X0-1IRc^x&@G!fa+Ym*4&ZIa^j$ImDVaawfwmuqAkd?4Mzz zn}k?~F-M!J$BEeE0m?=|qIg)4UnG$KV%D_-s4)ywYUtrXoZAA_90nOKr0sIQ=wJ^& zk2483kB=MTEe&4n%0cj&Q5+deWPJy)B6HuCf*o;~pg!qC@!!D76@ZDGBABkM+9_Ug z_&pB%xypaaPWw))zXM`)MH@fR9XW0Y8-BCu6=+(JC;m{a_|r<6zbv1@Oi) ztQO^XMo-jy1^9p>tD-^V=p?p%YzqLDuQN6@R^6k{fd7lSckIqB+_to1+jdrL+qP|E z#kOtR&Wdf@Ua@UEd9%+sRkf?yKD)lw`w#Bf?lzxij^6vY`iPyiuN8gv%`f3&o}~3{ zYvt+Ul1d_4KK}3wEy^)f#SZLJ>*E7$1J_&swV7i<|}K#_0)ZIpV@ z;|)-|_W62%HX2hKf~g-PhRpgcKZ2?vM(8?To5mBpA=^x^Mc?I&eaiicv-G?n$dZO+ zrb?IJl8STC(joWhc(BW_KYr7JXAv=^VT)!}r~qfv_9t*NO!%_*2_*$`rECrK+1Y*; z*h4Z-5@JbW-@Wmj`(i+dZ#9M5OS~XlRB^Wz4b1jRx$IhR3y|HtLQL&IYch7Yaz~<7 zQ1*X%F)i&!aEN=PX`94VU%dyA7u6O!5mLlg%}VI;uFv#uSFhDt&t$WqekZE%>+RT$ z$Ux+dhj5r+N+Ds^$qq}EHZsQ_n7QjZIx&lTN76r2b-aGi3{U*T5Vm}~_w+}UqE?S? z_^7%pmLIjDWrN#sc)@T#vlB@~LMI(<{)F8*+EgMGrCh(Ot(d?wiJ35AgQ0iltclgt zx5tUPJp%5%&5k74=yjpDro~}^6FMsbo5$%1h4b^pjY4mxo4R5h6MXMRhsPW;P9;fvAJyAqmC*89q z(@2s-t=MkS7NZ^Qps!;%6c&oCmr5&;$y>u-%1g-{36=5HFRD)K#Jcyh*!Y=!&&%H$ zR)GBaOOY0F_lS7to``EtkVg>cFzbu1J*U9gQM<>x$x8dSqT^ys?94qUsj#0&B$y9q zk3%Kyqnw4A&KK!&C{H&*nE0c5U$8+x&;nfa1yRX@(rs5^xw-e_*YxWEUNW}g2%S=5 z6^LNc_(K6cVPv&LlkSMSq#@?`BUQIfz4C{ewmhu5AY)@O=k)GMvjLa#5@U)_<0Xa! zVXA2B7y{w?6~9S^iD+UFyQS7BMGHxJhOtD+@!1Ws~m4@XqrD{8b;0h5yPmAgeKoRga<3+%xQ)x32O1V z^ct}@%xqATvBPNF$uc-*TxONzj*s()fuY3Uvzn~7_u znt;Do5eKmwteok2^|PI;ap%Wl#Cw_6<4xLJEU35)A@z0=QYhX4>^YW=_U^D{CVdJa zDQd|n7e%7dMWS-!&F_^@EtfYtTuCZuPvxBcdO|RvDbY~X9HEIg`-NCyrzm1qGp5-8 zS;>IK{(;@6lQj@wqd-$04$%8^Uyi92MbkDB{|?&?>lQ*-(2=8iHocE%Y0KEhk9)pf z{-OM8*+iX@tc{nSbA_F`?-sO^B5N!!-k(y-0hcRac$2X8252a+@PM3!Au8)HiU{cu zd)SzZWt&f@9%kX*HWj#Nv=#1}=q+o5rEcQ3@hboyl!dg3j8hFO*^sFA&TV^HyPArN zEv6V0nUU%4W;SGHCzxz?^32CFDau zT`*}cQWKofNQ$DT&-}&2^T==nF&a*#3vj2D%zFrz@El=~?w?tQ`4&K^EheMoI!-Y4 zN3Xh!C_SYlvg^kAu76n9~;49=oePqsEn2`9-x@%=l zKsVu<#@7M^Ar~;E$&c7dtcN zX+TF$NEZ^GmT?^@+v;^|!yAt4 z0X1Q0Oz(AD!yC725J`>@I0}2<^u^}!!ZIQl+9?B=Xz0i-P7J!#hv}_pj&W=fM)mpo{ZWU{ohzs85st2v6?<3>oFe9&26RIV7X`y zVgOPnb>RMg(A_=|kA;I%N;j)huhc{&)2Apy6?QWF1Zrf5pUth z1L@AEA!}l2k>gj}P$Kr0lep$85y#O;HtHEh4KqbFnIyKw`7|A`->88MH*k-}oNP8o zC=bjPlde+qfQ|4moe&Mc@JLU(3`KW8HLLw{VSNa6e&~D?vWbmOg+1ZjBGNOQl6Xk@ zN&33(ifAb>Xika;z9WL+D);yVkeM&$3khwT4Ymwfr4kHfB65A3!sL;Y%b=w#JB*_R z+k_c*Wo~tkL!qlSMqGc_S{YNdaoJ5_hVq&M*qSWqt1m0{$KKBuFO1b2kpu!vl<5$M zL3i+yqli4J9iExt6~F(|jVKu4qV9#cKYk5L(!j0jP&B=%UEq_ieK)gXErR53kGtPw0%o(#F8Ix~2IEIQf_&D&m+Zo*A$h{8F9x34b^AovlYJ zY4_dZ;jrcEHrTK}mD~>*AMm0S{p0}YK*C=s#7R=- zFk_r7W0D?cUSG(Hi_je6CBdKR^_x|M1`XPNl8@bk*WnMIvFj`_J?t+Nv$AKG;5{2Wj4Nlv!}6Z{6uRH-WLZaj#Bz#HXC zZQ=96-wY7GwtMDZr(WVum>i=QB|00;k)}c?!Z$B@v{oO!F|QGG3l4y~7B{V%WtAy< z@NS!xa+&X!aIxniYysUnl7jnXRJ2i&ol6~kB57kXZfe&M@(?lrtIT5$l}M)P#wzZb z=Q6Q}g{2iC5sbOV;mh^jw0-W%;I3{EVH;+9TcD8LeB7RONFek^GUQ6;@2SGe$UprI zKC!t~s3hjPGzoW}Q)*F{6TvO*sj);0DLwniRn~6$RPYBZNyS<)s`*k;{!+)qdUxF& zalhg?cTs5pC3Vs?=%*F@W!(yK6bNrXwKsskSKKZUmTqc+Fc>K-wEE_>`l!8-!HJNl zD-O#=G=wI|;x9#DGZaeM3M@gQ+*)wxcRhP%5l0;zDr4B1eFuLvOa=NqE*~9}OnnJQ ze>{<0FU_yF!UkP`qH?eCtD#ZEC)Ad*PXURbs~hd-Xyc=-y60(()gCen%zbaU%cGD^ zI5g8>5i|=Y4!HZ=!i#E+Ulko%cK*eVJ@(~hm)iQBt>(W{0KcPplb%; zmd?=vWRp4w!N=OgMQd$AG&~u9OL@gn8vCZNL*+Q-_Q} zdF}+IY(PvZR;}Jp&9tb&M8!bEEPz6qrBYK9(kg*nY z9U3m4v;M+%-ji-2-O)G+VT;4)?WO6>2a3RwU=&n3VHGvENG~r|XyFdCnHk+N|sMd?uxP*b@e&l70hzu?Z81vs$mvIe^f?~ zGtytYiQCU8KYmRI@`Sv0OQ$S&XP%$GmW%mR>hsO1fH=p%i+@@HI^vmP^D4$Fg{8l< zz=OPzyfwkPN5f)91Nob3$>FU=Gr=6^P?mx`S!TyvSpRICgq{fT8WlNaSudK0JT)pF zZK#$Ua||B^wY>>wVY_${ZD)%XL|Y5d2_9<+Z)yc%0VPzf_z=9BVf?#2W|epP3WBZ3 z>EUYiHipRe>e-f!-R=nPfa(Qc{*pv0lX>HbWEdkv$|*KO?DAm`?BzD7ZsaqAPi=sc zv$(gbAQ`jKlXmSyaL&b}!w{oWga;jE{yu!EkatvzIy%4Ix>Z`p&98)k(6L8V_?es! zKZbdO(Gy!S&@70jqPn+9p|HMY4NOm-_=!e3Z&f6xC^3bsFRnC0ff#cI(_iA9UYr4&R?dJArvi>HcuwLZFOiqpiVp28XK8%TXemYMjw$aLR; z#a?;M;nD>xgyhQ!2I1aY!uwB0VgWm=thnE{ZiX#c>n=Gz%KS0AcYmZ*rLgN_77c?( z`W@D36)i(Ade)V*`7`jgY^qN3s;G2rMy@7op}h_$r2~gkde<{7a{FnEms_)Chq->` zlO|q8UK)_VCo%Op?E!kU-ykV@{+7u$na4cOh_A?pQ6Ar5zOq-SIS*wN1XMtA=3=p9 zjp>d`ccHq(nm_`R4X+Kpusp6{Jr!gm_3sHvAhaCE&NK9c9+4!(gHW>gT zMcO8>Rj(8&UUFRs`)%!GUfg{k*Eg6gDO0Iq5Ebp6OdDz-OTIM;a!?S!_XFztY#|p0 zOMAYQstWla963kQA%NYCw^fznUV*F3%Vf&0V!>~~Z>i&WxbbNPX^K>D2iT^VbBRRc zBW5^h314Z*4w_SWuNo2Bw)lP-udCg$+Vvd!i8}B}Q;8ovW=GICiQ}PX^(W~fqFFw^l6B+b+y;j^{ zh793i6z`YCM_44EqK%DSoVofb2q$%Oz+vIKRgI-)aiioCfnJuTE4*$4y{HS3_XIsW ztU{n@xJ;!=plj!WEt14D;l*q4vNMZ|F5F_A1Bj^LHi$n7L%Om^~2LFE=>;Fl)_$Ooi zzX-ek|1j47gRc8;4Rn@&ZJ_@XTlinx#(#lxS$?Wd{&#S$jN=cNqw8I5M_fjP9xy;p zMM(*jD>3`RhP7Iq#c}q4U#YPwT-efNT;l8V2^=6G>4KdLe1Qa%b#}8xuZP#S>w2X{ zWiv9@m$#?&q&vc-;QkMSS9a`|H)jv;#>2KtH2ZhqS>|W+nlIWhw1>UQX4QIyo@>?1 zVQM5>>fXi5vt65dR(7aX=hb^fx2>w7d-sWpe*80alyuazhz(MDnRJl|WrKDkH%&3- z?+-1ds*KNtjvPJRkpdrtIs|xUPnT}>LBa5$OAU&NpFG-&|J67K>%@?uCtCRGUJcW4 zEuAG~Az%@B8ry>^%g&5o%5934@jNV)qI0g7 z3j5j)v|HtP*%0W*lL0H+Pbs65)B4;Q4^J5Ahtgb+8D)FU6qEX$#jV_y?&_t{nh$1I zV6uB-b~(}7E*^GcR(~Y*UI*It(~@q#gK&DIn&_HF0!N$ z;`jAo@^EvVrhh4;x0Bw3`j6;KxuyyH(msMx!2S07cz+Ft^t5N2=EWgB;y zJQ9cT*(Jt)$5}+y2T0m85nsLq>jFL7u5vBrCVnVlvucW9=j5)Soeml@;` zs$-=e=nKT6d!MZ0*Ye3uh{4P65^#dnM8ouhLPpH8SEetm)kKe+t7bQT)90$^QP)sZ z8kE*~mCK!H8tysRwsQ+y^l>w+_dTDTlsq{z#ZE{w>Y^`VzNGKT}4-A zXzgQRAXfD76wV+p1xa5!XC?#mxYUS4()4NV4aTi~z_GKOtn1n50kHgbhg?k)P8^(r zcu4r}D4VdG7FopFvNCIXL}tX_R6y#UBQgbrpaKnnCxE^}GyvkRo5O-i8shVwm?zN5 z1s$&Qt z)ORNbW#@2cou&k#?Z%iy!;Phw7B9u?Grlht4z{Ytf&z*-vcfl-bDe5@@~ z-4^G=WSSkqbcHK^ubVMDgGW6PY{vdQU0W(u-_-WG6gR`K6hnCtFPYm&-@&*0>xTJ( z0ay(M^FAT_S{QrtIPKJMR1>3lp-6OV+oU7In1HA4t4j#%i+?E;?5M&h@7#- z6bPG?$nQrNPh`|bX{kA9&xwj)mw>}%DQdjQKmHrab7ov`F61N2SyTw9TG{83shvMN z#Mn{VbAw+@E>|O^k-XGyn}Dn&B_s5g}q$ojBXGwvZP^<;EK5NsiSM{Ps+6#2Fn(w zVpG#hJQ*|!Fg^h%K^ zBvHH0lZAz_lB12qAd3L|fwIjv#iL!x8_%ohrJ`a!^-YKr)rGH0V{Nb0yHO}duw;z1 zTinJ1??Ee&58T*Hn$P|G#F-&fbSJ*6VZ1#u#q)+V{|YJ8h)#9J9Pfj`@vvm-C~=3+ z7B@Cl#+JA?E`%1OPdM*3T&zlL0CR+VLX&eW)`xX73s6Dv!M;ktk~X4jVA4dvF$|c> ze6(XVdYKo0^fK@$#Amz8iQ#OxQ}~tmTkMvJT6`agUpJLiy)|*=H>V)qX$5h9kicbQ zkKec^z+DB63Zl*q7|cTJu_&{1@xDTp|lMJR9t|M1GU~s@Obe`xE2fiE>oz{$noNW@_&L@iONqbEA z8J({{A=g}WXTs~Z{GLQ=Z2}n#1jP~8YJ(&~?zG8MvpgZE@gSSYCJM;_>?~8fIPTnA z;}y!w0Z!7=yInVi8g8JUO|HoSdd2mg+$DheJ~&Tc=g7AfW_?BV>B#Kg)^$T z=$=RNZ^jk!oxEVvH!jnfPL6B#0^Zn0Twa0V%l7bp&taAPndLYeSxk>)hl5g>nEy*T zmQdPJuSjPx(f0)j@yncc7m;`O?*nDpCA+lGN8y8w_6MG~Rxf*Bs?bdv01ibH9_tJX zS4?PvUm^}X+C(FD?wRwjG5LKGtuKmf3CI{CmCN79ns0|f$5=mOx5g+AtmE48+2Tzb zZ8kzFsv@MLl!8kodFh~68{5B8^M)FbV1P?-o@ZU!Ro_{3TybR zAmI>}dv_v++RnX|9Y`X5$qIv+i>q2nu?(ocM?8`*SZzS&Ji7zLFl({Seu;#SL?Igk zN&DeM9pr%k{oE5=12@Qp`E(Y`~eT3+rR~siW_Y3D^JRB2ZtLb zbmbo;)4Db*7Hm`%oWLBEwI&eLg_Q^(s!+Oa)rX~d>9gG5@MM48oTD049&gj4xIrl) z25F{!gy%r58mFBkh8cv}2a9l$5&MhR9-lhJTY;gti|zDB^mcV1%46z8JvCm1a`}(B zRpYSY((~ApJ|Tq7GTxZ~Ns0fpPd-c>^OdKKXbTxES8?2ep61iELJ7If1 zK9g_2v8(y}ca@9PXAblohAU{;BfS@?GS3x&qWuTV;r6Y8WMh)#?}}G+)qr0w3cfcD zIbxqO4V@qO{L<*;wT8_P%y(E3j~!-Qapl>+`80eM(ID8lVtTy_|3YEdKYbw{*JjFptadVMN*xw$S-*Bib`kZ|NU*nrK zt@hTS4xRrsr zx?6ZNuJ11yq?)iG^>~&n`89gJL-EMcjr4` zQuvrhMb)dHaX<82e*H6M=330fiech_o0%u^YI%Lu>}?@?Ak#d?yg<`qIt&gK1t=LR^L2* zqMR+qyGf(2PGhmN19Gu}^RcTD#4m&P9FC)QJyfk31L+pjU`=NbX%9W>$&k;MD94&@ zlYl&?3+WaS9<6WEbxy)%Hk2rRNnfBfTK#nLn3sOBiit{`x0W0`T;1RVz2RmITcN8Jf~~^ zXz+NFWx9NNO6Bq5L*6v?@Ev@iuD)_ygC3Ss)QQGlK+D%Nj^o?fP?Ts)vRsK-Rw;cG z`Qh%GUr>S*ygM+)D^hK!5>(@&`t!!8e5(U#V9dHZlbAw@kS54qTv(XzCIqLgPeqwR z{g@lKz``?N*OjxSB%P~6&uf~aul7!8aZwC-vFGD z9o%*wTR^3)$prwu2d=Zd@MGD&@{*n8aXRqNOKH}LJ5b=bZr&RiKSdSwxdWtTVSDt# zRz-AUQxwEuiyVNdu>kSqW_`6Hvzb>|Nx6J3!->Z6F!xPYHNoV`a{@l08p$%aCx;XF z&l2f3CpHJH)s|7vF*_eC^)9L(yrme@ce|0N7Y4 zVI(HUH#!#{xnzqHUs7RyI2bRu25FyHo9c`?apiC+=v0WLi#|dE9unZd6Ir}MQ+AFQ zJ3B(M+s+;TFhdY3{QX1F_m!q%`ib#EYYx!%=XJOFnN<`0)Ag$%Q@*Dm#(9bJA{2gQ zRIFh3_cH}q7nQ17!^#}8QENQWoKDr98%Se;E(pBbdVJ12uYZ} zU%Gd+F~!RcEv5UWWB9Z&`MZZGfEjXkX_G@eF8v zH@TABth(k~6(~u9{hlenvR1fZqwqiI_%jq=e^870H_11N`};Mf5j@J=)dpS877nQJ z3*y3!TR0nE&_^gwqWp-$k`T+`jg%d+cCfkI3lsDaoj1bsz@LlSv2Sy6dE${)fP#zu z&sPEbS9b$=SRi0a!ff7>gzl2CGP=} z(?Q;O9@Yn-)8CPXaSYgv#gp;${#F!Vm=rNL4=f_&NP2>+YbrcSDZx<(tkor+!c>8{ ztNC@LxTZ6xTIOZvXW7Hj6P3&4;et__`1b%=y5frF(Lrbuvv9P`7^w;>603wt2lWM! z{q)dY8;KU6T!k-W&uS%sB+QX_!e4P|RE>^TY)57tm+dL`998CJe6?~?cM@uoyA@-v ztsJ-C?Iu_18iwLd^rgJ*g24{R?i`Vw(gN|~>CT8>m;(`@U!(^oYmLt$-!uTalnv4b zq=ODBztNIiK*3c+eV$)Q>5gXP9a_#U7&UU(CSL4068ttdA2d@W9Fd*8* zKs!u)JWwh0(ru>&H5AwvNVi$`l#6pfjwWcI8_yPykMa9#(t(o@utZBcDdko{#Bt|Y z`0p-q!f7j}SxJ4a(iFJOh0h^Q%{r=$?~S9KzitoS7B>|9A1W=mII zBu;afvvbx=QvO&3sX682N{?if`(<~={083JzI?d%?%cRSICL-aNs3N)j!0%~r_s)1 zpwTgqyHlK*6&FXHBAE_zAqUY1RoE?kov@Sq5d((1f-hV4>{Jw2hH|KS6_$j zE~3gkV7jEWEH@AX=&_G&pH?eLZ#p7B<(=z{^PT1f67cbR-qrj70p!{Z0G`VVyR zf4_P2e?XL3>HnRKOFhYj2tWWMEI<|C`;CO%2=n#3x3{u+ea~P_^`%uB`ucCJVPDi~ zCBGP9?#mzeBFzFcSXd$^HrN{6ExPj{o8+|F5;F|B`$EA60jm82)`-+P_tL zS^u@t`;RC7?=S4XlwPKvg{^;9dI#0kZT5c%%rj~?3P7q+;^FNuV8A4&4<+cQB85i! z_}rfadI@eLDcp4%5??+Q5hxU637e@fM}*A83M7?hi{(nk=yH!|-DxWqD|$0@bd~6= z(Z(6Z(y;=^#}lucKB_%7sTUtV-veH2D005F_mDg<%G`g(3v^XT-!EGxT-7cv-iTpR z6C3WI5Ye4=Ch=3DS$T4mwf-vPrVnvPn2gV}@5PI!jibmZjwvSJw>sWT)vHtI4$JZM zv_r3YjR+3A+}~YzF+!&6C%!ji9YaD3x(PIJ2Ms8)u4cJU=6-xj4|7MObY@deCdO5g zSnxpz;)P&lPGU-fSiemoE?WKfIMEg-Dyl1^BdrES z&>F-QlncpsB_&eGX}JWv^;90~^1u{tPzp*Fb12#+lJ-=jwS~t~p+V3qd?&EOpWodc zcX8Ec35*#Lv51C@BLz@bce(C^5V!0o~_H<`4N??@!D zTcvHT$-h?EyLaUGYX|d5%mMTn;XC=ypM0)F^R`K|f+8-3gdAoV4*X-2fql`p}za<|TlyowL zjM?@BjA!M)-i1NZW&!+;nl7r`xshKAK_m4ec-rt)kw9PIqR3gjc-fhN=&dIb%o<=5 z>&C7pYEdoXmejf+ki%dI!ZcgKE!jruJNG6nn@##mt=6!Pt4i&fG`CioW;T!(n%Id< zEZH2Sjis3kly1m@l$|t!6UAeL+xvzW9X+fF<8)edb_HPJb54OZF{PUD8I4IIJGEn< zg@G6wbyd0+FKAlUh=J8?!s0o+grn&-9+~>uSP-4+fhH1UEzDKZ3;E5M=NOK9@)-E# zu+bnE6!)Z;l6bG1cjVGx_Q17HV-8&ln1IWNJ~PQOsvd>~0z%XrM_ z+V;0-p>tSAja^y!(}Okwj^h0O&gZp}`dA9QywyJmy-j(x^14~%6iqV#o5T(7&-h=| zjK6!YxXLBGD+_AwMg}(G_;yfkl(i0362p%c+pEd9)K_n=9n89<)gxme6@D(%(K=pA zwV2c6pbLjarggW+IrJ6K62O&;D=Wxtf}U9s$BR@)ju9Ajd1##?;qXWMEGS+9E-msm zFrhVPVoo1P-B{7~^9iql6J8IL5}(az;fi98<@1zA+yLOSdRu4+T7G_PhAhLu^Zmbs2|G`Rg`WSOXG5PIPwcEgg~W(x@I z{vYvHIEk8XR@q$8QVYP+<>jBo9^TkIg6EXrNw)@XqqSo3a}Xku>ojf~%MHLKSug6z zbFi}o)3>I!BvFnzM`V^uiP_#sVBng2CkAqw|Us(x<{YTZ0z+i0{Bca z#~vN8iFLFLK6>)!5fNb5G57nEG>2>qEf8A$QAwMgH{c4cNWVtPv_$#jHlZk$5*Sb^ zbG~6bN7R7?iLOB6y+$|UJ6yS^HwuQj3Lq0^Q;*!|H?T|jPwg|(t>n5e28QhIm{vzn zQWZ)ugNP6!hhNoDg%co2TVrM zaxK?Q^ReijPliiA$x5+}3R1(XV)Br|He&jA+xwosuf?e5Z%JL+TO#bWOd^PI!)ozR z>+8|-hA72}X%0PbLbn`PhIYAxo)L-q=y5i|vjQYuTg7hEw|o^~r-2xj6sUpAsrDIt zlt8axD%A&*#-yU(I_sX$g(Ok(-0v$;Zjtm5TnZ;JmGY=u9uG3RRP=NcxMX@@;YiHa zYpTJ=rZAm!=z{Hj*}M-__WkUDk9_feIX9%LKP2VX|I6imcP*6CaE4r6=G1>~8p#Q_ z(xU;p+AH?3x+4=uj6@A87Pk+q3O>iQ6q)y} zNR2NEjZv5%B^r9Wc1g%&9TN9X^h8sg!#yj&gxfMPg2CboJVVl+#D?3^YV2<|f2RQ) zM8pwRYhe#2U{+0oT6hkMt01d0mGuyd_H3La)57_8T43Cw*I(zAU4VIm`9^rPl|!Mr zAI;+VK7=f*yPEx;T6xEyJUWc?BJHgU;wE~jPcx0691n68QPqpjCdd#{@vf@}GJQ!m zpmwGC5*7Q+Kj za|!_)&X$3HBgHCmJd9$n?w-YN@anJIb${Q6a?9Y-Oy~GTSkn!@OSO_{z`CWOa8F`veB zbO2C9P^>MiZCq$Mg5MOX7A>Wjm16>VuB3AY>g^;xL@GQ`bqbG$-5BmJ$Ww2*@8&{j zPAes!gY*U*Qfl@~&jK(75AJV8GCR5ZoTk^?cSFKL4}xPp4up>;bgI(qg*Vx9y-5ug zpY0!ov$<98D}86lYCqYXY%7;De?_n+P-GmG58fBFT6u5;pdus6jT?L>wX)#>$gZ`2 z<7x#HaV@L^68>I;wj3<{RnjN!5tLFXP_5E@>ZC)0S{yKr(1|EhXs$U{^B|5+v0%iT zD5H7>Xhq*b*G}Z`3FT+gcLIE-$brL>?W8R2K-e60O_X@Bu`#)#)CiZm()2W-0XaJo>s)(2S2DRi69CJe&Cf5^O=1$W zYCg5qnwnJF1S*P8y@of@Of)ISd`J9kHnYoxOM${|O)UW&p_$S9?D56QHSY#_E*UA5 zB2bp0ueVbmD^OKhxfI#|QRQ^+Z<(l0T~jbLmmSyU=9?1@QL0#DPM~ zMAyGSZl0eq7reFENcPiu7pXjs>+~=ouHR76cn)FW@ky<$sk$S&7gH;JDuvo9c6uDTUU9K@&&VfWQ{^x*f#~PZ!-sKScshAb&mX2 z^}}^mRMNxXNV?{DHU+9M$(-n#f7JK6@V(P8ty^}DMx{rg$(fa#R+cFSL4QN5B#pWs|}b7hL~0A4);1 zqV2L~=?yz7QybD_aUZWmDO0`zNXFhWFAofcR*=5isWNaPM^W0(YFs$sbTE;chPMgy zRMAO2Ma_uA;h0Lo(XMCv{bSJm@29-~V=QO>w_`bDJ@bF0s~Q^t z{*kUqIg($4k<1jdKqmhUQ(y%6!%LygfZF_*r}!sc%Ksd}|C28AzXDcF|G=C4piKTB z9l`$%x#T~QEB{F@`M(FR*#6}>{vS{O-(S@KXe?)9q5t2eL8x%R`CcY8a>ChInQa_d`{b;Z7s`v-M(O5=@|d(UR-RP)wP zQp@LaqN&yG9*|d;j;{KHN|!~Z&8Ljj(7EXkydBYYIyNSDcKGBVqw4>tELRj?=y>_9 zVpFmIP^GrQ7O(D$7>}U0{2p0($cQ9KeN79cQDlhN2mhcfY^U)nB^KQuBGp&NoVbDA zUoSP&g)~7T;|)4eTvUNJ#R?D}3ht9q@dw8I{3qZ$D3XXh7#i1lKm^UYKbTPfG*_W^ z090K?q;>?h^Nmy+gWV(F$xdzIir5=b>ZLT`IUmZVUj#~vm*#t7rSj=5tXdEBTHg$f zJA@J*&TRxvPF9}~_QfK$QU;TD51>Ct@5um&Z9g0lHw2twD&PNGBH@`BSz;3!_|TcM zyym>59KWM;Sn*JF5YmJtMk$>{vnYJAtLRH}WlCESU2_bkKGm2}ZZv^iOlhsNO!}8$ zevxE|>Q~5-cdO4cG`s#xM6XEtNaJG$ZWK{;zIvma9$~+Q=|cOuD#Da^S)eHfD~-xg zs3|IQ+GZ#t^9KpR5G6Y<+#^HOdNEtvvejg$`am;HH7;HSLL&Y;x_Q&CBm-cTJt993 zRrN6{9iKjIGS^0X1J_!Fny9G7cBemwJHg+b2;E>bc{5O3>mkhQR$$#QXRO7S#_FBP z`+$gIR#v}*$*&xjo4y~401w&zJ=pp73hFi`Iksg8u8%6I4m1^1%4hnqzZ;h;k~2fv%t5mN51w+J+1zcHB3Xp&%Kuj%ZVx=#8aR}1WixgO>Ts2y9RgJ&K- z6e*xa4m8iJ9AwQP!B!-Diu)Q%ALYsTg}ftJ>&F$rebSfM$9QstnSfO@nAdJeSfm+j z0b|+n5*n=M-Hpw_=yJ=PYCt8&KJogEHGemE*+~eQLpt6z62Gc4@Ph(;ytG7BF3U%} zWm%Q>3;A`&-sv3P$stY}Nl&eMFGy$oD%4yEEpD~46RWksLM!a=I`^Zm|`IUNea?ipKJQBpx07E zA5^mf=zDlA|nH9{$s?vp;tEAqYu%^7G9TscGwYBw2~8P?Yr9g$xiv z@Xva}MvKQ%N!-^$RqV__8P~o$6lIymbw?@L)Texoe&)jnz2d0f*s7=UeuN#9J#mCVADkFMui>&zd9(nvsXo-w$KTa4oB2&>8>q zse8GAg_Yb~C*Wx~Viwd%En~N4c_e{#J9FJv8yT)MFqqYd7PTQ$$?0c1Dlv}8WV3@b zCBm3PbrR)BHcFVeQGVLzNEQH%#G)M3y&>i>mWS|{z>KAoCKRNZR$S;YssJftyi1!L zrCWlWa^JNN@6H6W2NZTRt1swJxhX>;>B6|-3L^T2$2tvexAIq(5Ybz%)_EXdQ0!kSEVMQu++_%2&-GxQFR*a>(_!xhahQA`atfKiuR& z1iIL;(dM{(Wh|bgjKl@j(dW8|ui3dr>i`31oNEOe+Pd#mVQ$sXcidas#t2aANcC{|rOU&* z4;Za^rqQQ>bUqiV{ydb6rR3|?+Xq|hw3wJ_+m_k(n^1GDcQA;=@{;&-aO~2aIb**0 z6_evRvk85l^ep2!Ulnkv#%;%c=+1#l)cXjtHhMyttwN&yz#BVbPu6C7qhs9lk(=+m ztZi3JjNS1;EGLO9r%`-qaxee#e@ECkmJu@9MDO&C=^+Q$_=CvO&v)nqo2s| z(=9?piYDHstuPJ^OXEe~aO|`=gE)5_$S)QzK6nX=SaHWMCz%!eaOr~ea@Y(YuILNI zr_-?~yw7IRdumX;HJ+B237g2%U{m;}FGAR|q{+K(2oeMob(zO4gS*c$%8uwNWy;zN z(a;?y_C>fUtv|k)#p1C_1dv9KrDtG2Mc@mAy)JER7A5zF<4UrBZOvcHjo_XVBHmG# z(UTWC5Dq?k_7RhATPur)SIZTqVx#q)^r&d|%uELj)@DNKC;74zi5V|?lcxyYb&9jd z@Nv0!MlPr4?4lnDrb|F{Tc0^{@N~4vfF`bGMwCAg|7LJXAr`#mJ{9xvsR={O!Xc(^ z;1g@BN93ho@cTZhbhDN$)EX^_Jn_ZQH9q3s#aLeE_uD zMdj6jM*#z|fir2$5lI(EQzB=w#H>WI(%nuFK9b1g+zK&nD|jl=MEWsp7&lhvzR5(Y z#ZcV2_(+lO6R(O|t#QfpT;bX|0f(m%JuaWo$j&X~&`^|ZD>PkWoD$OOm0jW%RpBPI zs9XW*ma7QxNNk3TG*=CNIc~%%y2}k9!Lqx6w)2FlwE=W5&sp3ghnma5}$KG1?jzZ}*L3-Q5wSck@vEuKfwc?nQi!61fZ zVH5LH8s>b2KUJyUXhsyievRM=P+=U@sD1r4XMdO%IPN4%iQg(A7)+#2l=s&+SJ!kv zP*Osf6hTT(Zbtg~{Q2s1V=t@HW^3j+5w3GYYs5$_dE>n3XP0cu!M56B}6JD79L z8#2@!#l5V;KBKe`*jV_1ylG+s;-5x}vMxO1KOv7SMUmfdbAX~e20;d2G3EJ&kEV-s zmAFBj5Y7~EHg6g5aKc^+LXHt$RzT1Dw-ja;Uv3eFX0EtyhiXeqb^B7>$6Wc^tQpjM*S{Q^Xy(-`fl;ou}N@k?Y4qgGkF4KdSesMCX8IblwL zM|r~b#q0W$KEmJ1nm;Mq%_D}CCsv2EcHk;QI7nrt-bwm>12a(lRy^~mK4Alf7@dB@NnY>#f7gBm(Pg zL+clLN#e_f1^X3t}=THIq-&xXVjRt@fSfv&b5zv z;sDQ~B!lQ^?S0rBp*-$9kMuGTNd+pfAaxNlky z^gUS5HBLl6+k1Piy`#WS+ZjWi7s$XO;3Ow^AAY~Bk&pD4tW>W37|Lq?TxGeqRMqh< zDS2B^_SO(=#Wmo>rtsXR`nco+kC1sML&SeK%wO=ViAZ@DOcJP#N>i!wkNoA{FRC() z8eDB+Ad3pFtzSW)Ky5%2J_;5-Xk zRx+vIjHu9+d|kxni9U6VY_uUjE%QE*Ma-}c35ywP0niu)BTUzs9ZiS!9n14NZY(oH zZanP*^J{1N2++)e995%_4-`zaC4=6Y1P5Ggp+!VR0p;i37*iSoSoAi=>CHN<-j2cMP(3&HC$IP^1Ns|ro2}d;l(pl zE!|H{FmmSPQ*_DL9cr(%s;v_|`FXC$33gdS{?|)oi`s>2q+!+-W=KoQ1FUP;; zUw{4L|A2q7vHUat8dtZrT^mOHZtktcAL-R`Oue#3AaJ?#pmg9RMq7oKQjj3BKA}nE zN>ify>b(2YMnWnziK(88Pa{2caDG0{JjLVqV6#?rX&R~4?(XR9z41<096vd$b8X7- z{B&??!*1JUO=bUZrKxS5W3nc0&zcv!w$fLg-SE2@&2~$#s|yjG{~ZQ@DKYZ*C*swr zOQVKb(JwvObXbjj<~t3s+DQ>KaVDjI5U^o4yKDJ{ii*Y(`Ht2W=z(3$AR!+5)5zZ= zBq&lcm-OERj0j=}(3@I_g9bUV1X}(%@dv#VO&x`QN#blp5R>HGrx3y@!}x#^;dUfY z!4FZYJE6PmvL{!5iuNpgGus33H1${ihz=+|VcTfvnsTV>!EXR|4$k-sF6r?Sd$pqo z-vH<<>1js&+eay^IT+i=_iASv3Uzv)e*@rVz8X<)Y3gBKG~@ISp5L=&t!iZc_E8{^ zKTAJAAX)=g*3*GfL>H<5k>g@`L_hiI&MA(awsT z3M%#@5T`wV%hvzMyG(oA7?mhdNhsvo!5iT!KAMz0(H z0t#EfHBws(3ROG|Q4d zbK`c@j8He3j$ZL*g%~tx9$x&V>bBM{D+=@b@~kL&JB=B!`lvy)GsE5ys&nNvSuQEZkpp;$1n3)`7OvGgcM`Z^;F7P`}B_qPAmR;rtW z|2Zz%7KhQL5z#B2&ASCljWYy#U^mxJRn%0L?QX%o(W`UK4*aFcO8^+eXxeE<(PwM|sG zq!?Oa25(T7El`J31Z-wTs8G{I&@ZbL9oyT{GWZ#<6wS;MdH~WEpWPWE@Q=Oxf`r-Y z+@WJicaW0hCNL6Cz_ExKG@?mqRUlFg22sTbtd|5FW1@trt)#o05<51=gY?hhFhVt> zrbgL;TUdOyw0PaL<5Pn~ji$WP^E@94V^>|56t5g_RXrWIGMrN!`g7J&y+=1~>3&1x zKH(_|s;B>j9(!NSBf%Q>L>PVEs;0r8nM6T+O_&$wUXowIAb=u-d?;U^5tfo4yu(e_ z(o0DR+^P(MCGh3vUn0(qugF=UVBN4=2kzt%SmpHxS!mz;QD2dzJ4m8s7u`tWxmIt7 zoQhS@7DEYcP=k|nr`%r7Z;RUvehRs%IYey6Z7q&L@?=lH+~Cf(&aKJI`@$|wt(q?(4T(KAEGktstrT2YT`j`?gYJvU zgWDLH_pEvPflQ#OJ!$&g5;GN2CTAFSBIj6^w#E$l75wnSgQA-L?~HoWgw5v3AJHSm zdu+3Lja^3kH;DrHtt$osuUqCD>VVIYkYK6Yu9*?gezL`3%0j%iU^t3Bzzn1PKTz*6 zaXC?UVPQ+PPQ){&l@E?fU2iblVBq@qE>wcmnnmcgJqa zSShimCu-^p{waz>?|d0R+Ucv z4(+403l^x;2{R__(<#$$dp144&G6N>dw5eMOk=3 zR5>tpk|)}G{t0qHGNH*?5W!z=nx8pX7v;oyuQ|;uA|8K}%da280c3prCc4PUoKp^T3PNA4&moHWy?g>(K5IrRVl9FcAIIWpP`(2R`wk6z zJLLc9v8(Khb~nSnI+)>lnMQ#9o_wUBl`E7lh0@`!GEhu%kT)x7Dr{0KG6^JfLNGiltgkjc zm8&y+&TmkEv_&m10yYfAXYD=U(@w2Bs?I=pG#LerEvP;fx8GPP_}lW^*|?q7Igt;i zFBqccCTk+L7EDien5VoBqqdvp=8K5L8fxA&w!ie?G^1hTNi?vHA@Ng1B>RIGHfwNn zHtc?O;fW>rc0bt%c%Q;)gA~w!e`bm0Z#z&REQ`Y}JT@g?&i(PP$n;009N3iQpO(O5fCc9zeEUC%8F> z4w+27Ttn5?XrcbR=)q#6i@ogJzO4e~pe{D^Vl7^4#|(}zReZuuNyf5KJ61%hx7oF? zSY)`JXwvx-f8iiUYRzWC&2TSSdaQByW!$0VvBnl~-O&{w4jl(cPkClfvkgX}03v#k z;T=ywrADL}A+O2DwvnbApC7PqFpSg^n`YZ7K1}Z$Gv146DMBJMPi>z8OVM(O z&lIu(rhl$Ax)bx1b%GYEGc|Y*mzkdIHCu z*{G84+$I*^y&KUaMe{2NXBb>KC(Y|d$1}AJ4-85+2VBbF^kSnff==>`u~sksfQ(Dp zpYxt)KfSPU_1(#a`d+w@!=g&*NM2Wg8_Sx(h7$;_xh&?KTlFCO3Se9F&QOj?8#J6C zkD{}Mjg)e0>O!eCI?O#!hJqRFJcomKvbgILshU3I0%rznUl$nbd04bor~*j(X}jDH zuY>490W#5Ep(cywbd#^sKyS`VJE$-D{GgBefSTBAfvW4=*93W@V~pS#8;9Wu{PlF2 zP;L1mJXYiKbtE~WJi-^s#KS!z?FgaquT%5pK;ZN}m zUETFrkN0l1JE1^ZkLe)p-YbF&mlY5OXxF*wX;|*YN-n|R9gXi{0W^PLHlRmmXz{Y| zsqBXuJ6fT?g=w}$j&)PT(cX+z&F9m(e`@zw?2s|>)x?MA01ds42x$Pk;z`sz`e(G< z{2B)9T;Zn(U_WN2lo7sl$cV(V{MZ%(vYewQ;6TvE-f71vUq8_DJeyHG`R;j_n5cW7 zzQAtu6l!!VzbX74$LgWNrmR8_*CwL+LZ!L_dip=v6|ELU^SPc!G4?@akK*Bm@DIor?eCz4suy+5 zo=9O&Fyu!C&TD{3Lsu5I^p>=A;sAa1P71Bd*wMFDel>hx>Ni#O!~B-2=sk)~3UPL- zD~K1)YG0RV*`kf9)+*O=#oiUTJ0bbe)$2o!nDXuw2B%Zzj3U6DQSYKFsbiJ}7k!|p zyK8!0=m3iEFYl`5{a8|p!x2d#{XSj!y7G8HhVmY^nJtS6A3B=2=LmLMHcq>xA)Q{g z_U6qnwpn$BxmbZpn{3zihd&cy@%jdah?^EP%MmWm9W#2;8-QsWur_wokLUzonAN?2 zMX#{9UKV_)eNmKw0>6gOO!q1ZTNX@&gDssDIK)_)TA9>JBrOFe zrV`>C|LK~YIKXsxz+T=6$l^!0^7BqG7YoxhEX4lU3kRx64G^$C4gF{x&EwFV?H4oy zfM-0=@W13v|Gv!WKhZE2#{Y(fDgBLxfz1d)34{r&JX!c^DF@$i2A^ugbW!MXy z|1Xd6PtmY{;IF?qQ~zXt{e8&l{}21?Z)oj*v|Gi;M=$n`;COhxho}Arkn7*(tT_KQ z*!rjV*?--r#lp$>e>G~=tINjjH6wOBRc|Xo4O%H~L7_p9-y1R4t$>1=19M_{DvbTn z#?*GOT48SM;(HKOO0sfx4cI3dLm46Zt=Hk<{Pnrap7G~&0G@x{_j%eI{Wrz`&>w1TZg&%A4T7L96yOb_u}!XM!h15yylmk{Gj0g~Xj`js`_jY1qx^Sy{L0Y_EOcEp$jgg&YyW zzI?Uq?SKhEiiX`am`14r-@*3?()VSmjUh!dse9=_D;9)FQrYz<;xNJR zz#pXR{GW5SM9J=mx0%UL>b{C@roMV3@DE*t;_<=0x7-38>nh~M5l zsz*kQ3I0i}MsMt`&rR~vF}>2S+qGL$w(9y9zwzq!DF)`d$Ij6X;ML`wNl2JNysie0 z)znX-+iuzCk_Y6PC~mNNOq)U>7ua_)NxnxJg1&fciYiCJkLd^RCj4D2$>bp-DT;E% zq)`$`(ez?1~oA1vAvghXpnKw zDBVa~)c&^??uemI?nRR|TL5)b)Fr@Y1KZ-v4D%Z9n;_^ycRJzDUUL3>DgH+~*xs5A zuIY$qVuKL16(%10-qGJ($O6By1ioVe;(^QU+l5-w>&&t%d)LUY#F@M^J8vO+p6_A> zrZ|PxW->JmrhpZs%7Y{DfC z5O8O4M{JYp)8nw1@bF;)Ps*>*#@TX?DQa|?MIX!ua;FagdY1SFaIw09)wD1@sl+vB zm^}^wR+l$7LJ=E5khwwGS0VSnR8mC3xS5n^<}Sk=KCIX}*Y@R{xeH|%1ocKn%Abwx z7U^SCI{VMd^l!QwcI^>HDLS~GI6rY0h_Pg*353-0u8=VHVXOBV!g76%Y?$oW`d&QSo#e&_dc^w!^>q{!<7LgsKwq(cHmqFqCq}|$ zwj4RPTW!T5X4Z%$^1?+7o;$EbRx%4K=L_lH`={o2zkza4${Y_9QbsTkTTH|!3X4AQ zw;9YZq%P%~L?z~J0|VD8ah`%KwTlDfHbpSz=M`9jBAVAY$S1)D&&N*SE4y~n&j*U&M{>d+& zYOj|5ih%~1g!cc@i%3Nj*H@s#BL4_H^_2#;RZcO=sb1@AaVv4+W4F+U8cU+4-JujM z!d^!1NK%$QsYx&o}EC~h2qm#f|t=e6>}$(>ftRwH?>R9h(N#WHeeGuA#`zH=rl zY)BXhvzL78rkOP9mxLP|QI$rbox_?kVQ z!ncGhu}a)o?Y^3P7G0w^&ZKTMwy#jutix+BK-s(%f>(j3e+%l|n18IOF@pMo(&Jt# z77kG?$G*KpET#}O3AA!hW7BSaDSx1vN7%BCQzGC;DK+Mw+6s;bTvj@&R>m@054&Z? z_E9sv9&~bhx?h%*uXLev+;eONCHQP>+Gw*5-49C{4ZKY)P5``y7!^nL&jb2{V%3VZ zh8Aj&742mb8FzQL^4nUih9jZQRZ?suxMP0C=>}{=ql!L?xu4>aU_9tY)k(J1I?BOL zg?UsmYUkFL({KuxvcI>TQ2Nv{hJP6PFnXOeNb4+Hi5_B*j#x2 zwYRwv;Yvg*ekH6l?>VY77=`%JSF{CipZE4mKa(=@lLSqDJysE+&BgH9&XX^lFTvlA z%xK*5jv-N>NeNx}J4f)ct+(f;v1SL`e70_L+xl9*BruMSB!^eC+}jgK3z`~&TOwkiU~aZZI+Pf{&M#geSRDlY1k$4%fuIvY@zCpURVn*wY3uT%G%m zX}Fle(^W?23YJ3rCq9?IIWI#rj*#|Ok*3^CP0)9CB?>yf^k1HeQq4%qYV!lOd)wSgzwnw?v!1+{=(q+vzHwx`G z8h1dZ<)L78gy*R=q%1M^<5$bYs6ULBF6hd-@EberZE}r-B5XfwU18WMMhQ#GW(%$A zc1|ZfPAkR>6S?huyOrc{14}LDl1?#fDv*A&l$HeG%7Hb>8#9bvWiYxUf2p?un32-c z3HxVdLRefqQ>4-4s~gRYwwaBDI%ulY+nOF6nEx>(sq@OLRbL7qiIGKuN9lv9v!fhz z2p2Jnh=l;oG#e4Se||bQwD!Y*$c&{4&I~C#D9P3@rIrMNqQU@OpkU}9P~fv-#2?E6 z{Mj4@Rb0b*`Ic8;3m0_Jq*=bRF?oS?Cy>UKC(Wk16iFM6)!LePn)R|Vh;|NmePp=+ zwzXv*@CTgB5{;Bu`^M>T-h%rxOPN+h!xa_TdrLQuh`sC!PEjACd;heE*#-yI zCI!E+Jghx!23$9{L*{`gZom?^JHozg=5&Ki5oBLGI^Cm1+0ip6_En)hkg5Jt>8T0Z ziRLL;SbIQIE!B&uDb^7HXUvNRE!1NX7bc;EuYoI2H9OugMhnn_dAhd7g9ugl04{&F zwJ34=mZ_9AEbEwY$+kLq!GpJZG1m`H%49Jh<|p+l1`WuE;Mqn6GP1e!kHT$fuSeDW zhAOfJ%r2iNvPy=sMB8KHgz!OZq}~I+OH$aKZnuay4U@DT%`tJaZZ+K*T^h!Xs$MZ% zN3vO?K5!g4v{y0Ti6*vOiOAjZ211j;RFgRDwvg|+YDq~NzD_Nw$nd>IU_yi#c`m`^ zr!O?alLic8S~U1_@4{XtX8h7VCBmsUloidEG*CN57<8^irXe*miQqpHJrh4uenbk0 zxRarBc*E<3LvB4woNKczEHg#TqdLl+4}ZU@NW)_-9&0(U^K=?;G8OGXg-R+kgAqJF z*g3N(Y1E^56#`(PTVKx5cO$S#q5nXouqtj4m94pE=TV1S0iGKm&aAlp+@?YtzM|g(UZV> zJKuB2!>B^M-3B-=roHUTWQ??w_tCVWPEt`>NCsYaxYVuT@sX_z1WhTaeBpcK(HgAf z(gADYnf*rf&)VR(N+(z zLnuIp!g_vERdqn?^kbBI0XZd+m8kkzyoQ>KQ%H?$2}AT+EnF3jf<$sksFNsp>z=@- ze)U1(Yw9-pZNpSEQ+=GU7wBZ_rSHh@hH!2gvPj45GD@I7cBo^89!+TeH?mS}0W|)y zlmm&B;RT70W~x+5^JWSGFWbm(!!jm-`qxRAz2Nn{!$+Gag$5(ZO^_gSo(T))q~W%b zvvp?nIg$GaGoKYW7&UiVE8`dsJ8P0u(wpW(%JN6B%a8|vHgx~OD0js?IKCt1eK>DG zawgF7lbN(G5l?1;h8uaa*P9Y~`e({}VKdR zqWv4$9hxILslj|o0wU8~`HSpd ziB*xWL|ocubZ6DILw_!^;nxk<67~itafXADuX~5?B-^3+HpQNs+X;5n zIsbSpdbty(c9?o`a=f^*@>+ZY>|ZwPTsRk{Y6R9Ex6sE{cS* zB5K!7&ji8JJTP(q=1bV(lI}0=+t;F%Db2UnIgfwtFFkqeU%-NI@#M_Y~zleH3t9ncWr zX%5mvJ#*3X9^+h%mU5fuqF4;Wf1r)V~a5 zP{1Omi=EMA2=c8>dF}FukYf1oyWVVpE9u3MB44eKON;{SPol3K>mOORV+gX@c)9pG0z{(p86?ct4kO2df?O7J@ud5EKXClOmpnRm*1k&l|s9gYInh7VarjQ3F>E+0i^WeIp(RZ?x}a- zM=8ze8WJ#pzHF`q4ypoQS0__H}Er=f*SEM~l^*0KTM%M^Z7UsliELIk(ZK$9)Ax*}g-i_f%8a zVyb3oU1hzkw)>-Go!ZM2%zq*7(t9%k$|zD}Y|+!`88QAa;Zr;6Bdx)qoAI5uEF_Y9Llv_~N3k`1gs&h@ zAH342$NgiK+E;u!N?R0rUtQe6*YuRmtIcvDDJPMxq@iM5Fma?PY48ypim5h<%z~Lo zc-o*do7orLQN`qAq&zq3`S~9uLb1+)&9_Hp=$5cRfWPZfK*TN<3Kwdb2omftbMS#p zQ%DT1T1P+izZeT+muH|JthqO1H0;cT0WG9E-HZm=QF~o!eei$wrpYbin7=rSd_zQ?e_lU$9MD7&f3rTe+eG{ zeZ|p#;-AdS{|*0C`y2mk{L2Ou2;q#<2Q%mg0>J=qFb1poSGQ;Rr|j)NxaVIg;CION zAKD+w-(#@#19|$k3%(`j|*NNJ^E^!5b^fYxAt!tNMnPh4flN5bP^epd`4zGL1T~#`GhBV zg$Ak4i7;`dr%Tmc^I|H;!_gBxVpsB8=R>_%m~g;^B29Txhi(}bLFfhd&eJVVy`OEP*dZ67~nHXApBxHc^`vd4)`l9PPLK6b=}F&P~kms0Nf58!zIws zAQBkjHpPC0VZy@+Aq^I>Dk^5Tc5vzl0(JwaCRyTIABsBnAJ%En$5>D_h6m zh79nBF#i{E4hu=jv@rK|}iNs~mESeYD6T7O$NPd&74YYIs6tC0{8FcRELj9HZQvaz(o-|GkhF88{-`(#Jj|0t0?w=Gd zK#t5_3Fd2z<Pbez|SvA?u%IJh)W+| zv1`XlRu900xXrqUUext?NVIc*G-j^NbC!@!aQjsZT!9*F5hRD_SNPXKU^^QA@Q1B8 zs^`~jG~I4pU{sK}$S2W|qNQ{Y6E%7m7$c&ad3HgK0 zeP1Mh3PanZE`)47` z&+T@F)mwb@iy1y&4Qec%W*zI1kZ0p)-W^!WEa`8=sffXZQX;fR14285TKDS>I0oLUUr46;HPZ3{76DEoZG$&z7MqyV@OkM za^mvWHNz(?n_4GNp>G3ZU4{0LT)Ll(7_W4cI!O|-P8PH*iW~+n@1e*b=c1A_KJL#f z6B8rFd7x)v>24R>%1Txf#2GW{m!m4IGID%CQ`?5c&ytBmAdk)Sd z#1w0sl#Bfd?*Nq26LZ2h30QD{O}nq|oAzo}d;OY|67Rl&}?%<;dReBw2o=GA_c1Ia~ zv?aWUp!B2y{hqk2a}r{4c_0(lAIB+9#5!NJ?owIY(t8b~Mlc54wr_5sOe0L4Cg+NI zDh`29nPxKNwA8SJ?X=E5@3+nd zE^s+qTz|GZsOim_%;%rrC4uQ^H{Ldz(!e*WqfE^&H5iBfFk`3epIyp`V66qv7Sg$; z(BYSKbdQDOR&GW&kW+^)Nw|`|z<5Zh`t6(%9=N#MuYGKF%ur=KUjo%CFIOF!#q0-- zeI=**W@W7P8H5ET{Yss#&LFOn)!HOO;czHKs`JWVv+q&C+O)7em);&XqOKBgrmN@} z7qisN0SGCZnUGryxXBy&t!a@t;)5jkr*GKg(Qexp}cq90s zf;=9p(BkL820|*PKP0fGxx)a(V~lUpihu&n4Q(od)uWvqou*^TFu+XfR0S%SH~-U> zgI~2xDUI^%w0fafLTrX(BsGf@O=#LB>McuQa9-z{_!4b%;1a%p5Jfs;bPC}8|yQr71LU$mxe3av>1ZGw_ih2?0LW8SoA}YY+ z6#^o631DVhy&JeL>!!!4f1+nw1seiI%Sd*EGD)lAE|=&YjArFChn+ADLtNM34^aQ8 zSZMAeG6@V)$5;=dZ4A@QtO@xdNt)Kp?)Jbj#>%~0MacppoHQaYNkaX=o$W)Aek8{s zZvKecP@$JNWv8FQS&t>EWXU7D)W#+Unm0Re2J%XZrN2TozzJxe`Jz;yC)ee6+`RRw zzhWOPR;<^;^51+wQ<(^4IY~s7&k6;BP@)lkQ=1Aa*ITKefO0xS_;_C#9&Tbr)CU1= zK%j8OzVRHbQ>flFhOU^)?>M5P8`g$y#Nk4k)5h^tjfkLl1b7k(ksyl6F=29*fMfi? z&>r-k`(q5Z7EQj|!?DNG{o<6aZelz%cMklcwHd$N=H1`?7GwjH#TR2_ba_`7VH7mT z%-oNI=sa^a;MzuhAHE@ot6iO}`~&=T4OsfdiCNW}ja98w4OI+W%J*!x-$SfGQ2yBN z3^aC(1|?xj*dVCr9_2x|w2N-BiaZ+7-ZdcB7vH0NO2dXD*fV%iqvlC)QoI{Vuu}hc zua@<>oB2ZSXnwkki^Nc98r{GmK-y=A9n=fQ{-l|8d>ocm>D6S@c@Gaj`BF~*WsFV( zd~uJiMiLg&u@~QCOfH8UqOD>}`pA*+v&kxPTK(k(?QE(`i~W#D$`aQqpv}QoJ!UAm z_!jkJ-jE>X>KAKRE$>mudXi`b{Y)oiOjRl#`rJDXfktLpJCM7O zt&}0wXk?9r8(&2BdKp9HM0I~yCio1MIX0!M5>~T zA0cOglA#o(<{;8ngQR3YupV^AV!?9F-(J^4T2jmob(mPV#=X=AQ-XKBkNyoPO1Pv$ zm&geqjwdk9yVXfUth1L=tvhF}w=w)n{YV3T_nicz&^@SJnWPV z6~Arl1g*ZB7zGtX9Q|y9&eB(P;CK1p4742KXg(4#s~{eT;*xd=3+N8 zgxRRzP9Nuf3`0^Q#hMrX)MeHxpWu5#&P+KsfkRbt_jyA~T6gfsxJ(VHa1m-mfT@Q%ruBO4Y=~-{W9ZLtvKk)qTm@(Z~ z`M&7Ms>Ea~$YjQ6mqljj3!o(Hr(%$IFla;qI1`}}Svyv8jxMGkEP~ z`WggOTrH{6CJzxob@PUCI{6vB^J+kZK0gjqgfpY|+Ax7+s2~vTcWuvp{8gidLOo#l zFHz24O4)x6GyXG4`tNe3ihm?L!lb{`rT;xJ>HkYol$G&cgQb5;)Be{&9WxvI|0UF|tLw(Dv7vc?H>DNSmvRz4 z=%Ro!C2m+p7$F)(ZGhV$Y0pSdvKKOF2kx=#3P(06zFRhyQr%Qrfjf6aDhoVqNi zY?NE~$MWlSHX}`aeqzz3&Y7ZrqD{3`rEk$%Ft+__)YkZ(sYJpg=R$W!8q+W)qE$*e z(AqbrOzautr!DuWoXEZQF`DktrB+n~?9Fm4dzEh2r`C|-TKgy@dHI+(_Gsno!QTn+YcTeNS zShF|HYb2Ay9x@ftngQO$W(O!>&2CrN7#CHJa9}%RJ{El_?!5>^+7M{Ui7Nl^mCBIp z@It)r?s5;R(p2lEh77){S`|^E-K2c1p}((G3MzcfG;SyhW|@&XhUuk`5q+bj=;B|^ zr8>n1`GrzLYM&?t-qqE5oqos~K{6!~g!cUfQry3;X?H;aJN*I3w`T-)i=137_9bbZ zE4*AO%aDx;Z(SHea~rWmMoj>lWOaKQh&j6b@j*H@8@cTp^k&}R!24M@4Xwt0#>C{w zsuJ$y^T14CyP6zitrYsBn{#NX1-N@PQ=Z#3K>)^$F~F^|_9oF?yu~3@1|F*MY^Ob8 zd*w@`GMU|N8-!fcPEl& zW$E*g3n%fv0voqISy+8s^AYVlwH7tI%y@z5flEMhyoS{z&+6M0{*XixhmaU|3M(*o z;1PbU=fFK+M0-O@(CC$RBddU1)+&J2Njb#btiGN#P@w#izp8lZ#XpFb(k%?<;k>el zj|)#4Gi*rf7_2}3>vwX$EdV{^UhvZKjpM$RRPmyxy4ftj-f*j?9oLQ3B(5;$g`(C| z(jc#&$8a)Ki>s`&J8y6E5e!Ev-fk46V_T7%A_XWDZ4DTztKB(qF#eA@9b|J3vS%=M zu8YOq7FEHl1wF|5ABozy$7I0s{>oQcBz~-3>N)o(rkqTe&x)Ahr;afoTb%N6g(B}u zZ?#Ejdylfg;2#`vf-+@yxxnv0XE~y}PK%nFS2VQH_%0*(=T+6@(JdB;;_MP#`_^Wtbw%%BO%l(*ajO$ilMLd;1+FOQq@FwJ{AU; zV=R2)Gpi?il2irD&=#)~V9r&?5>*V+&4Yur`8p& zsLFU&6jf0pz%RS>)esYtWcL|r7e(z+friYw$E{bWPwpO?nO~Pd;rd4mjN~icy}z2} z^_aj8obupW^A~a>Ic(=NfA*K$E8Z-f70}adMMvYoH)lH@aIt4c)UG`tXKbDXM*JK= zC$(O3^<6jL^yH-z3>ri8@jer9l0uG3xo_Cy%=&+Ld&elr{x#n-ZQHhOJJPmoJ1cG5 zuFR~ov(mO*X{*vUZ=G{`x=;7~=k&d6dgg7sh}e6r9r48T`F(+DK^dg~r{ZuVV}K@7 z1H6Y1$Qj$hVt>2f_uDy6a(HUUu>z6pPsAOj;4KIo_ATM2jA*h!zZ3E=BKh7ol7)enONDO zs*j^XSK7_2Ks{9fy%R>=L=|dbp{N55o{|%jUhHc=bo#2qjg8VAC#h9&#kVC@QQdak zJV79e9B8{k?|I)>M{)*^;OPco!4`Y2LGZIBuIXYr8-`{qG>C3O)TXp0U zBIy|9im|IHQ}&MnmC!UpH)xDYL6g8d4MXR`m(_5wLb3Bz3%9ut5#4O_g4y4u2Ls6I^J^C&!9**g) z9abb)S|W>;$jcX)hi9$=iOQi%_-k{U2?keYK<^rt(Y$AFC9R+2w`-ed?)7PZ|>b`Vej6IyW(x> zmlUnRkxJdc%-|P?`o+H3+YdUW=SG`Foj`QK_g!tGk2l!aA#-W6@PSF#g)Wep!g9NP zCkP5tIe1ju{F_@|G@1QvS~Ri>XZLFB$xqTt(MOW-L5^VK-{PY6<@+Zv7T|Tj3j}3o*kv9wgpAr?d5Rj#l{+|#H18?kfcG;*m*51pWn=J+j9_QS zgFeT;=ZC7`p-#4?B;U8izWkF_&vQ-snHP31nCIiNt%bYoz&GMiyILkx{%A(N|U5ys80B$F$@a6;Z*~vZhl+>j)2$T5nL3H#{6;YOv8DoVNHCdUV&y`**olz%IY zVnTZ@n}U8j4a>v$jq6gsD9T*gRTocMO~@vPu$TOV0SjF5n@jnYc7(JG)zZ#F?uyEn zBY{$}gk%&I2vz`O*kjdhD9q{?*m2#(9lS-9X}5YVmOAr8x?(x*Me**nN9HS+eKAS) z8LI`Getf9)X=j6$m=#Zdv{&nUK#JP7d{pV#(il2{@JE4Y1HPgBt`XSGa1l*1j6wJs zCyD5iWrFvt+U3>Jd{3=uYQmZ)KRb0CdUu+ea4P5X&_?$SUirWd>eYeO&Kv>MJ|AZW z1jzDQ89n~)o~~Hz%g(|siY!d7^cHK*yA+f*ScB2ASjh3+;-DfZz$CAumVGpr`h7go z`PF(qg*!PkY{|#1bjSSt`KgIos-y;`NlJPbzqKzSoRT5Zf>)$GtdZ_rOt~A^$am-r z-Zt^tgaTF^F)GHmh!XsRhjfHl+Ian1Q<5U#7m9+YZ2_2n1vPuvBcXM5{gSGAfdgwA z_R^hzFi^uRqC6_8t)hBs)IIbzDS1${Once71&jn`(9*B(rok0p*Kh5=9Fvo9K%Zer zs%TqsX=^PG2D*I4QVJCUeVCrK?T)h2iIZhy%+jQ-I_#p|u6{1UYG0wf4lDt&rQoR@ zz6KZGm#m-*M$^#ZWbTvD{yu_SW1@K@cF}e)4P$3D zA2gOd$-RYX%OB`~!29gqYk~XR5n`YzLA!Y&$a8UrZLPVanQ(?IkOocHXh8WbRX$S3~RYF z{5(8o%&$oY17cHRZ%$)RFiN*|YZKz$>hi$5=omjELhbOAq{(5x=;lBb)~eF`O>aIe z5=rsYc?8z3a)2b7kQHdeHDOhdzvqp%7f7X*rsOY|^?w~^^j~lR3;TZ~JW=}xE~x(_ zJo!5#z_|g{{L7F1C%@mnGlIXp_MZeN|A=w=AIX{eKQP)cimEXGEi3BpzQ2Fzy8DMS zP*wFmqNV<&`h?}LA;CX+4*vxtVEfPNlgU5rW`EQtzE2wWq-_yv=SRNGSiR}8NkgW{ zgZ&4>&~RDBrc*1$L@Lthr%&~{S6A~HCLPuP_owOxYgFUxZ1{gQnjgdj&>;vCJ)_{=Ms#{M5|od z4{!ZAoeNInO;OxfW%ZN>VTTa)>$c5Xv z4?!Nzgv@7h*uIN00Ll9{}{3MCfDPk&(%P5UN@2lNb{;WbTmQS--ql z>Aee~3+DN9S@=;R@?%+QKpX=T!Y%j9#GwH3hzO57kL@d2zxDUph!u~Z7S`YNm5hqE z;>H}a`=!MwCg|+~)gkeiiBpZrsPAu}NXT~p@Tb*X>oz551;Yx_HWqHO%AdXR1`oB~ zLbF~1m(CFso1Ku8kfmaB9#F39DdxCLpIsVEt zcbkHSa{~)Uegi)*;ugX})3a@;=-^vS_kc>Emu_Ncl*hiWxzb&>-#NGMtqQ8)D5y_i zf?H^@*wv-3I){^n=?5tcc5EQeIWXPIvL$Omn$Hjc0DQ9SA@P6I@)fNniQN%r1cU{L zk8LYdHen(dR9>lY6^{fjN>3k(*#qD43ADgnDTl1bobm?7ctQE+kF1rMV-3pV$KXFl z9GG)Tt#Jk)0|+1xPY0O1ap$8u1))}|zq`jE#7_9=et+PgFolpSJD;gl1B`w36IaiJ zBnHW0XPG#%s$>+>UaDQXYcy=&$Oek~C^xBhDcOpB4}W2cPS~u5qm$-ToSxKc+LT@uu+{rc z+X%ry{!1_Pk1$-_wJx1F@obyoirK$j$a5-gj;qsb9K1~`+ZhM*9_P7(LW#0!tr~aQ zL3OB3aff?OhUq4Beyn3%Hm)d@tmI$g{=_ao8fuWF1YUUgat`TH)avE=tyIJGPEu;( zINA|<`zZa=XzvU7tY?9rsiq3!#(o`0B~V>k0~*ZI&_$Gs_m9{qImgnMGPMe}_76BE z4g$Xd`WD6}SN8pkb36%ss$ehsOkLCwsD8-4k$4?fneA}PNPNG1nrN7hwZejN`|zSx z44*6<%Uh(2SpVnZ{0?z~B1z53OWoRuDg&D?#OL69GfJ{^YZpq33ePtWol&4;XE{t< zo&XW;M7DLy)513Yq_x?4nHMJgd|6foSr0;ZoMyBwfg}RKK$jQnTdN@t^t?8*^V~f|U%hz@KJV|I6k*Fp#SQ8Zvs=GUWKQ$$7tvX` z%ghG%k<$^jxDw~shCU#!H6Wecz&6xhKf#&};s||^{2}^7!=7E{avgg{B=+l|I|&E@ z7dqwI`%rBy@npn6(j?-FViBk(--g>I2T*ZH^H2tp3qGdbWLVA)=n>XTf+@CG%c%yW zdZ0fT*t%h+$g%v~y~Dm3q*(aINATo_m=O@N)*+`sD;WQJOV->|v zHi9ELCdm(vbArkGP;%LAgz?K#+M4^JP)quL1Y3Q8>U{0E-TODKijE(p%&QD}v@#N|1~QX!jLC13OEr0DKn^5Lpji z5r$8CWc}V?ith;me>%5|0PBbb+3Z_{I8S?6x!Z_&yn$$oBv5H-#E<`IHbbYqf;bo+ zt#0|i1D#&Gc-HAsB3%$KHj3c|pizb(GkHPHVi>VMi5}^ZpqfgDnw1njIbkA%y{r`0 z!F~mSUq%HI{2Dnk$uY#eZrZP=6cN|~u&IfXT>Y#^(99cb&j&jPUZDr?2#ebI@SoL* zDJ?Cjf6uqvxVvF1JfB-{xev>BWx4m!q-s?*xjB4WDyIa8oZ7vcf(riCp>G45Z!$^$icO&Rd4qAgIHCJf0r2fKDy{q04-G0ytR;WMES zvgBw^++0%|0|B}^TNqjzu5R5|L+`>z*{aYOSQ`Iw$CA`nLhQAcQOlKeiovk zrxRBb)j9M2p;~U}$M@neZ1uCpj&4$Q8i49xQFm1Lh>4X&3iXSrvBoM)m`LrZk28{< zV%bu$vJuWtjOz?8v#q8%aG_;O5^+&{mAZFbh6y{ZLo>*!6;?*w0o0A({Q)n=$!bSJ z8Z~rRZ`yB8wZ@%4o(#YLHSI&q+4NoI#cuz|Je-qCo5A$-7JKE!Lm$4bTlqd?i zfFg$>nym~3O1hPHAd83fr!!Dec*H$KLqAiXD!On1RY=^iRgjj@bx-+|n8hM00Zv_% zRL@kve}9kjQ0l*un^Ro6@dru9`8JXDqlrg&S_+#yJf}kit+Dmuz<^GbqvELKW3b?F zqAL|zh1WVvbqv)dMSWYEM+a9(lnPJME1X?)mJ#KT7M?HaxXusI2#Vtq+u3JK%H*-# zE%1?}+K=U4!%+^WfuDn^Ob_d|+UveDOj2}}FE-NaBDSE3z8P0`=sDrD8wmjOXwySy z7mrlqef9s<<|ghXeMN@v4GzgOfq0J&u$&#Uv(*(znBXgY!6R~HNkoZ+YWZjoRK!la04$d-hmldZJUC^w~mS_Vc zz!u0eeU(8y#h%fNkvxR3Rc2QlH^Trnan<`_Nz*Yw3%ix{R*x_lic1|N$Cs^5uHa^q z=uL^wvNa7k#C&qPae~MAuvp?+2o6T&UL0wPqC645L&(IQy&VLxjTCIFfgao(K6~q6 z%UuSe2rPufe3~FB<(0L@l3i~n#zNkR*4o`k=ajmZfS0K3v*+h|Je;%@a9@L8g>iE$nab>gQIW>-d zg-PM^%YM72N?FC#>9W5>IxcQG2G7{(r68MIb!({m^;1n68W)}T>Oy1XgIM)&5R?9S zO8Uvr%Me#143J;iHG)Hw+<>;bRC?WH!*mXDOYBOwGj=REs4*r#z=ZUXil-8(iM`$m z?P1SPv;(QACalCMOndV(8H3MQzn~5>$Q+d$!|$-AEL; zqngZ|uX~wxZCeHubMPeYuuqA9K>(6OnMt?hE)WSq2@5wCET#e1OXtLI27N-Gim_BP zLc}F58+{W`ynbWgIt=P5^EOOBhh=6jwdLUXrG(u-UBG$@9INnmx~}7uuSB5{S5Zi* zp}>|uY7(EkaZPyo7m(t`OeJJM#j9QXTY^(WR_QSlyn<6mqhJnQ=(6PV!Xr)$ljtZt zX?;md3b0aKfTkreiE|#(jrUn-pxUWWe*z2?eVJ4!$62aMxrv!s^_+f_CU@o36%<8Qz|BXBmXhelzn-YY(lv4ILrEr`rER|8v@sc~z|(b##LG(7Qht3NR8 zLE0vs-#Y890!oUEPwJ>mUUmh{a;A%+gKi}xZltkL-M%925l&`WvvpT3E=Jt6+?!CH zK(zBIksh^$dM!~v3LdBnx{KJo|5Pi?h^_!3De;rm%z;cfh#|zAI^(-ov0W6NPYTta z8IwYhwxr-K%nQ->n*J%Uo-RsOLqvjYG9be%cI z2+Rbl9}7Y;teiZ?2-qa!-%i@q=n;|s@@oFcHS+IU^uI05|9;ZW{%-~1|39C!|9`ec zv;O54{r9{4f0jM`U!`QsO#jKlv8W;A@+XwJ`%GgJSqhre@cLkBNgdFT>oy*zhN8ml z=o=uUxR(Wj1-)4F<=gI)2P7Qfr`7@7B`!`L>ycpg>A|{>o|&M$LGKn~M8c^WV1ao71G+PCrCJ%)4aP zr>MIc+R8WYF${}4cUY9eT=D*Wu>(2g2>P_`)J+M>DJ^2R%vap z!lMIqhIdhYqF^FlhS-eeB9~g8{!hV~Eq9>1)gMGpX?{rL`tUu4Eq&mcvF7^0xCf`n zgk(fK5g{$sR!^i|b=>dKogRJ%o6o`!Is(&Q#I^B@55#&s*IWJ58$Q=X{p)-9xG6dP zB1dDT5E>oxh{lK;A&jy4{ZmxTWJXj}DD&*sf^yN_79tGZ_64m3FsI_{ZWqC^Hj_mp zMo!X7vHLNl<%U}0;*4buYt7j()_?jfMepZxFsZGQ$jYLkWK!-@z>(wU3)3{pw%#UM zYI}j`VY|{TIVsS@nY5wx3r1jThlM<6un1p9KLLrDipLkh7mF58DSSyv2ehZaTF#;URSmG<11;oss+1t#t|dgd4k7ep1k#rS@;L1UAP zToL$@O2_0R(IIqs7}iQ{qMf}_9En$js^Xex$Z@!VW`kk&%c0N8OI=`OoFmgDcpgH? z7>Zg{e;Oy4pAOo0*b#nc>Y=+NK61KIopnF<_0g^@(X3uCeD11{!_>Bknph|*2GPJg zE?Exd=T?bFAZ?0guOFQxFp~5Yo~N?P1LUD0`+QP>+4(i%rB#nM=V^RRnr0OfX$_*W zh^8SlE%e>Eyi6N(mn&cE?JL&EWGhgI$7(_-bywgzR?}iv^G`mu%5O>9qjFZ5MhelX z=jg^5b3r&xi0PnTOAhB1`1U8rljSWAtaQuoExb0uQLKK@kH?0oxTBe;5B>HV#Lx%q zmSxiE-d-pSR84uJT#)7rAqa_sib3a#2N#gbC*crwLR>euNrgF7BhHeZqX>J2|IWLK zk?o*5xUp5vT%jU;;u+s5AxqUO)UJjD)0^0uh1q&E6b<7v=VPQ`Z1NP-2_Ie{_lO+> zZiu-3y`gcmJ=et%dkFxya_nm~y?u2BX#?fCuyl>YmltWSE=nyQl@@WCDqA|%syUxU z(VAE$+Y?07&|8|7`qM7tTp*iS*bagSaSDyL9lhxjv5A1cRNeAi8-@`_9psq{%nSs- zMEk6#ajo5NAjxY#T7Dl_`h?>%+L*X;$%)7O3on>T0=XGkEm*>oF}TAV^w}e8LCAhI zdh|rT{v4j&DIcp%fdN?QaV>GlBqH&GkZ@9sRbC49@5TYCBf;d%3zj( zY?@g5*~p?#VTm^tpQ7^!!ca^!UPk4{j1UqXA_(Xpf7bFRFT#C^sGZepGz2zMf+^)( z8j7tD!>K9EU32(m_|@wv19*;UtsFhn_Ly9d#qMGT{o3!1mBh-q8;`N0J0a zN+*oqlKA^$Jmifafxv0qZdfrPgKXiTzEhJ0Q1vQ4={;9w;7j}E5lVe=@!_DTG?{Gg+&Kr8eQBBR_z1B6-8#zNi`3|Fu(5vc2VfC+zMmU+n8U2pa_vJ!M<)JL+~ zm9%dEu<#bc#WhV_7$&1pq!&QFDUlT48cz6)hYDub{S<0}2Q&;3+2p?2FTz?^zeC!% z`KMyFg39F*te4iG?nyA|koP|EHcmdB#_4S+!AYqT>a=CN2=2qvCae|#fBYK%qk9W! z4flyvwox>*)EM}!1cw7*%o2)*^QO5G|x$8Wf1-iZ=ebY8)oH?(mm zh!Z|vdI?(}?c{2TTlF%L;mPL!-a0Q)U?fzbX}LFqZ)P%Mfa4Wrfakz_UCLW*5?X1b zD~kY<6=Te{E_xf)6#&7i$Xn;9nARiG6efzha9e%2|Fklq?sKlXC2F07b5CnU*!FW1 z=h9TxzYhgn%`UD}7{}}|F2sgR*$1ZxQfzwYT&Bat`dF06oX?f|?Sj*26Pa9|tf=_% zeH9X6G6G{kNJl0;#WlfClU4?NQ;lU!Y~A4^b9xE_7Sm^pG$-^FX0jW^W{dNY zx!ko!!z%4n6{(azLUuMpL^2N2PtDheaC363)yXm+XXVg5&FPgYGN!TMwTqxB+8o+g zOCW(=au{x`q0~xQegxQT(L&vGzw7Oml%5|lEwmQ*k|m|qAkvp;s%58VTu^1{?W5YS zG^Y`sD8oCmOV%`j|CFd859SNKnm1)xWR3;$@_6eIP{wX0r{8)dQ3nzoiz|8eb91&g8bDFn|oK8E69>qNT85jNS1@+^m2gFxp8#)@K7G&U4VYpyYmpZF=q zEPQq|cfOU$bk}2O&**$zl##C5yAe(M@+mU|PuC?^Rs*i%(2}`}E~AO&(M&(lQjLKmIPmvfStk=44~C}MxH%=D6*k)HUn`Rrke&K?JE;aJu4yozbKm8kR>n$Zk3tay7OTbkLSSky8q1_$n0?eu^$k5$NTT zbuzSJAhh_YJ7hm7*REGnS(TRa&)e;7=!pNk5%7ZVMxHZRsK?E0Ff~=!BKSF~%FK8i zJ-K)0ttPl6r?xIT?M9KWwS)cd}$mPBubqTvc@Lr@q74&JCO~Tu`Iw4ATJ!TkU(h!1xX;^8K1<<^N{>FGWL>z z4P7@9GX&@}YLaEiL)Y{?uNTk)d?=0i1zPkwO|(A7RUkXAazXSJAKXg1EL>@>GTER^ zl9Dt}jo(jpx|SJrro|Ml7eC+@4B6hW{y|@;?9Lkdol$gJCoM*+A;nmgh;E; zRJq{8{1(P3@?oY;*(GS%UQtNVpjQiSum#)eNhYXNq$30)xPBlKpL0-iUEucB>Z%z=)46RLH-E==UE`imkB%py)ot?r8A08}Z=QA}9L+OHCN%p`8Q-6@Ezn|f0tWTu z<6>|`#08UF)EmVSo7AxcD`~+(eo}x>0iT&oN)WOU*2m(6n7PY_bN(m3gN_;`7s@e{vj_-V1v+?zlxQ+qo^LY=j4rvca4chV zCKjxS8z8o}k~O$IhaXE~>@hohX6`HdO%3J3c~=Q+A7aUA%w#R8#mRM2jv$<%??)X2 zs3+1jPYeWAqC~&XC8R8yXX$MetJvFq42KZt*JFwjDLboHDrl$%>~)b<&U&{0ESCZs zXtoA$c`t5~Yi1H-mCGBkxV2tBR)07zdo4t7GHXplseM;#v$2|)Y9bRw6hCK2DNZ@q8ZJ^e>uEL{zj6PMdQj| zTd%!wTP75A9ly`0r+84usgk`g;6DlsJFkbz8!fug{su)Wfc$fyh)Ae`#QK%vK)956 zJB=%Q`!mTFKbi|(w^q(usFWF;Sc2_mB%JIG!6t9J(O_7fGnEyCV67zbvn2!`V|t{* zt7HMe9% zb>52ngPphIQ-ypN9y(XU%1GXmHgCaDUkk9&H<*FnBpQV_8`1i7eM9@hI$3vam=X{5 z2ngSN7H|1LNf9={Du(PDKRhB{7k{+ho{MyfFelT+bQSLb(-Rc$muHKr@%1~)zuwb| z3v^MoPIIf^l<>nO-ot2DDE&-vA@x;?&>f%Ynm6Ua7Pg*n!r<|z74-3yq2-T7UIVu+ z{P4unQ+q}RpxLCpo-FYig4lGkBW{Js9)&7dHnJ78H@nYhU%tBkDHvg1Z63u7lv-dJ z5Sccac8AJbNoOE<3|r2Bz;U0-aA2wiu*fK^#eMrDnQ&qsnQs$g4d3PvRd>Ecq0Q`y zPd#sep1 zWA56&$Ktt~JK$_zTbDq6Y?zxG)q9Or3>fQ7D=T8fq=pB1(hcpXE z=zj9$UU#uVm8kQFke5zw;h+rA$#;9tYW$C7yz6~!L?^b28MK%iYXkv+DQEK&?UILZ z$q0PW3m6KDQTva0WS_}Py9km=i+0PMQ-uavv>ang+>daq{89G1IkgOW=97%J)#Qsw=J!udu!T zw+N378%L0M0`ohl=Df;You7Y?@BsM};odn;e#5W+byd}4M za=kDX{t|CMXzxZRxJZvVJDeZ{`ird*i*7$oz%C58JU|q*$0-M%bbrDS31a-r2GC{K zQM0+$8K!KO2xT&EjYYu{2bws@f<^Z5yZK_8+>L-LQ{-_^)tuQ2W#~9+I$GL{?nt`A z^ZamRz4h)ZV zk6XdVmW~NnA>&Fr1y}YIhtMrpG*^zr7Aq!}0huaF0~0r7=0|>IEG3r0NOg(EG|5$Y z7-TnJG=c^{^0A}hY}j*ie(07=?Bk8xc^o1x{7yhxltqZro^LLAHzh~R=BNz;ugGa~ zK_;#41c#-MuGLc^_`Mov<_7zV_H7ltfctR2duv06QpSB;Kn(utdiU%$%ogG?TqB3Cui! z51%@Ad>4FYj+tY=m zJ`?5o_xx>1{T;z~mB-aS!Cbr%LgP~Tat|r%OkzMmpDs6FMvSGctd{h!ukj@{xi9)$ z5o6PKV%uaXX?nR%)PXC7n(LT~*|P@-w-5~AfXg6fcZbYBH@GjtewJ8aTGfB_Pf@_i z(-wq&kO6%}33P(4VCW9w*APJRTU90>wCXx@sRvk3+Uqwbwtd_F=_3B|psllRxD9C@ zMMh5cxrr$LM$qn>h+!;wE=^3`D+N+naH2X2Z}D}uAG|3hHSC*vVED_m5H8bUIVe0v z5Ov?ypu@MqO52t$Zk$!%B-+hb_TxEFv!Y(f9GhPzFi){3W$ue1U*w1#G)Er_v5c6~ z0gN~Tos{Cg{C|+7`z?~*Ykvct_hcb^B&%y|UOBz4wR2&0oVKEAR+v}LnVEw4;k0bA zb(Da9{M5J}%VYop!{m53gx}nvgYP!|SVnAhLr5S?6dY^yJQ4*-Fr%7z_(U*i;-|qu zWxvn8M#@`Cf5T1I|O=J*t;x9xNsy?{GcevT%;?Y&C(l zMtPFrB=~SRJYq+ye%5wBAY%_o@+Rx&zQ-^2wjjj57JH~6&br^16%WQj8Y(b8cf4=X; z5W})Es&PIX+NRGEU=2}wBZRaKz!{fXD5#$NULz;^CZj_6DU;o>eWun`o88cC#d_}S z;!!K+1!Uu?>qLcHmN3f(g<>KYtWHJ&y+B22({!Z}U77u+H@Bj9=eN8LZhw9^+*Gb2 zr|4cz-J5cZ`3l4h1Q;XDq#%haK#d-H9Dk#;gWNxjhjiX69a_XzO#ui7+oi~|Ad^3b zq%9Ds>g?DWVXheG8a0#Hs-02P$CFpdV_YbocYGcSAh>xl zc18O{?QkDCcX1B>iHWRzWEf#{(q@%to7El7O$GTzN%r(ZnY@Ya(}^AUfuLI^x(vEl zgt#+q5ehv-*cwJCl*YXHNXZkT+~^jgm?si`UU4uAsqJ3Cgs;Yp&RqN4@teFt+oA5{ zjdDbKAY?K_dM6WBFDiVj>jyJeh%I1)43L`C>` z4ZG#3*Vzjvb>KI*i@bGPnqLL=E(!jP%_VBG1QC*38~spNaG{FNqiw8Uvi z)g9IWV|weFn%a6+wITfJ;(6wk(=Rgw3v!cIC=wXp{DvSPa6iGS9Jk+~oS==bCO_=H zzlI-20{IQhhg694h?OuFIj)C{?pMsF-Q=VUS&2ddFSZ=j;8(`8;nu2~|HP?*AY(n+ zd4)cxjd;KDKgVAh1aWpvxQ6E(7%i)6gE14zVl$3b}{dJ|fZSO}V(2bB~5D*MZs`LkMZM4(VlqhLfI?ri-wf?~3`31|&xzT5$HJ zKWp$D5Q%J7z@}+`JOND42CeDclY~JA89^kqcLU=_A24TJu9C0Y2|A2G)EDFf4aWfF zEN!eAvsI|_pwC)h9*_!QvzH1UyA+Pf(fa2>$Eex8M%yOVW#?gi1v@SVXUm7IsYkcF z$?bMMiWT@X6M~xukqLM|!r=-TiF8rU7m+j1muNj}oF*rUI4vhMGP9++sV35%0z1_f zL10O4l0unW$r)lol4Y~xRy2_O*{3=qKspL^T!YDJ1{7^cSrJc%0(g z7sZq^xaJ&f%4vOF6w+BbUA`S*Y)x5&OR`8MT!P&Tx9cAg9ia8Y2#NzOcY^gpI}B3z z1mdG}k5|JD>XG4usH>48_}p1J58q8u5OyI`SB$2O`tBHnYgO`v>Y=9h~x3@&DW`hcGr0a ziXxr|b!(XW+JW;NF2q07qEwi9h0?Xn`8HnG7Mz1=%dp1+*owS7i7WfFS|=SOmDICup@d z3eJ?UA1)4SR_SIfmt)_-7!T~rgfzFdrT$}HajaJl-T;{tk&K2O!hJyKMB?+6iyhPg zH|?Y9)}=A6*rAybK(!TTvq5&zv`AS)uHvhNBHmM55VtNfilb7$jsTj)OhI_$pdCSg z9_XG#5wHA7j>Oj;Ue6nWq1DH?t8Jg5v>QS$co~&OL8HYW2G_pY<(3n7eKX0mYiW<_c#DdtI4lEtdF-i zS2C-y3X`QeP0{9}JTI@ST*(6bxwl>RX~>t=RL}oy?`0{|37`GT7~T*$MFg+t;c=Q2 z9p78?xK?o}(hAkJXr*Aq@wYkT@{i&v;zgW6oaPz_4i$8BoAZQ`F#H<2UE=Y2rx2zr z+$Eye^s2rk<>3(@{Oz9%UnXq{l3T4o?rL664m#-eU9~Ye4bxh${p;H<(P%$&diVp9ZRF^hF^&6}%%;sVUN31*qQR#IjVJe6G6{3E_nZhTj7 z-ua?r$&{MXk0Y)|^hYQ5ra0F&*ig%pVcrKR(4t)MkE&OvDw}CP*)kvUJ?cTU6FmeU z#A$0w9)F^XRti7HlmVxGA3d2f6<@>ye*+a-$abOQ`!X!SpoWSIUr(Ef7!i7U4%Ux- zn5ab?-X1tgqn)X5Zm8UYlzmCH(o^}2S?6KuNKF#hi0MZTd#T3Cgk!+x#b1V2bEQon z8+h_agN#Iu&-wOQgkPZiBTc|iKVI{@EzB{d$OLBRBV1%*sTKx+LrG^~z)*`q9nX_i zr}P-$O3WjXhcJo)ZZWVb*&sZ+FuKV(s6<<5gTNXQ?7B&LX~*h4sV`Q8M?R&I9V;Cp zle2p&N(Wyqx{#oFuyR6dr(O>IxI@t^9>zreHMuJZB?15F7{X53Dj%A@@hNQ{Mixam ztv2Aeo~hsl{cswGr5(rqZc7Y`JOMnz0b$0+q>(suSHlZ_wNBIPlwW`F5NQCu-63}r z@V>Cx;r?i!LBi58NPmPg#d>j#49-cUR}o~CDe-bIzqN&$D!$PD0HVf=;(BOW5ydVc zWrf58QcqN{IYxt>9SUFI`^)1|MI%sCdcN4AM9@q2D{pJ(LoKRr5 zZq(wgKGH)Q^YDlEfOB4p&!VO6k3u*R!9X%N(*om&d8y$@L_o@zqw#Zmg{DUMa`kkr z13y*#4lv$?Ime6(BHDpRqpUe|D*?kb-QBDv$csHi@7P!FOQCEKl*XOgpB!hj5tG0( zz>EwrBQPaQs(=XH!X&3p+MdVA)ZmztuM)34$x>XUh|=zHBt5iZ1=SAWexx zL?^Nj=(3)l)t`)tdIn=)bI~Z766XM{WLkU%nndJQ>5AixiTG&u57|FZrrQhgvtVYx z=CLBB_(KZKEW&tNXU}nCK;V)%TtAO^R-@li5C7HMIK_p zAdv9k5HBL-aD`Hz0ZvQodHQm0NO9^+@CE4uuTP*V2V-T!D2I52L`d3UPy}}0T*rPH zow%V!)E(>4Aj)=u>hsg#LYR=<`E0+((a#ZM@o_1IYOPQ3L;r_gRK2LS)g;>h8Hbwy z_91IB+E4oa)E%RiPl%{U|l|#bTt`LU& zRYSTBXXDU~9f`I+gW9i2KsETipd3h$KH@TGk>wRM4{beSe2EFvIRv@5jCK>Xn%QJ`Dx9kI(TIl}NpWeL`&Ln%svH~b)kbLna z4&k{f0KcLA$fJTyn&S*d+k$Loc3{oW-kZvrUHxXvk|wOuA@G?PpK3{asPpiDxiPz# zT6AQ|F;PhumE*7BS!m8Vehh?NkH+I?YvQ_`=jj>am-Rixa8uc?;b6k%<-3uu8)_XM z8!yF1j}PwRpi`*$agFM#l~LrD^`BijNp1$DlgH3}M5{s@6iPn}mcVe>-95_r=-7G0 z*?({yj76u=Jc5==T;aS;9mDZuf3HLHF^SxlnX7qwb|pS=j|kc@ho3^gH?X+lN(>n zZMTla$!$6I6Q{YHD3hI zeLMqxM5ypZc~Uum)(k~r)ew#FOq~Dmr@v4aTOIZ#U$vKsGtD6?O@xwZVwew%n-TRHcXQDzTSnD>17IdL0XXe>LnVddv` zAPr2Jv-oDrtX#Z!K7gUno0qeci}k^fvyQC9i(ljQ193A6vc8 zJdEL2IBUGS;j+kyy_&A5VzEsafNigI2BKZ@^h&F9eeYo{Y!I>bq*kz>4-6#ziup3x^#aXDeYd+smd6-lIW)Ts!HTES8URgR z>A=air#KO!*r9k=D#b}qGM4XGM8aHRzcx6Vf0E6E{+n;IyFkB+Bm0*#u14mKGIKd~ zg>sgxO<&A(2Q~(-I0f)xZiir(N+Ms{t0eSJp3qMSjag@#%)b z*~I(IE!4sU=ypP|&LNlcgImUV2L-hv;TseKDKckPttoPNXR9E=ufcwGyKxR~KVZAF zCCe&8e}QG+NVj;g} zn0_v5qSCd1_|g)#;Oj|FGtO|@h@q(gH+nb_QPl={&ylJhLgMs;tIZ^N+fwhiU-P(z zGG>{Y9Go2@jys5(3f28j`TRI5hdwlLWM* zR+@;J^hKi?r{@}co*W8b+Z_NxftQxlFetx-!<{D6i#ne`0%ut?dW(r{(v;$X(1G6nBj zyyTNrXcCb!%({=gFN|sJt8$u<46k1(PEZyUtpW6gieRcKQ(9k%CNOR13_y*P&CKaV z0k0Z7XEo*Aj;c!#%gfy7q3NXz9$?_%T z2t?mUgMl}j5b=%*jhOslhWB7Qub9SUvRy$*ryrNrg;3s)^&7%#sh)Y7HX(Ghs8EIS z2C8P_4#et9-87$>53%2-P~S<%V$v|5U2YJ#t0bRP2DBd9S~Ko)r#<6`W^R}}Ig!`( zND!6;_5L(ps!ywYrM^*GlN|;}PyR@-FrVzn&O=8gB%p{k^N=#5u6@3s-0c>;IZSC5 zhorFc2uP=RW&Dy;_WVcz^zjk8k~WyI`DA|ZG*W^tEKcu#@%D~Ex*u}u{ z=nLCZ1^MLOjlB%agk&6xIwW7Z4ag8lHPN|0+Cf9zh#6Jo($0_Ki#~~T-;lUsASS43)Hkse{tH*p@ZJ_9ZFre-y25v4;GCB8^RVhm z;)NhTVwRt-O3ILdS}QjoK#1;RN{4sP?~up+YsJU>$MBu z3tEn6*wN%ow9r!NrtO(|EZRt&&sZRz0M?BaTtnmofdp&5v*CHW%DZ4rU2ljo9bt|*b`%PYt2oL z*7kg#djbRb7VGpWWfe?t(hNp-cIlIfq{*IAgGFWa;0&eEh{;IX=i|mCsKzwIUKa#?mrfb*-V?=@D{U z9CmtMkn>m6a(a4;$}3`qz8x$Qj-BxvHcBmxY9+{)of;^cSPr0C&yZ>7f)u~RlU>z| z)0VbGox>g;qM;D8sQYR}ejgeu3xm#;A|&lXUZ8+=QF06F()Kr}5jkW+qLWbdH%Cx; z#mYH25dC7>!muXi4Q=0X7xyss^6`{2B6RVeOHcA}7SHl1-|B4ueNsv@_SpjIr-JJ% zXIzB5Xe8Alqn;{fA_(v8dp6*l3N+4^oalqBm6sIyCULK8fO#SdJ-|jW_rhKP@~ns| zo|esN_g`t~DI5OKz=OIZN?h|hefDSpD;>+wgH=#AR!!2BAhP|f9SR)dzl0lf8jMPo zvS#p*j$R=QJ0}laAx$A*GbL)|lLy9oL{#P@YXj$Qd|DHAyrs?#*eJl1G)o{n-tA+VTiXhO1q6p^MjPJZ zzPdin;hPt7mBLuUR-VJji#%xaxGeT{I^C9?oX!9{4Q}RzPQOu&8na&Y({@P>u(zA( z%IW8j0$J=t8l#0}*-L|8c7!f}D%OGO|7*X^CsmbIq(@3n?Iai#5T#{qc11F;v zM8Z<_{e!hQ?!~$Sy{O~t%v1R+jcAyFEpdGML_+k4j)lK;jvt3G?@VDcBo_}2U3B^& z3NdeA*49xUs|JE|KSp8L`buJ7rFF!*jq?ao;vOC>SIvy_AG$mLu1OlGiF|M(N+HV5 z?FwH+%6#__#Qf7EnPP!|$+$>uWg-RWRoi;& zC#!Tjs5WlFBb2d9hTb+esQf}Dv0ATuU2V2)Yw29Gw3efXqgA{(*z-(|!2*zCsOrGC z7kQiIRlwtqmFdok^Y`g?G;v7g(SYXW$yO3(nqa1VvMC z&ei?65y2A;(F)AN)>4fW@--Mcq)e42;1qk$HR6zJ`_JLkN3KXMeGhMmdC{qxePW8D zssgwof$r{V#`F}=NuIF^flRjfGX1*%-N(vEw3y&#)PV@6wF}?1QL3=&5Ms+kY_bPO(AV70 z-9t@c`dWs2T;*wF@tN)4VWE;D!A>gc;W3YsW);-m0-yw4V`6;ALJxxczkR#EjXN#V z<{Bs6e1kgNH0f`{!glEwVtxVR2N+o0 zg5nQ!OU|OmpSt$zN{{tH@_abQc{Agx32*GlrZ1oG=w>1|k~_Ow-zX&iOmhvm-8cE} zQSYR{7n9SqJfZvi+D$KOXZ_{hcN!&HwrPWc8?*r@Mr;oxHvgNFC#Qus3-+za0tYjh z^7^4!{4fyFI~cBk1TI=|eA*hPvQc;(bDNFV*v}Bh?NIuKu;Nfpp;bt7pThZqx0H&# zIg*8u$VLQbzuoz2)mSyLnQI%LSeWD{{&2%{B_aFUp_d$N?O-v*!bMPt>}mk{Z$5fj zPyh6ipSzIa=xrw-;X)2;yMKrFm` zIo&ApqkJIpmBX=4k5h9h>^jCc__B3sDu9cd{DjZ0*)lWc*)d4g4<=mKWYd^-AkJrt zL)q}7!ns{5@RecqJBlhhwWXwfaq$7hD!87v0$YL@i>}&&W)F=BiO1*sl8E>*2PT~6 z>QTXoqYXx#j}w}P?K88_q2iI>d(%-&ZqY9WtCeWfHQYh%W}|}FJgPPDBnuR6u6sF8 z^~Oa-V*;0v2sHRQVMo#SB%1BDI5HA9mX~J{k3HgM=?Pa#EUwc}@0B=R5!xun6OB?V zrFtIodF>tp_*}o{&HSQ{$!hrZb<`xxu8WxJWncVMm?T+P?uTbrOSu58IaWc>$<#Gw zja6{G^h@jP#nPUrDvKRfW9Yj&yeG&C?;=AKk#V#=&daNBOj2Lb{`xY%+dN zPH6VgOL2;h}d~Qq7M4h+}TXd<92Gsoa0gB#U3J#7D z61<6vi${mDb>OU9_4%x;1Tfu_BQIaBEhgS4hjQQ8FGWOcLL0zBM)=tP>8f-xn|RPP zt>lZBD|Z?Vt|7nWz3`BxCJZTIEr7;+MZP#1{V-_fVe*asU$|8S!q|=vx@e@HZreco zClFG|6x1r~Y?(-o1*9*A9G=5WJMRf-`WyxHxq`Q!sQ7uZMQ-5PLdLuMb;!0aog8AR z(Zd%Ye{vMs$kyI9TX**|Tw(t@aI&fd{1%sCnSw*0wKplf%VIJclvvOUWiQ0i(ZAKG zFQXRO?-SG91QyD*Fx?7Zk5mAKzQlTlS0smFeP{breZ9Tx1^@9RwiIO0r6XXb`!9Tj?jK54|3HNle!v#`|6*AF2k^p3|Ifhx z$-MlJfBt`EUb6m64CX(w|F?KPGd%3W6h6zm2!`s?+e5Eg%(RF$NSqphdq_nPwF*>qjy)4Kh)uomjWj$o_CvT?h$`R)Dmax?4R-j&NsD-BK6 z2lZB?RjX@Nl7Y7}6KBnc$5CyhL1q|Lv{8XV>_M0mBXyk-+5XnhvvO<2)3Hlun;XR7 z9)SQMT*TX6*)M@yp<74#_(wio=H1b@jxdk6O zSYwQ%JT-_GIlYH;F=L+j4imf&FNRFe;ZY^<^HC<~r1;YC04VQ%y#V-~(lk(Jnw{d1 zQ6{i?#lnE%V3q4r5tJKQ&~9m^8@kk@ngfSh_${5(OTjg7ypE7W^_t8Qu9aOp?1qe< z0H&RCtn8*kBP>t=qsvNjSbG;7^u~$qA#I!B(4HqA!8U_ z5XXaUN;FSe;c|}|I&13l@sNte{k+qmwo=|dD(@As^upVyxV?m9Jaz+WZ5pN>0RF<; z_1=^z5|<0(mC$M$L^TrQ2Q5godzk4R7xT8|qktco0cL0-4+qBtMJQ6IaRAo$YkiR- zM~Yx}Z|%qS_6J1>1$K8G0DtHj%$8VCB05aEM$@8auF89CNT||qA;t3i6cUkS(VW%% z9t0R-*YihEjZdjgOQX}3G9`AF1$}W>m8mhw2l#GQYJYM4jkg;>_hJFWyQfA=S%1Oj z)X4{q)loe{V*TSDL`E(?7vhNL31$0-qlBd8~%oLAbm5C?}kiR0V~ zCA$<-``@ ztWf946eJAC(bZ}Z0r6mn4W?HE(m2<%ByYlOg-ryphK4I>F%JSE;P5I8p1r-ImbPsx zKoZX0+BhsaGdf(yYRk={bu3CZ0hH+5lc~k9MWh!%?)~V<^f^A^k9CNYQOUd7%&0Gq zvblN3(Z|sT;Wz65*d(hMfQueeIw3q(-gUIBle|yi1o6NJ3XfwU*VM?`qj;fl)?TZk zb

Lnu$z4LZ1If$ff{zx@ve@?}#zhBcW^GOnW|@>|0Y^aTRbXn))?OYk>Vl@>EEj z>ipx__L1aa5MXx44*IlF$$;$=Wj&%;=og|yx&>ulK4sW(Whtbw0O;wTQ-;&oLyrVt zkf7uEahaIxG%T07q3PiM8s^Fjf4HirTJqvX80wp3ZArXblAT-?%i3;yUo6cNzBD#5 z&wmWdL}o{VnUuZ${%T-gf-41dj$5@i)XqS`H@mtVIf|Ls5S-bV zI&qJ2Osx|F;9PgE51$l?87T_G@?oQp((QM+qRF+B3YMkG<+W{56qbeZEq!W75tW)> z@LGbGX$Y+j5|!SlxcKdiR5D0g_p{$)etsoBP76Ls)R2hT)Twx3aA6qP8CPny#+c$u zW;F5&=>iB0AcWuuH&o<%W3ShqO+ewqUHJxu_E_^4w#ck6g8x9TyHvIAQ@I5)j zGKViU-*}l76-ka&qUNnTP7>G(4k{%2tYDf2OkU{rsm9x&{`5K5=$0M0DYFctKXbMau)FP3OQTn+Lk=fK zK=JZ1fugO|DKlcRjE984Sf`+yVR%*+{UPfn{;0sC4L!&B2xbPux?#1qlXG3v^8LG_ zrg9BzQnUH^VbBFZQtla#8W|ZB@&M4gl@~pGi5iPu@;rHsQItyV{qGK2c<=X!q$lB9 za!ls;?5aQUFG8ZBH4?w|4irs39Y53rlU@n{)bEB$qoHiSyAnDMW z5cW1=j}FjeVI^X{8c5K?uISWX+z#wAnNzU%dmjBp@C_EUSExh$4Mdx;$yH1DV;mmC z-O5SMSh32ZBD>gChMqWY9lL0)$_g(*1y`f|D*+)y*eiBq#Z1_orohkUhGJ>8qTWMQ zON;wrY)w6?a`7ApHpDU|QU?{(T8>MefDwHJCZ>dM^Di)#0n{2XIwTI3yY)|T;Zbb9 zxPdTc;QakDf+jQ|*}&NgegX4t4p3%KcTmO}X7* zHOFYo?Nxa4d-|_dDK{MIQ8m?0C0^Z5g?wf5h8Xxx>pe?2^Q2F@;9oU^Y7!+NP+<9a zhf)!2iF}<5sPAWcr;lnbzAl7WK}3M!s0yyfezc8xE@M;;ZfOl+k{Zo^e^mI7gv_Ry zpz=1SEUanwhA1O+u8NjiBkL!^WU!_G3ax^;@uu3reDKUe=br{W1TJ|J6N--5;EFP^ zy5NlN{Tb1r>zugb@p=^Dvudc?Z}_^oTf&4qVy9H6*^V4AqvWWRwRMSFZRW+;jg$K= zym0$RO=6X@PJY^GZ(je=Ujw}yEn}($zJgv^1HRZ?uAUVA{#GB_^GyhR0fG-wTf(fjkS@4J z6!E85j$pywH|v~5zk=nz^B1VaY!{3*VeFs@bDo*?{p3^uA@Hm&$b46QL zqD;gnJK4=>zU+~aq!aSWNBxxnhRvAw(*`@4rye@NGW+lVzop;{alvpW$-LQ#N^;;O z5>n>yz7lQTTrQ27+{!%^i#MEYH9*oE_VSYOcS!+12|{Y|sZ?W^zfG(VQ`B5VszZ{8 z8(hdV31wlheXA(gN^#R-4q5w2#Pz?TqI+y_;!-NBsD|kUmw;Qo$1SlWSa{+>K4uu# zoR!m05l;rgMg3xaA*w_vI)6MiVas$gf~1fX!72Q63K6H zTL+(vi=iapVuNCnI~R;(NG+nF?tO!H|FCRlHyRj=24Bs=gjbv#uY*(ERc!aw3y?u| zId-)tQzSQo#YbZOnijKjeJXG%Y9`e(4yv{SMlCN#up&vMmIy^7j@K;c5cg=3~hu7{c2BA*s&zu#E%!Lu|p)_zK}_KKf}8 zfL)rqZq~E&o)0hfnB!@nn}<_;$`vQ2_YDGE3`4E3h+d%>qnLi2*l_S=W_>bNF&ZFw@F30pyBnI6#lr8b`=nd;Eu&_IL`0KgXdHfDF(26}> zVXWyTEOhvb^t<%aP$Ck(l-{R7;K525rl)}Y`IAVqW^7>chGrlQN@DqNxB#|`s!^3WM?|tV z`}s;qClN5|iu-N1L3RY#@J6^bx657nj9d{Q;qpv`AdV1VH&lkqO=* zq;Y_=9Rgg!*#Y`B0t?!&w{K?2l~jg0p@+f%brz5`1$;^G-){>JyT8YvE}%b;DEZa*kF@*e$Z^M(jW6h z_|pt5M3FbxThqfmv36xKv396&^whW{hL$H{j&p4CGE)Mp%PdKn$->BXaMNpEVc7x2 zj*>G~uR^kP)|e_Rb~bLW&VR=2;5Fd*ci&c@MyrwtcXtFsgRu9jp|n25q1wXhEAXph zk|*!lBQR^zNP8*0{;2;gs#G7kr9}Gr__Mci*KOn%$`DIxFpFFq&Zs#8Hodm<3g>*QZy>(DBRu95BdB=M~3(ww>Weaig9*gJYAjo?(O)RFpj}4omu69{ZyBL(^<`!0%zCAs zc^DlCm?&{OD`mtaudxOff?pF*CyKgqUrco_f97UNOu!MXm?8tp zzf=YL$qf}OE{$PoUKJV&r|T5)5!)e)ksX9RdyvCNdF)V7K?c=x&qsWWo|eDn78Gyv zKB*{Qwg&99+tiE$Xxi>~as6+vgzln~iwh^Axzy(loDR(f_;*;v6RvcVpw=c-yx#l57$tiRi86n4_ zXmyHFB$eXy=cD7xr3}S<^dAho?Ahjw*8*1;mL8+Q|)Pr||r#8af;&}6^Iwv(YALM~6ZTzFtvU*jK(9TIGgj4yOX9xUK(^DI4 zG&79Dp!cmTcnXft=*Rxrl>_ro(MW;}3nPk|&6tF2*zS{M zK(c;QdIQg@N^f~X+o+K1efOjXXR3-g?5EQM7^Ss)zUP@Y=AV|yOkeao_t;%EWswJr zt2V!RHuvERY(4HNiE$d-Xqv#ipx~Oq(aJO97fjYl4h#spJUV5I27OnCG(#V6?t|E( z7?)&9?yIDDRPvrX)aEDASdS1V!r#H;1NtDoTLd9~Cz{1Qc*ywDf09LJ7Gk0TVA%i~ zw!OH_KKjK7W_BbIGlV4sg+v~Ur6dU?yO+N|USDvOxYy(mC zLt?izFGALF4dX6d`5K)G8%~`Y6cYtm>!Q?a=k-1TxdeJGt810%N_aq3)|!4|7x7qB z<;K}ratxbU5vC&ikywHv-7;InSyw=knZ2So5U3fFH#`aU#57>w`jtD$y0%Z()@$T49cdRlaoeKhvuON zJ5Mic0pdPzv=?o=y(_#Y)JC28GGqMePDjUCaNcKXR<~&0`ulI(^Ey%G1-frUJpjWr zXqP6(-G2|_vi)li_a8U@TM(C?mF53Nq*J$97eVlTu3i}?)vIb5 zZ-)SS5!*wf#~J{-%K-zUo{cjPQ!=I)?!Ly6v=^6@!G9w#ZylbvU}fBfAxD2V`Ba<0 zzCD^exU2gjG|3hJnVg@;|M1G(z{N|;ZdZ`%LXmFWMtdixPn{bmHfXI1H_=wzyIdbX zr?h*zLc>d*uMaEV%QFjqEz=5JHPX1|BRZcTLKPuHHA5+rBPW_NNGuR3u&c`R+e@!g zOL(8xAXjzqOZlNOE^z#{c`4`hn|QWPg4tbtN|p zMmkIvD~6}cwvLj8FX0QROF;|vBbZ2_|4R%z80>4G4Vwu07o)G=hx=G*veW#+`c`%B zoH|KgN(WYOShyi^oLZ`%;`XxBeVU?IzB1HSgks{Lk$6J>?Pj z@-$bE$M;6M$_@l;Gtx z2x!6L3My>nCw6OVYxPLy&stAdl?@zoglzJ&ia1{(Y?yi;fxI&dtL!>|rmC%o!KV*$ zbxV_&jtA@)z!V317joWE!Hh?b?Iw4~wxhcQ3Dk0``P~8ao7CL_P(Pj!RL@O7^Ql+z z3Grs9KwxAK-Vo7mtpMvmw1RtjF=Spatx#TG0MB9JDBx+qoANC^djVzhT@~3$pSYLKcSQ_XU zDy5dWux;p-7q62HVq%LxQ~LmT)Ql^Hyo3hn04|m{@ZKsY zhwUL41Z%NG?rr1CQ;*NaNx@^4>k>uBU8W<*X#-1`wl?Vd4Tw`!-IG6ltbDHY<7NrwB zoBHayLX=lDdn%sGyOg=yAG4H-QneZH%nQ$IknARq9_K=jk5HlD>J7zq8Ycza;PTS$XjTyXS_QiQu7XZl9> zgCI5PrJ940_33}plV7gpQ%tW@4;I;^$%$<#*JP-jZYmd>rbHK=JNg#d{FVL%w7zjw zKLx)Fb1Xiv9;4W&C{DnR3&f0#ap@JQDk&*dOd(LEu>V4B$)4KMEF_V*Z5k}ZfEUXk z?{P$H2xDzfMWYGQ2u%1k)F(0y?#boY=x09s0n*MZzuf2ID27z+yn}OLt@9}Kke8`G zQ^o&VEh}(fnXvKR3$N=!n?uklu4&*V3Va39y6-8OBiGX@2uXIy-HB1^z=2IT8{#~HF8 zbTOZ0TJl=2v)twzZSWX1la=9(gf2iWk!? z+>#g?fBg~hZGg}9P-TO>nP=G?2laZ63>z-CL_l@>)kv$JOC+zch&1bs1H%bi>2X&p zh9X9)5l(y#N!T(c_f?iX_eI;L-vM zc^8agh_x!Ht;&R156qiMi{zGwDDwY^# zt3mpQyemQ&=kUkZnEm|o-QCTlBODbKWvS& z_ST&mHSmWTb#%6a3|^@O2(ywO2M;~*ph%D4(F>^<>7n%lw#E<=4h_1xAqI@TvLk-Q zx@GiTc^0Y)r2gzNI%s1IG=nabn>@Y~*4WX!$d?JC7b^zJfPEu0(DhL&c$N5634dm; ze!URrnjc%^A>fWTu2fP2_oBR=+}wHaH^QTTw0&ijY*|x{Z4b^rvMY4V9tlfzLGX8s z@Gi*9AT$PZ5zi-2Btlcn(2&)?2<+YEVi% z5w5aB*>t=39G=w{7@6?Cb?+~M5OIL15MKd{)C=l935W+Wh!NsX+czaPSTy%jt-9%C z;rc;P3%*9$IIY}U&#aQMSfy+ZWc#>3O@{q=MC#DpVF+kXEw#kMJ41-yX+~a|Z-pPg z%joz!#_*)0JZKok|NQSR(ZB%g0>K-5LLXLS%EhQy^j<@m2l9u?+J>K;2xKug#X5` zm210P&YoQASF^BpS>&m~KzMG!p|EIx!F~}esC+45MYM+I{l!Rcy&`lfeoKpi?x5l5 zE*%9jlMbOZ>`x*NPh=yQ(*nbX&!+V_K*uByID#xxh6e#}L?(EE6h5t5*919EA__c6 zeSkrbSTf5~{#&S5VYdCGjgjB!3aJ5k{em$qlyGktRyZy#{Ox`7?Zcp+MnWqNEh-wNSX_B@G7K6}8(_v4>pEW!c=Z9-#S_h542d$v)qcu<) z3J0{_p_k5eXE!W71D4d>?XTC$nuzver_cp081}jhs-(ATsNDuxe1GHDQcH(>PBcAK zud7HCK%OsRvzSz`V2l!BucW9WbH8j&gR2~g{EADYf7?OrZJ8jg$)!?mqMjiwST06Z zOCbteH&x^S*IVJR$s%;vFLoDRf=gv z_OBUX(gB1!*ajR!IxX-VQIZK_svD0h7zjSLn946bL?g?n>&#b$_1zors=0k`=qbl) zm`W0t4^4z+&wQvX5L$}@%ah7d-5{B8Rmp949nhonaPM`gfz&3<80Sgu+Tk&3=q8MA zizpt5g`&p6av}&5%Qn7#tPMtCq!!)k$-h}9HoR(SSleuJAUx;Em8B2TR@2M4>L<%7{Ob}Db^$61FO@H{N8WyOqpG#EJRNmZhf3UY`R75F1J*NLw!o0y1- z$t#@*u&_+50I*|`bkkavaP3e2pvVmAqD|+KA$yE)3gmcsc*^h6s9X`nt%y-AeZ7Y@ z`Yw7O)>ei-D4Z5$jB9UtgO8zDhnObvHbs3Dxyiv3#K~q%$Gn7Ax#bNkZa3r!@&h-( z441Pk*Tw$()RB4~cmh!bL8D`X0ic`OGK_@Do!mV9Gz5-pcht6z!N4wuHFFa~^0||- zNxxcf`x*yr)kYvlr@YH{G;F#}gz2KQ%9W0(4%iE~6)zD9Ci$uh<#h&w)>OMA9E;Ax*t0MI1a3Ta!(Z3b3NyzKJ$m@d8 zyrpuQYyki`!FG7}m7GFiVnT^(lpf~oBUsIDIg!RPwf-g%)695_E_)T}Oa0x_i%Lzr zWyw5Zv;?nQ>UX0Uroe&wv##i@gk_UiNZ?Q)u*iKz2))#Q2Vw*nIDFDkKd+gSafj!p zpf79Z3iLHFdb6 zSs%rM)h{j!!9=y{9ESQJHfXnrpJ%QJU)-j0T8g@7#a5CTdlDP*J#0$GrQ(|vWtqhA z78niw6${1e2#lk{%GR^T1#N~VpG3L>No3iAiF#A&8nZC0xif6m4__Ze`q zfWysNnq=!!VISOB7*Hl#M2K0FDV}b-{Pvq~p7D$oKM{&P^rWhHyuz(7@F1CO0hbUJ z-EU5+kYRshPN}x&QKS}wDW+z)DKM=Ps@WiHL?$x-G(MRUUKEB4&SeG1T`;~AYCkjN zJqC{npxt7Vn4t|bV8giaC^Bpe*cOxw$CeRliW?h9E=n*>(cE$C`nhpzXHwgM$Q!IP z+}5OXo=knA4#(Lct|&;1f}5z|jdH+vrL1naql!Ei?eFXzg*J?SPaWUU)Z66DyN`Y7IAQHa}PO0`eWMUJ>JaFam z=%9wrG|A!DdIp~LiVV9666Y$!vi4>?u6cD^A=6#dQQ}EZm|vuU&7|_spKv;>jMb?g z&AKymIL%H9O%@hnpHFaq9cYWbXncmRNx+1%dSymRF&Vl^3>P&RTbFJy4}&Ew95mx; zm>T7?M@NUknw6@fW$u188TL&M1jo(B;?318c!jogvqn0mq3KH&lgenh--K|SheXeu zx;chTC;1S=|ApEuxB=B=uB?YPlCkPH!a#-=6s5ogIG9bd$g)V>) zh18r=wHP;RIk*4O5H&RGWYx4yn~etW)&%R8)mv&cB$0;4vL%L-Xq);V@A->;e`AqZ z>i$tD6`4e;yQ90p$}2^*Cy4;1*5A;YE}czYBO_zDe$$b61+n6awW&F_yfB@{Zq~s? zQ}OdEXc;6glCyVCqMT(FY?AV=f!OGcDsw^9VyBse?bwBlX}qi|wpv+(WR7F@8t2(-DH8yG+Jixw+EJpH}4X&6csL1Ocq2!q$OHUSK}VZ zko2a%#m6O>y<^e>cFea>5x0<5tatm6#G^Q{>JeDWZ*}{Sl8r7-PD7Ds3K26Fr%YxR z3Ii2DWr(>wRBWAjpa(fKYP;AKafM|J|LW4uwLkl>g$%x{A1%%K0i$l0V#YY)y5Gr9 zb!O4~L%lbf-2%h%i{^7i^}HrFO_swnHVSe16!`XXR!tdfjil`7cSS7U-hlr2ddMjz zF+(AdiM@Ym0sUP8I({wDC~>DsbE{uop*GAc-LPB^@phWyh)Jt)!^-hKPS=Tjz7wa5 zsMC;GCP3<|%x{EHCqfaP3Wnmv`)|{A+pMuf>U{`!ziPo_euvlS<{c8z$y?0Q$33?2 zQx9Z!mA(ih2D;#P7hdhlF4?&<@Ve&k9!-Hf)dLF=I`V*ggb2f{#eQJLbLaqK`JmD0 zA1Z&vdeOu~%!4`*ovNpU!!v8IpeCI5NmyC$6gJ^j2VgZhQB^#|5XZih#j+qqVJHfmQ{=bn7@xktTSyak@VkNwC&Cl~sH zDCl<>B;KBrUnKMNLGA+fzQ5vrdj`^Zb-s9p8R0yvK81Uly0RbIU3ha1EZk+z3ucYr zoo5k{vxXG(9)CkzM$qQ-7{kA_U%sy&3r!4%2y^#5o2u=Bp^jXEw|Q(JgphajZ;#8|V55^x_qLy4pPh6n9F_hfJqt$q)}xcg0$?&5o)t^8h&i z*~@HqJPrnH%EdNJ`SP{{hMi89)I5Y)2VpBghhw)z}vy1x!Bkh`QoSs4iZT@4#w@Q zp*B;E@TeQbYQ1 z?b=)Jeh2V+;^DgET5-%NxVmV_*1Q9V9=YrB8~f23iYoDpdxoHEtye4=%~zFm-B&oe&R?_DD$BO6#M7TDi27Ly*UO-|Z3!ykq-oed#) ztSgWTQT>@{GZo0k#8@6?8?fFn4693fCfV(>PqLWI{=&Wg>33fbnT^n^t6Gu!6rX6$ zTmQ84cJi_}CsCMDp)}TtITq$nW?#W#RAk;ikrMq`B0g+HN=Zt|ioZc$Y&aIQ2<}NX z!NnF*^=odpl5j0y;=xPm2xrRja0r3M%?u`_g{h5_HVPaq`R4!||h;KF3C( zYl8lW7^JuIGc|(Q2DkyLyWEMC9$uP0-kHlY>y4jJM?2TW74Ocn4K4W#?8hxw_nB_r zF{Rp`Pn6FLx?Xp8ApZfD_b1R-zt8n8Ys*>d%z6WRTzY!$vkT&MWl~*NygRl+KDZs1 zWvhh_6YJ*1Zakdr{d~J^;H5x?vHjO#MY}OnbDtzBvwLZ=u^>EiL%JIePW!HByFmP$ zbBYxC3%BZ$cC$jVfVt8i$42{`8fdKNjqB_hPM>=%Alr16Emww#O3EZsa+?vwfegcb zH{M9<+Uvq0f|A#v9oGx#i@=En0#{WPLSIW8+a5;Tr(rp9nl0klV`0gLQxNqkR&SN~ z&raz`47+ZaVxSSD2|gVF5^Cy5{kprM$bC>9p~Zza4~yViAf7nOGwe?Kd{;j*d6q}- z1{Ob8KpJrtkl3o=Cu!Aq;Ka!w<^ch9oXtdZ(U^q6+nGCqtc$9?N?gw^F3e`O*vx_T z;|>gLl8Ve*#smY4CGqtmL9)vH0ZGxNh<<|Hq=PBHHBX?_Mk@(a3v3*0(v`RkcD?aB z{X=img(h6mWDkS^`|X`qlG>z$tO<>yC&ZPeu*90lXz_;44#QXyBx}}dw&o$9BvJy#)-Vri`gIcreb`z#G*Oqs!C7KsJFbR~lwuF=9jvxMa^z(xjFDobWdmxwp{N0? z`&=Sxjyesz47QD*U_;Yo36PtmONp!l4EJ0m?T-m)#)A{zl|$E^6T(|yvm)lHN4Zm5 zCN7BwtdJ?g(sAQM+ueqV#7Xw7x9qH=y4iNYT8ojBH+w|(ns9T8s`hTF1A?5e?5zGK zg{3D$4Up!GXsIVQ*6S^X{=_Z=6NzPKtiCkAqY>1SX9+>H8O5=0QqQzc5s}}1w|5d- z$A+h&Gcn(pWyhH;FC57O*t{7Ez<%ue&W%w+^6eWewcby&bdlB$n`Lwo9h|8_3%?h+ z-U`!8OsLs!m6U9^PM}!Z)JtmDcBxgg4ylg7KYo|9YXUmMl0mHc>P=YoPBqn-IW8pM zpn^D#^L6`U-`I?dxSo^HjQhv4n_4Gqp2%F-JYz>00R3Ll9CxkpZpeg&g@!Za2{xv1 zS5LZBKUXfJps(lbCV9CcLg<*PcV|C(kxE z+=`zx4UQSslN>B-#04uFsN8>3Md*l)@BI!k{y0<)C})50;JHujHzQ%bfgI*J-YJV~ z5WT-Jk7MCb>|w&!s#FbN>WWjz`W*ve9aMY)LL`fwm?UOpCxxXeP7@PJZX}dLF&!4H zSyj5`nn0?i$(1~$8aLjbM$|l{a@91oBvMPNqv;th(oc{)REO+I+-HC7*vB$PO)#E; zOt9W^%<(5x3f}lYDVNl|v0S_)=$K1Q1YfZxsUo$}#4om?r6D+JU6avj5{RkJ-dv}X z{eFo`jzppuZ&s7hZW2hP&e7b=)XAVqeMc&x$aRkSFg32~w=h+@BxR70CXS~H8hCNT z;O@eh!Ix0E-ICtJ@~qQ^7gETdW_+ZO+@}xVvvQx}=VaB8&2iT1n{khl0oG(yvXreK zzT(T_`j5HhIb1vISNT@;tz;iw|J3*Pc+Dg#nvL2W5buZX7iTV=r9C6EJC+wlysGZ1WElkQ-y6QBl4$z9)^TE zwyd9rf$>|;&x7IRJ(8)t^$4k@7%vlk2hVr???Km|L8BM4#0~e96zdmz&Lys=*EgWg zD_Zys?w4P82W=5`fW^gxc_VYX2h*+%saSJs0wK4|*B~kULLa>|Qh_*)qC%2|A5+9G zm{{|v>R)TyWwF0xtrLTnkjR9S8;RC9yfMgam%maGobLinZ#E)FLX3|{_sx?R+j&P5cUY?P11GM(?@xD<7>akbEPMV070#&$V0<)xx)TB-8 z$^xe%NCo1RNgMJq-1Tf!AgLvb!KO5o1t{lAG4mF&a*aAL$cYQ!Y;Lk9LfX!BHw|VSO zDjAylypn-%_Oj$m?S4^#)|>*uri1(IQYa#2{!@CRraJayz-0vtY7BA7FQp0g-Q#i{ z1#=1%tY$F1l4{Bdt+VH`%b3%$j2?DQYF%iv zV5C2BC8Y!~7e)rL+$a%>=pXx(YE4oTM=r8mSYd_D3-}Fg70s$C8EsZ7hf#^wHBFQ$ zp^>V47%DSJ#u3^(5-w_C?Z^LQ4#KaF!~IiPU4AO-#GSzxhW6mm*74%nOUsnWA3DG; z!1R7k$H>V}+ASG9hjbUz^ms;hb!-xr_>DE&wLX)dL@j?3UD?>V`c<2HutYY8#=_`Q zIIjE0VO^erh1Hcu+>)%dQQbCi@~1voS|gYLT$;}23f3Cj>ku?K(f+d*uUW^=NnYD( zH4Vn{C)L}T99r2@rRpdNacBxFW3*(Jofh>*N+fE!j^f=1`cM>`|1uc(P&BaZ3CRK>ZQ)?JFV1|_)Mv@Gav#^v4UYV6+()=BdDXZv$HD{Zz(Ty2IJdTtSZuz4Hr zx?tVDmrJ@!%yBz+&)j+rk|3W0-mVb+&%y6N8=#A{RloEohAt&=1NhHSwK*R3XUegK)DT&|ePjFZQIc`{YT&qS~JjO%fg4L#== zJlD2~AVJ&MV7Xc}Z9p<`8XIJYajcC})T|c%@zI{(FK^#c|n9e$nSZ=KrAX z9fM?R*KF;wZQC|i*|u%lTxHv~ZPzMum2KO`Dt)#0?$c*?pZ7E(`uzBQWX_1ph|K57 z8IdvXF~_(DlEl@{5!RVqiSo}zM{>wHz%R>a(-+*(dHgR<*xapOuXbvD4#zG005&z1 zsJJ~yEz(4JgcN{SN|^jY@<1%Lk}b^?437b$C=1EJwqfL*Uq{B%QfTjPQQRver%v-M9PO4pYopQ&= zs|nC9u5@rQFUP0)VW7PL~MaQfHq*+Vp$TG(k#*+;U|_5EYxP3wpAV6(3hapFX^9 zb4hQf)HRdquLYXEB1o@u8q*##R5z&@acLnOkGSqLz-r3%v7jl$S`>Rno*FfV46u~( zfX@GYcb8o_Mw;!(aPuQmw4oMpUO4mSUf}bR^;q*m7@?>4)Cs8>Ll3X0*$tn>p{FFz zAks$QvWA-N!Ep0k`vDe>LG+OG2&jzFV>l&*REC=Jbl|%fXpECmsY=>W5ou^ezr!IK zNTu)R%5!GOFy}GM7-|3Q(sMPX-wGyDb|$vKPi?yCu7n6|_7+^Pw?FU)W%bU;inT|9l@k>!6snQ!_^TN@81leqgZ5v^$qX_u zs=4w0Sm+G0xOR^;*%Cv9*Q+M$)WQ@Az{C$m;u2=4e}3iuKDO9Fs7~y6K}X+m52nBR z?hb=;pLzp&Ks4Lf-H*bkvBuG9=k?CgaJV+esxB9<$|Z3dN8pmmvfG8hh z$jyL(EVmf&rt+a|O&PU?BJT_hzjzP(!XWzFiQ^W=BW@_o8RfF0jgLfiXi8s?7GNsl z0URqzu_QSk#3r%I!U`E9cmg3qE_Ion$2<=k>xgcWURr5^NfNkJpg^8xN^()8O=2Bx zqqb1=N*f={kT%Iy7cy}kme*+AV@y^sewaUyO-~sYl+%qTY7M^$XTmfLJ3wQgtdBUn zZ;vI6(RyKsa36gX5O#nr(M6Jr=MBNI%^woqGaxrc18L! z>T|$L{VwwB)w4ciuFa@!98cf|OE-1T)GTfiCZJH=KaZe=eFVv0g7piU_|N%aBd)IR z;P!U+hkPfjDGs>NBAdL^7S$x~G&Jx&g(`yk$bF)^^baYY=V?$2?^vTnRfy3 z=4w>L3CX?m*$9Xu(pzIA5s=0N`Sz9QD>_ZX!b^~6!(jBnWGY>da=c;3>P}mJ=@YPP zpuh=LhplK;OVm!as7J4%XOAXD?ALC?xK-+3oCX@dMlN(2m#$stpVYn`jXIRqLx1U9 z_>Z}(|BiIa!tfs&E7=(SLAqu6XVNW(dOybh@>K#_Kpy|)D*h5s$o4-7DEx02CjPgt zQd#-m<>bFZ^M8-||3`s%Hip0UMf{hbgiMTV9RD^DuVc5)hT=0_J5%^Tpz)pr0t1S~ zuG3i~)PZp#BoCr{R<)|yDWy~O@!Z)}BrZvWt0{rMN7Av!^Bnn%Loa*%?ACgo>GL!% z7undj*?JK%E83F&Ndw-qMNilBS^xFvkx08${hawGcmCGXI0S{SUG>$qUi0kMY4dck zx_WQo>i7x9JEckvjlbvIqqpt7;XPV|R=H_*Q^u&J%P5;!wOeHDST&}aD|=U^{^26% zvB&_pvt>!K_vf+0R8tLS9M8g;#3hC}E z81MNz{v$w60^N26`olcE$XK6?gt>3$KlO#SGX+a36V_CVBVjvca{kH|12F4dq zagzYwu&mBjZF{zT9DPhsEj)%nT$33!AO)ByskqF~A~$V+mX(u|ssL{y%Pc}g2_v9J zoI+br#U~dL8KN9=N^b{8+)UmrI$;3sUu`GCCG4eJuR$@Dd`~3%AiP)Xz4eEZX0e}rnW zqVHPjV!olwFjhIGnjaj&$AVU$W{^8;fXZ-18{%Ed`^yL__xbD=7tJh`()t6DgNh&$ zE)u08nE|Ni$;{7*!xTJ5D)`5@|Lo#&eLbR;;^k#Eptwky_70Gv(LZ-&}+0s6@5@S_`t3Xue5?i zs^?K`wuj|sGo{1bH>m@W-gn`7j`Xpr?g`0(x6cHdAMx3o0&;m)A8%1H|+O1 z#^c>nRho;QI+SwH=IR~jJVEJFj_&b%Q+2!SHwvdE6e6#sVl)oxZAk2&WqIj?aO(pBH(U~zZbujK}TnxlSxUT|NVB(NdIMv&P) z^|&9%z~cNZ<+b9r=(<)RSNjYIn20jQ-y7EC+DZr-XzockX~m9i9mm42% zAM#?6OjbR@@j&k`o6~EMsrg1%gw29A9x~8O-YrWs8Mb{sI~O1|1mT-qo$MN7@Y`1{ zYdZ#Io)M9QLv(??P5qZ0Z81v(H%=^KZgccvJsT!t49}0I40;`Nmo6F6C=}6_ZP=NM zw9F_BY6zP}QcYa9M8v}2{A$sKQd+M>#C_0w8J>EnKlPUh)M?FdQ6&Vsg+Bd#g+7Ac z$KJlet-*Ou!C67ojav+kbg@|=`pwuPbE*YO4-gUZVzzwqObKT@d87h!;l>FJ0jYfR z+N4GbF3LDa5%MA#A(UwI6b!QoIk{QgwSixiEzqwFBIF`W`6oeL8es*9lL>QCuEZi< z68L#1BvphWolSx$K2<_r0pOH>)I)77FFnEvST*J1z%9Q3M7$RH5(*ND(&U{<^rU7L zE0%}#Sl{Sz%$;F)_OQL2^;rsVqhoT#tWkuU`_ZdovchbTQ6Xka8X2lz6RBV!Lu5 za89ltE8&+EweTMaICvYB1wgpK{^XqSJ?)k;*i%7C{z6g;e|4cKB=ILIY@>i7Tv!M+n<&T>hKdg?zoj&N$bL=K z$l>qffTl>Xd;>v~6f`+d6%4HO^u#Hi5zcH;T# z8Io9jaV+GQg2e z35hos4Kc>k(p}bBb1qfS(n2m86K)pl;?Zh%DdHi}S?CKL?jb=_rn1DsOQM7K@7PWU zr<3V^i6MLwd zoywzKN)so`;oM?l8SX%fS#A{7ifB<6Bfj-Ka_?#07w;Un^<0wZ3bp=%W&p6c^e+0# zOSJ!!dGrnA|2skGe=Gx)OR_^P)scv{z#9SJv;r9)nL@6veiiW@acDVLsWLlDUr6+R@Q zW===1r)X%G1_$r4sx0Br=KU^_G$1=OV?^F-zpAlCi%gd{yme@hL~HiL1{Lgy&`!Bed!`BO37!4H5v+h? z-CqWg!Jud{)9-?2?Et8{_~XPLxRy6m0jhu)6+D*bl6NLhy*C%i7gFU0L6p<_wd(~O zzxLslnadZmD>PfA4q65C;x>v_a!Q{N{-tJM(Fj#k0>HLElx||jIEjP!I}f%P)o z;CPar9tCcj*QN_~mig&^;YL^Ux2#b@R6zzw`$U8K!S}lYALZAvwV8ic>2 zSjG&MMMgwWnULcXsIaYGW%GUUueP=WLCJ*BpohMp($UdkMvlU!^Felua;AB95pWt{ zt@)BG^Y)Z3IGv_}0w0NZgCC=<=@U?g+z2SyW@Hw=P)4Ka92_AEqU_+~@D6JE?>sch z)NR%+FKzicnJELu*nei%%71XXoO#WHI^7i{6|RrZOsXJ;1H(;-+cr&sdsMGqAoU7}}eW zU0?NOiY0Xp+Yt&Pl&LQ8@VdB{kBz#n*KFBt1!yVwTV`N zDJPSwjf^JC)BYeljIUwn7fg;U9ji_ncPgIVz)E8fZUYf>)GB=q5QPu65y9K#5NKSjO_r(oGMholD>lg#Se<`bNqgnX5$BWd)<`Z8gmYKX+8(&pRW4d~K95*u z(QGh^+R|LvPHk!tc~WLXP9Uf<6Y6Eg(h`s1Of~|dtmJ{uFbRRnrjc2Hn?XF?0zk_5 zAtm7Z1<&XwK~Dkn=Z1THpbj%HO>0Ve>Olu^MME$Ba^0!4nvH*(W!7D9f+Jn7`GfrC zu{qC++eQxEnO)|bIAbUWKF$sR?^HD9Cx-3r+8KOWsxY#FNm8r8v^vUoHbS%lO;)Kp znl)g>sQC`-t=0;D*?Orz@4JJ>?%VR;80jY*dbpKovP$^J~R*4tb3|YLY6YQ4N~YC5cs_2C6*y zT(tQ2lIwB_=1}Q1g^S`8PO5M6C5G~asWZm2ZFcb)%>5ju2pJ79{S3KU5E-&P{LG~~ zEN%y1bqI;1w?%oO6W%IUpW1ni*|Gp`zZCtv_ny9MZWSo6t9(H{o^E3g`1CszpU_NL z&ljHF;qh2pzzDX0T-{@8-}}# zjFUY1>kh&@G1eV(|JoY^Td=sw2uLcx*hvFh$C@)9)|E(tZk0@qdbaEpaN-YQ_nTP( z_S_6FV!mulq%)}#!~q@avS2Vs&U^}scgz2av-s(1kxU>`j`OK*5dun@% zpJ68V2X7@q4s#9aSnCVL?a{*n!WE7`lB6+q)P8t$W>1AbG#}5YiSoj4VykJ^Cb=gu zrxWpWYP4x-Q7phU(-Ko{{4f_4Ffr`|1lNAkS!5-#V-nWcp{FxZgz;)RJ^F<3`UPL# zTJjmCUUAH3JUN;_L}+Z=9U4q4Bj=JfA5llrDe%KZ8auVca5-brsypO5Ec^O~D5gy> zkHqkz%_ul@CWX}l44y;bYyhmD5bNilnlo?B9_*FpY-EwsBtfs-$Rx2gyC;88xZj9O zA;Os~JO%8}GE~d%+050h`+1@9`M}AVt)13w@v;Bi%alhTdZ5-R# z$*7pg#yxDwM7n|v7}JlLXdTpHu1L+rPKgp<^D>gMP|Gbk-~lw9Zg4AH&4q}5-|v(o z^BHlaKm@87~C+s zB%=jua#iM+0>fnodikMOW$rPdp&?|PfVIpbuQ|Nn7K8aUcn?e0@uFo-kl$3y#|ZkE z2x0+fmd2j^2@PFQkEBiWX2%=Au*su+qRM z|WIAL-u%zUn{rP$~~a>US2wX-U6mrXKGZj%#$ zF?*5aV6^X%HE1X6eoC`#Xk^aq^I@!K^%cdcE!xc1W@i}of)Qo><0GVfYAwxk9 zXC#I(x(WG zFa$fn_5cKuSY{>t``BSrZ+Sh+mX+}_McbVqkKL=QG`6r5}G3Whv66#A2qAs_AkiV#CiT*MK>1J zl~ZGuEcFof5li7FQP3HG?wOn4q8(Fue{v#7F7+n2`Lh#x+BGQe5Q<8|&$QmiqNPLCN zaajJ@FdepQIMSwM1$OAi)>0~jkr*V@#^Yn!WSlFcP7da^cwZx)%@-}vLbH0abAZ}{ zNhYhBXs{)lC=?M_OpW$5VowVx3&W27uYY!ZZWWWPOZm|u&3VMn?l$h*x@Q?(;ACuC zWDVxqw8UW({7FaztR2Tdf#FLqAgWP}Yi0-I&rJMaP6YPlx38$`SgiCd;h5_gR$NJgE#C;Ko_0ny>980Khq zwGT>&)@Th10B2nF-^|@v}R2y2+r;I*y=Cg&z#FT9d%ImcdYgEk9HI3nKw-1Ndf7m_R8#METD8n10cLYxrq{h{%FqpML6 zcGX(+U&`d){xwQ9lVJ3x;grKrmT8QLluW~vY-B6z(6l_=rB`ifyhzr3gLvuSkmJZ# z^8{L>4w_m3y;A1)074p}*7#ZMa0U#S6OuyBz-qOGTD#xLz}4~V&0fr)AYV#N-B@cb zhovre-Ab{Nt^JCXti)2rqN8D}o^xZQ!x&i%*g4b)LnY?yB@lO;Z@>rXR{BU&Di zs>KUYeW3bQP>l#sf44vm+pg$dycK!A9hK*Djq&hup<{n`2jZ_iJP1ifv8kLU6l98V zyRtmwh>gk@wUslyx;1bN)I33x_)P@9WotVHKDDRh>tz*~EpJExWq5-pcs!djK*`%J z80{`GgaN7|)kneBoUwd!OQ(4C)~#PHy#uWWEYwm)(5(agRS_ig`nf+vWo**DF3~|j zxg2eFKRc%Yf&jFJY8(v41w_+(-7&Os9EePpI4+QY#;9@@Clgx9SQ37ITxHwrGz*=n z(no!HYB%?J;1Fox=*P;xb;SlQkJF&{+^V!e)M$9=9@ctU1?0)0GMWUG1PtmN-R0Du zQ%oTcD5xMtZqZj#HL}A^*fCVCiOMkNqi)&0dE&3d^>!m@7LG!`kQvF(cReiFt-@`~ z-G}r4P6SUm5@r+dQ9ajcEI&46aW0`IYGhyfF}-Xv&TK+cX$x@)_61hZ3cK3H|oY~Wsye?V2JM7W~bbOYcmQv%@e z$2g*yu=2jzD!$w8p{~jG<#l+8>VQ>SoHYtNDY~TaV>*oS?=O!|y>!_WH7aj6NWa2W z=2zP28kF&OMsm1Bn$!9L^WLh2j^(J&tzsACzM^n{kj_iCfYR`)I9g(H3OQL0S)zrZ z(3xkhav$WYG61~mg+gCuS_{%u7fcy7W5XNlM1*J}WhD<0?3GJ^W}6@nGC&yL%@0^kqOQ^<>Adp#OxZBA_*TRY`o;;^ z`s`pH+Xk;ypaPeIhX{TPO$=s~<47#zzjIde@FSdj?5l_lb)R%Js=Vn>VHr%e?P3l| zH4?zE{sX=}&JPd8lM011<2(lBF+=;VP;q6Qkzr(&T_h`v&k*kEml{Wqw!Ns<@y^}w z{8`VlS#-8RLCw%Q8z6={eA8^k)FU|;W4A&fypdhYDTVtELRDQL@_h!k z^w(2RjAldKH0GP;V@pTFyfFxpR?qA|`Hw)^O#t;cH3#+;YIPu-nhFP?HM!N)I1YJ+ zr$PaXw)b-al{iiXvTiW***vxUnyov*es zv+kGE(sg+J4!%K>zspLvxbHk0zUy*t`0Jt@pQTrsnI2Is4~+i_!w^9S7>l*Xvg3n6r8bWr{41*6#6{UJtrwg<~; z5@5Dsjy*{dG;-d63&lkhD2JH=*`d&U5Zh@g(~hMBeuflMt%MeGxWkBvFu;pZ!D6+E z_F;kRE2{`ZBD+6O;uP0?XYcw73Rk&35sY4x`t0(degzlel>CVL8ahg5!;gQJxWP0< z>myS(sT0Ar(&h~G|FP4FCicdKLkbuKiND4G{JYIiIC58z>S(6-XG(Hte7LbU0vOk_ zQ|O=k^Ecc5Dc1N3X_184axw|JMjA8^vi(4zkK*$`QlVVmveM2&gJ24#N5VH}mKR-R zR5~h2d3ENu9`eKW@6F@bI^e*#TXjYfWoyR1RtTsc1}lHW%1KtkOcayF7&v}_lNJ)O zGPH%Zfy3sI;MMgzDg5a*b6?*-jL*p(h}go(7K1au_G65o;=RMtpPd@QEvUA-{e#o5 zIcfXN{ouBmFr+U?5=p|t-|CKzxGN|?4t`3__$UT;=MTGUNMZyFf70yeHf>Y$d#@5( z;N3e+2Rq5m1N>DDI#~qI-SvjMeM;~c<MI)L1h2R(@;b~2|yytkc}RsFHEib>jIfiO)E?+YVKGliJ1TND@``Mhj8 zdx6D)iT%8?Pd(%vyh}dSaEreR3_w>w^NUvi=%XL2M4voXt|PC?i-zt|+FsmqI?#v-o$KV}UIBgWK#jP`A9VfW zfnYHVIz!uO4}WUfFlWrbCYHcR1(n2b;29tt;B!Q4PiW#kvXvtgv8APQaw6kvXw7hf zE8aaC1C}La2qYR1P}Kk?1eSQW&xyz2E6(gE7N$Bb&$UTr@Bsqk+t!ijl&#|L_)y7Sog4mvpL zp+TvR7H<&wNU=F0=JCBsWZrRPAdS3bkL;;9TY|CQ-JyC|tKk@T3Ni&D_SA)TdQq`3 zUk28l*8#2&1H16QfDM}k)=rLO#QkGW5e$g;dTx>@{MxMCfwOpagK)WRoF`yTK~3#f z_liR*^1iiQIe{p9PwyeiFX$KFP<9P%QS>mP&JYSZa|^=KOFV2W*8;VlhH!(m{ZDp8~an5dA|G z(IKyi$oq3@mbhQPcwggAC}ehT50cfNr z;u{{aSuWwGbGSgmK#at-vWq6X(MCx$mZlgeVo&P2fe}3IhTEDdlfCzbck}qSr&&&k z23}EE;$9>(WZpdmHLtQkheAr-zDu5sk_LSjjD*;q<|jQgSYI`apq3TaC?2dY7F!N$ zIVesg>(i;{WLODbCSSDghp$dem3o%mZ&T+i+uQh}F6T%e6iJdevw0+8Dimm-Tk8pR z4mCGbhTPGBK5;U<`m`dMHR_fX6wgrI!ya#?Pn}l-hICW%nF!OSK=# zh)M51%&6t2KQJ(gD6*lYH%*qH5KrY`6^#33++(_3jq{X{X-Z?A*;3d+X9`DJL+^-{ z!p=b4|BPB1y%8o@Iz-lw;IW3dI!=#Wt7vYCq5RskCBqtjw)bdF$;y^=hKac#z6p$j zZfLW>d~fZIJk=g;WVH4Is^3-G;C6JQwqfjU6BUBM-V&H68xzr$gC z)ft3+4cg~~lcI&A(d$V%&Ue)n*q`4GMBTs{b#leE8a6$zaqz)WqxbGIp^9UD3NJF4 zy|rm!wqjXR)3BjZM8rx9rkZbDY}%yg*%~O0HxGCOSsv0^4V>Lu7I_*#%ov9Rw*fG~&oeNwi`G z%;#KvW<3Mq?JIh5qTj@J^svg6u~RxbtA zPTG#gj9X37k1%7q-2;{bXw@bPrO1e-CA`OYFm}5P)>PKue7RjdvJCixX}?`=l-Z~+ z&(`1@VyR2LJ3?S&WA*Rc@hb?vCQQ4aR@f2QF334EB6Sp_mh{!>Yc^hP7*nniTlCfY z)oLg>&#k|kZRjluN#H?v=l$&$nG|DBbEuMR47l%{`jbO-*3{{L2>S(y+xH2v76+qb z{;J|rgrnV|ozG83Pksz5^#pvwQJq*z%1WY{B#ablzrkAdPe6T;)@BrUbK%?;nOwt& zfL5!|Fpc{zetacbc~ZHgEZS!MDBn&t)bR_cQf8XPd@DIRf?*_IMqtL1G>F%OaVMdq zE_a-6-0_cD^2gU0r;j(|lItEQ05bedpd*>PWzBG%eScahb4<8d+@8Rn6EPq*UTal7 zp{F=JK?B*X$kGT+o;JqB-eunp14VA@OP3}6$(}U95wF3!(n6OZyPju?Wsa3@I{Y#9 zsK_mAfN8SF+8v0});okw0N;U-%swhQyI<|TVZ9q`?iiSgmg6bE#yL4i-ZjF`Q7e{C zVxOI@Yc=p7Vma<`X(3Q4a(la5aE;N0zj!eh{~OPa_X&k2Ev2-LgI*!@TeF_q7HsGI z>w9lI`M~Sbm~{d{4R%dAZ6)?HR0GAWs||U*yScQFGuzg!Cr)ri#Sb(}B-QIt`of*+ zrCA$ncG^G;IMrp}|JcDSaLpO4P0j_ zWI1v1kN`aPuCa*p7<55naW0dM7Eu>-4^Ikika!Q9;F&eBB4iN7YaDL5yStWEHTWnP z@=$tMee{}WaEOW8c3IUFbtNVVw1?ZNAqk#l*5ZJAePDZ4heSY7)Oq>fskQr&WA-+0 zdfid84tCt623qB6RHJ<^nm?`rkixCZDSXicKV}4l?Yt`E+Rk@>or~iqzUkIkKw|`b z0QB)nR%LM%!$o}E>(X}GU@JVRwI%tbEy7Upb~q8DQ|h}bn58-kNHx%sAcCqXXZreF zdxfBoLYHR-gK84dn;qqN;gQXxI(e#wIAel$T?cu^r!+qukZ%Yu0OP>D`3;92m#y%4 zJerasSa6$C2>#%Gl#Uw2_{mU4f~$l*-PPL%U?lrN2tx43KeBmU47Yf(7a7uvMf9YE zFV-3kCqz;6W0BG*_mI*(zHHICz&S$Bbafd9?F0~__i`aQ(aRbz(26z*?)up2Cu9K! zltPyyU_l^Vd6R84u3B{ogK4Cjf3Aismr-7rIQC*>3v#x}SY2JQ6K;q)+**ASW};Gi zkNrS}m6nD)%MG!a)JE-mrWD5BI}ice_U>S~x;@oH*E3pH!c>A1hpq6J-%Ouwd21|C zHVm1^@v}zS7*&QuDH=~Tbhbd(AfvO&*Y~#Hn%ZV-p9S z$HRjw)4C&f2SmMv5${=Ya6PpQ`>WT-`+|`+7$TeWx*mea5!XdbjHf=0G;h#C6au7n zZf24cQk0UBimfukidU%UhFW?Arm+DwQ@l~Pg~l> zLV91)S(2albWhkUAF%J2>^Q)GT#lbOT4SNiI;NtI+eAEll1Opscd33WLPR4zZp`kA z!)X|BwD%bd9NIi#zbDL%ltK!}f^K-h{(PB$&ABK&)sN277^;50z?~0!l=VbxuAKf2 zL4+CxAqs|Q>t;Qk4S(TqAT>pHI*m~A>q*JWB z#5KsG7>Fx($s@tq(m{Tc!U%*mb#Q>{NG!S&l3|FTu_sG(Wvr!zDQONXX@Of=QKvDv zw-nwJ5fpMfb~k(7_X$g|M$VPU#8`QUI3~vmHm=Vv=*}CZ&72CE=vYw(eyejHottad z7Mqq;IOFF+M^!>2)CGA0s&jsyPN?Xt*vO(PuG^NZq_k=ipU-OKRQZ88bdf{aLUqSV z>`+dm=@k}bge9pYm|S35=r;bNNQO^8+Y9Dd2Fz<~Iko!pDu%ZSynw1d=1;LPKWR{n zVA4)V?SAd7p=n^#s<|%oH^bk)Z&ICq(9DVXs0VCSN%4fGoLAzH{kVCy#}>aeQR3R| zk{uG88qq{;$Bh9c-~bg@orB!^SY3&@{Epm z9bboa0W%6SAR4T!nzw>mY&+oBwB0bAJLZF0$82KCW=wJ11}&P30Ti^!2YW!t!&Q8h zg5bY+SP`5x71=Au$%oLRKa1O4bFQt_fcbdC`jrAnHEOqD$x!GC=!My)WSKESdcZjm zu2Yl@u%G9n1_SV81CiXs5WVLjOhVr7Ij+)k34a^_ridn%l(a*fO%>5Z3#Vp)L|Xn5(8iYq-cw_pNn!-f6nKO>Ln&u> z`$3(-0^~1V!n^Ydm6NJB_~j2X&yTRuhgp7yB$;zl zQCx3uLxMAXG6MWlZn;3y-76G7>9C(VWy~xGzAIAO&m4oVyA>PXgaQh4CL|gNfrF_6 z%^7q(nCyqRRY^37Op~JKtS4s~?QKF$tC7V0+at{0g@|WLIyxrqdXnl;D9Ba8k%{P1 zxLoMdZOm2l*qN}?eh92w&@>IoZsDa0=!@TEdv=OD$07!buqcI%_L|0>-~03(r|25y z)1!-KN`&RCm;uEf(z%{&$|HFOxd`Oj_~u_9`3>%xiDQ}Rwu;~X{a?cew^ekaGkmo1 zR>Rvr<3`oTw-r}G;J+$-|03x9 zD-4nSZ@GhS$6 z^8dss_KyWCD>MDCa=ic0^UwU(3g3Smu8=mdHFGxq{(Q!N!$NFQ8*^M2N9sPOK7)`= zBV7ofEhr>5Ugf#8s4OfT3gayE$rG165C`I5Ik`M1PpuFHBrzXjc0vF_vm6`ue!V+< z9je*fm_)DsmiX?MYcs!VcTR}tLHSC%f8tNoKDS;jUrJ_uIc?Rr%H3}Ua^%kT;@`i? z@Yu1-(E5J9K4wZeJUv0-)tZ?d-y)*haO-`u86BK>ty=1cb2T(As_29bqFJcWHPXkr zW(r2BEWE(KIx^_hI&YPx>1&nhUnmp_dFjp??^kJ1B`2{#R;5e6 z;%kD;^)!KR$7@&D3L zed0-!+;jpSa-k`!&1|vB>1e|$m!y^!RbVAc)hH3kk=TSjqnDY_f%@*j)8PNR#;1{> z!7?=TH|v-ZsR~=u-N(7OahxfCnc~5S@tH#kY6B0UrA_{RZ`$=42qfXeiOp=79 zz-*?Rb%VAoLsm6(hitFupV^z9M<_DLutv!n8+OJq@~{3*>*v;3gGV4UzAAXGue(nJzGR+F(XY;H0xW)pMPus<)uEO7{TQ%4{P_OOW?;UM+?Ik&ht$%EuGp# zMN`KRtn}v%1I4y?FHdZmN%-fzCReToF2HW3IpgQ6J$(z?aa#D)+$uLKbgKkUq+vkY z_|a{s8|k)|*FA8T)O*0_^Jl}#y@aH6We8^19zk@#C|mqS_QCY5Ff512pAEoQ8`=lJ zKmR~bjzu21$Fr^NAZ^SjKJn>N6=RF zX~4dVakERAfqbI3CR&8woa)#ige`F2z~B%!!R5O(*^NgtEU^`0!MffkJ51H^(u@Qb%ep@_~IpFqjXj!+`oP zGeS2ZZ&_ncU#59dQ&3^(EBn~hIYUt%Up~?k*Jl^|?Hr+tkNGTsfn0Sv2OJ)BBZC;( zn>T%qjcMeqZ2S(jcrQQ&|1osv2)uT55s*@RGorS~Mqm)exZe$QV>D8j$V>+WsHJ`N z@p{|uQ)dSm29WW(e!$`Ymt+ULjyNPAY768-p>i%{)#1#0v{iqQD%3YY^LO2D)qwEM zJTmawu8?>>mxHDNdelqRb2JuGaN%iPy{)&^12f`VyG$z1hfa=2(jvvPsL)C&XvOoe z(vP+-gEI7HP&%SzqEDZExUv0yJ#-cf<0*B!FA|XmGtPLDO`Fg;LL z1lB)f;&uZmVwZ6#6;w6WnJDzlXbLmzCVAEGu=XwM4f(1)j872L_anlMAd{ual0)Lab$@5Dh z>nKC|YKeh|&>t=2uW(%n7bf$nntsZGeoy{lMTBD}2{)=A6ncnG?i4okq<6Tk(jW6a z=c3lO+)VhD0gXNW?868y(k#=MNRfEym0^>oZyd$=yVvRg&-5oCoO<^7k_u8j!0d!M zDaoe(_xpoY(CL+_jLcri=%Oco=nSkZ#_`FlO+M+PsxN-ZtzxO8jWPg7d^ilNCJRmxm&eaZ1G2KQdyX_={h(Vf>vN{$P_bkpo~3OXc?u}RyOPi#qo>#4@MiFRxkQ2KS- zkbLHmdGjE4+$vxAwu|e^s^dN@0;XTHDsoLmB~&vS(_A4GtQhiCY};89u)6%{ zE^8CQwqv9gz{~x!&Bf|ykZk6<=NVhoHRdXgzCXm@Q^%91s!W~fr*n{mUe7BXSquHl zlMk{JLfoAa!>LN%sfC?YQ&2@TG4!4AAZin)O{34l-W_F`^i?z30m6>wn%i%;9ygKI znWuG^Wn-Ud-Fh25A^4V)i%mCM0KX?qZVGxA^P3d_foii8sIycCb(erXXHtK)7$x-- zwL?g#`BNtf8lWax=5($2azQNcGx!rTitT+HBbwQjv_ckBe`u)sTmSnfB6l-l9H7haftu`cN^Hf7@{Uv+F_Jv+C@DwnI9ft}q zexBYkmv+QmmMlk>_F)8Wa;q};W5q{HXk`PtN=dXB1@jNuVspQW%H8&f2s-*iY_(j$ zK2G6!RdRW66x-zD2-N^A7jvhVsa$Zo4;**=N*9G%piAZWXX_V!Iwa`Qx%NeZ+Xi?n zyT$A8WC-Ch?y}7Ax&elAC#rI*R~PlkDAg+M?PEMPLVwa>sQJynW;o3c`S=-Ygc8U{ ztGS|@@YAa~$~n|nJSjxh4B~hg-c^NceNcFRK}TjRi%k4o5G~`CO+}pLta@#N+YdnG zo#Yia*x`UrPnMX0*8Dr1gF|;oF5eT>`B|>DbCI(U=+Uf`(ey)F#Wf29YapdoJLO3A zgI0f7Z+d<2KGNo%8}=0#NlJ8QD1>I-*(urJ74;x~TE8m*Rc|n#M%%$~OWcV)Z20kG zLRzjF-bh?M$*2TQ#5x0T&$l$cB{HM{dZmda_3}G)#>`YoQ?I~0Oa;=eT650^r64UTSc&O-!R%nx(GTXT$zohNB&e{LZCxrW^1 zR0);COHiKg-o74B<*^roo@@0UJuadhzth%hL=S%u{|P4tyK^2qL>P$DogG6BqhoH) zA*~|V2;e_7e58R?_#HT$sAncB;EWFkTkR?B(96JP$Emy=j|9v>S{pagIkZsG?NUxY zhdDFe>b%XnTv$6&5Lyp)$IM3eS{j`bvh>?fCp9{CDlBDg;&Q0wp!|aix^S%UTZYS! zvl%3q7$?r1eHJb>C0)rzo-Wc_Ki$o8$>|KxCC!YLTLGiGd2Z0axYekGGIwmP&H0=I zLckQ>6!ZVR`{W_p zYASs<7F2P7x6Zr*q{dssY6XCE_GE!h#<;Tx`AjaXz10OKdl-M7)U?3a5H@~^5v)~@ z-eJ5rgCOx8k2abaP_~a~+W1V7@EwL=5Nk)a(JjL$3RbaZ1Pr^I0)$HPECJGZ9n zl!U%`B>>wcVsEhUIYoQiEJ!zDDs^S}*{+~P8~{KtfbX7@;lCWf{wYoUUlk17`pKjFA5?E$RQ^P{{JvL*YLKss7^Z{L4_t!o>J*Y#&K# z7Ixo5;m4KUEdE&Vf?=XHUY|a|dOQ0Ttf7o6U^F5Hl1r71BC(Q0SC8-ci7lIj#wq}$ z4C`A(h1#`>%+&LxE3019Uh6g)I{G9$qtxCHY7oAWZa((J^MU0j2et6Ao+F#)p4@A@ zA!A8k{Nr;Q9=q1>f$-~N@35G|%LOzx@^p2OZ?DVdI8vq=rE#ojwJ*loG=7{U+9|#? zTw0mTIo~8@glM@~+6%m=`slai*L!*5?{je)yl>CPKml*2bZNZ2VFwqrNqV>g2>wZR z)J~nY^z}3lqU@IwS}Btpi+48Nq&SI7$}8~xx95Yf1R9VYitU4rNh67IIvv!m-vJY+ z-vP&ZzyXhDpk6W%Do4RS6qw;}e0<0se-L7nRy?cr^cQC?cs>x6UJ3)?^Py@69Ke)p zORittO=4nCUNmmtOpp3}$|CebzY!f%SYQo)+m<*s$GaSmy)rL_aI9HT+@ejGwnc`n zuhel1B(U z*}nznWtxU7)7r|kEg4l6Pj_jzsk*io4?9!!{Y6HdD=?4_W>-8}$i&RSk$pw7gzdLc zaG9W|gp%t@cNNk&mVVQMJ%WEA)Pnt5R0LN40ZxT^J`r%F)C>g(?-dvU)Q=1v839~m zf`xngA!e3z7sbT!EC}f~eJ6-^bfwfgUjhZ_SjhmH!TlovZ}K^g0UP{La|07#1VRqA z>4gkSn=s`Ym4<)yT4N#KYP%+W0jJkl%wY%jrNutCV2TH~EH!&!Fv9wgcl{-jhp$8A z%F+BPY9!UcH-&s)~K% z(`a}$q$}pCzJz<$$IbnspUw>VC5sXXB!}Ph-k2*sTN+znj`{^|Dvy}Td!2Y|khLLr z^KB>U0VXLxCd&UuqEcOW=U_njNt`q@j+y%cCx=V8TbCPJ(jlzQ-e_rjo3Lv<&MJbn zx~7t$2>@7Z*mWCDJ)z^E8|cn3%!bAd`2WY;I|Ydr1z478+qP}nwryv=v~AnAU)r{9 z+qTiKx_YK&CaNdqbN=qWidU9tDtHhRhg^W%6wC&`A9ckhS_Uv4~ixbAzP9VxMY ztemBXU(A(A+4^1iBxS5zdn&Z5^m~3|n;#+cG z9c*NpIibRmUjm8m%>m4G=Bbc(yjt5Pl^kpPNLmQimePmzQq5%H%#-~@UO&bqRyWhX z@EmM*LL7`dq7Flg-dc)U&FZ%FU*PQr$`?uOn-hjW&h4f~Jbb;~WGS5hXJOEanjl#T z#zn-$#Df^4yx=y&RxUJ-XPxPku&G>tc7OwVkx-mj6H z0D6C33KV&9*TV~trJ)8ymqK8fC60c%+j}^tH>xKPA($7t?}9W*+O>~Jke0lo`kV)W|| zkr6UtxTucPgiGJ&&iU@qn2HP5C%&@qh7&rSfxd4VJ`A?>xYWsXKd3o)M1?9xMB(JV zc6C_(WdPysc!T!NX$w<5t(ALNGZ`w-DG%< z>e$pBYhq=^xnP-!e@f`AiBd23NIv9!`zVoLE?KU)tOzPLb9Nw=th7l5MZhQrGrKjA zPK#l=mO|7IHi=^}%s@Voe|OmOW-M~i?ba@0Bk~6(Yl3JfYi+JZ#e=OS?Ls61Knlwa zyJ>0Vk+MM$rnw~#>vRgh7?+04eVTSfotCc>^qX^!Lbg2j6vM)tC)V(UqfN+8_(s>2 zjZKNHXD)yifAs;;Oc|Q za%N{Gs#LTLFQqo*<1Ywtd2%86k5D5AWgSL57ZoRWwbw?SLD*j#$?Z$Mu_~0t5p;g- z{6=!z1Xt5}fN*9p%z&B)v*%6u@&M8szZfUdijfFheP!s5V%qkzu0a>~O(1KNkPP*X zo-%4ao`bg5Ofjcis~4F2NK_sHL=ucxpvFV;f7*$wX3+PoQ+pIt$3@yoYQ3NGu7ee3 zgq8FVw2M}uHKQ#v$m{p!JOqN`dXyZLC)Os8e=94!uqICV={j4TBXuLAAK^UXE{`#2 zG!b=hqf3*L_VfTfOK421H9j7ZTviSy8&xeMHF_?`R2E`ZZkpGp0+RM9OXa9tKICSk zbX~Y2OfFj|*IVMby^n>37+12jsf@RkhZ*52M#68kGdIBOpz7^49jg2Lt% z0*XUov#MoMITEyaZgM6VY00y15=^Wza(jT@zfq=oN@GONg)WtS()ry5;}KK+vk82# z8AoOD1r(Inqz|_Mq7fs=w2_f}%TG`__CdF^7vgs(0v}hVPTA`q*0`DBgBOUC8}(cW z&=_|;OsspvQ@hP9)M=J4PM#87%zw|8fx#5cCiDi^Xa`50aBc)D7Om|o+P?hbtgQ?c z(Yxva!|Qo2rEq&2WEqg7-R&Et*=jpeiyx8@E_7u6?P_ghA_$Nw@u2c8a6s=Q%+rw! z=DN?vqt=*^e4O2wYix+WBII>9;YziES93&^%!pu(GBNFK5ae&Th7IqUEELWqtr_3m z`P%}0t*H^Jj8`cv(tmZqC-4!tvENzT-jYdUfp}zuN&IqI#3AuPu+)V_JbW+&7ncQ! z!$@{CG&`$Wk?3891yH^AESYp>g@CHH)yG*J0w-{8Nych<7Y-~;E7`@=Ld07w;*)(vb}=)yF-bETHKKT zYotVvD^Z&X(HE{=wvP3B>sBG}8Ymxqi8r%z2%CsnUgEu%+0HVMRV23S@o1*nU79L$l9lm5xfBP1YS}bX+T9Wak7y%91}B@rN-S?XXum zv|LXS+c~2QQK_LGaUQM919Hhsdi7)1(JHrdE3qhiiertR$stL1ttH&QIG-Ch0*W}N z4y()l5B~Gn0!;Vvrle2K17S|CloW5H-aru%>9Y@~G1ErT#Qp*p^h_)x)p9-iZ zTH4_L%zqO`CtMW*IDLYV4j+|dC5BrhidanNFb{}|?|83gooxpLzW`pM_;^M!7xDCk zy9mmLPEPKS?y4GddTB>%q<1}{`p0IJZJO}|(WiF$V<|mDQiRV(Ggd{1p%Y#CVtewe zSnTjUCY#%a;F2ANQNCR*uP}cH{~AUmv9xW$TVRvh9K@JEZW2R|ru8tFwh8-E!?!Il zp-O7L)813l{!W+c%n;6#B<+Aiz(^Svwsbq2)5%M-Uzo4f;`Xs6uP~JGC-1|Ay_cHlZ-$82QWF(I`br3reqT0mD32CXB zrgg960Kwb4d85UBpnF4&y)*Ky_hW6uJGWNem0m}EV`uPZTISIbu<@uX`nqBoFOaQz z;F@vQK(WiNq(UCSGyHHa03l5lCRe7{pqs-V@b()ibSK)D(&BQg(h5>3I+9sd!rY5~ zK{K;*5UNJu;R731IoLL~Dp``XN@)+gT*NO=p`zxeE{*aE=RQ6#B;Chgc8`c!Spbar zK&kQ-pApCaBrRNf>KljJAqaz*jeHPEy=mdz_@1O_7cv&i{e2omN7&Pwy|i%|Pw2kHqRIg`T}Ov)*PMAn_=d=eAB@SM_awpD=~J} zCnM@Ldj*#J%^1!|GF-6P4w}au|FAF3YboRZ@=S|{;45`;{TYwc^Lo~DH(79~=y)(@ zHT5Hg|a9pWJu+j=aJOvi(`fo%>?RqM^Sh;0e{{gr6wqBbEQNdO6^t}wPj`` zI1Zi=7~Z~TR6{R&u<@WO^vb-k=*;BjR1r1-uhw-q(|NF${f_RLGyp@#x3I|lWrFZ;0eimgFG`c8@Jz26(g|AR z?b}rizU>`4qrO9(UZNJ06r5cm4#Cj^DJyt&b!WkIuvcUW$F<%?1_lK~bA-*8tNf1f zx5bQx|FS_K3RfDjC~8Y#^A20WM5;^N$T*@IE80iNh{|2N0>VZU_Xiq0_ z^h9VaZ$B3cxETQWH_9EnbM7Yxqjc)q0~sMWO9Tc0qm-c?T(7L6H2OMb>{<{@5#0w` zG{F%e=_*XD`kIcTkWzs104DY|_c8n8#q`8HOZ*kiJVJh$}5lkKZe0QD64BX$~F zfwdI`OnXGjMz^%P6>gQ|nYOmZIAboZJ?&|QZyAliO;-UNIr2xJbgrNqX-{LNHP5aQ zPgDGA$yrU|m91b~*R;+mx*w%a^m#XoffvSbeK;=W_JVpk6nJ0nkwaJ6OvJWl7bGw> zvV9J02L^}iae-u=5-`alwq0HobB8_EUFjY5iJCSC9PUE>{&#_1ry2yh-i1+l$~SzP zYGqq1-M9G!uvkL6`3Ygio!3cZW`VEX%o`Xf$|_Tz08nuH?alX9YJ0Se~R zwPGfb6fhsQI!-taapYj*b}7fShxt-49*w2ZT8echuu*aM2{+CTf-pESX+4jF`EEYQ~Q}wU_1P}oIsu+MEqN{bRH@%@F zZ_E16HH1`?)vueB=#W_D1pYxlFlXX$eVm-9+_%Ji03Fuong8vZ{a-pw|0iAgPXhE` zbcy4CwT%9MOPBsrp7S4(gMW#D|LH#bf5l6z|1DnnpI`aEKcxQyFR?N)|JQLyL@cTQ z^9*vJ{qqd&Ez*FlHAzH%P=EoG*lvu=aA8NsP@>t@+uM$glO;qi$&r6vzWOR4m5iO6 zmC=71te1{WPI}GqzS=u__jdN^(6}9p;**n;FXtKA$)=tJ<{i$gmW@0Adc3yjMvaFJ z+xhslw)G+=VZ!Rru&o{&x#^}Hl;z3obfJlt-R?Uuac2f=BjvtiPQ;8#VXvcU>Mmv!G4V6QLbUHt=Llnbpr3xq7IS z6QLer=*`sA8?>ImiXgiMf;*wOh6Ah zojmBW&>it%VSC`lFt`_q*&wzp4Bm3)fO+j--yg_3lBUC zj#x3u=?|7K(0^*bUl?uq)65x|liU(!j?rG*L?_|El*W6daJFd+>r)WOzLdq_T*aao zF@=RU>@D^-+qGs!5+2p#-W2ii69UB9k+VTLv&`f#zMe~E2U|(x$2(G>~Scz5Y z*=3{wfL}H>DM=?{vp|SRruN<$j6$VF=Lr{}v{*)Ra8y-m(rzxe#v?|aL<|DI5OBhs zI%eFndIpI4Cur0bgIw2$PvE=%@R~w^`V2p=sB0| z7}UQ!IFH&fCYC&T5#TBe+%$jMoJS1PEg?Q&fVp6aVSBoIEDOwm%aCX$HUNxFP52-a zBpBo$d1=rSprF(L)47e7TT7Niy7Yh!vCq3i`=iF7GjNIp?)LU4=w{#)kPlg>Ic(Pa zZA*`yIA;J;nd(PxT3D8KYq&gnPmr+(rTn^|5n>_9kR=x|%(D`M*=6m6pGI+W*;F~R zY!0JzQ*?c3a_N2`)ggQcIJY50Z7yj6r$?)SD+hL18(4TvrC2jSS_W(zOb+hyW>q}R zw#aIIz_&36W=>)c$2pB%w$S|Oj$t-P?Qk; z{=L>-YdsW9zWJEf#HawvI77X`2bq^Of>e{vN5|^zpqBj&{%SeM2s&|Z(%QmDl#7&nDr!J z8PHcIcVFC~PZxOon%Y|~bF_hvCl~g&JdtSbMYXvCj|`Eb@cVSrF4oKCQE1`tEkom+ z1%x7WYKf5+H=dU>GrL2;>&%Z$uZ2cHWV5M+ zT;?AxXp%fpNeo{io8Tda4$ELSJZT0a&0}qktnbJx*m>essFhP;66gj4LKzhQU0s3( z{OL8?Ztntc)mlA>sSYS=4s9s^D?kCZ zhcT#-ul=XHJDxOQa@`Uo&JE#<7XN3yQGd`Mh@{N?GO>iER&Qmz(wn41g?BC&W=qw1 zaL1X|D=Hfv7s{i{fy(wlHmw~b4qnmj=8wcarC0%UgAAs+_AMAj8?qEh_?|1`4Hs9raih7r=hO=DYA^7g!v#8r%igiA?Qod6=9yk{%|lmU z6WXA8j(mH%o0uOk>r;ADTktHBfdC>uO7li7*9@=*v=yzCqgf@_d)bMbheoGVZpeEl zu!V`H%5Xe~six1T>2M6e2xIpmus3+g8k@7!i+Y{ay?YVCqM;FV=h}e>)0#dVhG`^w zF^PW2Nzr>Uqb%CZl<0`EG|uw7?PuVvj+HyaTLZ+Mu^M%g0%?&tG4?9fzigCDcETb8 zmP3VdKe#a8v`k_@c!v7^S=-OHATrLM7@Fx%z%3xNl=A1cFauoI6NY@HhvFgRx_VwW zfv1_F84%b5SwzIS26n!K$F<>bpWi51Uj@DwN~1#kC+69%nNeM&+67K6=xZKnDb&<{xHdooIR$EI^R+HvQ_}O)5S9*@;mlhIXIw9 zaR2i6Gw*L|VI-(u=aAzaxMi3OZug;S;s|VmD9DRJnBwNIQK>tJcgC4-?_p+T)^ui- zPK0?;q8Okbkfoh|>TO#kzYD5&Sf-S=yRb0@8_f2r;7G`&V}Y7- zrWv}h@cLfIxW97&u@t1Z1ts$fxHx&p0g#JzdWay+9;5zrz9jFHC~y7C$x#pRmZ0yS z9(e`~va@8F*=V5^hnUz*hg#N>KM?$(jESXEEKB?XXa`r%*Rxg3;~X7DSrcZj%(HfU zwkf(;kt$eF8-^01cb!s3*?9xD79`JJ=3bCcrKWjhAri=xv#!xS@OXV~E;Ndnenus$ z&A1b2f~j)em}_Vut}i*;Pn4Aw(UCZE)E1v94njRlbQhuLaaB0J);1-;<7zv%x+){+ zrvo=5upeMriq-be!9!WBnX^L;U5K ziqBvqZI)+m-{CFKE7wx(m_=Vt;b@L=dCSe;2abpR18Dl!(oA7AQ;E1`Q{=ta%7PS{ zqhIlwYso|)R3v=QBghEouo^O%26IFmvU~b%Zi?S>8VHtjj$*OvXPDYGHV-;>)=aB* z#V3|^k7Cxz90fkjJY5s*s~*)LX1GYV#6LnXr9R+YFm0x^l@J9>Aa|=0JocOWa)R2X z4rah0ndy;@(BR60{RlW4Q50Ki2K-3{9ZZzlR2MySL_%nqJ1xN?>mAJ-g5xPfZJFdEA+4?3(4;7g+*Eun2)_nnko{9}))^ zqF(~2!s=oOs=D;m6VrWsAc_YgnHO-^QRu`7noVJ~vI}NeJ7SjdWJ#3DSxm`s#-bV! zBDIV;TsOO;ZFFz;T6WegZ}7LO3!5#7V!snOr2d)qpsQ)wGIf@Ezj?;KBwqcV5xxGvQsMDh9P_2w`~K>Kb5Y-MMr`0w*VH1J?`1;``u*!!xjqr6C@;FSob;x%`LQk z(9dGq$;mdRkBgYopsDHC+UMGrWbJMLZ3dvh#mmY|VV}{=%NVM-F<+Z|rCn!ye@itUx zBl1AZcF?@~2znao7T^iKygpV5aqi_;@BHL%4n}D@aP%1LR<29kFaxrjk%zK9EVhnx&n~y0@p`F!mnPvm=8 zz0r?@bIM6dBV}&5caVRldbu#$!|&Om%cs=rtWlcOuIErO7dC^it!H~W<2bxln8aQ7 z*Eg~f4u2~l>mryp9LL4PgbY(j0ziMl-aYiOee^3cr!I!pl8^}fOWp)0M$n(E0Uq)Y z_3wh2@HAic{>tp>jN%=mM|)W;g!G63(E=6y#SohR%$j%3zV@1IaX(7{9hUV*pY2=x zQHlW>#Lu3a4oG*tqYduom6pM66qAzWGX#LL<|7DYki&Q#0O>@{URdh%_`b5!t%< z0p`b2_IlD=f_AW*RUjb@d9^`7Bs$jIb>_L}&6sx!v_ik}mXjpM*=`K%jM|ikO2!+2 zvXpg5i8io(dTRB6sj*N1EgalC#2}Gjb&ZGSUs{0EyaEmWxD;ez)(+HLNjBOREhF4f z>DN!TN`nC3&CiSYSzOD(t?-!Urpt9K1JV72`(SjoPLAVefiS$czoiGt@*fLO>Fd7W z>;V+Q$6|$qQTz#qXxc z=W39|*YQY+AKMC|p+ezuQzn1_25j~psVDzQ(_`DCaXF03la5_^%bK)IRCn$8Sfbg~?yr51H3wR8TLADY9X2gdAK(igppumm*!O|Sqh4TkTv5%R zLswx%08A-5u|U(m1ONf5+zHE3z}yg{%VhQtgKV)FQVt$2sg(~F=x|B7|W z2Ata7y8fGLpm7m@egvbzXOOTA?@bO+Vw{>aT5wxn21GCcDy0hmgSg}_jTH}l;5ji2 zWWZvq zPF?t0&CUT@e{k@ID|-FKnNc6k2pgyf+<-ROG>0-Z0efJz1QXylQ-47*03aUzt(QQV z+L<^xJDM8W!v1^u575KN@&8FA!^p(M$xQ#B{D+Z%nS+h#{}wP>QnS=R5y$ea>WCCh z4|n$$Im->eqgO+`CmQ;UCUPHw4#DIUImSQ~jK+x|!4e%Vk2pY#HjfrN^bv)1*bbh? z($zI43d*lmT=mh>WgW*N*oC3_v^i+}ZT)=jxTyN*=(zZ<+-dCxz)%1HomV{U2#rm5 zzJ_U5iPt<({@Zdpg3|~@17u+uIXkgr2lsA-GRK7$za!a#Sz)scjSj$%y?4V*OiE69 zIv-vjuxI4?Yw1|Xx^MPKyX~K^^Wa6M9Q1i-#vkt&|CN8VH1!^2Wp`C@!7eZ=PI-Q> z#&XGzhVe~0XX#hz(lc$Ip$|_$9(K^oVr^yQD)X%MunK&TNb6&FvW0^Em*7uo(Qayj z1+>JO#N(_s)LU!_QQ|*a%a%#Rm0Xnqw$w3FBFENNdsr0rsnC8QR#y+dZ%vuv);I#E z+~OR5c~u&`HGYG}YDKno-%hXKqsq-BI<7wP5C9ywF9@86$|0cr{xl9OObqXDJBFx} zdCZ}oKI|W=se#cu5WGQtlYO4d<3^yCXulz{Mh&)<>B9(~jM<~cH=Eo6We+U*Q#Q20 z(z_sUe*U3@cMS-wPH3Hdu_`Mtcz2~zeE}!;0?@AcM6+!y+dW|YrVxBR5$?A}QEwLN zBVrGfV{b&Ld$OGYBbxqAjYw090mte-DHLPWoqnt8eyy&k*E+aM#X#7#F|VwfY z7Q9Kdx)2*uygNv5p1iS-2E#98X8|1k{?~`WFNoH=#xD~3!@PPX-$?CuXJ0UGhbU48 zYW@lSV}~yQ{^^rC7`B)O6aeDVzR2DNo76$G22C3<>66d5QyHSXKc~tXRMLZx*(Q^k zbXn6%Z*~(d7%ER96ktXOp#}hChD?9$i53inu_iSA*n(pWh}SjH#zz`0UZ}7k_Gu|n z0u#@rGVHS(cqZ2PNOd#Hjwq#5h)km#1zFH_MU&m7Tf7VMeldR^{9yt4EB>nbxrAd? z$f=N5F7ilAniu67q@AE3RE>J@a}3{viBL_Z8wV$y=Fxa{5sJSp8`I zuyqP<@Ab%EH;QE^&oFPjUFYNHAm!+cnw9I#88PpotE=hh*=etW4K|~^-u#v-Z4jU& zK~ba7PKg?j4i2}d4_5I1)%@-7S3<3 zUjDE^+#sM;YqZSJs#n_x7s?_-A&NPSj*1?ZrD!5R*~kRKQmjiy%BHXoOwIQae4Z60 z?0^h0>Q>qP#zvWd46zOtaz}=WIMECiQmZ57NK&|4ui!Oy$wUTK`yKM=ig>-i=?dX8 z9;~nJ*3dqr$XnjY`LLF1+YGzOoh6RC&|B(k=9I@O)jFPb zS!lg}y>h!=e!+$%UT@uWi??n4)AXkyZB5!g(}qb2x-&~xZL79TSy9=cYM_?z!!zSJ z^SFWMX1$-dAz&9{jZ!H=9U$$Iu9%Q+Rp&xyl-pB>zAc1X>Q(V3)r=Ich^K)Eif4ki zh9&w)asT{MbFY2=G5l0>7HgI}mmka9Qk%1$F&NWxv9UE^hrO6(2s`q(+jUS$IAI@n zu!YPoY!Ids>tC(A@(;_EAKpsnv}`kUD0OI6?>RmBXJ!9SRd;mZ?Jw2aZ`LlVk-CvN zU&b4IB8MkNePhb>zPKh$UY{6(+wJjtm;dvz`MQ+R!aYx%yg8;f`0lt{J;;G!N_yX! zN(5XnazQbQ;ZQv=7AISUBME&K)6_F}rUVFT{xW zS!CQi?B_hR_#C$QJoqR=?BQ8}usdWKLV)}|lUxz~Og{e1Lf(KmM)a;R#gD(?8xZjw zX=o8n?Oq&41iv8)-JZ3`0a4+ddGrWrbU&)dA=AhKy*uhE1DyIjE!iHoj3{bp0akt6 zxdP?Tp%=*k9g;n`WLp^KztOORTZs`L)1(H>#(RW{H}aD`@Tp<>v7ufD_}Tc7yo4An z8v8Fi#sF^=2K27}tvfEKVSW=QMopOnJtDCd1m$6tb6O+O?7FekhLpOmeFO?T;vw9}&g~6-Ak(#k2T+tqi_?)53hDLW`uqe@n zDvD&n1KIVMCq@;T)a_v;Ofs+VR#W+Ul^3K*lRt_ipW(+$I@R&?5h+YUD3fW!RO{+5 z$hYCkj{X|N;E~M-y7##_Aj~aA#IF5;SoLynXqU$bibP?Nu#V|#6k$=aj>T^w#~r>? zfm(+EKe>-QvtGHk1=#0_H(18bVadS~>{4E}6cAvB@rBNAZmSRpNMg)Jk}+WC7=R?#b%(aPhU0NLv zSmz_wCET@1cwKs)4`AoR*d?t`2|W*@1VTN@zFiUQ(tbMw+NS<6N48Ait&ea!MJSI_foj(J1nlKI-=_1 zs;-1Jk7FQpWoIlo1&uvWIc3n!2}I{ex~0@FOu8i$i!)&#l_;`| zDAEon@~dZD+xc0}QAlTO+a;e*@py~Ko>H@CoZb2APoYTX-CEN(gG~&i4*sxZYk48K zLA+%LOWACqgqWPj<|j1SDUD8WGvn#I2!BhOn`0wb)n$@3O$(>38b_*Gv8TwKc4Qd+()p)Vn5SjfnWto!C#Tq)rgWHR zr`VyVgc`?a*rcbF8mDO3si&kGXKYx?rYxOCvRM<%8rO|lGOo?S)-7E#w#{lG**~Xz zo%(QCC~sK(vWQRWIIUtc4o`wPEs3T$oyK(-(5Fpd=PR|Oq^&XkRxLjv)0RCqC$g3Rkv@eFd?u}W84&D;=)vOXz=xJkj8=$@R=|u_1f7~somz;UTELxJ1g@4(u2zVy zR=}=S1iz9`zj6RwJ(#8u=ey?rvlb4x796k^Ew~ogzZOZj7Ft_}``Zuqh0cA?>S{>g zdWe11-?|oM2r+7ZHR!e$hsfcdz%l01&c}Wvww0dc*qH3lbg;6Oy6NDxLtflx?jdmRHWg26pnyPVv(VnrR(m^88Mz%nO`piyLKBx47oD&x9Gig6n5IUqc*4 zn2!wSb#L+{U%}Q3<#GKiMXitA`xW%WAALo^&rGOw_NRIu=@45Y)cV`E!F zag{_Ml}MNrGHFvF;uzAx29?+eWMxp@+ANnMyJgr8QTB5-+tR;HAuTH>9~CW2Y@RB= z^M{`QG+dC^1>MbQFUz?b;_{asyjAGW(Ry=ESQX#S@o;m58~Wgv0^d@|Pq{nu;GWvI zm%2XXyO$K-ilb-nTv~n$^&d*RXV$Vy>K|&O=OQ`zNiJP-3nmCKAb<=sfn2!f@sODD zpyvsgmTZfCB%C5WDy^ykVxg}2YRO8vy#4d;5i`JYepu&Y5G z`tX+E*M&JvS*}Yp8=9Tg$s?B0dW#k@s~X)k!Y*yTRm4x&-*RE+5=SC>M`FH5LVQPp zfVwqRE+sX~s2yVJ-*K;2fU%WagQp3doTp@G6AD<{=kBd(vXV&{R0QzCYh;~-jD<%f zuceeb)04%f@o3`WLJ~m1!LYaD0HY3C2%uK?G%J~N8;XFV>eIM(-Eos-r^o_t%2Jj8w8m!FkAbb(# z4}Tu-uZ&%jan14TkgG#j8{e-+uM~3!8$IYc`(_#!ZQ^r`%{AaJ%_cR_XAj-(9KOi- z0-19sdQ22^XE-+p*cxT41^W@P+PaIK3zIw1f0tHoaz%hHnMm@sMzr6$rfL^~(pHDm`%(syT76Ht#+HDp#3T90!z z@{7R9>WBF$LP08OSeIfr){zm`C z_{m*JvhtVc>;-+X@DGN)Y5K?pjSyRzxbp_al>m7&&TE2@}ENGi`zohD}`M@^9K zH8`CWTU@hF)dKY#odWM(0N>o>nYr`iLOP+jUiN5rlDpN`3fiH zDQ+rE@b>}KZ_=HBJ7zdaAyt>~`RIkPCR^Gg++&r!)7=KyU4GotgzhqDyURw4_XT-& zN8y-4wg9vO9brdrlcClMMgun21+#IXJHwB&-c%&d-|jQ0y<3&5yK3;T{ezO z_i4XwXFuDG_HMg}WKkFRflGibKgQoq>OdawQO67SXGwj7JpDp#uW}aXcMa6XwT^mh z*59iKfab-rkE8)Rec!7OxTx=#4tSRr`cq_>_};coyR_f!-AAF@tW&C4p}D>99WTIh zy7dsC1{`x@YvT&5X_GbS*`e@CMEVf*tlrwb1nBbWja?XpspdI8D0(--Co6{YT{kah z$xi;_Px9|d@wD8ST0ZZY=o9pl^b?aw54Phj@PaghFI>YeX6@LlAaOob?6q{ub8M9} zy-wM+xYvB`TAu~3;^2=i5t zo$UK=_R;U;vKz0+#hu{8aN7LRkv7cCuP0ZGfLcA_noqNZ>JkvgHtS3ZSoAZDlrkBS z>V3SMaoQc?D&&xEU%64*Tu}K8f6;!y`flyFUW}!Rx~f|8Y{zd;#%t=fvKCVj2L=Vz zSB;-%f74fInOp|Xc};|e+tEq%^i#}yuZP!2x%L)&oP;u`vQ6DLtMxktKf5^eMJ5NzT(hr^ zvR$9MsQ^1GqeYzg#$mFIHU?neDl~JGftZs3^4daR;amNQ{T!5kuSBL+F*<8yJ10e9 zX+Hz3m{Bz!ec&jXftSDtSeY1v5%Ez&;74Eq7-Q8I1Q?HAt)+4K*bAwSDADpuN%L5` zVlCRZ2Wwa1Zp=mX)e~4Zp^7yGR-mLI7ED;@Fzq^#G<P^aDD6RX?`V*LW#j@iQf#>T@aP~7+LFT_5 zA7<|`tf9{WeHGd%gkOLdZ^HLFK?x2X2KWsG5onsBj>1Rhk3$sQbBq82NxT*nELW8G zmPRTG0%|_qsbkP!COTRSHlW!6CG_1}7y^gJqeRFovr&ihASM$WLuW)5Qf3S`EG!Y5 zwDQ1Lvt-kE?jgIu4;dZv787*mb_p&psgE1f8;u05*44-(Ophw~i!cWTrB`SGT8v-k zA@BM2!kspXu1yU3%yr!|S;s^0pqK zwB*By)=MRj&1hzxnx-0_D~E6iV58tnuBNG>p5$@a0H+NYdbXYp602KU4jfDPm*vpn zUq5$U`O7@2A9K>!1D+)bxAnqR3>!SqM@qzsm~>TCQ_}(H@`fU#yvK4U|SHD zys9I3fszTT7Gfj*GZ=FS51SMTbl7gO<00HYSxq$paxGikMU^eYN_{7-8kT69b zz6S<3AYuU04h+39f3sM~TYcD7h?;Jmhwllw?-S~Uf=4ct$(GZmVc zXy>F#Ze}MhbARe#4DyX<`sV~9X2+*ho1J_w0Zt?s2H}SnCs?>F1NH<T zC8?8(myx*MNdEA>^nC8|lqr{ViYIb$JZ9Ri^!I!nSvRvoZM!FXR}yr z8DEsa?FGqcQZyZ%TafPe*tMgCSPhy&PPW`JQ>HXl?zosr&oW=`8x|Hpo0QU25k0)L5ZGP4g z8iI>&C7vP~od(kPqAN8sctpLpI2c6v0Loy;iH{xQsI+kS?8Mg_MS<^U-_}#m(E-f5 z?ih8{i7UvJh*2(@cM#R%oD>|#1=z7n>Ca=ePHLtpXrxRw)#vBIeTvi>h8EEZ|I9_ybp}aMxkfE zEO=`EowatIehw%X`Mq{1($6i|wM5g$=v+M%TiluSLq>e-SGMCk{_0Jdwe(d>`g&xp zo%5+v0-HvicRpuOQ|mw5#*dbY-K!QMk6WSjnt52upD^2EQ^Pw#FeznIxky2bzl3{; z)z=nhs*sy@;47zVwH^D=A>~z0dz{MIt6p)Nv3)09FIU(xzd<~wPFH?M#)&@1uPP6% zlsyu;=J4JJ`Ek|E*S~2!#0La*BUi$g4C0?Nb-j;2zM~a~oSRz-t4E4!MsQbluilU* z>v@r~S1Zv>?&`2Fx}zLIjyPpKjxc>*)!o2+0PfLiooCxDzE2on9ye&TPO~V(NDS5`MMRD5ZE_QeGOfNa66!3R+@3Zv_ zBOtNAN78!ex%&M&W`ZlJIG5%JoO{BK=?kZoKV-MFwMhIVc+5@-egS^Th~DfIYG%Fm zcoX$jK;Flu(H_{|XNFFxqeg!i$oFnHoW8lr0qDW&KFm0nW;6;WI1c?->P~--j+jn) zBTjvf-Ql!xIq=j5$@$1LydWdn6JLtA0GJ}Q=Ual^{oOOih%9(Af?9Bz=6Pk%h{HO} zKCHR*Ss#c%ka8By1?O67U&P|!@ZO`m5o`cYv$-bCg5H8BWQR;WTZZNXPUp&gpz1zt z69sQWf=)Vla*w+2=&A5QYcY%td2FRtbbG;{_Bg|L0oZpKoPQ?%2;RgKTCji1Zoa&7 zI32z{^!%YuI8ZoZKwoPSB=G}JhwXuz=uUD>9lsT{#zuD~L0Oy(eFuSqR=Z+0*N_5p#ZMS$Z zb|pUkE>nMjJvo`+sC&&v0D~pI19{iN@mDwm6JGn%F%&9 zddgvZ|C1eXOP}b3K9NFN3!6vXTD6uVws}%4)rG&72c7fU3CO>D9Q~+}*i!GtvLGtQ z{g$iHY&R=u5Vp+Ymg(t@41vu z6TRq4AeaYPM_aqdJHo~W9)3-K)0xR3*d_NBn;z$~UH*lWxw-Ar0XjwOO2I$6SFeTB zu6EWljadcDbq#d$NOzV}77d>(2&1)sm}5Uu{|d0u4svOD?r7j=1498p->WaIz z+Y8;jzJsHUM#0}x=jXSWjgH1E&qwYHgxw_6D;2#7=EVd2P6>C2Lm|H0Gr@(+;$8-G zK&|2n)?Q2L1o!A|^*`8qry$L`b!{`9m9}l$wry7WP209@Rob>~R@!!@S;?R z`};e-UjIJmj_8i)vp43^m}ABpF~>ch`?~6F1=Q=iG~bZ_+-S`KXIxUFGj#v1yTO%K72B{k&y=8Q*B{m4HcvN41{3P@D;C+%gUdjQLuULwPHgOo>O_*cTCGxb8c2gL;@a&9_N%M1PCrY#xE3m{8ljejLrd1CXIJb=jhQ6 zAU^A|-L#DCiHA{+q}CY_t0jjh4Y*wPXoTJOe9_JaUMVdkPT%ghM5(*=^BU&IZr3B6 zq<31Ky<+S0dy=$LcRM}uU>m&AKV>ssv9Hl4n7({dc4WDqsGnpzt<%8VPbS^k3E{(1NjhnBW^ z61?Q(yHRHe=9~5)$fs@+Ac;#P3E32+gbDajD2TH`2dS~&mnl4Q*#bojlJ1NHA2u+; z>AL6IaYuW#mpC*8bIgsYM7cfA*R2cTXv*X;qjJ69!sVIAg!)o~E{A9)@ z(a1C5;_u|hJM4rR&4Av7iZa`;)~6=U{vOE8yva4rwH3swL9J-cxhQwf_RQ-)h{ zGin}NoGMiGq$_q-3#m9AtJodw`|(c-l36pA9e$*V4Ps|s3Cc%t`Me?)cHx#+hF1VS zftW|wts8l{?|fWHSs9@;giaJ1djKU!aUyL$rbR-0U3i(GBzqmQ>%kQ=JeMeaz1nS$ zVqcJms7UcL^4Yv1 z=O*ylok-k6s8%=JnTdr8W>N!a+jViiV zOD@b$Wu53gR>M{+8TE&H`f1FcZm$n)@3uP%KFy-JJ2nViHLuep$6IzdS1LPpk)J`o zTje4-SQckq&_DG)4|zt+-nds%c}i2wG#uZB?q7-$TJU1-r?$MZ5Pm(gy^-6%H(WCy zT2cLCzk1_j!CR@%=Yd@LU8Y6;hrc$L3kjvY2Iq?JU3nzMYPA~@QtnV&-d6k`_a~l4 z+zK;*wp(u{`Jk)6P3za=+{4T3g-DwCqg|z8$r?GH&*y>@MSJKykF;ad%g2=|fq435 z$Iit$?n23q!r8)855fsq< ztNyh9Q|7#$ajZmldF`rM?hJ>lf>udfeoo^xyNj(#{+d2t*d=*_m+Bo)Bb+C}pUiig ztudx;yJYDe3NEH@!w(x5ksmzzmqx#8i+^#<{a)ai;!jKWF}E6_mgRz=_PO@>C9{^M zi?VgiVd~KcDw+%VXN^OSnGEl^(zhl1$9%~#l|nlEyZrbIWNo&YZ)Pa_`*vD_G6U>>oU2 zcI>Z2D+R?n3^n_h&+lii>5t{1mp|*1o-|~(FBeyxpZGW)yk(cq_4QLBt8CR(s2+A` zty2GF#5GT0TRFQq=5T9WezddUy#I{v_1aSYd}q$()9Z4--5-v^VECW?c%!aQ{&PuO zS;?)lR3?&A&8!v+P3^LZG$o^&X{#o;_^w<+^GN$-O@>Oq-Et)_swz$j4~Ee!x}q@;G43TPlDMOR;&-`7yPJ1#@kVpoMrYDw3D zfevbLjY&ppFb&uciPIz_IdKuT#X=n^&edBz((WETWUKyunUk8ZIldF_&#TPv#HRMo_vt#?DJC66skI=-@|a}xZZRUa z%Zm_s+tNs zHJZ5&BGf3c8lZlpvIr%Tt6$MulkqH>Yi@T|krqE1f+owosfw7hfR=kR;v_ZM^5~;= z94Z74YXmA-#NkvQn;{C)SYE4*SOOxvO4PA5;FoyG*;82t$aWt#|741BDczL0?Spgb8vAeoC5d{XvO;;` zo8(Kt9p4epL~Al%vK^(qN>9N>iMQ06*qYQ!U_;Oy7k~$V1E2xG09XJhM=TSi$qJMN zD*OfBk}v*u`~Z?8zKPCcH%dE|?Sd{TkD!KtJ8l3U0Pl!xLVKbzS%cDwl0ikUpi9yt z;Eor-cEmN&m~2L=ppsL-E2Sl-CFK!#$9cpw0XyT0{qn+rS^+ts zXx#l^5fXXJEi0OL44>wqUYWoo4he^(WdWa#j%AB^KQ*JH$?EV`V*JTW`gFDMPGVhY zG8awiw^@jdpZXXFE#smF*0(>@AV)n!0S%k|0a^VXL6<`9prBfy>hN`yklvhqSI015 zcOdHe`WA$>i`r_IQ?8#f-aa71J=+0i+v@UmR;~$Z-%YBi8;Hf!eG}A9ct~akH4sm( zenZ}+t?h}5FAN;HrttA5{2+VSv*l}_sL@Sy(A--`bCqT1+UYTu3X>0+_g30(TMn7L zXcDbNX)sv^(u_DzK&He{M=par^Vi-T9dfV%7XaD;-2vVK@$$FxuSKDA0@DZX3}6WO z-Y35cV*q)>Rfo3c2KI*a2K9!t4q6k6 z7nm2G7orLXA3POw5_#%9P{KbfAk4qG519~{_Zu$|EyNY%73dW>I`9(^OaOSFw*kIA zhdqWpg*_xAMmp$E2wO;7P*dPmkX9fLP!3=Y5DuUf&=uenkQE>eP?ms7f0_Uqe?K5U zNO}carxF7Qdn`u0bO>4S5}>32nED`YHiEJQ3wCa?sE z1W*`ghM-Cw)J>-!v;I!6(hmpR(f8-{5Kf^OXClf5pxXr^;e6*Z%LA}eQcp`MVSil^ z0Bdm&h|=XZ#6pdF5K9Oi!=u$7zsaJFgJHaq>NudhuHiz?Um4E?6f5p&x^rfp5^@Rf zU+T#=B1ei7=nE3i2sGzrBO%lmTMKf~T&l~AL#CkDpbbNcLdNPi++%ugpc!sKW^Q2> zZo%SizaiWLJvkt_IY72Lz;HN#t~kJHIDn@)Kw&rlvz3i_WDRO%^;={ONM-do;HoGu z6>c+)B~R*^>J4XV^@h| zN$KTI;m9fezM&{E5_7GhP@lFvv!EUwB^`2(tuKFc_X^xnH+otF zOl`}$m`ZpRR2EnjoV)X*Zc`$CQ%e?Wwmm0pPF1pmF>@{YCLRe=EP6}Do{Ow{n5SH> zO2aPqLOpIlgW+u}D-dnY5p%YpR=eul!>LC+*NRBgekh!tPbEElqRZ^+Y^2=fM7?gC5s2 zHvc^I@cA>X>=N#cW4L-{rx`cA-(nV1Ja&-qqg}MHmxhFwWm|qQ$|X#HUrJH|cAVXp z-sGF4a_YA|IK2Zjy?yYU1FW2VDEtE?{C(i71N^Ifc-sSX+kJ?d1MHf8X!-+W`h5^f z#hBlmVU3(Y3!EWIoB;=%q5t-I<$(xUi*vCXlCT@_foO&`YX&V*icq~V?hQ&;^*PcG zD%18G(GCdH_VNAD3E+XTg~5g3;w-i^RWO%@E)(R?AH09)CW)Rhfep^eiu|Gvv#;!{#Ye~ISw&6 zJi$oEf@kZ22=oNsx3-Lu=0Ol%GaXJXA}26suZe3i+&KB3zXnxLH6F(TOgVkFLs?D? zWlnK0ieTZK@!Ycx^UZ$fT?FRQxSn>O5M5&!e&7;#37cE_y~7<8jIwEkSzCw$u*>0p zCaIRjzX`nOjg0$fsh7}gmn0Or$889YAMfIi+x$!{kug;E8{ z5G2h57uDk5ZCEp4Yvlz)Bn4YG^)}x44r)@-r=hnfoiH?#VGB`Qe=D+j`F8D5oDvP+ zz#I3JMv1eazJ%5mMolDJ!*XWKDzXeqK|iX(H3Q_;r^_i!*%I)8|FK0 zF)LCJhXKP80{(N7a>mp4|bfm9CC>S$s?C7lMsqNwR zb8gkrHC>4LQak^Sih8qSV&aZP?q4xHxPE^ze>MDFmlO4Sr*Z1-ioLTF>DU71T`$>HaVuAZU7De@nBGiFAP{L?E22`jQytm>o0E#lZ{5sXj( zR4b&g@}`)O+?mF;w;Hdd-=l?sve#mSL+{QmuKf6r!V$MeTGR`Yu%PZjZWcgM!$#g- zzo7iya#?Hu*23;QIqeC8p-QR*m=Mh0kePCz+msCMlfq3BEpu3IKTZ);?9LIITW`YZ zIev#tcsjK~Y$JaWTZ*@#DQ-@>1)2;z%bMIW7B2q71-)&h??K{+*`NG$kk-}ZI0@PG zHsl4&Vtl=<5z#urmTJxw958m!i56?i>}SqvkHTN|hNvzc1&k#kBHd`UD*&5RVg^!< zW1H9J3jRR6V|}+lT_avG1dXDs8WUlHX^9k~%n|s9c80@82+xwtI9v~NlC&s0n926b z5;M$=W+5*B^z`}A%ev;sJl5MuzFzRkx6|-}|*tsZKlM7~NwGZfdP-kh030TAo&hH$I)9(+4WpC8o6am?M zPM4oD_Xs+>LAeIuxK!U~lJp#w61d$CjJXUeo|L|nv&dm?7}K#E6JjsnJfdOc4ExU8 zrx35e$Z+b=EkyEy24`|^mC0PtZ_PGo?k=u}zp?f@bbaN!4*R_Izc@-DyEYQVWP4YR$O9QP8*@q~jpqHr}q*|8!lBgRva4mvQN ztCmjhl!cj&evj}4FDI1!$Iku<9SPomT#~yXJj$=tJYqDRymce@paD1qgDJBQ z#@k;6@l@uuI$a_HR?ZSE4AIs=OAA7RpP@BD*z|e~>KtUOsxdrhP82y=&rbW_7k-^K zd_dJ69~~v>zaJKG(#eU1N#2d2$qjePfT4{PvKyHLZ4sSz_HsCruOF$D;T5{HP;*p7 z#Tcob?!RECV3pwu@+cMbF>QIX+w!zY!`<|T@2d(QsR(M_AiZ7vGD}mlsC*vAr4?+B zDF))G!=uXB?OC^=R1dW{fdhE%t$9s@_#-WV*z0Q(y7f8Xn9L>`pxE`~cO=r?DmY>} zN?oTHPExm&zj0OLje>4;@WKzRm>`9329J#L!~yk8I*&wPvevsO9^Z3HH$ zjd!YdqLOXx?Gj$qMe{Cw@+m){Y}m!fBj9i_KI`C?2}uL6e`i}`9)s3xmdV(4aRwd9 zB5{yI`613L@}yL8fMx$Eh-(0C)X!tOFIwTrF5d+Yv8SyX|JY4(*#zwVGs6_vUayQd zcrG|R1(J9a+5mb|Y5@eiHj2|)Ew?#(z9XrZ+$#P1ygh4RDPJJl0Jl|Bz7hpCnxV;T zH<{eUZd)3Spg&T-^IE1=~PMls3~`*gyQKP zuf)YQ1S5c>@iylgtl+FENs-!eV!pP`(ljGg+6I^MZGXBHYg7o<94DdCH7+yb0HBG5 z{0j3@rLvvpl8LNyZ`a;Ia76h+*EYdP!aqBQfwre#Ry^+v(6Lskh0(k+jJhpIeMnea zZ2%#4I%@*%lcYZZxa|*o;HyJXsolCS4o%uNi2JPi{6Z7T>gj18d|?~Tayc7b z_83cO@ucWPGFA!pG)q|#7ne!3ig^L|8H9^K-bUTeitV6F#P9I|2`Vr8(ZBs(I@p2o zXgaI$v{<5x>K}N0}~kwS`gSPNWxo$-)! zwjRU|3kAlM#1s~b7#iTvsH%qWRL zKc54h(JGfEnncM3P$2;s^HN6CCw3<03p-gIeS7YaByBR~Dz6!=iS_9DQd%(g326{p zFjZ)on%d;GLywWj#R5THsuS62G(Ky6i>Fa3$Jv$Zl~_lr{aO*S`+<$r6K7}Nt(Q73 zK-N?5db3~ZVqVbPIJo;P(5BP7O_TXIED}(_`2dwzSwdWOv=a!NJ~D!e$1Oqj5~#Bv zq7ET<7(G`Dq$!#>XP6855V=0jZj&i^&HFR##N>WwTW4VfsuocTK2Dw&4 z?yEr#3#F=rct5=F_>!S>4l$cHW|FAcmn5LkVDeyNPX$!nu@^&P*jmN$I1xm`oFwti zsA`x>ofEjXq$IRL3_WYYt#u-zk2Nk4i4?<13nN}JG*&(ooH{SJ@gr$eH2wE=C_&0+e+o(U2!*)KDGYl|7Lq-HpL5@e;#YbDD|eV1NBxFPTwO zKcWx~0Cacw;*#($rCx*A$ivGv&$#K^FFm@OH?fBfVlR^-b46W{GF>#8=+0Tu2}^#( z)18}RhiYRUI}eoZsD(%wUvUl`b8uh>HlZ)AA}9F`w3M&-i!7Cd8c#2+2_E=?C1H-S ziR)T6vK|`Lb;HRe9+3;jj1n5#Muj%dur@zU$a0muuleZ27&|y*cCu@0kL}%O3pG1g zJT{x5ZUK|d+y+~38B{}yE9&#llWt#D5n}`=MxZXyVMqS04shX3gJ4&Z>wtQ&s#H(o zxA>kB)43M*sA-!B)^U6vsuBhl(U}uxgI-HjpLZi361;ruxD$*NSR67C7B|8i&W!KY zVVxt61Y+l_6f_G#vPC@+v0$?)szaMtr4ly@@bb2JA}3gKMrYZ^3{@5B-s-5OzIR3ZHdv`6u-1+xyQvO4eF*((E89XR+tDowVNA<0q6x zQ3@Cw+|q6DR!5DB1Vpy8Do|xL#9O_+i}m^h$NJ<}#1%VxZ>qVb{9&679h(N|S4bEj`T0Mr5^KD|H z;@r}EMJkF+!Ftq`vi_rMw_n*?&A?uuC6S(jyR~b>zo%KQ%c}ZCtp6~bqD@mwHp!au z71fo;3#&Hxpi{M2E9~ivwlUFXK!W}p?FuY%@FV7tRe7L&6hd|9T-p^OEItRo@chU zZ@OQ+ur~!uY2Z9t=t9JSiqxyF3=6)Mnm90^SMOKKjWMB=+3z^3cRhtWKlvlp5%%9MtcH`U+U+%5nMqcYIeyAK))9<#~hn)LA z-%EA`PQ4}smL+V|XMEQG_?s=T_?tDzbSEq^aug%sAcN-2wKuSIXwcB~6&9uwG5>ND zDT<;ztP+ehV}>;W7SOq(s*Jz8JwdSmY5|Y4ZFx8-2~}}hX-DxiO)!#^6x`tBqH=(+ zO^OhcS08f}CqkFvc(g6kYb&`b0dRN#Bd0_pFWEShE#yrZsIx+V(Ksb-l?U_!4op=g^=C;>2sEr z#dd~OIOZnKIIvgEyexBRQ-vJEZx&C|n~WO@7e`LOZq|}w>qq$-S7UO9zF~?p zdzxWw9C5KCY1!p42G=&Vv@KNS|7i=r|lr=QfSgv($tVUn>8KA^C?6_+!;YNOw|k%I96?u8|RjsSbm z4>aGAGC1-RQTT`pu_1_1lw@TMj2b58QO?Tmi#YM-*l7Q^$MwJ1eEu)P4)%Y+CJ6s` zB-qX`5-jY{tQxa$+s*eN^!iV1!eE--iM^>!b>*SUCYS)QHxW?hSwV1clRmAihw0aE zj6fAr7;1mJvcK4M{wqlMCrg##ze%uM|3O;)zePU#|A=SkKi2mT5-c0%Kb=VbHdNsF z+fd=3H~GH>!Lo32aQ-#A8%o=7+~7nGy?#e+BdsJ2=R_neEh(Xn%^oD3b|;y-Qnygn zX(C&Tw3#!OIXB7m?ZhF3QX(cJ%NY*=K!XH`6Y#(Bx!=}h&D>3ry}lgx-#Xle#H7f| zPF$GzUG9|j+q=}ic?tl6{EjA12fu&V4?>V&bI#QMit^@SK)*=Sk5$lXC|qE9=ETQ? zd!y5%O?x?da2^;5QgT3`S?5!kN=b#1Y1)^@L@#CHh%k}La_DZk{b9kM!QXVv?amdw zdPEY1>D8r+3nyL}4#l2{2jFF?BIrlBPZq1w8W4Ar0EUssjVt;U5xKWAHxwhv%RWMI zBEcJ}%-&&gLufEZVd|bK#bbus1^=p52mQ*N9`;U}9_*7JMCC2sPY2&uHWmmjUso7s zs6ROGLlmSX)JSjwgVQg7)P*3%2>S|A;*I@o*@|KN8s0(xX=iAPyct3acjXYLqstWv zjIvq^9!vkbt`@`|#aAns=^~m&ya|(@w8#bSE$+a1iv=mo>MK`h0e@)Iq>t+4u8E9N zWkFL>O=0GZC1YVqvl8-AL!GS2do13C5@nEg$4(-1h%9Mq3lAQxVrUEhp`KIdVaBkN z(+51!%;@fJ`dqu)WIOD@D1PiMD$xyc-8~ehSvpREYdZSOhTy{J@$FJ1P6>8qmt~g^ zhj@(Lu+c`Qj=4rPgZdbO9R^!zSDQ!SsrvYwBXos}9I^|Pz0S=Xp5He_K)hwzVffVw zwzZ}s=W6qE$3LH=CZ10^qhdvsPU%zx=J%7holsU zHiIs>5a_oRv|j>m{yXOBdmm9W>&^j%N4z|afEBF%qOSX8IIoaxD*NSPufkFORHSDo zwM)T;OLmqftR4HFx@FIvIaL$sh3nuCA&9%7H?<`ni z8IximleRlU(bMm`p(pDMu-k=X7aMF9BdSRzlTfsQFnfJ|)0YpRb|^O5RE-!0CrtXmkIN|i z-S2bgFmn{2+G->{YWf}>T@GwHcp!MMF04atrD-&+Qqkzxx+`G%YXm~+LQGF8V7k(! zbNvK{L8NT{d(7^>1O&1tm36e&Y1B*?XzNR^!jhA)CA$yI`Xb!1bK z%h0K82c&7Jx_%B?pDVEk>b}5@*r~%f^8L9j87)hs1qA9%DTHM}@zf%_DHoScp+@ds zW6r?gv67b@qULA8rP2EnYkp4!)>rE=g~L$~oV`I5aOla_1`S(`llve?=cPTuxc5F! zZvV6u;#sdKXm+^I@nxu?ZM>TESV<|wjKtJFt1#De^1(=zhUg=ql@Alq#LeL-BVmSz zi<7{KXj=1^!!RSvyUfeavreIxPniA4ne0Ikpo;9aT>{ZsGbZSx8uqKsT;N41hB5Ds zy4&PANxG0AxZPA9q41gV2qMjyj_;rs|dp22+2+^AUlk4!=Q=+Jyx|p#xA^2Q6 zYr#!H!8-Z~$iak5(`6E8QnF?e;3@wm=`|Z|HiDm$Ln?@KpT}dPLgk^CK>a`-3#xy3 zL1FJI2$wXeJY+QsM9k>lEnzByAW#zt5Nql3rA3dnXD**@{xH_5s#8N6fE=WPc!80s zI0dY??(hcsVVam@GlA5UoOkEX;H%83V6kgSA+52;(Kf9P3E6|ICQIZKY-(M#3_9j)EAWqC6Zk`nI3XoY0k*WDXs3a__e zZ`!=UKOuJlno-giEQ07l?gq_4#yK5U=G9RhWPZCRx)9L>ccEU8F~+&)dZu4KS)ntK zkK+}Q!wP)-0yBS4R)nGJc#n(s)xNO8#Qfu_jOjUdCJT)4Fw8go) zXS9kg5SQkzgAa0y3#C#XdWYzynEf-$qszUsy34C+Zs>Z1&di@r>pRNn_VIksD&mma z`^?4MYW5jkPwr-Z#AW_!=uYhle#FDmcf%W!@g~;G_0oATkvF3)%IX;{#on^gix_RF z*LofjfktE?MosFyCB;+Y zF<3}iA_8$fhhIV>Q;`SUfcTbRD9k2||#&Gcip z%^)v;|L6f(!QN?3|H1dDV}4)~;G=|Z1{qkisRI-LGBbe}Zoy@QN|$@;vs(xs94F1D zu$w!50oV$`qsjxw7}^GYKAsR4@$0fvqBSRp|0qL;6X|H_7iv`_v|O?eg&M&+9f0xz zWum+|a?%0tMTn+lY}^mov+3)k6QNd|6I^|2sXeqV;S#WHWkcid-PjfvMAnoQv_2qy zizLPDCfJwaIRUyR+Yy2MK2xt7Va07yKgWsPq3}nC+i8?U_`XrgWrBzWW_-doHHGbSM zp0L|w9qUW4#vF*~U*mx{QPD0a@FWSOYG0-rr9Z(&KuK~M0w=Ec0#!lw3IuXId>Wrv zTfyjWdMN!98g5KR4ILx{mO#P2iY&N=9c2)kC&84cRR3WCZ^%-)Cb7i~0+|#h=+HR2O zs=Aq-hNtlPO;LeVmXq+`K60NOPvvmJ2Tt^Fk!1`#@E?C#<1{NTsYi%n#Aqr(BO^1j z22{L1btA;-KW8L5BlQirj~uig`-YtbNeqv_wjZlNvRZ!Yuz$gX5fWK|!fw=;#Npz= zj(+?MLb7#-#oU7FO9y#LN|-Kz%c`1X3JsAPR>>$sP91e6*EYB&kCPX5)7*DRaj`ITYD^lF5eU= zPQj2pS(5u1ei_~Xq!4ef{o*mq#w;tyROLR^Gz$goQ7*MxI#VZ{rqdYnizF}R14ynm z*I)Uqn{(uy49Z?^+pn@ewVT{n79y`ctI|M%SbQidYLO;h9-_vGn;uF9{urW6aRBDW zDSVrZO^SBq&>vH~=Q)7gGVN-~W#<)g3UDEwn`ENVu8%Vcgt467)tZlu`8CBZt5SAR zn!4JUX+t#!b`IJj#u>-e!*hG+6_(p8k3s_yc*YK(Hq zWx#^yJE?PX%e8w|a-H+`^&`%6SEwWBL0a9fn+SLYqJ4!$EZ31Q0go~_H!BOou?Zr# z%Y%1OZy0XolUq7pntLe%U!qka4fTYQD(b=fE~IYnK~n8m$?|;s7UIv_@?=p;Op8rKh?C^d(i+UoJ}H*$~&pu%t=l@yXPI9KafBBCa$j zD5rv??1{1xB#;;Ah`*WnA%sU6)e&&P2NQ&K>r2>%Jst(e?!JGnR@CE%X}=yz-Hm;{ zXe1oqIz|xOMli)oyO3oWPAKsLy}Pe{eccLMRG^#@(jPDh739i#qVOhJmX%OyJrbr* zUP1arA)hSk_8Q%`mve*v`SgR|E+Y57mk*!dmht=$DS4VLjykYIl55C$q{;6{i6+t< zUnCz^eJs+qKR7!BhCDc;NW2L<1Omu}Jeb9c#UU8H5=bH%I5wL)c0v^1AN0dqW*5lE z(xPaPIA(|$O|d!^s>sHV-w8!-zMTKpS9kDO@y@mL(2Mc9;P)dNJ@R_3<)F`}z z;@Bs(B6tcZq+U)Efn0HvqI4y$zA#WK5F=_hezkZ|XU`+gkGXm-pS>WkK)D35<$squ zaQ($V^FK&L{xSUVZ`?Tlorw4!v_jAseX@lk*1S<;5d3OUsp0FO{mMc>z)j-){mcV_ zIingm&$DOH41i)?vFQGGZT}G8{3{9guT12hvBF<7k$*r$zosqzKRjH)`S&@<|A>yt z!p6b+*BoRq&Bkei9eH%;Rb`jV!&nGR(BGtH1&2pL?nhCEW_OPwFP3&9;JbCAv5c)x z?uRP|7?iR?YSOl@o@NOlVrSpg$IZ^(_@$#mhC=E^7(r-h=~1qAm5GtEsQ_fQ-|=AM z((qk=X8qpvi=(So(vGj;AQGJS*l`rGKK(@C`}4tR)bYj15zjLx9v++z3dJOVzdciK z&7G5)Mssdm*0{zfqmEQv7i!SCqFstq#(Cr*b}!fT#o4uU>+I%5Ylz&9*66}k85d6C zA2CP=?}WSQDq$eO2~|{|=72cIKVpzi{zN8)X8p}qn? z#=!;Ib*_V57tXOjHO}~cVqDUGR@`vT^nNo`-2O-M-9cHBcF9pK6!T-ObhQTkp)OPc3MYAS%Yb5` zdQtNsCfkfeqxOV_*9!wawM(~>BHAu+L5E#*6cr|q8oDeFD%vECOIXF!s0N!;f11PdqbL8DC#!<5KXUlBRPgmW*sE`cVcOt`cxoCH9fQAuBM?C1 zQ>M?{(#U!G*IPz3PH@2;E}_NN%U~B0J(8;m)i+70FzD@~z|~_A$#w2liKkoVH>fWsI$B||v4A$D6F5pRzqY|1Nu(h})F&@KI#s76W^mdKj%i^=qYvyFK2 zUTJI3kZ*9*Fnxd^)Ohagr-MjZ9nsqXnXQey)7R`nM&2LDH(8qGC);y9)yooyHasGa z#Evg^bYUxjFxFZtS4xN3)Ae(JSug}WCHB$q@!2}uhTuHyz4a6vvp*03Q#;SXP4{fMe8 z4qNx?c8~2_GK$_U=gVh;xPY}f0QD_WIt+m|JdBqY)zCGi4~=A&$U)iHid#L{_2HBZ z_*7n7l2BZa&Y{-Wgz-87!jq;Khp)1pU|ZU*RbPHkq|8|!$D{O+4O>S>WFQQq5LV&( zF82pUdmzB2-gMKrVhLIIqOH-=?pyl9sx}FNU#B2<)!n+O(PXX^rxa}d$ret3y_=W|CZ?B1 z3!Z-hk3R}d5*tU(|1waA1qh{QU ztBJu>Xek@wemoQRVClZWNyfP>51~ky7FJnkSeHK)dKY92B?ItUW&mFdVq*6uN$r2C zT`=TBZ|*G;Fu+r~3AmPG?STRxPeqjkvY0-pF96G?jR$U5DS)WVmqD3TvwZ-4F}T65 z&tI^_Loln!w83j`C0t@fXFxZ7#fdEQ-gg&NF2BEZ+$z$OAhn@;4aK(a_E9x*9{Xu@v*@|m- z9_p5aG*FdEt>oUsG#h;P(ZDyuR#Ftk3_gT%0+EgFHLg=;9WEh)BM2iKcT25C2)7G3 zp%3ub7)!*HM7K$lurCHhazX}So4z#sE%{2f?dt%Hcna~yVqe&E3(AwUTc^cvY_y}z zLFmlIc9sh_K(DZ|BM$5^(k8NM3`|G-nc5u`Kyuyk2hT%#cW803AGR4<@;a;s8D}in z>Hq`JRm-OOa64o61nnwdvz2cXs~k@vU0ji6Vq*u_*;m})oH>PrWIP4$Bd!PIb9rb+DQ?Tlu1`VA^FI0X}qb0jrF?-SC} zPDQez{3>Y_qm+rN`#SKCZ&*cY!qtW#UrnS5LL!9Xlrh~;cc5DNK`=Chz(5IGW0d4` zPr=-glJxV{R(P@B8Us*>cNqOp1<$CQbJ7R9RM8C;Rz5K9NrO!wgY}}=e)gbtw9O1ignNnx+m5 zZ_!SQkeyfM3VCcbTXf8Qa8jjIbLj0OzLQX#=A8v&ysnC;*aB0u-paB1zk)j%|N7I` z0kd!tqJeSU=c#w-L_pI_-Z1T#IN!9g#7Stq-J(XF(+_>&BC^?hh2Zmn8Z5lz1$?;K zNn#TLK3%C`*sCSeM&acfI{uXpF5p++7-9hdtC!=KCXzQjYWj=_b&wdiNUSqXq*A7Lp^dDS=NEwXa!_qpUS?)I%>{6qYD9(o)DPh=~=VeJ_|Ui02J1rz(Qd z^kv!_ZC8G%E1EN&bc&;-D&Nr3p=dvG_jHf2mNG18^#*S= zRr+8-o@;c6Bt^EN+MF61(%7N=h~(K&+SK0}-c4T_zpwqRosG9TSF{oIwrEOzpzN9` zbR+q!%zif?2)&ATto=T5GW#cn8GeE}w5F_FFT7A?zO#a>Whoj)kPRn5tXqam(!qeP zQkGYmjPPBoIuYElRb_LiAFM)$6bJ0?vunG*Fxbc0?e+Z+Ux$YHg(70piu{j7V*i-g z+ZNC=>_|=r;KPgXD-44n`dpSFc_xSbJR8KU6$*G6r`qwlkvR>0iFkrG_8Jdl_y(Y4 z(C9{$R&lQWgO9)(B^9Ih?uIlhHZMYxB>2>ITx>{n3j@i>lx}h&kE(sadc%VTG-<^s z9&Qw?7oH1Q!b|oomUNwFdqg@gfm!SrdaXi4?OSx2B|RMaond@n;w5?ih=*SkWH*$B z-oSU?u#{0`GSWppn{-0Qo)w*k&kV2<#S}#wmWdvYnh+Ee$#)9cg>HX&08e_$2rFGa z;bey@%9$Al>ua<@&d^e0 zgYS_uwXCU|)W5rAiC8zcS#6zp7`A#zOiQ=z9ijUwF`%rhVDY~Nj8koHBidp|tE87h zb(3xK8Mw{&?now`){rM{jF;w8kK)U8G$C}oq~#r-NXxBmzA-lO~r-C$;`qVOYA`CB$|x7`WpUKfI9Ld zVASV8+wteveS?r{%3!M?aaQb7RkOpel4_quc+I)&zS_fM6#xk2TNij}yqX|QabG>? z3N4uR$9BZ1fA5iteLUmXQtB=2^-(Ya|LqvmhneJ-qn+Pe@u{p&&nG{qi26aJl`m?^ zcS~c@AqoQ34-x?$mGPyu(1FTK&_?`y-Ht%v!fSy%qbk{G@ngi*di1#SICy}5^3*W( zNLEByjGS5}umf$@Xc(T5TBV*%sV@P|jGAW}Dtpv%K(#)6cK$b+aWoLiK6nWR(*X3E zSC2F#rae6Ck1wP+{U>fsxiJ;=QLld*k*I}|B>~ZblSO_)vuU`B79?7S<%v?3XJFBi zwa0VV)>_!lei;yPvX|9;v{4!UJrj$39b0c$#3E^K_pT-}X8f{40I>y^FliG9boIpi zJ4o*U!s7uN?SZqW|IEXbTsB%AxoEQ7ZR@v-SfR!P)AyZIfasMKguc_UP$(lH1bXHf zEV3z5ETO(F8~UQegFce*23s&eJEAk{E6*oF`>=CKj7U+VnevQLn?K4;&35{m8tKdQews#FmjQBcvI zfXLBsF|b&x=eJo9p8WSo2nMQ6(rc>wcB^;`zQr@lDn-k=8b(<4R*N4`md8uJdnP?f zd&2rm;4Ca4jm65sPt*hFx7vi|R_@i4gFhd%*ZF6x#^(n&v>cW4l8TXEcxTi^9p$wT z?shN1v5vMj2(hZ?Jy)`C2%PtYnDdk6Acre%+&LNFE`;c8g)|;_3V^^zlkDws6N3h4 z+MN17m&=-FREL>39AJ$0XReazhyzt*8sL_}>$vqZcNPoL+BZ^!QTdF8P;7csU|LLV zxbk_TMEf-Y#v*U5EoejZ606Stl1bgaYac+%$b` zk+ibsF*aXC$Xz@MT}c}%ps}m^l&6e00lH@!;{h52T#fKq&mT~XKywhnlz)46{`dWt z|FiR!>EEC0|J!*hCbs-f=k3rxoVO)J)SZi`Yh)3?+~$2>V-6=_km>%7PLFQ<8eg4> z9s`WBzg^nDZ=C%9)tvwIqj3GDT=F;B%zthaJ2NNSfB8{3SUEZVS3gRshpL(c@DZ<25LL8KS5t+!Zz(wwRJ`AtjhtA>p^Sz0kQbrV&Krf_^x(ez=&jwkRI) zm`o;PX6DTBPX089ekO9pcR>~>NAlmF_gt-ZYA!47?=Eid(;+~Lpr`TSCunP&MGEF0 z4rni(o(0AzMv3?Po5lX-aEnn<+3}W=S~4#WD?n=MBV;kMZTm}hAiFF0_E$X2zK)yO z?@H=~CiN4oG1y2fZ`)aqJpLu86L$R4;G3~vQ)<2ecRmtiMe>ff7XpeeY5u{>w-NPB zWhl>lb?e_>E>Vog$+RHbs>AG0Yk%Pw8zYNH*Dg`1znuLj8I+2kDyL#Vt`$*)d39I5 z3ooi3@oCn@23lMsl4DN*hE#PJb2hIqrLFajIu1txWNNW+NkS$SF3Jjs(AXiUaOpMQ zNDlhXt3r&{H83I_BvVDghO0d?weRHTzFS$20>Kp!2c(qJPQk=#vv~>bE{*xIzl1$~ z+A43RP63K8NLQk!8Zy+sJ1z4biaV%nqOP!wnPd>F9JzA|$tNcrnq-idhFcYgUd3-5 z$~!2u_)A&11*25tQCH-1XDvx}_Q7f_2IELpcc5?X1+15WDlCfeEw21*DE~zr1Sne# z)Jl&*V1jgFLLe~Y=N|#_4h(rl#d?N~y5T}UfbotX(~PW*j{s=aB#^VF$}RrsHWWh? zMvM@W9QvyIQCEqhHzb7FJY&DC28d7&8R8CuzxhW!BcP52xzZn2 zpD5%8ka2&7P5F+ zye)o~AWMj)r)8*Rwzs4AS|2B$Mn0K7=X_IqpITwntj<sfuRf!0v#OzS-B zBI_By82=gmH~h;RzHC$;U$_lY7F_5KhUz(h) zlq{5M<$2RQ2VbQG^~=1nbm+BpL+`3byizhuPuC0d6Z%E{vi?{vHDI_YC5?>`Bg|;6 zlngauj3gt~$S|^ADRKW@Nw_7_GRu=RZ2LgN3KV%@e&nEMENh}rFJ>xb?kjv-s{41Z{R%SkHuJOZ%_VP znO-dfu0OK(*P?+3o9wX{R~7z-y;xF`3zXC;$t-cXUQJEXmF?kbe%1FnKdq5d<9v-vF1{{7m6KhXyCl0@aw#>tnmx^J%^l5s z%*lW`!#-wSSZSFv%!^citTMxV`}NB0mie~%uIoiNXScJJF)~r@Kf@SluPwJSep<1= zkKLboI0GK89`1JEqhh81=e(1OHi}lVjp9*==BX$ZCL_eJa<{lCTZ$sZvdyaQXNY`x zSgWhZQY@#*-r_g8PUgs5VN+bvRZUqcsM$bqvxy>SQ^lP2ub4AaakO6rcLrjR%tMso zj-5Vv&kfzX7euJjDt#i9T(>aJ{aAQiRnH)?X z@FZ?Rvp9riyXCnR(}y&N=5kY-$D#BQ&F5y^oF~)A+=A1%Ay46{JdLN*0{VpCq=ob; zhfxN7#w}?Px1!IvH7({g^aZ!2FS#8p;r6tYJJ46$k>8?B?nKMDGcD&Xw1VHJmE4t9 zaW~51cW5(~w(5_Y~P~2UDg&4%07)j8C zc!;|q!ClISeSXEWS|+P+c>{0cP0j)5Aa8aKIfr?RbCkC_ZoG}R^A6tW9OGTQ+d08| zcrWkc{mx0poewyt`5+(S!;S|Z;iHZxyYVr{i;wdOKFO!polmoe9O?vd7>9ENNAfw2;%JWHSdQcK9M1`y$QSt%U*;=( zm6JG`Q#h5=IGr;%le0J*d$AAuaR3K#2#0Y5N8yHJIF1uIiBoXLX?VaBUhsy`n@xn{ zjCBEL;R`?bBLIO2LNG!QiZFyD0+Ii}M!}nRq~jc-5RDkbA`a&fj|3#*0xp{AW`>z* zW|`S$j+txbnfYdcS!foS#b$|FYL=NV&2qECd}UUeuPt9I&8ayo9&#v5MOaXfx1?628Ju_=^A$==zNa79k>3go$twAtGJZiYO6n6s+(mSg2OIbKc_w?&<}BkqcO;=Xtw9*Rfe zv3Me$if7`vcp+YjSK_tpq9dGgr^2bEjkJc=(okAY!)Q2-piwl27EoJiNA0Ntb)-(z znYvI{>PFqE2lb?0)SLQHU+PEwX#fqRK{S|#&`26hvuO^^rFpc3#?m;NKoe;aO{OU{ zmENIu={g#a*AzX+jf1s zf!)wqAu2T->QXb_~0Toga6;lb7QZ?031(i`HRZ%V7pqo@j zcjzwNrdxE6?wcf&PcihsB%1kHgOU*x9x5;xPAxw*c=E%a4xsgt;sPG(nqjipXurBm6_Y1~?;v(_2RI+LNZ*ywC- zqpx#YouhNPozCO-I-fh}0`8~_xsxv9&bpYpa93Tz-E=8;*Ja#8mvc{D!M$`P_tsV1 zM^|%SUBmr!E%(9yYq*wg@J+tOx4Dk*@Lj&g_xS-o)9hGPUq;w_BAXpF&FjKg?Lz(h>KWK6+Syp4D8F5biY z_y8Z`BYccc@F_mS=a_~sFdZ{66SFWIb1)b4FdqxB5R0%FORyBn@FkXG1-`;ce2qWg zkN6Y*jKAQo_#3{#-|-Ku!fJer@9psSPq;ZIlISlPpx5Ws%w{i`6z+ zqPELYwL_Mvow8i*k`-#VtW8ZFN-EDK~jX z9g}y}ad}UjkoVO|`9PhL50$%oq)yAn%0oU;p7N>klFyX4e6D=t3w1`mRA=QYB&)b9t^>sMy~Lf(!Vp>OBnLY=_??#{He+pR?zM^Ae(TtArBL9V*@3-5rAs z*4=~AwGGDDU?WC%3P?+%Sf7jc>b)k$_v$+#$Aor|G_ndp) zz2IJSFS(c9D<<3&nFv#CBF%miWe%8VbI`ZEoHd!|oLOVeo3-YG$ubwsI&;aaHA3qd1OZIDw-%o?|(Y%ejn^3}ytEGKv-h z8NgC3?c47w@#XmPe8tS>dhXyl=CC3wum-EMrZ3;O+qZ`Yd4PLa$o<^M4s6eMZ0jrZ z75Mh@20!N&Ugk-j;&pz;e(cLW?9HCOBHupWE`GQn$k$B>l3Q|!jd5gUz-coN_z;me_7#LVO5K_fk5%415hk&2Gl>t8n{1otu z7ZO-5uzX+{Z@HB%)vVKAsJGe+ww46MSi#l`Yo!%pg<7ku)mE4lZbev;0f|Y^> zu~wWFZzWiXR+5!$rC6y}nw4&4cwt_+7vV*EQC_qcy5*L@&up_ENl5 zFU?E$GQ3Qymo$>b(nOkiS?(RTl6zYQ$zT~GLuHr@mk}~jM#*RyBV%QpjF$;AQ6|Y` znIcnVnoO4&GE-*BY?&i-WuDBJ1+q{U$zoX|OJ$iXmta{TDmJ~^qG)b2X$&@v+R+-qWkehPLIw7~MlUAuj38;3kn+!-h@F}xCjE_LphX(2R|yHA}XP>Re-lp1#eqLRx#c| zRa8TD)Id$tLT%JRUDQK;G(bbVi}&z88lf?opea7UhiHc8Xn~ga2(8c>ZO|6&&>kJo z5uMN(UCcO{6CdLfe2QM^jXvm$e&~+@7>Gd_j3F3`VHl1P7>jWjj|rHFNtlc& zn2Kqbjv1JVS(uGEn2ULsj|EtWMOcg_Sc+v>j$o|7N`xR3tFRhj2uB1W5rt^PAQo|m zM*36=_IE1~Rb*YmtR@SdR_Zh)vjxE!c`}*p3~@Mhzwt@24|zQ38!(!+3aj_wmRFK?amG-8z*tfU*2EUU(H|LU&CM1U&~+HU&mk9 zU(a9P%e79GC0=M7ZL96Hy>`%!+DSWW7wxLuw7d4up8BzVqMvFn?X7*ZulCdaIzR{N zARVkjbf^x~;W|P`>L?wpV|1*J)A2e%C+Z}ftW$KVPSfc+LucwNovm|puFli>x-LD7qpdQi^ zJ*-Fcs2;Pc+12eDc1^pMUE8i>*R|`}_3Z|BL;GF(J^OvTk=@vCVmGxvus^h$+0E@1 zc1!ysyOrJAZezE#+u7~y4t7WTANH#}ys0XSpO=>eC~bbHB}gG4iJ?*~P!`3CfVFLa zQVONEkd`fl6iPs36=ahgCM}4-Ac`O&paVF(j_h`vf&+s%Ac%mdBdY@fGL=PV7-&2% z-Pk_;KIi-9FHhdR_uRAGbM86k-j~-2oso<#NI_Smq8qv+4Ly*KXOMwR^h7VXk%esZ zMj!M=KlFzOIT(O}@FEw3Fc^6l0w0EA7@oy&Jckh&iBTAhF&K;IF%IML0w!Q0CSfwB z;6+TuG)%_~%)~6r#vIJW@8Cy13NR1z@e&r`Wh}%ZEXMD#1g~HzUd3xzhS#wiEAR$Z z;!UhFpErMNzJRyP7cC!~FIf&)4w}ES9JU;_9JhRBIbk_%`NneIa?x_x^1bDnunz070q^3E*a!`U_!Hj4CTzwQY{fQg#}2%YBK#R2;4dh~ zPVB;m_$zi}5B`R|_z3&(G5(JIIDk)Z5Qp$74&yT%!BHH;=Qxfp@Fl*&Kkzk9;3Q7r zG|u2GzQI3n4(IVLF5n_A;X7Q$zfgkjaRpa#4cGAle#F1=AKbuyaTB*tiU1QUY+#j* zY+^H4CAskKmC!ibwMp9?Q@3I3CY0@C2U7lXx;u;TL%-Pvhx4gJ<$Ao^5Lx zVYb$>9<)YU>ssqs4_QC8+N_7I^{t;-8(5E6k6IgAk6XV8KN@~4yfi#e@AG=+lsA>t z$~t9@vfeb0LWoS6#1un8FxC;g9MqyRJWvu?uJ;x6()21^v%?RQ}^d4;}r*ELm zG)q=2lCJ_CvZy=Fq?OWV2ql}6DU5zQ<%{kT!x+Of!+dor zjiE`jg1)9}w18eJ-4!VL6%oB17SK}J#Y~znIn7qvt7)pIGPl13TP0pQB{t(FD{Cbm zD`F#)|Iw5C!w9K`@ltWq#m*?%xtj(_b&ZuVPukc@U)m@gXs%@aEu|Tq6?y_q zq2Y={X+e8vt`bQTM1HwY7t;=QP1EG0ye{W!veeKfIqN%VA#IVdKxQlD1ieD< zN)4=)TAC`pR@u#_*3u8w`(O+#QW;cfw^*kK4h8lH?e4G8DxF`6ACc4irJUz9xhsBy z-{AiUhH5v@m`)gyO)x2ei^g@;jHSJm>%v$gCw`Ii6XiGf{<`Wd^}K0g;Ie5)X@V)- z)Tnf1=_GlV9i!v)3GJpcbXY2TADvTMsJqou>J_6$J}mc}R?%i-a~elW?q1J0+?Z$V zVO(v@Ha0gkmU(@7SEW-gc~^PleK}O_H8L%z*3!7p)YIfuuc$XnOGRy%+puMotcrI+BKz1a_3GRlM)l2j(@5{`*u&ZeIoAhHjh2px>d^-vCW$` zjd`R=bmNDk8aW?w*c(3BpuVkM-N-r*)V9`&2xnNrLTiT9s9w!%GO7lJnktc6WTM;U z)9NOAv`}Y~lkHk)x02LYjUsFgXSls}Y*w>!Da{n4QLPlsn&vK~__!?19CNowH%*P= zD-Kakwb@;oF-jh1mmIGao$hux`KYaupCvk4y+pUeVbcsz@+1pOo-R3duf}OYci74( z$r`1(^|mc=I!=fb=g5*i-K{-XG0n=lpOf3=W-7kR&2EaH3q$KBCTV0Xq|nnE)z-yI z;^f~%J1sg!9AUwNt5K|`Sg&YGElsJNDlXq?LRUK(cb^!zygsMPJ47Pt^;E^Qq%5Ki zyWj3lcZatYH0X4Swzr47ux3c2Gch+r2oxkxVMtA()YJ*G#E3#Aw1W~v456;Jg$Ak~ zCgF?F-F4~Br^V-c1aT%w+=NrBinA?HTyQ&!M6Cj=Rfd&0p_vo4YGqE?hiLIR8s*yy zn-=>EwlNLx#DsdC-kd&eP0bO3LQ`n zdZc$I=}EtX?#;{f=w+gKoJqo~k?5Y|D7I-4GP<Ff}WDt%CrTmG1(hTPJDijU7I?aVtFXN9-r>AFGt^|%V+oJ=LWZ05Zs#7tKH>G(wnX*)lZqCo$YqzIbBr- zW#fXVQFrS)99rENUD5A%=`QAY#rrasg;C|dekN=&iujYL#b*Qu$_VD*C&sHaHj3*C zpLy)8->lyz!(%q%HIH3v;|H;+u@^tqm~Ak|PPM}$>|%)4gdj>E;65U!ZDMz!Rn)2q zB~_)URE=nV42ZTSRTJ8fMyYBkRhlSmL{+tjAQeJWkwPf2{mz})WwB`RYQMero^$V= zbH01dxwCj;IMHjlB6q@OUdEbuuVwk2bH`NM8<_Oen?{Ov^akvTpwE96x-%<6(M_?w zUR`)^%RR9*muvLP=tnnZ#ZnFCq#7>k4oA^pGdko%K{E3bA8`^`&YnD`y7R@ZGd22* z)8874n2~rY6)}xSDxOMArzduphR;l$C@Mx&yA{Uyj>I;7i?!d7R)p%L8xbxw)2I-- z?Q7$dg+Y(RkQ1XKe#H-RWf+_5oHdFS=o=e$$wFkh!NWCh$dzIfzn}Bz&~%t~U|>%S z#hkH0JM@zjZV2KWy~>N>`dOqT{Y!j}$kctpl%|GGgr%wB&9T!yB4cVKc2bt)o_M!)q87T? zX@e*%q%uoc$OD516qT^sNjXo<*H4FuCWKZM0x=$)mPp9*GBSxqr)5Xx%gAKNR7VyT zGUu(LuOs zj$C88*X&04BRLFC^;FzM`lA5HNS7=QVGb8LU`(gesGu{bynb5?Yz0tY^MjUwC|-|w z{fs!o{r1FYg6-fiRk)T9j9PYHc0)`Vu=BA)zPkfvM8q1t1+Zuo!J05d7h)vGgk@Vn zerRlrH!uvm53Ms-+ghEor>Bk|)L^B`%qDT2XaTz*Fv(3mY~w;GBszw8nB|b-bxsJCa?v)z!LP0tLFOBqxIkiwo@hdcLc?qF-2#4e|9_mSema zO}Z2CY@b~Si|TTybQgdfs0P>;d?#TlPyG#VGc-+`%!9~5Hd^fDh1xC-3?xu+ob`o} zm~ENG*eg$nc#pG=)XSb1i5J?D>GV&`aevNdMaabM9OJ+w9q*7~vQo1J_7)V*yxG!X zAx@?8itoL^;VZ8=+p^H;8$+y;fjb2>YIOsNLv3X(E5P>4_%HH|XM~ngh#IMdqTxVg zL(9@-Aqp)lTv^`|Dy*+B47I4Ot7uuUvAL|gyrib4ex*WPGffTncAa}?rm3_{s%~$< z$2a4fDQ))6H2Kc`Ze^p?+R`aM)Ty+#)R_;K$X>H`)vD&EMRH{Y1|>>mWp$<5DwX<6 z8S+C~)$-cvI(^>SPNT7Qetx{;n>~@y&IR*oJ3>ZXrMK)^>EN!)Cc-mj_(EN@*h zzoEI^jBcu^ZCdoTMfD4tBg^VK*F;wMLraz}(DuIY!tL|w(f>ZCUc2!#IEZY=U#yQ_ zeQeV!Pi>#q@e%2H;-A^+3rE&)`+6k#<*O~1qE#Q(T){9Om4H_O-lMnMiK;hWZMk)< z>O&#T`RIe5US<(fUv;3~Buza}jLdKCRA@DGfiq;dHO@OfJ3$#>-1GxP<| zeqc2nP!G|#Xs^?_a-H_6FH^Utfi|muqAKrps#3p0mFlxpranxMis#G$+6P1owr4ij zKF<&}2^;SR*q;mb#WN-RX%;>mp~Zj%gn?F|!j+GEiiFR_8K1KlqGI(1^!r^7_TS;+ zKG??_tBX0J|*Qn zG^kvqE_F9}X{TY?Pad_2y5tabX)n^SdI0Fc{Lj&V8mFW(NW;oLdQ|>71=PnO%cF9w zi54ntyBpA+vS5)tjLfbaCvayd7AnKC!dTh4DB#{V9qiCceA0sOM`L; z&f$3qFt74k)GD~01DN|<@EPPnoQDfF3Hv)?TY^@q+tRny&w{%v7$2wIYBl8A?q7;MlHlT}ou zyoY$>=ga}xA_r{GYzX^KQZL(hU$FhTfG_H0;ZLjZDM96OB7FgE3$P4OTzOLcf$&+% z_?*RS$j`mdv;VnZ4n7%ihBoeSl>u`$8-l|On9DhUxy}W1o(twb17R<7*b5)8(5ICM zwaGuFZOR7PCVxQNq<>OWzD8@60a`2lmNrPIs8X3vVd*q-={?$@3Abn7^DYZ>UL8PMGXZlM1XFb&=7 z4A}ED1N{!jZ=jvRID-ClwByn-ssT>}z;X1S&{Xsm_CwJ5Lr5p5ZrG!oa z!AvX9(0L%+A|AE_ciP8kgZdynAwU#UPoc(S-Y?bzXQ^4-&3q3%>AX9L`UiGYe9fDwp*#6p7#C=!%_A|V1oyden?Ad&b{ z2(h7r0;#|_I@l_;BO)Nhu~kt=nL3mTYKxW(mWU(Dv{jpqLGJCdd(TY~$JReO{?R-0 z%)7gH&wlUw{+6>`?&9X1l%p~HB3Wbi6^*5#X;?ESA$I%S;O5S?O z#&dI+{17yRt$zmgA?{h3Hsgn?{Eo>F@K-au6`Ji0;2z$ZJWd>|@^9zcQQGQXrF_=u zT=LIyPda!*xhK|tze()R(+b8`>{YPl`*pgHP%~Tcg|Bmm4<}p9E{zOg)Tck&zHQsK z30bjG$$hRJzDeWirv2`7jPVL(I=^f0=i!u3b$$r{SV7MS;x7wn<#>(Hpu@OG!kI@&&cpY%t3zfzS>FGYGU(r(u{ts0FXtWs7u zAI%!1`eSXX%xmZ>JD%%wfzv+|RO4U9Sy;_}RVv%+2eU(Gc~A1+PRcCfVG-XKG2R`d z?LO9F(Lhc+bz$v^t)_~-u)1fZtJ61_rP&t$ZyIdA#CE5jSI{`U-2Yu{pWZ~$V>N*C zSuXxZ8Ya>s7dZVDjUhOXH7g>GmT~dij-i#lhcJdR&Q31Y?stJZ@3Xvp*hXd9{SS61 z$2+3ieA;$A3@<6movG-owVfv$W9@nBraq=nQ(XLhgT5B%qF@(of$6Wo!41?OP5tA# zEqH)*gSy&2iC?qK%^K_-(_|ORqF9_he@;h4V+d+FCofR{_Edk2VYbO3ZO5}snViXk zk>eKv9TVY5y5SRaNj8%~T;#s^yEWouP(GwF%YB&rvUD z<8V4>Cr3-2jccRZ$053wvf<9gxntGIhx|jtq8wd_9ZJ~%_ABm{Xkoi?_^Hno=f5gq z+r{2yUEyuPzvbv^G{P%h62wxG(mBF7Xbj zuXmWS6yuMMvIm7)=)FsOalW5~@1`DXjcfyqfkwC=*0_3cd_^9|Ljw$6mx$4Ku91pDzoIB zlulDdJe=TXtG7J^{uE8{W@?lf!JZR;T+Arcvq6m}n_sD$f1}nBx2AmC@w)fX7PQb> zw9ief?*Z&&=nNakx1gQ=3LoN2o~EoDch1^R;cfoEC9><#S*}{>ccuT@=FU=k?I!*k<0Par(bmXL=(voAw*LVT>(PJ#4*L zv%Vi&#{YI6E78XnX@OU+!q|G&U>o^C^l?7vFR(di<$T8Se=rC8cJaFEj#yu9gZ|za z&9V3`nwCyG`e;0qK^6>wesC4cfJu-G|Fd@R@1^LFO8|YIIGcP0)Ti`!VjhfyzR;br z1U4IcTuT#sNavD%GvX+G21S(Dr}Vhf_{mh;C*FgfM{k2yB7MI-dKXyje_lilY?Hox z8T$y_$@f#S#G^zn>~gE`Nl%LEA4$DOPLxICeKhLp3({w~vFgNw(VQNJiK%hfpB=xO z!;hmj)A^}UpF5+mSvLMVpFrUz=sU3tI#Bj$G`7!EYh>3jf!a(ACT-s`E&e!SQKi~j zeQci4%wQ_oc9KS0UF5XQP|fp7iCNcck+(n_yw|zA^EJvZS5LHZPxQ?h)T#6CMW3Qi z?04QK{O2Z(;0%lRgg9g3O`?nfGUcfm;&%%jx%y|UlS;fYmEcQCIG?|cF>3#9Rfbn< zg6T({dbIW`v|mX6TwQM-ROetWTJKram^}3V)B2%#S@T`2AA(L}e@&|yS5IR5CEi|Q`K2leZ_r(4 zFn4AH?M`u;w41+IYta3DEPl1l4;IkZ3fAk0>hY6PyrSepGfVYmnK}g9i6JlQrpVri zhIQ5qQZ3vOQEUIVpj#}{!fNUrQE}P^AM?MZHEOR_TWmY_Ef+LW-l*ka55Dc8yV}=c z*Q>(cs2M(Kcq?s9XY4Y~3l{O-T%>!Z*qt;tubG4JV)n>C>RlI%H204u7FMwV)hsn>U zevR^C$F!Sw>yq?XzsBKH>=)lq4{Se{Z)^*Ad;G`$+uFJy1+jAbx2gX%4xU1DL^}U1 z+rcNb@wNY-WB(v*(Z>JEAGG^qOflbH&s$_HdU7ZGA6dT-dk^@trXZy4Occ5d9`=7?)GiFJKY<0ygPI2HOY+EK*$L$rcFSj+#k!+ zQ+{viHR4MjqrAOiF>~DRpgnsY-I;uzd+@v(f^)RnV(x*}tqv~jne(BuAG?qGtuXgMiu`@pWHY%TXBW&fo5 z*d8^*#clQw@5n=%-G0Ac-hRJ63_9tPlgJD@`Qxmm?%0#aj_t{K%38BHHo+{`FH_c~ z(`DJVO@E+dUekU{hYz(BuJn%)=fM1qyO@o}TdHPg8%xllk~C%&>;Il!2=bZRa21=l z&NUNw*KOAVvkUuMs8y-Cj(oF9|3!Y8KV^Acas12k+~;{;hzMF_aUm{Iabqljpau)5 zRgnfw(1t|=Q8b7F0~8f&C&t#0kZr0dj&YPMYBEYp?Pwi3wWecYqa{MLS{Gb0ZcS`s zTU+DSdHQ)?mp`KY;moS3;^m)VSs%~P&9uCexMRhSFV&r$`s@iFtb#+viWvEEgI^|Jby zGmL+s9Bi`yy%d<15WRnPA$hdGKA=)Nf_Qd8Q%%15xQlrwRnh*~zRLUEysJ(YF>fC^ zp%F|RH0Q{v5zG%06N5lkZ?e0agXJokzZ{ z0e?cjHbH~!IK|M@^t5(^|HtIHZ*Ql7OW2HWZ`g-#Yfd$}8hi6?4I7HOXKUC{V#v;Z z1MRgtbmWDQy^;G2a%73olV-YR znupj&F4G-ZPvSSez5R|}@Mo-T<4hf}m3bG%Q!A91&E6sB9mWS4*Q&~H<(=)&u1AK& z)CW15>z45^_w-f>RPb-U-uP!#bQSW}@S@0O?xHB`1huQL zw0?%4d?$)F4G&7J5j_*KQ5lcE+beU#CR^Sf*ujSqSl;gU8m3KL!7+8_?j963$Voz z*z|hr_FM2#=yMfk3~{npbxAQ;q@tY`f7}!WZ`yf6@ ztS}w$MZIrr(KF^W@~(h~SZH3xfBWHw$m1--@2<1{0D3senHc7)6z@i=#L4y!xuY1* z!$wZwhc|d9lk>Ncp*McG13$c}0f7PNt_rz3;O#89k^ZlV{J&NTESBg`Mc#}c*UPj@ z!9TISo7&(9ri||Ig_) z&M@EMuX)$V`wC`3cN5r&j5nY^#l3kzv3fXuJ@Qv*;+VI|asCXhQ+%L-chB?hsq}FR z;J;IOp*s>46O4pr2;)*k1WLiNQuqq)h-cic`L4hAp^{x>HGklq4)}iuem{T)e)u1` zmCSz)Oj9m(mP{1 z%=18%`nz4(2ZA%8872q&5q22w>{f<565D!O-*a;4iE=fLTDJxp{*JQ)Up%kqU>Gz> zz;hae4W?0h526k&V}Fvf5IajyT40~joM>!gA9ns1`qP^lZheCtT*A*&Rb|zXdxTE} zlwu86ss#_$c51vKygdb8#v;=(YVO_|gzZ#W|D<*()FakX{62>ma!NzpDDIxFNE54J zwltRd-K7?+cPD8HHBUW0SWit`YtK==ovCm)k+nmr4@{tr??d0ZRn^Y38t1%??afwf z@KI{VYW`}Xx|`o)he;Yq&oSQ_h0p#{<>cw9=5_wDSee#sa`AcZHg=V1T~TJ>Bjr2g z$~WIY|0p_2g8p0L+;Kfc{2R!b9O}&|_Y!e)1ODGYyv@}#>b(qi7QWi9GWR8A_}HmO z*R#Pl6ph~|;-889cOtPOF<7Yy?kk*mpT92ve-2)*?9l)J3qSAV|7R*KxSIdRXWfUn z^L_L-PqpSrYKbxY`S$k6800%Ook$z8mPz5SYQdMXJ$wo*IB24<-z^{qpl8AqJSwf}|>l&i&F zOrKK@SgN%A`opH?$WTj|r}x4~i_n{Q=r zf%)Sd>;&11qu2Z)@>FXJHBxNg7=1)5@_dXu6XCfKdG@N=Zc!q}au0Y2d@tgTkI5+=p}bONKdswBTpyeHr}`jE$ikJ=wmp44*G-8EJL0WT>qQ@WP9K_;-X*h1^YEJr{-@jt^KP3@wa7Z656oG7ut9^& zP3<&kdVt!m?7zI14Q!Rw8OQ&<@7HZ?H$RTk$Vlh}1Lcc;)U zr@sg1aJUMwrG$1dF|7(#z)Xn1DA))KVFpZtd-#S`uoE7FS}=XPVPU&I6H1AN+hI4X zg)ML|ybhb-aaaYl_|CVs$CC9y{yW$$vBaopwJsN6ioft}0P27&wUv$X! zJTb^Wg81DL3j=@L9%qaXGyW{_1Ht_X&K-4GL1?W;5L;8M*~;gejI##f>+Zq#KcnTa z6Kdf}7zNv4DQti$J3>zQt~Lc&Q?(7!ob{Y7!)j{7sFvVA9&6PdLOoXX}RW0Agc*-Qe^k79G$W zdj{X>Q~dycUX5R=Mz7|*)9m@wgXc9iv_>;Rk6=?HwLqQ5coF=o$M-$Jd`pvfw%U3_ zGptjZZ}sK8T%>QEGTpyxkUfbrv`#mYzeWOk>|Ly9JnJ#`@c+gZvsPn|v4uH3J8bEx zMZO*6^~NUdPrh%h?seCyg1$=YD19r5 zxyy)CmuL@B)U89`tY2D#b3!EhK)t?~;DZ~9SauoajLvq;^ z+Jn`db90+}AGX{;?$v}HE%e8bbN%PL`}P$uCkG#kE$)MQ#Ce@{*;?h?jUDCWf_pfJ zZ}1($*p`nUdx!B9zHPn#9i#ShtSO6mG3JfICmDMRmBTq1!MYY$e_>8C-+vuzj3-|j z*LunQ3GH%~QS(&T{rR7X*!Mm4Azv*chg#w#D<`-E9sBm0Vz3LA!*ehO{s61sh4%h( z^8ZZ%QcXh`C&Hz`Jbvfdqutb!ysjb5pkl?8n?gnJO<9>mCZW9~z6r`F>G z+T=;r@u8mKo)@}>+?V4T6yA@$zhaM_HUrm|vnO&#vbFR+2L-X065^-_l_Bxi1UTU7M}0rjp1xyhqqAU-Q}9*ysFjY ztsiq&GMzLv*!OF#b%|%9>g9~pR^I=yGn%!fYLJ=7S-FY$9`o%utaK+wuaTFpBo~hn zm(n>Gz1+RDn~8>ZO`ndj9y+#N3JDjek z{O>zm+kO6~-khrnM;-SHyi=?Q0FyFAR; zIr5w+cW044!+#WqcT(#PC%<@{x~ZOfGEFO;3}QmBn`^9iFXJI2GFz-R0&)}JQecw9A{ocnp+Nd1v%uSxj)2`+GSK+fOkbylFu$K23 zd-#82i&?9&$JoLedk3~S?`w=1Gjlp@s^D4daX!;%&%(E3i+4A3mg{!*r=B`?CAC*A zzM;4CP1ZJA_4ZQqyI6Cj8mVJ9Q4`d1wr+IZLW(=MRSSK4c2I{tOM5NP}^4&VgTSl(%HQJ%GZ8);> zoDA!EY9<*>pLFRh*k_4id zp(AQ0w`sO=V1k;_v(dBBv(dAyHEOiJpr&dfU;jX>(1)Q9Lm!4djQXRQI-waq+iVY| z{R+=C6H}65sIo&%DmPd?V7%X+4g=8#q7VE}|KBk_;a`j&^!u)S$3fkAm3en{Ap_`- z+hW?B)qb4AK5m@)x!;0x^<#hgxto;czNs|t0i}i0l-BK!O0!>8#EmKvzC)1&a$)ZY zMdF`TByl+UJmeuouDOkPbWA0|yesC+y*$UczU^J{v*T6-Z8N`fJHC6|3kmDg`oW<+(9ROl)=5g^B1(8c7mZ@-_|kC|B};=>@5EsW=^MhrClKg z`g@No!;jP%JCLPzDe=1$xlbbv2{qc1;%aYK8NNxMCQjCca8{dij%klKtDOVNPe@UI zAmfi{kMq2G8~f-vhi8Qm;685?ka4D|Mfl2JH*F;vl>TD znMWSzK}baz%}b#+8{W>r=`3dV>5R6s%v;$$IpwD^Am(GYMYjmXLH-NT|mkk{*=Ig!9~ zbNCP|tW;7N!u@7SjA#9G7n4#^LR=^Ymo%OZ9$mYMuMdfH8$b71kFWmw#8*{xr>Q;d zb@Aorh=URPcZdlR)7rz=^Tc$t(JaKKu^+>k`~> z@osju#GTgXREM0N;r3j80nTapQjX6nRq%r=(;G!BT=AG;D2}t$7K8VC+|Z!6*&Drs zVjTfeSwh_V(3QZvBeF5endGlU_($xwI^gGrN!5^?BluLq*J z|L^(1NB;nEYOi&j%6hWLQti@}ZnM<>8I4_^2kDH)uuTol6JM#OX^_R~YeU2B?J!+Y z&iFLDuXnX}1Xl*vZLMUXWKDU+yqYciuz3w4Y{BEZA5WH4<3H`N(sBIMx~-XULgr`{ z4ALv7x!Lq3WXS#{j`y#BqF$@PZEcX3N7`5a8eLbN&j_-#UVX<_UeRcrmo#K2w8}gD zpTwcq*AMHO?3dHpjuDLlEn7jDz2omv3BS9qmhr^rjdG888!a~`2U`_sC5RK=p2t5D zsdm1R_ro%p0~@#lU~xpN`K=bif>;jE2n)YSoC z7H_OYe&sA}t>rB@O}$u)QH(s#A9eh&)jIIFZ&> zg$=zO4@{{CEsCxBNut;9LU$M$&Jun7JG$H9aAO}%wHKpOGRzQDzZHiQc?v-x2Eb(+ z*Fq;;8JMt#bo$Mr&rM@HSO_;zXqoT4tSZ9Zc1Gr^&k0xNht;4^5Pl}kftcQz@S%`u zQz1x^lj#lYp7A!QkaO;4Duek5Zwz)~x(OMaxDYG@mXK%eUBD}y)?uk{d}dz}xCqPwHRGR;;sdj_p_jGi#! z)qg`ND^K7S;`%C(I?k5Mkr`KO$29c9!XDs(x}0oKY#0}6ioabVdPVW}55p~j4 zcVlAz?CZ5I%QmlrgzA$Y2*4f9o?{*q6-q@VGV#J%fA}R>radNTNzW6#rY0T@+6>mo zRmO#T*bZ5JNqVvmhgUWD;O6H#)Dzq&Hj>8Lctvbd50G^9a_UgMU!<-~U)IqsfMIIj zzX!CwB>6m?ng)e83maAu!0Hf#To4ZU;wHzhJh5c9$SZpbC~wlhm)rQCOisO=pmL zWAOpP-5F1<`tmYA$O(o1Vn7%Bvco$=Wx%y4&;ltnNh+hZAim5p(KdYH1;oLcSbiZ^ z9@2k%O^jdc*Fn?KjDv~M_s7F+E1$cspBwwFYSHjTxNDLKhA^wL2!^o7-ucJpMe9jM zk03sDmb{Zn+#^1GK)xnAOj@9(^r)d`-Xa_-A1JR`|Fb+}b%?5^J-bCLJi|l9A7mSG za~)^iM{@|;vS-46>(P7qLW`~D01*|k*$x1y^;%4?fpAhc=f0CxRp}1MZt6zCB#!5^ z@Y-70G2o^cw4bO-(X@e5J8~KM$kF=aJn|rF{wH$DpkCQIa%Q z@25HSA zD{pC69<0gcthm1NNRis)zJWe{-+?P=C*%#xcC*$guIP@$Y37J&u3awVNM*>II+K|R zd06*5EP2DW3Unp6Jz7%k{^P3nL~8_b;B54Z9XItd>f5yiQgs)XUR~Rd3p80D-l{Wj zA^_E}&VvfzQaV;2mftD~|7ba?N;^(P)+8b+tI2-oe&&1(Gucl&ttyybI6yskCTVU? zl~mf%Jf#vZ6tK^}e4JPK7PTsQ!W-LVj6h$#zy7Cy^Ef$gI(CH=xOISfa?$SN>e(pz zjpyqPEH$HnUqV2&V8BO~;3ZY;CLRe)RWi=!ePVvey?cu@YPcEh3Hjn-TMhGk-tum{ zBdZ@*a~bTO7@;lML$OF>x^;he9o10KiS(X$`Jr2eePJS*(6J(UB&M(fDf7hJ?Gw>T zw$_l*37bP%u&{o~-~FH3d7hJTg>C6}+b5uPZfwly=uB(=m|*|?gb|&BIDEknk#GXh z-YI7pjZjze$(XM#9~Z9J8*5-ym`8)RQEX!y;f>1v5fSc~hZMf^+3k#boUz08_fcz! z@||=j@|O4QXp#;E=Nr4DtMilzKETjMdl;J}E4swX{cl_OYQtsBWGYN&7`v&E?)(bz zK<0jJ9Jg_SJV8|LAa%HLeTDl$6^m_>VSSxAt8LsBlsigDehkOrSC*_=Mu5v~^+St{b}eVsvEY#< zz-Kn!jK;M-#|3r^yOHh7IR<@C;WZJfK~_&j<)+iUvy z{qs}nh+}~mJ&wujEJXyrG$K)wRO(RW~wB#(5MMV8F`|D7A%^lZ%%(G+HvDfr##(itXw7ai|Tkyrau0d<_ zD!4@yQ8_xpRFyRqw%Yo5D#2q0=}o_2hx)#m`Z?0c=UIZ2lHnoHa5A=C81|G{>Ggnk z>FWF%k6#+nc{-Q<8k@)B>ivp{Nd}zV$zCR#)H^X$+JRkZ-Wc8M*8M_gY zN3QtL{xM5OaKofMX19Ngm1~ap=+i*ldE4M;&Fgya8=?Z+uX|km8Sd`+bAk8h^AqUR z1IX5?ib+*8tkPE;d_?t_n43%4;cP?Nh9jh-c!W5O<3ja7?FICmsj&mk?cFkm;~LH2 zTDgf`5jlp=Q)!gtTfmx#@`1qhL-a(XQZ2onEwwG-bAg#AzQ2XPCOb|vdScId+@YNS zm%FO6depRz8e%uQwsb9`vc}&-2mIb-5WhSwU8{AO+?(6-8S%$17ON?MS0C_ zP$MGHr!w$I276^F7M!wukuK>FPvCFkD_}cK!|A2Ojj18oU*(7l7a0`k*9mNminu#m zaZgw#GLWY9mQoqrqBivQyedoSoci};SaJc?Gzu}DB=aMO>NfN{JZV@a`fhu`-UGV5 z+LQCNt)131*jHPGsWKH=#|1!+&Eo|)t;Fk5jIL?-DOi&lEr=P?iiJ9#_-s&XGjP4d zQ(zg@Y&)ioug~0T&4FvC{KVVs@$i7^KXi;cVcn$(-iLO0`J1)*G&vRxf0dqMmG0TVNeR%r`;3OaHK3Wf^(J zLO+d_C@xE2DhFj=>tjXlc^8a-5@}xzo(MeG@mzbh^mH-(`r*}lw79) zZ!kPiAGQmimPx&NIt5z}bx*BOu6(I)6`~e|9wyefe^b;A*O1C1S`7NoIl!4*?~_h@ zAp+$}4TaNn2e~WJ`B)XoR_dm&J0Iq9eHGeY={$Hc_=4Zd&@TnKU^x`^%i1uoDiL}2 zi=Qz3#*Un|r{OS}^##Q)#W`ybX-2(y#tzXSW7h>^)%JBS-XRi)e+l_l0OtqeHJ$Sb zY6T`6$O}QXzpW8F;fh}=`rhm8mB)f$oD0_V>ASg<7_?YVrD%WDh;%i3xw^;<%yk zg(L9=zRPnEXUuYp6cQ5OJwLiFDb%NHxLJ9y`FXIJe}Hqx-P)9arG%zKqp(a$ntF>r z(weyOMAAx~{yHCfOJhpGKgIX}6dES`3$y}oKdA9;4|$SGTyHPWP4?ED&?JaO(HJ(oO2M0(oZ^POk%6TV>KpP=n{m6vu-;QaZ)*SUh%;p|t9e(p!$Uj2$vpw7_blCK>j~g#OL(z9 zq5*0*2Ka(@{LaQ9N6+;NheY4}j?UQ57i{A?ib_Ir#eC(AURqEq-wnJh)r6HmRQ_{~ zIo@sB<9#Px&4_j9`te?V@Nx=>iXUxqvFde`Q|vw0a4SBA>#5oXFQU0|Z+ti=e!O%8r*r6U5@U(*) zl^wGk8*ddYzQHuImR#>-`*1zZPnXa)Nz`q}uF=4#!*ARRF2IDu$n05>T2-Q$Uv+cv zVb4Z+m}7oSED)DpY*4ut3VF!pkiL$werKCNXIgFQwYc0%z(z%-zgwS9C&?@bX70GC z>(G{3>fOb6gLO5hHDcC$=i)&yEH42ovvW|4^bRfcR6w4!L3=JCGM^JX67N?Q^fTkZ zao2qy<-blb|ori6dIj!>&W=SpcqV^X_WdE?4nYR|I!`z)#xX*}h0r zagaVp@0oGoTQ*b4WV0xcc;TOIVr6ZzdAGen%z{JggwD11hT0WBDXPUWQ_e*JlW&gQ zoUY_l`e4gJ_ui_Rpo0$Gjsd2xB@4<~LcNbBy}Kgq+BfbHUYXxG6!VBp`E|OtdjyC=dd+@d@iE3o=!>KCy594 z?pi=`mxtVCKdbwQk3{@}ySq?jKh1U=yo~JGod1LB&v#?@K6u3liDhe&JsT{t7sH}n zIR2;!pAvBQ)>5Jz{ht{#fm%+Du!X8sh(V(+ZBLX|ZC}=I3wn)69 z71c)Y$`Gj5BTMaq@^aE!3G-%~=QPiUU+WWL9XAgfgv6d!Rqq`t9^M`$L+`2_&`dBS z^`1ItW)T-|5nWI&|B&ooi%|U~kO7DzJFY13v!MuPo;POmxhzy5?q@fQJF7_IpPDW# zz7OU?yCtbziO;YH?S=!sop29jZb0eZaj`C_$C$vbA`Kkb94#vyU{@{Z!k~#%W;HL( zGX|_DIe#aGj7U0X2~F*cxi23D-8$0^XGdNZom`UevfV$5e(8J9dV_IQJ(rum0Wk95 z${!aXuFvz&oG;v?*~W=+v6EoGP!Y~Qlr-1|9ayggxSPfL7BG@GqzpDk(kTQxg+8{h zs}N)_l6SVb?ovh@7aB;46046_H^igsY9(PCo~>bhc<>3Ob?RE(S^=z&l@$MGW&C*# zW%!2(3@C-)IL~Jee?6T_XcB!A$c*lUanJd+GcBFq*w3xy=?-KYsSO;@O_<-{K8^FR zMC?QFUr$weyV9)S0eaDT3I0$p^!myEMFs9^YZ>eX7lg(qw+CEX(KcOAY_39X4~UCF z9n}lK;{t0CTO%^V0F~`DL8>GcG!P#WnlBO z*uLjs1~YZaVMD}^2U=MOf!iTR4PkWEMfAbLr;KxFCTI zvhDP)K^G4D^ z1ppz?E)}E$IB$mbJzPIL5a?&3-fYlq?^=oIeRz+tMmMZ+nx}~AWBgt5j`;(xcb42q z8$u_ z6ZUpyvA-Na#9f2YcjHkr@eDk(g?TfFc`=K@T=5*ZeuliYW0(_{$V9;3NEdeug~Jcq z3T#u8gWYx)BZa+=&La1!3XQe> z4u134Ad@X#TOh!Qac0~^jn&-u1%1v1b-~RK!ccVCS^q%j>O|Z_Rlw!5u^5_eFEYs1 zk{lc%E<%qq55s)!Rg2`>|Jg~-a+1$V_+3%}tc7?-ae$~9Hw?+J@fk;n-TUl}dDs4V zq8ZWUE3}@iN5gUwxSurq<&vEn`+^^}aBkMp^}BUn{9;!6r3zON_BixZ_66wCI_ za2zSOApU_2rvlZ+v2=^CtsFM74=Q?yKHC4U$Jeo$5IbXO;8m_Ec_QdzE>{`;9oL2{ z+3AzKM6oWPbDghGX_ML_ z?CM#)+Wy;sxxw=2W%=)-AF25tsTg*g``P+aFa~q3s6kyIEpl}wY5hO)pbK(H&cb&3 z7V#Rv7*I4~hIRv@FfWYeEN2#_u)_p(NPN&a&QJ?6Nx~<~Vo>siy{U5T+P~-{bX+7A zmqK?f4`+WE)Sl@#Pwt;&%mg8&IL^74-X1&}_qf`T-1#|J<5^MS$YkmcuureYJ<6jB zLRpMqYh0$_xdoIZBBkh}`F9+bONKgh!!g*JdPy3KyK+QIh|G}!yG3#>6c)h&hElqM zf$ZyLm60M7tNvnm8wfWsrDT*e>Br;LWfod!O=Vpr2cYv2DL|AOsK3*dP1xith4cLF zdTd4dmB085m0Px;S4HC^n7OBi8v;l|=qUqKkUo!v%Jclg)R7;K_5EVG_?=7G(O@+F zM^dT}YD;q%D8TP$2%nom&6>QF(&(eT=etV|lewRK-)A^5Q3tbytl_#{7#hzZxIh{a z?sHtEQ_IIb6jLe3v{Z^DQzgf|Dken7w3Lnl7;q^ZRZ`&+*z+ge$1+rk6jNOj*mEcB zjiV{SwE?!Aq(kxnWCrqJ`%|8OZ>0;e1uo*9M301}sD)X{9x7)L3e#fT)Q`9dvJ#)= z3$6uS)Q@2cykei73;hb)@oymu+Y*!e`?O*~xDi3)~8JR+0BC9i1AxcNth z3toVa`lZ&*w1d)y>2(eOpW*`X@E>DCVSPFx$LHGBP~_-cT|;!x zToW?E1A%6lK$oDv#nScTA@6=k^a|rQhiEkFGQm{{AV%w%Oh^a*rFtYKpruZKJRk=K zr~266N~bD-0=GXIjaKg}GK|OTW3v5h-J$1JK+S*ip9QM3KlUo`^3NVkd<9yf@>@`e z<m?rGG@G|1DA z!LOTP_Ym{D2I>Yr`{8Gare`bI)?#}52DKo1yUJg`u-EGA9^C}Eq+B-L!XQ){fZ5nr zxd~7eBMl_}B;MC+)%Jx;4sxk~r<5ets!XgW$6MkcNxwTx1S|`o4PUzOFJQWSc6{#N zL2>b9t4Xcy2xIY*t*en`25~|--6p67AWtHf!(V=t|Gjjla@<3pSHvN#joZzQIWE`ZTI$Rb)Co5*E z^&{z>4z&fEB)jH&O%C%FltM)DSTN)-r&suOD-US+JDWF}hr#s+#m_+wyo;J-T$vKV z+dkhxI9D+%pw4^oIlN1)(LmRC!BPp@8|pVbZ|Y8m92*pI(1n!$T!WYHmeL2?-sNTfk~O1nN- zj2dzPGJ?SzeIX5qsQkb3f5Gu{{3%8YydPlf8QzMDUFW?CRGpHGpZS76sX9ZaSwu{^ zI)#o0Mo&75k53}XDdl#%rBq`rp}J8ezAQSdpbjXVT|BTmm>j>=+@2oQ>Ve=26QnSv z{|Sf*lQ;43(6-e>NlV`<8#1+7XfOM8Jp_UT9xU6|A~xU5I(mtQo2K0)#fKzw`wo?Vquf)nOji87a=)dGEWd zRyCq-$H5YNd)MBS1OxTCXSejZ>;qOt5h*zB7lwU&Hin~l%jnJ*jf!tiw2Q=~J0`meS}1V&pe#b;e(I&^q}x z69%5a@}uYCV|IsyOR0<$&BgtRf^Wgv{ynwucuM3`q!(ks>~zk(FU|dm=ENI)po?lB zWdDsk$bE3ihHXzO!$0^kV#ROV=?z37A!Mtt+WAU@RE^&$c&HFuVr5L8$Pgq&NdQxi z%hGDxX!9yB};Uuh|T~TtePoJty`L^;$!H~MR32li%1#v#|)3f@#YIE5+{WN4gg6u<7n8Ki_ z<{N3cyQZ5p*?RPoXZyQ>);A+%_Ym}36urnGt zOMe2*z^KjM9qisWYW_}2G%>Kx%-)FToH?8X7KqI0N0Z|IIL&nn)}~WPm2{)GH@C}> zmMx@m>*&*XjpDJRCymL8^kHe?>UQFt>Lp<0L}j4CV(=R<_!2p{i%`fp#DY_F>}(|N zIT_;W4}rHvom}y>yi;z47+;)mbDmKc=%%)M=L(w)~! zX)ebQQ;IUP?&I$3r6Q{|KCPWy`meSi!St+Fk6DF*df2}^ElQYIPVg7ETwRWwY$e8O`mZ=3?P9K4r zh82bAH6Ge`OoKGpddY+=T(_{YvWZ#_UJ4lD#55b3KB2bxYu>)7{%!O<^^no5$a|UR^qHUj()p3epyeyD)Y9_UI|}* zE$R=^Mx<<}HyvU>8(Vd;8nPNYzF*pLW!9q7pG_TI@Nq@05H4%#d~EX){MlVKA#xw? z0Z`E(w;VVGdiOuLhR+}O0QrJt`*-_U>VbVgYJrXK{CVMS0i_9m+Hu|CVuO$gbnWHb zp}rwe12&ce741Pv`L(};=Z1FB)2a6xIshmZ%ryXd1*DB1!VDsv58;KMSg!K=R`!+j z!_X(H`lj3~Rre_QMcBtb`xy7G{sWqKGTALPd(ZYp_zS}~xO)QiMe;+KXWaCK$v1>| zGSVvo_bBE@oTnG=7T7n8cVzg5%-0WhXM6XPYrK1Cz9VV-A^bJFIrn>-;TuWsSC$0a zf6XUm{a*~={{sVu@&CiT(TeT0=%<4Yx%T;ky)I~fHtzD_rW94ME1U+^zWjF<83+`kBooLBS|7k&cus+p9G6!MBlR8r9O7LNvzNP`7P4M zQuf!^n55f*o@C8*l38_u2u6{Ae_vZl(EH1+%k%!goC{HGbN@@S65IdHU@@{X(fz-? zAZB)Y`v09D3{dfQLRv)r-h4K3NhXSg`F#MQ-y|r24~ZM_Hy#az1(PQrHuhJ$SL-+l zk)IyuLB2tHU+&PMpar{LF0eumfpu!sfQ<|#DAHkuz<6!qe7+enxkHIrv@&_{#lsYd zP#-RLW;>&!@AKC4)o{D*lm9QGJRF4nVpV9*k?(Wb;`#$PujxOPI)yOq=T9Vl9>8UF zB~>P?A0L(oHIMoo)kn9^kxOQ@ILDA_D_yls9BPlts+-e5;>-Zi`{Dr>h zd8x9bD&6_u@3{vy=z&D6LspYuu;6B8^Z`1<`nCfvN_dvIg0gt*CW#W~RXYXlqnTZ^ zMJ2tnWJQn&2g2!3PMb_-4E&iA^iF&BhCg)AN5Os|)dOF2HxQ;rKDg&80!e~>4(EaK=>_so+2c)kk#xxre z>ZlCsV+`|glUFi3>B&IZBe0#O3$j@Od8=ON9BKrv}?s>N< z6AYEzznVG&)@MTv{ALCjzXug2x@ru6F&Q~}G1fx!3V}2^ptRS*4ckQx$x~+*(+auD z)5???Kbb$7Z^|@vn#wSB8{>-G**DbJB3Zg0K)s;5+>Io!E^A6vn&rKA0YEVvbfOnr2;-=gutE(93;VYh@q#LvO55pB_z}lNAv<1wr~TRmY#f20 zghDAx&WR-GE^DsbrD8y#lxbdqq4eC09hNwBl%{Ut-#W8lVPo1h#C$cqYNY@7SaGNv zqmSS!gQJKvb;^g>eIDM$oDl1|f>7@xT(WOvSA;&_oo(M8Ve~5iZJ4Wo>qLCAl z3f(B~E!b&sZa!WURj0g?=aO!ixz8vHUA>DfG~@I=RSfQ+0NR~9T|jU?$i9mhzDHm! z_aznR2KEQ?jD>1g6>B%}p$bi~-Z<>Y?vrJPAT2W4h}WG@rkCBBU3r9|Odz-p%5{Mi zUa3D%{tq+N`biR7goYKO1B5zS;Pek zyUGz~JUVtlwnGYXv{74hPRGRiG2)gc^`WNjQSKui3B+Xori7;3zJ6@hc`xORp5zte_ChZJ;vUGvLA3~3mt)snXLCq5 zQ@x$2)A6TL?{*wVbGw7qEMYL%tKt0+7nj9>AYlj`W?K3`ZljxL2xDW|h3?}y@CV-- zT@eori*24x+>=Gd!PJd>PXz-3*3iu&3WdK89UkI*jCH^v@rAN!36}cYBLO?y0e-o) zB?aYCxnO%H8mf}4nlx?GbDK7&R}DnPDq(#aSd-25Wa@(o2+Axw=O&ZPEJLzzXET_s#|K4v5RZX7wk% z8me)J_S}eST9JTAZxoY}bdy{N5B9$(py;Gvn>HQE*}jX5pY(~0>f&vQf5Sc)_4M$X z-YTDr)wew}m<-sxsZXoz-f*p6GUopD$qF2fn26_V3DUORrASgSyO6fJdxu^dpkby& zq%42>D08kRhNPMW*t~&uvUCvjSpLIdp)<@Zr{~_>wDJKsRijiroYEL-gh~ z#a!p|nW1)MF_SNMh%pRhZo(*-n_T`ijN9>=`!LM&rIx$4J=i(hFCFDXEBLiP6>|Is zHN59mEi1amF;`cIV)0cXSGYThungs~cr()LIVgtCon@kQwRjTje@bzD=xg~Tk8A8q z%Cn*{(4CEYU#j)@0Yq9tjo)G5YnZ5AnLAB2(%#zBrgnicIbYc-2(@kd;}%~fU7k)4 zD<@e?roZJ>o6jXFALj1Zc_uMoofeuJ>Yyx^kDbpr*z%KyNg0J8NEwW@#--V-=U0Hc zhy+IuvZ=Qgl;zt4+Zkx^D+lbrNg8=SA!(Ms*~u)LH1hYVuS1-J%nwXm2f?)Ml5gVs z;z3mEgv<{!?AUT6qgh{q!en3$;aQoLCT^KIU%l4kPA!p$xcni%M);Oq)0lg(lOtqG zgPEXm6I%2!g9)p~%v?Yo?JE&b2kglt!p5;&jf3Qr8pbp1(REwBoDM*m6!P!XlfSp1 zHa@>4eHI5rm+L>Rn1f{z+1BdJp|K=tdU>QHBXR}6tW{&fP%cDe^X4Sn2LCy>YyG2t z`q?Jj238^z>LJPz3i1((i+sfNKP4)E1qV=5Z$? zrfHk6GL+K3>&NIAlygS6c}1fU16=a%a;6~>k=2>4biomqP+v}!x7(Xft4IF=Z&z1* z*S@3wIgJ8nES;1pwJA9PJhFJYSrnI4lmm4o(({bMiU1$FRj3}Nz`HdGE`kLk`boN9 zwm%pjWe$)$9&`op7Vy&V#s_q}UtKm=)9PGe)>=(ok}l&KX;4k#%VyC|!4d~v8syTk zP;CYSx<;uY{3F6inMSIAT4awz7gR@u%^VR8PYMlAffsw4r|OdL=$=GYET{?>@Tx^S zC=)_`WJXw5Q0)En5W7&NrDUo_fNecX|;e}B8 zDOZ~6&tvV*8*i7aI>gyJK*I(zqGhV+W@QO@3S1fJrC-c9UVwI>r?9#YEs)S zqQ%<39x#nCfjr6(HOip*`wONezp7B-5Cd%HJ4BR&;n?ZdS33M~8erDn<-ZX^rIu^!=J4$j>t@QK?FM{un(#rN+8Z8Ng7ji zR(KFkDhufcC=vE#GGzg_>;}OvYkt=FL*_kL-xQ7?;D#B{M$(Fp5$%Bes~(x-MAZX!48S7qH*M zHeI+;h7fgVp0Ru?WqcHpPV;xUt%Amcy&esgmDy|aVb|-7^6Bx!Bep4Z(67>J;$s5T z((lr#vrrS3W5APon9}M&KIP<+v2g#nuVj`(+Qp6#X<7VJXjscq#!B*mzmQti>RQ|? z=E0e`$((z$)?pI$G?J@4uDM;)^7QQF;(SqKuk-fu^H%!H&sN$ne~Y7rIqk#-mb+a7 z{~(ncN%RPve+!;8N(KnuNLSB z-N%7Y=YA;wd1a8Bf~ zz_Crz?bBUW+qLw5?#Alac*jXR(J{MytQDf<*PHzDr9_BiW_bL-(m;FobAiR!cz-~{ zVObVa``uvq6P2fmGs9%p(*ngPZQ=ns@oFv+EBWYh!oeTnRw3~uN^xy*6!KB`2<^sF z73UBR=}8r3Wffv&Iq}9~#$+yzZ@i1T#D7$`3JgZ&J(Buop71FYa)^Z{;XwE^YdyfF zw`gE_8fP-)l#_M#(e*7DYGWuld?mecWwRjW#PjiGOCaRNk8MrxLm_=LMM2(dy1{F6 zRQ<}H_wHj%A^_tjoC#KTtfG!D57AC*b(_mS+lmn`^$WPVu%DxpH6A;_?5nU?6B!lP zfIY4XJ8Q}Yal!P1wSrt{q=INrjdg{7uw5l&R0c48mm$w7#*z98mt00ZPMYoCFmHm^gn?NtwhQOCZ`$y> zZh!zP4hG*7+1)4JKdd76>JQi4Md}Hz?AfBwu-wFZe_0{i2k>!za2C)W0U|=asJ{N? z>mo`PzxT~-Z_x2cF|YHuyXiJ!O)u3=I#SvMhO0jA5o-S>1WK8vmE$vG?-ViErY5J? z=(U)GFEC2~GgQ^q|8jSkV^YtZa%PV068s)t9H(`7Y4^fH8&xyWTH&^ER5{iEbOi$c zbX?Eg0bFFo`B9^qQ(@d3g7`ce8j76|CSP|83@_kUFSKhVkS42=Iz42{RY z!p!m?d@>y!t&+Q)F|DkDg`$)7|1xQ|b)jKti`k0&N%^soHxhOYQS)2R^qi!JMQvcq zWE~H;Av*SVV_}cV%vJYwD^`3K{IH1%)!VNCKlnj*$Lrn0&d`jw#2o-5F(_gmq!STh znx+#H;t+=u(?1!7VLJ2#fnkP8kSMkyWVjPIlZJd5y`i19cr0v?8l)4@me_c7NtXhF zVGAcF#_0Z1rXSj1wD9mO@h||@36;uI8eD(Xb2K5~@K>lruLIpLN;TL6-~*~qm_jCw zq#p20KE3e@IZ&4qHegQ^;z=biC#IgX{QX}`0qSYZeI?4^dNAW-AQk%RlED3SETAFA z*uSC2Mc~nr=mnkh(1XxYYm!LJ^j7CUVW>FM@C<^0{OA$S%>uxaxTR-$>WU#T)Sj65 zU+VpTgxF_5si`2+a*!wN)9^&Fiy<`zcYyQ;*2Mp`Fi)V%BvU!6{Pa4*(P)?vm2DMr z!cj`KHwYC8rXaZ3Nv_8|ik-YTg>@?@Kh;&u;t={iuB?6nLKbZ}AeL!QJvKxH7>Z(t zW?G$2jr$M`D#-zQ9yY5#Sd|J?m@o};DjfaeH%&Ok9xYp?-yLjcirrSeDVRFU2|K77 zzCwVfMkSy5q*u_>m0vCGTfQq-JXKpZuBPqp z)~`Obo$b6GFAZIWUcw|@+Z(#u*1Wh+uC;6Ip7n3~nSbX}77vqkVY_{{Dypu!tf~w` zzapo$ZFsSGHoG0wM!!w|nNZnFuk{Td-E!;XTM%0dTFow~X$BxK^Ux->qI~YXo;&_W4 z^LZM%uI4)HU*+9N-dDlx3uLj&DF;51=q>Hx4q}n0wk%1w%zS!e_CwOVUl2x8(EV>k z?*DEu%o?RY4|;kLqyVE{^7}XU`ioB-xk2oq77a-YrJ*cBpNCIW%kYa&B*X zyJD1H+`nw0V_aVe-&?{46#fI+Yt{a+&%wfw&gfG-r3~HuE9)n@O{lwlQLr` z*QFc?An7Z35uy1|g|uKE7>GSLX!Q1rn1{#^tW+2-%2IHY`~B3YFSlvK7NQ#3_l8ux zlKhM%Vvmt6S6i2dDW*s}WJ9F!-ivMe_4Y9DSqDtYPWv2phi#4Nl#}H0QSeJjS(_x$ zBlbIdm+h}Jmv6YKKK@upEP2wSM3LxCzNQ32x@w~a^EecQ=&^XRo4fW9AP!EmPpD`= zDQ82gt5O`QL~{JtuMkB%IWe=rP?|=g+inNH&L$$&M12e}JlAA35mRulnh3<*P4znB z!p^&f$dr8nDYG(~g(tm+7*7%H86d26it$ft-Au2ER7gAh0#yCjC;jH0=zWWq{9Pmn zOgYNJx2|FBTh%-YLcQ4Z6$x4T)i=E%DIi^p@=#Na4T_rLd=}^p$#}8@Dki+{Xk$8? z0(-7FBEyoxhI{y!7uV!jxNH8Upua`RbW1{=G{P&Q4hQ=kM}EoZx;$~wmEzoJH(Oz; zu%E8+8^+jsFDG{0P!?*o33hxRZq=orVu+`#Vv2^M{NWZOfw>5diY1ZRa&!Lf3@1)C z_oDP|?U6$e<5AkUyYzxJ@w2FowCv*f8Zw<5ri4xYw=4xg!`0O>_a0?PZ52YSe9(2 zx3J$lPBAW2=(0AAp7kO3U9j3D)*YA-nx$G=#;V7{ML#<43QIi?Q7hvwJ+gflD|VbI3T?*}9FP3j!AhYmx9rHoCZ zdfa*8GYR4A4C)WQnHfF>#hGyM7;0JBval%=b<)UUi}JJIHb(orS$8?_Ywc%~$G+c7 zdUXzTyRE-|UG*Q?JSN>9PtPa25-F+;Czq?1v!s-|rJl?``A2x;!}H z`bYAk_!i_TjDmelc%8Ix)HZkb<*!zuepBg5+r3yC7AlSojEV||N&0vmXCo6k4Oakm zK{?X|%e>Narue^YDM3eGP<8PXb6Uybg#YM53bbF;pJ5`Fa+NT5&~hW=7E_lrR~&FU z&izm02JExIP-`AG^YujR{B?o8px!VE3F&r~x9EcZSU$kBLWZ8#LEL`aNci~AV9`>5 zF-qevdJCj|m|%p2@~=zuIMo|SZ>KW7QZo?+5K?^3O$IP~$2|SC_OG*Vnxoy+LYgPd z|2fUw%n>HmZBwrnd7S;v$S1*VsGJq1CmJ~6M$P_6_t_6qHCXXfrF89|oHWJWnDp?d z)S8i&Tq;Bzu+G;2=k9jp&HiMnWrvV-2ZN6dVW`fUEsJkEdf#c9k%5M;M~G;4yfOW! zA95_cDG?&|TGszl^v)1wF|V~-V>U7GKTp+%CrzzmjZydW%30^_v|#*~_Z|m4M|~mb ze#s-Tkp7oGEdBrTVTIkCjBSh@@&1Qnt3dl-ETjDopO%f4?SJ~T^z00D|7V}JAkEtL zpb37s`;YDxe%L@LI?peEpmv!n0E;;$XJt&4UgQ;&(^A>4roVAV-&9CB2*xEk{crF>r{6YE*v`hXo zDj5VV_F{njPJcE%5J7ZeEyv!#e$dVmSccGxGyEqA5&r^Ph-rXn@=ofGx>G@R2x$CH zYU2Y|7Wy(|7c_($iQ5yxuK?5O8>yQMLN1VX(1%d8tiVuH&=Db+=^93Os(~9U5H3b8 zQqWAaEJUxrDN+$P6a4B?E%2flVsy8r`USr~n_+YDZ4-!TsnKX<=Jx395MUbCP6~q) z=jWiV6B!Y#9wyc0pXJ%(`Ff{n0k$(^ufGHaW`)rqjr=8vsN?@22)52+n8%GsYzGd= zhv;5m4eJ^p1*yjq|8ZQ1XrvPjT!1%uF`%57B~O~o7aJkb7x`xY%{Y`r7$Vl;h>(ev z$cj))RvKcT6Ql(~DHCgD7ll~O?v($EsTji!cAsiepLRh-uh5`><7D-?D1zpPil9#bhVh z#TK2(b+q~ut# z^pwiljd#jvB!-{6415-J>ptjCnR#yq4&#!Zz0NsbHAM$NRoDjKIry(wjnyM6d`oY@ z_Lgb-C1O@515ocwNWKdJxZ(5Vq1uHJsiqT8;x;tUWQ{+zT@17K3Y6ynpKdCP zt9ULGsSfJ7H1_Akk}3sE&pq2+KSg0BDMJ9PHfT|+Yz`DFFgxcs+gT^0d;{b5Z@}}F zL_XZKOut)2b)o0$t0Xm&i0AWZ9fsAWI^16LgB3i1F)jL1oL$k#UDW;?Y2O&6OSEL$ zwr$(?Y1=w&+qP}nwsG3VY1_7K<~9*QA8bJ*Pm#|1O}&J(>1_Q`U_!=k!Uky*FEr5(rfoSY9)m6)C49% zTU=RhL5ZbFU;1Z&GvVGlfeJGUR9gPgB$h8tq-Cg}w2;9bD#~OmxCZLdYHO@6DQo(- zGxd6G6$kXIP_9LskaPsrfn`<(1pMRx{aKt*XE#(k^NIo$^%h^VIH}5H#CcMoL%~?A z&|kMGgRN<#lT*zU44riGSE$BnwzlLvzhc0l;_fggy*&F(%=Yg$jT{->T&Wtp(7c-) z%pAI3gM(60QZ^rrQD{@bZ(J%bt@hJoj>br|JK{GV#%kOtJ1T38#`$UXEz7>1LEb z^8g1b#$(%DGg`JYa|nEPvk2h+n{v3dk}=v84xw{*8l{%E{TbHzNl_*xQh;Fu(9j~J zRJ5}b+LnD?3EgXfpZ((}Ei2lbI)`K0jJpz?RFgxTHI-iD&$zcYc2nqeYjBoT$4vda z6(#cY0>4*_LjsF|G$&>-!`SOcpri7LzJD9qoBGC*pUzyy<+qx{G+CTPPcPkSkZ*5V zkpK3G;x&d!%5(B^b3>9`(8sp&SREe*H9;N;Y$|gS>`G}XBHBNDWfYfWB>wS)p)$Pa z!7K}WBmaJ12+pFZg^J$<%dJG&!OY->nbRVWk&7h8%Oi}Crf|7jb0V;ji!_JLia*Cj zo!(pf3pB|k$z9hrCCCA1)}B9(w@lrhj;lA^@d3hhWo)rK-0nzSndM-oyWL!RNl!Wb%M_sFi`t(4yZoGaemQAz zU(DAbprtaf--7R`M?%)#v+~uT-e*=WoiTQbL2d8o4`hE|z8hst!w3LtTs=>|oO#-v}@kBfBTGe&n$>*b){`q3Wu2u~q%)9@TPaCU} zzTT|0JtINljVDYDk1!>$(Z79m7Ln7pqJOT(8%oC4avOHley<{ZX6!Oo7&f2l;L5L*_i;cQlFxtp41PhU&)bhy4d;LXSh*oV-$>T4|&q#KUF@UlDN{h=fNh1*#BHp5u#A_Ma zYE!C7Mq#v(?D*FgfprAOSFR~xw3%Snaltl|Q|kFLs7W6r!CDxULJ188RX0{bv}RC& zNza-ib|h!11ZFf?w>?re!#=g9*Y1o(6|WSey^HKoHPS-L88L3SjHejkT8Aq3j;xA? zt`_7=9|=5B8o@?sO#({VQ7YHxYWJ5E^wEV+Y7G{fNrlua^1-oj7<>L9NsYLa9kml! z$IX*&TB$yB@Iw} z;Zmz+!eI*b19RcjAE!3lgzQmq1HcOSX@ZIj_!$EJt{*q~ac<}Ik3>K!^^P31F#upO zd@t*|n$m%Bd-1s(qqlbU_NeywYPlR|UalWDdtUZ8nubP7ouh9#(Pz1AyKFo2(bc-7 za&Y5?AXaQ?1xwzn=po*5h}pJBZazI(JP#OjXkB06IHR|GZ?3*<*Dm#3m~v^>vUiwH z!>OIb>T~YxD0=mVE9t`<0t!5GblMFcFyB)mLCTQuAcx1jfIM?dSe4GK#|K~!kg*5~ z%8)RjON8_8%A2Xg!Uk9;(65jLSBH&3K(fc5Y-~#G0mIGeo3uuHG#lcNsA9c_pP0sb zIZ7B`lko|RHbgpeU_n-D`L&EXRnsXqf-in)c0Zi}_>8ZqO@8s$9 zNfU5X;f^JAo|&DlZGb>JZW0RXO3~|d`}O2Qx-HGHbfq|XvC_2aAU8RsF|e_-$<1g<4{B%-bmCKHI;D+4Mdr$ z_#lYe06{VZQg{)~>j>||kw+2NFo2N8z|WimNjP#)Wkvf1gvg3?2<}%@(y!pPH-$qc zYNYAK8VTV>T7*o{Mrp^gOaL)q=W%^>xfqOaNaC1`AZO3=3PbekdBg~x7qizup%yJw z2H4yjYFq(=#<$GI+td3KQ=(5gU9L(c2n)%Re8vydw!I6nl-=%bS4Z-qHwUPf@(3F&qBn3=_;7X_;hC7l;uPgqIIG1tw z9NH_>vz?#@^M^3Nz@6srTdQSOx5N3g+CRxnQ^hj-wn4vK`Q!RMt{g`s2xLU&SM}w} zQxYf$1^9DHu)O>W(z{WXlyGElXWIne8BZwHW(px33W&u!7omGqBuhCg61$ykMh3LJ zOL*TeZwU?_K`|{7VAgn?7ssmQdG0<3WSK^324pqJS7i@<$VX#O9st@DJ*3Iox5Lj- zZZyCn$LB5*%_52(8U17x;~}MT9-Xt(Cji`&%3=sI3+hcn43^XLwoBo}^Ce&zC#JyS zF^TbiO6izVD;BQ(AcM8 zdV)lz8T--4wB~m)J2oOr#!*xE0L6-59#+QvW(y?eB)weQ!pNpI1e6_fMdqduonFKF ztBM(~>}h%=y4ml!${~G0LT1ZqD2thcEXF%`aLK_9-)BC$%h8CO7IwMDM2wlcZK6#% z1>329k2mULvQlypzk?Gw0wKY?7H;ff14dhocG;^sK30*Pm@=R> z8FaiUN<%Zs)cwZf%C(>$7zj}GbuV#4QR}MvPpM<$S)g&{2p zPg~*wn=fhJSwW61Qk&gDfZR6pgW?I!(Ks2UYmbTb>RgB*tFs~qtNuzCB1;Pq-34T) zWIwl32YbaQAJU)Z#j^}1#*yitY_!x*5nAo25K30GP7ErkOd6Ewo@`_lN61|nZ}E1n zBlnpFuLvBRquGath9i|>lnrl0WgduMS(yTlD>8SN2s2qx>_uE#K?=Qt$iRJ=Whz;{ z|2uj2*QDz_$JVB$^6=^KZW*8M>#^c}_|4+prO9WxOQ(8ziM?|FF>>+AvJ1U$AE&My>+ugb+?P%dHAB z^QXY^q0)@s2+&+tzp9&dO5wgpbU2Sr?{ck=kUuwG0Y1HxC}rYQEDuB%%~%hAW+~w+ zW46qaoPtM9npSakmd8co4afQ>?*-`4$}Rk-42k)F#gJJ4(<{Qk{@*a9PL1^#oEF4S zR3E@nx+()i29Oa32f(|Z@9;Td3syOb1dYQju(B#k~ zOJ(a1rGq1w(Vex5occtd=X;w_ZC)Eu!#h396P9%*?y-uM2Aq@3Fvyifdo%3xMNQEu zX+rRTBDor%r89b))%2fZLeSxuSUh&^)53|IRxm|C5?hh5%*8OH{l%z6utZxe5bwPf zA~+6-^PDHeVnnCKy2OX6+xs1C%@I;O_2$6F0n@hhIAMDI_$e=0XvZ*it5K666F^(Ug6I|zq3WA0Sjp$h?4dwyzX&L0m} z*dFLPzgX-I^Xfb_Ti=W%Xty{PhBoSA;0}={7G~DpjBIpOZA>-- zOeocQ?29 z``19vmN&;|@l2)aGrMgfGsBx}arT$p+y3dhE8k}KUo2%Sdh_JE?HgWaJxJ zxJ8QUCN$lH{i=G1_{q#hc9;^v8uh3h3_jWQgGiBHm?^qmL<-nu%vAwk2=>{QbC)+` z1RO|(5;=-LSK54INIHN&9C)S1YGPHkdoyNDgj(08qGzYWyhkOA8kunqqzT>Uh&SOx%+wep z2rX19%u1CCA}Q%{Rj#rZZ-Yf}Mc~daU{u)&b55c+|ixBRU%aE?ilwjRS{BxkT`A$Tb^x-_<8 zRPv?B38d)sS7VmTKnV5KlKjM#NpOw$MoIxmxCm!Skv2^{s-ei*b1|i3TY~}W#A0*MS4Z}sb$EQA&JuvYb9Ik%t zuu7iMv*Y`Iy^a@Sb3M9kP*OJ=mx@lG7zs|#WVO9@=SrU8FCfCmU?gsZ6$8y{cll#=nJ84WVo|LgwMymy@`pZ9 zwhd(%8EE1ch6Hq*RmW^yHFTEV_*-2edk=Hz4L3`59?edEPK-QJ+|~)9Mw)n!-5Fxm zS*N=PAro6DGw|@=PmqrDlTj$(cm%}A7oRA;Amwb~m267UN3ggMPl;KStA~}qKL}$Y@zzYg)>qw=6<$Vk zPLaw;mo3bnMdN?s%zs7h0|2zuMgSX>GCI8W1PD;zbYpKk|5>P!6q!TqA=ipPW=QLM1sN2mnfixECb|F6>pUr?v}y^ z&mSbvm>XPQgp^8M870RNLq*I7<_$LL0Q}C@5%Hs@O<-gw^h(#YI8rNw&?=}DhfsY` z$h+j7O@G!+XbLnyf6Dtj+Tf%bjPA_ku9ErCX zA;%RHL|obmyFH5W-+ApU`&Gf=dy4Y|r@$2;q$V1PA7s1IxFNC+=3(fP_!nhOq_U_3yI#f&Q zgp75Bf+V7ZGv%G~b~$BPGm~8~+UuRXm6|gHiCGgz6|=+%*|!=KIhr*2#AhiyX$%g4 zj!lXAW@IHo;C3W#Hh_vja)zdCJ{RxsHnY_^^ z`4Maj1)afgQ!=ZS-PMlnyd%)DOA@kK+|I?^5k+~Ktz5WQFcI0ytGQfJ&OOr&k9@Wy z)*#Iq4CPdQFltUzJ`)n$VJ8O?lw*Q{`BvGi4Vh#69c#;u#U6GByV@hms3&!EIT{gxcHl!_{9|D6Af)>WJCWEr3 z4V2jMVsEenqD7>lHh(@ML>fk;h{mj2VB49>9~$c6krd;@+HNlId$4QvwU*MJughz< zI+>S~lYRWs<ZT__6rLv=g!QCId|@CFCte{z z1}@()32nki;PDhBPzDK;o&<{(sf`8w96vrNp*<-QcT$pi{yo&)&^QQ$s9bv(oOm4F z{&*=ICs-V|6pj~ym?DQMF{wO!K@inIq@~|3ZG&dv!Z8+urm1rRWP9M%AlzOcxR4U~ zg4`sX!Ud{=L{cPT_mu=TV^sL@5PM{1-C=_%;xR|dH!(JX^5zDX&K>zvkufo7=HLUV z3?EnwOS7IVjS-6YH?^m;rsHG#QA>E~sm)P(A9RJfB>S`af5JVd(}V6Z(w zMyHj+s)7wps$3b`>J6&V{2R_g@s3V~SWwsR^K$kypUDSU9A2l@9t)bIGsu11gdNKx zVY@BNC3-?3GHZ)nS{{|Sk18Tly(rP~vw4scm0{RK6A8@J&)0D@U5h7@;wf7eUJ4H0 zaqnr`v>F%-v zcgALa92U^?%ByZj&AztXY?_N}3mZvn8%d59r*;>23AqV1)gKT$Xb#82l(~4M9Pk&j z`3MN5U#dQc5)Lta%<|xSL>?` zEmZsJs|-BcK7$#>4MaVvE~YjIbt^9FV=#M=5}Im0#}^~_}fZ|{<=gf-#8}oX}u6*9ELsC1TFYu<04uy>i{>-I3hC? z-32*9?~}sa5*lO6D%tC}n>AaC#GaI7+=%c>(v8N2BjqwYwsL1g zmz_m=`uNW9j{J2tH<#ulOPqK)UBv*0xPA4BICiPxBw^9KzqsNVcXiSdxqT_vIMq3& zvew=$xVznUj12jgZFM`4K()HK8Gff&!vv4L8&j?8bD8$3walcbReOED_tjZT99{g4 zezrJ%8WArMBK77-qgccI$~{mR_EO_QKmjYq}{=r5UrwsXysSmGERq9Bl*wdoL&-B_eDEh+&L6CXw@voyP{C=gP*9#f6Vd z<7vbacsdCt*FbS+#7HidWbR2xI<4%L;A|mdt)*TBn2vx*mfM%GmR5md7LY13R>nQA9T3DYn0om zYqTJqXN%)tw3_5GYE*(iROu~nmnqopxTEwt$tMn@Ea`!n!4)v~S{*R`uWzhvIm1xc zGi+hliFMsivl+=jZD11T0Lntt+mh)5RNXcgZzGV&`9U2|^VQ-R?=jNSS@YlhLq{B< z43TMvL2Nkk;Wju-QdGq2S9>oopfLo4z4Xq3kYTEDh(Iao%FOg7c?^Qp@2Kf%;tdPx z9&wnVIt-%NHV$~!ppR7qA=y)ETW`!>p_pS?It-3>)o^ts1DSzJ;!{%Qdu1|Y#p2;Q zLRen_FF4^i!u+eT*9X3Y43D5D$0N`> z>K+h@xJOkdvF~JT#wejmv^RkOhp00#@8s_m$)c@y^kAx&MOucD_8P{Cv6*>5O}E8t zfv7Wu*(?+ZU4_bjn&8jf*-B&t1YeU=D{6097`0>voEy7j54>~AEH@7ek_gX_3!B(H z(wEVf6UeMXt6O}p;gkGhYf=RQ$oIURISoAngI}-DFSEOK_HDaq>gwAMNg1~3D-5-rKGhlz?1K=n-#7-T)Ji8+9)4qiACY^3lXB%>)rSm<4gMN-tv84J#(#;?=w z!EShyZh`&69W)=B6iAJaly)+nK^7;VEK9#9S zC+hw4`*dU>*(ioy<1c!X49PC4dU-5|tj~4z93hz9hgU$K6h;RM2^$r03tv?fK^!oI zf9@(7K?6v`IR9ofk-gA#FF|X38MRr%XIKISI@GB>!1N@ZZ7W`?XIlB{7Qdf zq^IJ3XBCs}QW%+Bvyyc`usYf%3PoVyuI2^IOw`~%QUa@3nEWe3;TgVAX*2J zza9p2UB(KRYGF0OtzzRjUXcr`LaTu?caX_Qy5$j`Db3_{rP3${y-4kEx*|Y2oC1wf z*J>)DLFG)7E^d2n!eR3niELoaq!TaJ)_Z>DSm3pgbFVbRZ`3Nuk#9pfn_)$$*JfFf z{G?(WPQsuzh}qfG@jb!g0ACtpKil&K7biS7l1nOS$8GnxlumBr0ySWF>+xItLtO`= zm*ArS-w$1jG1E!m4zx}=S%C`V4lEG3oe6|ae-99FQCH!zj^@PY6TL=by(GBWx?Y!mO*BPv1(#Tv zfE_>Jvg#-sy*k*R<@PZUsTNtY7y&U{WykAC;@{Md{pg8YYZwZ2R}2w3u!Wtre=x?w zz7?^fNgRISgWwq&{`TV;94?Kk@_`)x4q724TljvhrFmtRI^ze4I%1w%`EZPMQvcd{ z4{(_rU;E0v9{f=C0|d|w+YtJv41w)`!w@+CFGJv9Xa8@EcXiE3+_oR%{SA~$QTult zMkh@hKVH8FA$nq{(K5VLiao=A=*PR);xIG!uTA7^v);ZmmDsbSZo1^|b_x&bYT@f( zTzIM}H@j2Xo1{6B+l4u?;d!V>l>7b+Uwjdw!$J(h91VMHH-ilrxu4>>FnM+G{Qy{P zrC}v!SxhiM2qRLeW4f>~G{L}uehe*h(S%VV?VNv@SvNfTTxZmXu;1!3)>XXrqauMkwv ztPdNwV2B&&Pl+KF6?~08F2`opyrC-{c?>o%xYx!F zH<=^9iv?H{gb*lm9&Xtu17#O5!zwzQy5Ut@mYGB{TC5B+*PJ3BKuwLvHPjU{A zp#C1x(yG>|kK_U4d8ZrZXBKVYTmP0VtPS`3O67BNl&G>Ouyg%Ej8Qgf3_yBO4bkiM zD*dAMDnpd@3yQ$GZEUz@0;?tVZLMz4?c4NfFW=XD-7l!z(9oDUi}&@z&BGRa-D>Gr~T4gUa$6# z@;~gh-JP9saQ(HuuCJem@9=zEJ>>Iac2Rg5u%3OJ75$~%?$59I&*h)+eA}P*?NjR=9kI``;zLA8mmF_?~f_uy)*!T38vau1>py*5Snb*~d3D}NJ_8VD@pmXT{HyBO+flZFq zqamkcud*tkgMbp!@iP^fe{^sUs;GV=xCQAadbK#AyT=5rp+MHQkD9RpuL2`UhE6`C z$30LYsc&Wz5Kw}BgRCAZ0ssl(FTNpy%h;b-(iFsUBnZWMcLoK1Q^#CyiCu)POozRo zVLp~J#RM74h`LxZW>-Jb^qYbCr^}N$kqZiRs5xWt*&ujy82d$m^6v z#7nluDrb=nLEyr44UuK>K|9gI;~@}`@Z+Srl2r>2Z=&N6@%2zinA+IIM>2m~eEJ!l zRd-@hE9@(yd8lE!2O60H%-T3Lg))-)=RfPdHd;U4ZyxLVxKDZ9@frMsXK=6JxWk@+ z*0Dxv82P0#4;r5w5NfgB^#b~PVI`BpEqNYld(l=Xdc(i{`#(b zT`?X%uDlFZ8*V7^ZqCB3W9B-$Kk&L315KFU*w{l;k`3Onp&eVyk#qRpe2ouRTTi3dy1>!W*6XDi~!fBU|)p5(O26hM~p4#eJPq?{GFn5jA97aIR`gxa6>os)cGd$t4d|` z=~v-fb5$eI2%Htt{MvHLDJ%EiGk9v-;>4r(qhWC0=+n{LW-o_cG71MQRUt&7?B08K z^bn{D+!2xzk=Ey0U|~~{jc)G|FCpSP(tK>m+Rx8KmP~3cThA+0^{gd3L(WA7HC5;c z)dfpXEp0I!ZPS7QF>E<*T*Fr^=v!{}25!le&RqW2lz3n=paP+CB&7_TdMy?O4Pj=z z9fG4pnG9ijg3kj2Sf=ZGNovQ#PD`FY`390J^`a0gPNCb8&ZE{Jd*{+CCap~V``{@!?mtsdiTYj;l5#(*z5ZASE#|21YoW~dn58^;Yu)GW-Dwj<5gMC{y6oW3_5bP{Ap(fAwp zvW`*bc6g3|D_`w`Q6B03!c5|)B$&MS2wZ5*d^RHE35_+n62@O4ChP9S%ci1}&(5C4 z&d@sYz<6{2Wp%){Jb<`yq=_76W0#=`Dmt7Pf8*|03ri{K9f%*nc+bUGy{V48ZaKTaD-m> zQ~NV$fxw?2u1*ZxPMjDPYm|LE3vaGBhV;kT{=^wFeY*|+{W(28-%`8%aekONudC?( zbg{Vu`b*&T_MyY5lrQVc?d|JOE|$JZJ;YzIMcoePJVU^&OoB=Jad`fInBCUbpBnc0ARn1aPK7v}D@$QO82tFr&J!?`sq_MaSnv+zDgfsRkwztxBT?%l5 z8u6?Q2^Q(VY2U3#&5bvg#ud(f&dv(F{2NxprQj?a1^-T4V8>cMO0g!ce1r~he1quT zx;P1QyBb9j012z=3!y`c6d!?mz~dmD=_IqcN#Owkmct~2Y6CNI0Ztx|f1|Z3rO>_K z5A-`TQH*lb*s(h3Mez9l@m^05&}dyk7)8~YzoiFOSGG8J6ObdgY@JUi>?`+sGhl@u z48quv=B#V}Vzcb=B(xX3TkiS%H3<=V$;9U6^Y(d;uj9K^q`{X{G$L;!eTJ_hm{-M; zzA0hP2uZp6SQP3g+a|^5W_zHQ>>9ffb_?7_yoY0OFEDw|pm?qd*uXZ@e%>z7qE$lJ zZ|wR7z1>^SF1!&aprR|Z>b$8ZowLd3e`FzogTOZXP09SIeyn~zaYTu{dMsr`~DUs=mmGD zDMWEnd4%6}AyetjdP%?u&S^K76F|JJuQVLd(K~%Or6pjfiw*=AT;gtCums+yL^%l~ z5jroUX8Yf2U3dk4ji)ZaiCV22h`MH^F4wNA8gd>SJ=j)Qz+F!lw@YA=a}gJLa;jSw zYtGM2=P{Qym}4_Tt|V-RS>E|M)WcRoI1xSFE?cwB)_tq!Z>Zr>Y1a921nS(^D_6<8 zOi7o@miz@2vz|}=&$NIE1D93WCQ+9QSYHB3r}}U%s>$v&LpG}qT$JaQa*JcxqeGq~ z$vnF+WC`^qR_nfrdZDX{u0hC$nIPQh7>$rM%Pq`HQPz)BeouH3*85s{TcQm={d^Mq zqFK1b=EjZ{_36z+l7-3{m%OZ9;H5rPahQo8<#sW2iL%mlN*)Yx%RARS=1LnLuUtDp zF?<1Lk~SmV#RB(vvnP@r;*@A?%WdDij3tKGLZV$Iys#N!6j<>!c=QIaMqOsrX{XU0 z{AH*mLqgn6ya>{1gMgEo0?$9EZtl1Gy5RX*aI;1lbe`ckQFJTBJRYA?b!#MCIU#uu zmWt?AzIE)g;BQ+vcpqX+o%F}i@|BLM+totR5V{MLqPKHUPrI2qH*)9^94k`NXjq$Jzbf4)jB2~>8M6z zII?BHX~qck`^o8k*2VCTsG&VXPNbvGBTtriUdg)n#sWN@)>FQZf9)V;R#}#(Oo*pi zW-Y4HT{ik&hRB6HWDk4f`_bgM)`(*Gr+nkTC|mx|`3493e|%bCVr6Fi*L-6_!`BvN z6!DYlQ?4kh;PSNQ3pYCs1|4EfJE#samB5Jhy#AMdS7UlQ>u}3-j-F0t&tcn!UrEZ(?y05uSdgu{t*>P!$n3mWBWuR$smX(8KhB+v~=P@RtKr8kPeu6EA=s^ zxoD)5P-_3mMvtvGsbJvVLy<@A%2=KU*VEO)VvufSMgvjZbq2+o(yRpBs^`pO7`w+X zj6DGpAUAyKas8}toPwZSfUIE7L>n?sVm;Ew71<)!hk>=rs0+v-+zHH@lt@utFcAe ze5Ujy=0s$tW^f=vkh%&sy>q@cY}d~oF>IMGZVxA55xEe-UN1-yG0%eG#uWNgD5ypK@tyvHaD4H4-JqnK z(1)e$I4)5K%f25<2SLEjNKmnrrk4_)%#g2_CP+Xjc3anJL5H~#djMY^bF;kFavSb< zO!Y2@M!hkdym2Yw%k3+Di-hNr!j7-VNc(<`Aw6rmugw(HU3G4|((Rpc1N#&t~ z7Ua=WkNo{?OQh*a?8GqUL4&A;eZE&hp~CE}SUzGBy251wOC5$t4v(gZG;?3t2@mKA z@~Gp9S1Rm%AVwzAR0dpYj=AOvK?jt;D?kAdMS#YX1U>c&s-k4)qWa2Hwq7sff{aMN zF~L$|!o&W?_ZNz!t>2DH8WVVw$n*-4IM&Ls?dF;|{i&i7%6DfI*c2&&W@jT*oWtM) zDj4T4DbbUn=-UxQ*~P^HjLhBHOWMg+0Bb&kkyc`6pdhOtKpv%_y*DK%<|q%g9f|c; zlI>+lFP`b9!d2e4vLFCz2=cfXfQX!C#^(Hd=g%I7jk7wsXcC1FC z`UKYwK3Ea#`iI2H@PtsWX!g|pFpbp$%$#3tr!Wxl1!4Cgj~5(6B08aqTC6HFLoUih z2i6q>1cHK*OLjE6+7-*lC21{yRwz9~frANYs`i;pEDSWa`6J5la=4R=r|KnMKE24WsP+Hj-tv}j2JwP_71IJrr_Y-SPE)xjsnV?6UsWxi|rC!9>#6PTYhYDPQ&nA=|+isclO0Y)>rVxTEQMOVAGwKzW4zm^Sp>R)ND7z#XtykBkv*i2Q&C+`YOSkz6nVVrfd z7mvp`J^Yeb`Md+(A8r#1`QkhjU44I=!InXYeAmA5%P&?<#$AuCTRIY~ zT@}1n=u|FE{0=Nyv|Gj-yFHpMr_P)k%irfMy)EP>f4zh@Yd<%Xhu+g%I<#u2nr_~C zscBt+Sy?t>EPWPiRkg>i>oaOCW&T}sXp+C`A%XYosOf;FDKnl|*-m4<>#c55yEaajefj&SMfFDa*bZ8d5`bIxwsLV>fQWwI{voZQ8G^OHs5ou-)#IiIFkU zzxaHEn=&$~LQA$9OWXI4ZmBZDfPv&=Vy3l`b9aKhibkYIp$r)imXWuXmry#m8+&Et zppJsp3tk$xjQ|7kTPR`-6XcDLLp0&&bst=X!qLT-AejVrcU(BUolF`kvDbA~)F@-Z zpWWr4x|x$^niNe*{Npweh&1iU0@8qqy6@b}EYG5;9p z5ydv>X!q2;QN6|#8a8wY-1~`KLApIBAnbP!ue%n{3zt9GkB_Oe}{P5}E0{l@Z4w7%H^*XS?Qv^(xGJsd?aFI&q+K>pejpc0eB zIb?oU{?a$JjK1(=x8AJLeo4&EbN7D2(9j!u;)poJ(RNAmP8nM5JXE4k5D0emlIehA z2bO_BD3mdIFH9)RiE0PlQi01j6XH%8V_u<+RUz6ZwR$ZG|U||^)Mk`Y<*Jog+`1v_UCUqU<&Rg^)i-G zakjJ|5-0FRzP$S(!g6#casOUdAE_b1OMU&gvd*hI&;Kd4{4W}E|6^*&&c^f~)RK|? zM+#1s;72>-|DpcYsV?)AHH+{`&C?9-C^2Ox&W0cCZQ)nIP=N9aL_m+bfp{<(RKkAE zcbk1dL!wh*spI@XFSBxSqSKo>%U$bzC)<7q{95MWjUD4UfNtbPyj>3&=bR`5p^$Dc z4q2N-i1CyN+eIDyuN{pV2)OSVM{j-EF@m(iBQjI^nd?eYkq=O(2P{T_j%YOgPO8Hv z7LM2_DiECr3qhYkO<tbR{h)z$BLc2nc8>YOj)VDagacnQ6dpW#6P5VOc}@A;}7B zMuAQslh9-ev?sEnh*(78@F9_rxURXruy_$eI}pa?Utl6RmFRXTR8&(TE0$VxJJkgd zQ*kIw0PjhU`VY6|VLtF5jkR)^t9E5yaeyVt*HEobi&9K9tgLP{vLf+B&{^u%w`y6D zTOxR^bs9VMttiGqBf_cY5%Ek4Y$O>N7Ff4x@fHpIo(CDA{E_tyk@UnZSj2>zif2%- zvTZd1;hdCc9}&eVss_Y04!H%0^bt^`1P5oEMnUCUlcEL{u95M|_kc*Gja!pj;-eny zg=O;2?L>(#X^1@j{MsUkM3gBsWPQ;!2@;r1Zx80omS0}4g1$Z;J)1Q?<{5IiysLk` zJk4#FR43o=1byY*r)<7gzdk-*2V~U1G8j@B7J@>=05gSTw4wrK!oV^RTyEqO2aW5( z@o|DVz1cWLKl7Zj+*h+EDk9bI&$bskIos5GGHWw?qCCGkyWD~XNWNwZz02e<2M;amn$DTr+D8%UlOh86~Dp93ODbK_?3l&V{Aha?K zo%I|Jf6B&j0Ec)6-mT9HGFm+ia!ScZEF-B*0Pq%SJLM1ml;1mfsFO((JSo;AJW&%J zHK1s0LGl8Rco2%OKfTSsn{YY*sL^5hH4B>QU79O@2A&{VN*;(D28c8+&Xlah89uA7 ztP3uA6`DY9kJJ>KhkC&&&=q$6l=y{rK2Aa)xCK@=#Z3@^d#FXuxv{N3umX1;*+Rul zYHa6s5Y~byiQ0hdSurp7^8V{dKuO7{&I}Q#>6iDYt;!G?(f4Tx!im={_*(oQ_TDlouBKTS#UXfb0))Zc-Q8V++W> z?!gJ}9-IVs3Bd!w-4lW*!SxP#{5{V(-?#2MzwWv;tJz)MU0q$)wKscruTN%&ywIC4 z36UYUFOqu3e0EHNSt#VUppdH*`ZP8B^r{h6>Fy&^$)ud4EbvH%2p3*Kkh(WfI0%J_ z^E3rV`PM0^rrI%(?mS1CqKNOzb z&hll>q^7!`9l6w7ziPkOizDwVR5ArRu#xw;I)5BHcyBBcB&Hz8hQE7ox*I)m%x|)4 zQt{n-&Bx>2%&obzyWcgaQp{x3WMQGas`DGYFx+OEfsTcDo1h{1&QM(mT=Qk*>iXcC z18#807(w8QLRnhCB%FLJefAawPu+$%FPNRr$)jO@@oLBM-9xK)V@peR&DZFPoyqX4 zE|Tarweg&6)3vNFW8++wH)^b1_XCq{S4c_@7;3AzVmCMT@3LziF4RA^J;W+&$W?iV zQ=ha2%Y+3TqRcI`DTdH)*fg|Fdd$nLVN>{E=aT{M3Zy-s#b-m-a0?#7u=gSom)Q_4 zGm_U7Jo*wH`k%y0e&)+0BgNwXuHs`ohkD_S%9?{t^8$HgRCv8TwdC8^Y^QKpDXYYF zn7y;AFMfa>*apGF*DKlaaiSYGeaIocUgG7kuYkI7**;`gp(PKCB6-_Nuc~PI4#})4 zw(->iA;OPpyUf7l!~Mt{j=i67@K#eZ3tn)XP1W?I|U(mH9;c{57$UFx(bJC4IUS)2yE zoyZ6OHVH*ZE%vQc1j@*xsnVK&(9`Fr*q@Qv2_CR6`%n$m#Uv&rgWfymi!;cfVGP?E z5V41rh9VYXu#h%WcK%rRz2y(Mpx4GnMA)ziOhmJ_rE!+2e-ZeALS_@%78(cE$V>cA zeE0r4G3tb%qC`i@T68YIcRs?Nb4JdS>5bwN{5_Nm7!<^Mc4{>nBSaiFbopgvCNxbu z2_|ICvo$ZHvo*x{MMXI60Pd(&#Xu%P2^^WQbH!=kkYng@v8yxk-U)(k{^aMEk~A7} zYSGeU`Kd92l6lA4ur}li`3QL|MJftLq0N$F0C9tSDiow@^lJ2PF9?!(ooo71;)I0h zskUMRn10kqDkjsUGI1NU3>L|ft>Q3JYg}Qk6~toS1!B&IQWq@-5l|{RH%Hx1Kt0p< zHWr%LI(OK)uhkyF zrrVYJt)}Kv+uq*Ke8agVyNCUnX6VWc>n#v*Lif}LObOQ^-*q(HxG0+-dT+99W<`0U z;k!~*(dBpDe0lBYd@)Y=!H~-EJ1t}DeM!fWMte;g)_Tz0@yzDl@z~rK6zM|H>Aluj z;l67!6MAG~Ci%>di5WF~X3R{L1<@{n%)~(sXKTgY9AWGy&%uu>luIk)}12oZT5ybvESS=!l({Y$Y=YJvl;u97l(}pjF;AD zWCSm6v>VuK7OOjJQco}AT#RcJub(+q>{zr^U1Z*6atUY8%uKmRyG&QETeaEedaNuR z)h%{*WRGopk<)d!pcjb@?e?6*ZPywZJ`qLsy>owv@XI!n*d&K}J7AgJ( z`xc43N&cX^s@3}j=BM{XL^~@c59f9HGPQ>*Yp84QWwqzGdr_m@87CCFI4ejoV@rMigN#lwt7q3|mhudGlRgW3lAdhakJ>>yi= zU|XB>`h~nnzr<`UC5&F=&BMq`i81kZ?3l|T`=_>*jA>S#HHdUy&MG3l0?h{cOJj9w zC~}YrLqc$o8EJB8i?eci>{!xfohn_}aM-k~g(j3UemD{xC-Io0EItnmkw3N$InmaA zMRxV#2hUI$QCAe9_mOGjtv`Je6Lat@9lDZFI*OSq!s5_JS1#JJ8;#3vusD=)>g(?! zk$xsuc87(0)5&O6QYUlBoB=Lmd-tF5m7DaXSUyj*vvl0U4K%vbVSVtmnT&bStC6q* z*^Z#_E8S?-e;z@W#wcAB5)zFBsX@Tg1V%|Tq#tzH0{I3=DOE-OeK0B6SniK z+LG#qyN2}$s1%j=xRpx_l$&dGEm_AC*lcG#>4)vCtkH@~xY5SAmGn-x)(Q+Zv5|7$ zh9hwl#bD{h!RjDWkjW=7U;S=TOwQCe@XH0|0 z5v8ji*muxF`y8i+_Ji1Qn_X~e2*x<_O*IS0NYZ(k*K3@l4qc89&qa^lHXeL>jz{7X z^SaS8l=Gw)U6xu35@ILx@ zai%rupr4-aCvvq~<`~c?bruTqk6lEmPj4I3nNbSNh_*S3cNr1c zV?Lu55MoAr-exKVrA);xt`9arLWYbKHuS%An&>`(Wn{9d9oS1ycf<4cxa=H=&o5MA zeAT#Sp$<+h)z2_Zwqg2FQui|~B`K{RB`M_7S{u&I;W;cxpVrizksbu(F~7APcIY)F zRv`P@?Mkl*cipSGg*Rx`TuMn;$;fwWK7hJ?(6{er^wF~GbThZ)C(v9Sikrp2K)QJ8 z`?$;%!5ncP&L30=$c`%h)ozueaScmO<=NG$b66O$6LFa%6{w~4II%lgmfH&cT!WZS z%P8eLY9e@<8z1;`jJ=`=!+hPxd|9UsGV}#ZV9&JM*zKlSsRi~ukFy*IBT{m&r0@0C z7ic2LQ|hBd@hjwynVIwX_T64f&Eb4Ou7p+4KbO%NH;W}C)Wq<0z*+(^kCHoD|K!u} zEgowk{j^tymNd-P+%K_pUVCJxjHJ_g?VTWp^EHYT=MA!nG9?G0d~L=o))MD^_Es_( zoA?CnD~z6pi0v6Xgs*G>`R4RK<6wC5_Bc1rniZm(soUBu#miUF|9T0f2u=QLu=Q`d zc>apO{)b&W1^E8zY zo3e?BqMk#jSSEoKTHlSvG9gP$1;GbZPLz3Vgt%+O5e_7<>!j3v{w|67u~#q9i8;pd zV;#E={VhQ1RW*C{p!rU`u8#Z@{F>e1=`h_QIUM##=m#UK6bqF*HHn9dmnrv?%t49T z>5SEbVH$6AsqHC_@^WYU$%=-(bO1wP$Vkcq`R1C~Y1uaL5zosu6(>=*bg7362qxgz zuO;>*3>%Sj0)Rv)u{x|rXKiFzhQRV;&|6)KqRr_bjKx`Q~05<=L>r%d@4JwwsXeq_Yp@ z9%==zFwN`Ay_UrKqV;AU(2k?T(P>`6qLAZjS*@UBhl;|(Nd-mjuUxm-M$VUrq}L=K zXtws3szg0ek&qn@ft;wHQw?E{qL8`8_?Jy%srfI5yS-ZpTai*8R~4=olG^m2ce}HD zN4)3KP-t%F9(OjCukynaeK$f=bB?h{meQgOBWEN};j?;*rm}f&74ui3iwRtsWe~0+ zqYwpbB!WIQP`Mg3AL8~owz)hNFIM5DQOWb@J#Zw{(N4J2b-1A3yx6b6&oW40tlR7d z->~K@gy+tU<2}a=z zEQ@P!^UFhO+<(UvxW^-6IYOEqLY4HqUVtg%$BpBRShSc<<~1;-xSsh)q=eHlgS~{A zCrtExT!kAy16&$5-A0Ol`V&`M$1yAZq81ss>l!8J8xL}Kj{cE8|F!ou;b|e3!gcDg zHN3cKbjgdV8g_g#>2A})1(4yWI40J^5=zIMx7XB&!i(-=JXCrE}04pW30_gHg>VhPfe$uhABGrZ^=qMzYe+Kr;5Mdv{zk z3qRB^fQtDnN5Ya#>CgGU2w?@%@tM$~Ry^_BOX;jbkh|7)N$xhPsu-pwGULpJ^j&#n zd{A}Jp#A!MEk25wmZqjY@e>^K=%$&E;QdO&m_CuUs)K$0Z zoc4p;oW{V>?1p3*QAx#irmAnSzB(s7;$~CL-P_gg%AVw%JW^63v909pkXgrYvG3(0 zT3hphbc1T16hk&F(H8p8YKK!oG(TN&-j&3EF0P{&!bxUUdq+<UMo zi?|tU0~W0BZEp5YA1*yRnw?NT3OPjnD5BOFGib7&9lIstf$ymym%;v~Uu-RL$%Sp; zD{O!5-Ss3idJ~cU*$AjIsSf3k6p!Uo-nxg;3ohysU)MO@Pfu{fqPLkpU41bTI~?>^ z5^?xB{od`Ny`w*@ZiiTft}yn^yTCv$bbuq_W_7v3_1F0x*5tJJA1bIU<9O~JD7M(p zGZpmQ{QbY4`prdScD+lPCbcmCl-x^F!{GIGa!n!{mKpk8q~kbSw~WDO6N9nw$+WS_ z1)+gmgZtP7iXOBD$3#avGt9_S;>4Elm{EV*5-SSjk^D3}K>@vD#d8rbUJ2}>$C8p*Y8iHL&aAH^n|#w=6;VFm_hDlqC19C~_yr>6_G>+=epF zXvO8w#-*S8_+ttm+bOMcdLgoE2}Z5_ZQJ~=E|`&S#EGZ)*IX-4TSdUeUF}TBSQA}j zK<2OR*O>990{3j)Fl2T{I{z9@{B46!Wssva#0J2{&(Fc8 zS^^=!zs|4`7V~rhTD$~70OlZTTSpPf=j1fkcDU^Ys?uO^U}B9s!36#zXI zb$}Gu1q9$>Fhk!O=3r-I7hvP$W8vTummi>J=Xi0?91_E&sWMlL2@L=`eVgNg~R;E@KoTDaIcL2SW}fJeMQbFdpkgp%@cpg(_ppO=Hvp949%vi_PQs|DDB z%@gRv#=*+Y_9td&M`0*W1XaM6wpL!!KnO?_IxB7#c5W699&HY8K~4cd9u83!7Iqa^>-Y9Yxy5I{t)%QVEM0<{H_+i+WrTgUn78Bw83C| zQ3(qdusIL{P?wff0loyeK%f4xasaHrE&yGyi{)>#`g{A|Q3a`|su|0U8#hW!Wp z|3VrnD*v-Q931|ONK{k=rNI_%4j@N}g0!fco2{iF9}ke*lHJOJ#gZQgV&UfI;AG(! z09mncgUmUA7M9!~b`bBc>HM?rKUo8lu3$Fmbl{qIT7nhaAZ^Hj)-~WQw)y>=vWC8huProVUFK%gpoc{^^gN^pKqMW}m z8hZ5?!pG`g?Eh!SKPLAd1O2Z_%F^ObF*&)p*#ELlOA9uTJ?K#ku29qA_@|sKEd-%@ zYc=+S)$+5;V} zMJPR4EJ0R4H+u*r+kfEr#jbxM`9;Kkj`v7|M1?cGN1a^S{l!0DQ z`_ckgxVhLuya4pt%32HnCl|20Ep$kL1IWS#=xFQe;0l0%AHfi)wg6l~j;^*4TXzt^ z66)gtme8(_)&MBaEg&vnM_UVkrLC(g$idd}7jpqXs5QGn5!iqL3XTwviz5ia3edK( zbp>cSfh=r+_5gJ^^FKuK*Vxuj1A5f?$B`fVy8asA(U$@pEddr_M=M)vH<#ar09yf| zg#RKCYyq|h*g81&08w)P-^1MuZ9LanTzls=O0<_ZEpyUKzrK`u}-6dZq3 zjmI(6UBDI~S7^j(4bXy`o+Z!)dH@E!tb#g}J&)t7Drjl5xPYK88mefPASgjDUQQ6O zwF}V62Fd}Ty)_uhP#XtV=3mWSK+bNqE>Mm_Y@mMakyfZ?{y7O~cMoV|z$1DVbD--Z zI$I|js9;>(Y$4EIz~9&hWvQ)$n*#u!5WoS^y&Z3X_FI!=JO7eEo{=mvE00&x6VI(b+D z|GnIR_O4*)dJeTZTdT(b9_I*pJUGM^%1+l`&KoKP3md2upxlIjp;%qroPOz`gPZGP zSn!AFEq`$g43)XdU-Uy+@)tJN|H%RUi$(pr^*q{@#p4PHcKH{Z`llcJn`eZ&J#AYD zfI8Uzr4V#&a`9qyuy_gd5{H@zgw-1CE~Eu?gn(Ub08%#Kmyg!-KN0)SQGS{Ke~|RQ z<|?5+?oVI&j}QY|0rity|MZhkpdi!+EZrYq&T6nvaF(9uiu(On1u zBqij3@Ae1$e|@GlP~&&C0slAAq|+bO|23xkvr5ng1hllbb^Ny_@1N@WA4c(~2m1fl zl>dKC`Ty3M@=v{o+VCHC^k0nS-+k6^Q0RRn^p=+G&wE;E;m^BbXb>Sn=>omy{nz{M zzim7H-yXij!T#6RDjb~LJpYH|w<DJdcd(@e%Z#49M4}dFsqCf z9P1|+)z9ea=*pEZx}F_}4{ioq5~$^zOHNL%C{tq%B0RxHFp;m^8DCMTJgb(u>snEW zU$r?J2Pe~dTu+d6b}`o3a4lagKG<-rA`&j2CrxM4Xghpq186qormT+o?;9Qmd3bFt zdULh-(haq&+Sl1>9Z8}H8n|@wy*yb&_n^4OX;_epmTAmAn@lV^wDx2U!9|>NpRK)Om|4h5H{ME}cj=kwNbSagkPm<(SR;9jD(_d)! z?gR7R6ihu4G&rtQA*%{~txsMhFj;L9TYaCK_cem zUY75YUT2vzx^2b#ymMq)6J!5k(A3$$w?sSL=~>4=iJ7eerp=bx6RIC^l?8<}4tp|k6-(36 zvFG(XQL(d)4fKMGBdRQd21{16h=ey`i~c$d=}oyj4!n2zgf5l!hkJ~R%LEl8H0NZ+ zV*6XCR@1p>puI5%tA;q8KBe%=G10SE4;Or|JUN~p9o2wEyIy&FKHTn1_4P$l2z?Cv` zgT^m}VE>!TvyaC@TdAtYomU%&R%O>Cgi<`u0%eO5Y%HIJ$`((WJhMxam9p{t14i<7 zg!jXz)vi~*ov-{aF7L+($&c2$uEv@!)INdzFOH7MbDyu?p0C`k40U#TWpW*e4XuKDysiX{~f94Wkq5KT@^RV~V|HT=ZDjzIqX& zwn(3P;yzW;6M2+E{K86m!x);T*T-bl4vzcpRsWui-j5#~|0AoODC1sdb|TazW?% zR=PU~U;9nuOEemVl-b!&*$5cidL~2)yV6CnG`3?Xt?>j32Z?3I?Jq}biD$;k++`cM z-ezkY#@JbF3w4#9({%Ygd?$aH_<0#Ra(*iL)*}C=t-14OyVAbUhOx<-z-r3Q%DnF$ zN#!x)@#1xLjE4m|=_+V-=$iHWc+D~8u}%w$OJs!+LQDS()*z!zMnVZz=aguvzh$Pyg~) zCcB81PrB3C|0@lP!mg*klzQ(hCFmP>in!u!@6CA(R(9T>vt)3uemEohQEBTJK5AzI z<(R>&hp$i7h>+pM;w;#Vb*$=GZ^>PTb@ZKa0@}+Z_ZFkQ6Xc%9oU4eBIU3<}8uMJ+Cz!9E+W=J+`S#Ta#5G)b-I+of7|E)~Be|H)z ztz4~s5HM-wx-LN7k}t!kf=MWd)&U2|q5Yxt z=R;`4qzt{~)TG9}3v{IM7BtI1<58kTg%+)CdEzeYSETd?2q1Jw!hF|s>r(*3dGUGR zxv@J!n(qNT^l`(%t^d;63fU5x5Y2N2P5W#IFNYTn91a)`-(>A2>M=i(suSO;_vqsb z8*BuzEq@8EXme6ab!O<2|2A6pY`J-7_)KtNVR$18b66HVMkWQmG$_e8&!A=(a zq5RXM;>m(yT&R#-icoZQTU^vsq96En-^Jl-i3|qckV$+>-;Bfa2L`QEc|h5-(VAhE zOK)I?KO~2CUym@kIkKh1w%Q@$dvLZT#1h-!bVqQsI4QZNUUYybopU8@IO-dp#iA)1 z!T0mSRV^CVW;Poe$3k`eC)LqOXiaf zC#uVpqh~71>7&jndaL~}La0@!8*mlQr9X=$jH=F%OU?D4KWdeo6)#6rI|{ zfkPY-0!!&}&kw2RgTqh@HU(L6zNzQKdHtOtVJCKo{x%8&2N%82%}Vs?c*5j&1d9N* z04^nhCnD0D=m|>x+p^>>s&Yr%3DNS~{V}8^2{h4_+Lniux=n+LGH;k`Edwj0x+&0z zA_I6(cKyrF?by-#y3b?_da%Fj$)f{kp8c#G|ZFZs^=yCJxg_ae_6nVeV#jr9gE7LS5fqz2gPzjsAr z&#n=QQTbFpYkuQ0!IR9T#{cA7>pH|(@rs^IC<-^(X>H`91G^$)ZgzAj~AvVB3x zmxy&Udf+@|FJ;87cacQ#Yl;ij{sU|CkUV0JYgUcEw-2>4NvV$f$(!< zh7Z))V2TZFeB?hu#r*miC`)EFy)nUmP;MbY<{#4+=X}Y*I*~3;HuQtAT!@nFjj4ww zfub?}Ae!UTc@rrs!hCA%y0;$s@p&b5IBErvDRD%Zhe_)BmFEe ztW@Hw;_qo;Z108~W2>4+V=e}ZEs)LF_oy*VT`L=3;QbB` z*`muUOot&0H%)y7uP?%l?lu-LFJy}~pivei_RoX~`6Hb}9usZ9jdbWlT z4Rf+*5lWzcb2W}fMgIz;bCcpS8MP?7Sy>;<`R}-@A%7%R$ggzXV&jw6$vsWewW#1s z6Pg0$_-NM0p*fNS*}M%~bj0d}@jp`F_#Y`}{I_I95}e!Bz07R^UR_WvM=K^{=;baR zpRa_ew0@IkD08ec+4kbpkwAnmrQ3nlsq4wQAsip{JXJ=g2ejp9@J*j!dZ_~LQ9^v+ zVDrS~>Gk5vVs{(ZRwl+AU+D#k0zV*z;>+UcOl$B5B*Sfd6&DwT`Hb5C7JhPH97Y6N z6ANJh-lzW0nKI}XwR*B6=WR6gw2H!- zMdR@;>RHU@F42RoFPX}#ixyN91<0phhWVQ}D3qF_cT z13sX@@CNnJa^Pxy9d*%5-~=MhhTwddS1=lQ~YyAWp-iigLd?tll(iL3I)J7E;* z!VfNr7jVTRpl>6?m-mV5aKt;ITVBEqb%~p>!?0uOW8y;N>%t+-Bp!=GNx{l@`6Y6t z(KW`A3g67S5tCSnyg1&0iOJ|Y61?yUhbu%odLTm++QI}Qn&RJ5ML#mVDnbcQbTWGq zo*ol?v}yS8e)N6C_xD)NXfm?7ksY|w~b#;)$^~Z42-Yjxd+w|}QQPl6cw?rJDXb;Il@KikYh?%0wXk~Dp z%VoS!*G?*U5*Iu%%#tVH0Hz$(BWzcfzmgWN0otVkkR!JtKiWJcO=)Zl@qXI#(9fK| zEl27Mz^~NSqQ6CCvMVonTathIR_9hjWhxWH;M*3VBEKd=m)@wB>UKO`6e&XoF!=*? zmoACi)Dfe>DHbqhVItjSQ#QQm>&TGLB#}s_WLpW8MyNp>-n7s~E)p68K#Rg5N|8=q zpM)LT{u~p8(LfD&9n*WWWo;z6o6B;G(i~`9luKdV-d&d`z=kUon_X?ki3I{fBjdXP&WN9N=v8x2a?nRRu^>?rZVx0hfOL zK=dTl;02s%$2_TLyYW_<@6@=W{HO?H1`rDa0d>*toKF`hmk3b9w@fi*jbc04C@#E~ zRvTDe-o&C^NJq*kg7wIK8ZNCAZqE*>T_io`z8!7byV6-_3UREm=1! z^ZHSIbe)=*i)DH6y8hhg2gMlkQcwz3oe`N+dwa%qdkAr{+Nb?B;+j$ssY<#v{hAt^ z_M@tpRElL!eO3N1xK}&gl5BLRzLIKmr=F6~Z3#pZ0488k(Q1u1Ip4OGo?OAEun#Xz ztzff@iV>j~R*Gz4@P3~~HC3CcyQRZHrSoBKu4rprbI!PW{%yPIp0VTNWBUdnPZmAg z+t1@gOlKiqhgua{S93>Hx9X>w54bsxUJEG+Ej99!JVmC+AzJe^_l2!P5NGCs`h$J2 znw6WmQ=yOs?Z@r9!SDU+16;xTj`mmTKdVx=sNf|M)t82XoQ30le402ekbdylx*ml& zLdX;5q9J7%iGXQ*7Ep!cUYN39fJJv9F(C}*5=k9q#=hI~_zJwF+8CsJLyd5=F$s^e1 zFFZxcl|n%qG~~~_fkSCo9Yp5zxrl#zB5kMBZLC-43Je|lwZhO0DH>=>tkzInyY(YF zKz*#?NfUE*s@rE5CEQv0}_;Irewlq~EwIwEZ&+L;wv?J=n6i65tD2znZ zrbMhGsXn;oe(3iyHJJhzg>boNuyiy56h%8LAW z{1qxkvdI-Eb4!)>HlFWCI!PP9{Vrn2byp4at++HV?=7~>#5C8t7;6S+zxSWCQ(KOL zxKnW`_AdsivBXNF8i?c z48oa=q5X>CNIac9at9Ce+>anx6hp15~&%ClSxiG+$?GR`^Q z;t(?FczLu;E}T~{r$NJWV3X`4wOzVgJyrLkv*-lyTn9l{uS**^vLBuVMB1EFcC6|Q z+k#Y-wioCUSKrm zB2sH5WN*eLU*Vd!bmmw4#OB1jjZZ#*8QT!2wQIQ+i|1enTcv14X|HSL&x6`+v2U7n z6)}1N63q^gs?YA!T2A;(62Y}Zy`Ql89Z5JRh}@)~rbRTVmp+sMzeIXaDBU0}pW_CO z=!sddMmyKYi!^Z{TnbisjAIyKo(3n)D#J(7RBFF_tJVFKLnrgn&B$z2I(Npb`EsZF zmEIxuHcZs-hu%bUO-|;ieUZi{tfa<_%#z0V_p<%7d;$rfS%Z?QmK1dL@va{i=Sb@0 z)|Q9lk1VOJ_kxWfbo)*-rESy??uSDg!D0*5K3>n`Q<1wt$w_fZOZm#C`zk%grexNt z3ul)#HFPG?Tw-S2y@q#$5g{EfDWU@1RjI_JtTNoo>-+n-zzv$O__{Tnp7a;Y z;YO?8y)G>ppnv%(DSc~Amoy;qb8PZi6J6U>Ze^_4mEfx%ZukAXo9Y@G+xs)Z8#Et2 z!0EO)OHY&NOzV0`H7Qm#`Fk0kPh)s7M#i}Kp{oXQqUUR5pp(A{Y6%<`>f zEWegEd^Hr}ygvLopqY(tbGS20@5V)h`DkQ_reWK#zHeU35tRHB-o2_&Fvi`OvI25<#N1`%kD?=Ym0q8m{!FnnK}5vR14T zF4pNtfJXF^@Z&~?oP_D4q4e(}=^K6|8$i3ZeB42|W(`QkO>bpVBTFgu9=0A|7!^=L zOeqa|%eOv?YNZpe$*8uGTMRbj>uF|b3iPEk*1#y5p_2Db5kb6u2!^qTI20< z%X9{}{P((eIO;h;so9j36}6wkwcnU3ncW)PZO-+ks{Ua51`sOX)voYFRTpkx$l1hE zOAjd>?9)=)C@(!pe(4!ap_kXqdg)%%6oI^h*dbxPDNb--7muOt3#JfUKU?huuWyKm zL$KDD;rTBBqayJxRPYna z@G<9re31!jtl5QN+jH?qk@=0)Jm{H$`aOfFvO4VbK?(RVm1dTRR7@x_%kcE)fC!Px zt>0QmP$t_FIKy?o#~%=h-zMlrmelx>SaPLf5pa2!-hOeb0bP)#d^+i5FH8i zr~33ze$U|@n`m@%Uz&MLSz0V$5421QkTU!frT9!f--`QzoS`P}`APV5?Hy%XxnY0d zos;u!c-umHTpAYWno!BLNu^$2Yg8{>i-T3r@1wjcJyb)#uH{YjS&kselzzWvGthU9Sw04G=hV@piu{HFc?Z-FpW#*$-XOBW%hP;U_OuY>u zOXXvtV;u!Bo+-|KFg4agzlR=R-`u}^Nzu)};!Pg`Z z>THQXx2f>xwKpZ(E`MfT%D03bwrsb=hOyTr z>nnez*J4YmTU{;;$5hHYr`J@wraCS1nm>LoelbPXcw)DTxAoux;Ko(h^y3@vO2INO zAH0pK2YN&8m#(tVGodtP{ot423c*t2uFv}`3 zsghbqKSlEvjaCrN>@9@(b`E5ARqY5Yq+y=!MKv*Ms-4vDuN%iwAt&uwyUI7@fWLbi z>c&6h=b?h-?DD29^fkPQuTjf;FRRKd(e&f0#<}jna#hT>MH8Q-mF=C1&;~=9^Qjqe zmV)f)l*_TCilT>m?lm!ZNI$F`6^g`VjuyzTG{f*>6a{PcQ#mahXPXn}oonmRsPKA`1jXro*?xYT z*20I-hH;1acvi^aaPatRzifyjERrDKx!Y_@KAKnN5)n6;i|F0>68eC31y;1gWR}Xd zDB*N?6@8e29;*k%KDzL7BXNuxyS}@2OMZso7_ef2aiVNzxg0hz` zKWBX96KopJ%Y^JUjyVkGIa~|VEKH>({p zLtlg-X&W~>AhF@RR<&j)P@0Mfl&+^Ghi9-+Q9$}Ukx_PFm-JEUbGYF1AGhk-`If@k zFN_9k-!=zl8qD+awphtfQIL$KsDgt;(P!;|3dMGfJR$E_znT-u#CXzQ<4Fb?P9isd zTjpHOSj^$zYp4ZYc4Vw#06qINPBAQHdGJXGW9@?YgKnnYJ2&B3ke&f6x*guJI#pc9 z5A<0X4@fuB_o$~g%RG4oZa4T+Co18AGEzqHMk&gwh03!KdKGOwO%MCkNE@xKYm%(@ zx-4`@@$JyhL9e$AXWcZWxQ9=Rl1kR#=Uh~VFZi~UW_rVbEEdaFwR=V}nY`~@?0hcP z>|IIVG>!?nQwm_n3eh3@R=Et^Sg+*AZRb~lTm$k!2^|Tp$&|qWdsE8^$!SU8F>q3pTHEIiPWYB z$i9C6#(9@vpf~Cs&uj7_7Ao9gcXf?tPqQ-3`AVSfb0F^-O&%PFBZ0f#NOP#sK7RH- z0o{}5QKDySF_Ah8N@VzwI6bQjwf)1C_>MS%Ul~w7yk#mJz#48omw8RD9!bpOuMUUh zP_r2GObq6%be^E%M7uv4KY$T)U{4C4%mT4EDn0%A4-+3k9!2pw6K(Q0j33H0;G&3T znhjdg_`XNQe+W9uRi4Gc)$%R z#K~0XYNI3<-4`_$Dxl^IM`bBHGuFVURy*&M=Y1hs=<|RitGeuBP ziU_I@uCLOWC?cO)cI!7_W>1iC0@Iv5-2a$)Wfx{f@^kF# zt1c4pK0ed?%Z-gOQ~$fJhwBPc<5gcr#BA48w$(54veeA$`ntS zGr*wqx$b4CU)T;^Mt&1OA zuD20>mde=R`c7^y;VM52BV?DfKRz&g&D+-3n{n%tUO~%OT`_)kb2x*OrC2chIo$#&z2UNi|0igi|m zrBHDnFws5SIPveva+2ZlX`cuY<3KIvHc`lJP`u$TMxSRlaZC^_Z`u1qvU6DTJ}1(! zY4+Q&4PA5_OZsAjyntjCaQDIlJ(3=Wax{fHdMeyea8Z0bBTn!^k(D|=e{eZxOk7U@ z=LA+C7V+D`cqOEoVJf?6l6BnxBx0Lkrx!>a!{UMH)Sw9lcx~d*;doFO9~a&;ejeCC zTJiOMY)d3gF4Bzrsj^dC7*9A!wSbc+L}K~o>f$?3*oE`os+VycU`YM!Cf+F&tm9iJ z-S{oVU}{3Q4w6(<4Z{lJ5|qEQ1a}jY6%A;XwH{#b7Y1uvp*Cc6z*dykz!05>7ou8+coHfc%W$=n|mI#G-LgaEEfl3N#L6}8TQ zf}2U&JhZz)Jz#~z2{X(jm}^*1eZV~Y`LKMQ2bgFTW1OCfW)nJ}Mfl;t`e*memKd^Uf6BZv3 zNkAD!O&yw5j0cIoEr}1ouP@R>|71fi2Z?X5B(iEIT761J?Zb@3Njt?{)EJ7_5RXL2 zxg(iqDDQ;kL_z`2M!g)0-US1P?F0wsE#8fXvLHlJN@sZ;a+@1Jjr%!R@~xIQ3rYlH zPcYuCT#pa*{tUK~HE*G~AQ+zvp?Q_WNfwz&P;as)3Uk(MLKR93UJ+Q&|nkHB)~-A76PUiyU~1EU1hTB`2Z1Qdv*v+}(Xin#*T`Vq6v z-+DsZV)(w)TOt!j`vc(^Oygfv{A!zQe2Rpv52?)$#vm&qDC7&6Cjn-x}KQnn@v(2+mLRg#EW579v$*=&b&=ogp_7ch)4dbD{)Z zVvOXM+DF`p?Y~r-TDJI+mS00;_Eca5IshCXwIC_GKH}eiB!cn;I)h`V3%V3^OC(OL zNLOJ{%TP6z4@V<*wGHFu8*S|w!!ULDcacY@^e$ZQHhO+eXK>ZQFkP z{J!I!_n-IfsGVB1XVsc(WskAGJ$Frf?I+!RoInlmdxgk?UA-xsyn@f1rgyMf0GB-| z8MfoHobVw3YJt)e#w>a0-b|g^)((58bI`ATAbv^u1IVi%Zug}vq zZ1cR3-yJ}afCz04sL;8$Fjw@{#0Y3NN1MI@ur);e8SFa($uh8rBY#W=>Q_}HV18#K za7dsn+$|@gdl>HxTyH1HrF8>pId3K)C&=b?U~?x$0(NM4oO|%d6%dhMK6!=RsMPM> zOb%cZp;W~cj`PsOTau(KM`?hkMQJ9I%Eag%TallOD*IO*6%4yV;~9m#V>}5Lbo8$} zTiBEAX#oL9a9k97*{OdH(&rrk@baIMe@#sZTq_HL82x>5g?tY*a=C`UAG4&Y0?_II zTh)|6i1NWd*Z=D(q-rIw|GBv41m_QpHAH|ztr%IfHZoKRl$ zgNNp%ILglU^Zn;QzQPo0;(ti~F~wXa1VGE9X@MwsSNo|vKV`Gse~h`xtZPrmsP&rU zo!F~bF3ose)ypgsj?IRj)ulTVL#qt7RK(jCZ_d$yrX;X{-G8GaXz^J+`)zi8uI6<* z4_CeG$gsQ}Z$E#ey&k@D$?&SdaEWDmdVb#ByiW4+dc8i~)zNJNXk4Fn`8r?soe*SX z_&#=CtGe1guf5v69(!-;K0cl%wLEP;Kc26HtS&uo&v%_R0db{|w`^T5Zy&wyulQaM zhnrs4`*fF{@5`Sj`9(pmhfYzKUT&XnS9O~oP7Vw&F=?$MhbSLuVV?(UT@jid-IJZ3 zu5TwF&lk6w9~Ux@9#dR4UD0h_JiLH_)WISv&yNEH1ip{1ji+rRt&i(4t&L-A{4BSs zk9GX44W7>>F&REjXV16S+4mqTueawkJ|54y@UkOL?8n=pK`UO*_bJGagX=O#+=mN5 z^r>sB*NdEpBcIo8|L$N>6+UN{lb74$)%aMk>0F!B)6=|{W69^)XwmB?F#fRYG3qD`h>($-Uq|+w%oAu*+?;=$Ne&^@*#k1V-18$b#63k+t@j?9K_Ltt>Hp$Zp7Ti3J8WV zO`^B23Q^K%U*W_pYS=~z@6bGq7k}9Rcf)7G0~I%Q{__3wRq6+dTX3G;tlcy#g%V@x zi!};LeTBMq1-4hlZ#Pd|$;lY70kVfOditOMf_JzclzXej6%mfv`RYLgG6IPxD4%&| zLBZBu)3yz5LvIKOkh$qa;^3N#&R!3LcXzoOYFK88kMYKH0`+q-M>H&?=Or!aRVUXe zOHn7#k=L`E5Qzc*#4$3wUR_xhs$#H4z&%?ceU#V0jTC-1vYD zPCDRXx{U_#P3y|6WXM!I+p#8rvA=zOjA|G%W`VmWV(xy^@2dS}SckW%sFrbozKo+1 z1OwS4GonIn#h&!)6>2?J9>=yDCOvZL?*l^$Hx}SC`Ye|HQrokt&{CCqaA;-;|DCT; zFP`6!Q=b91HXZ)?8wH8l!wGDcVE1|bfGOvmCFhUdTGa%k^kQE$`a3`H$tB*mu(hUFjXojXPnUjD^H5Yq>rk!*%n;*BCP7$3hd|=;cYZW027$zRUB7TXCt$3} zhcB#O@8H;_2|+j`sUWy7h_T%@)WN@GhhtHs_OwcCd~ojd8AR%kf5X)ywA6<`4aQFC zg$?>HWlnhMy@x9U3&k~oEW0_0?5V_D;Q({(bNc+uBB(sbL*qr0jq|kcJjOT(ElY!o zO}YNH8h6=x2$w%4?`p`$41A@J78hwK=DYDADmVD-#ZXMEsR*`9Xq9i7tFowqn(8uN zQ5?5fMNtAbHWA#_P0)!+m^m$O(G!zhxWdla58R^Y5P7con=zdtycCV|Ibwv8+qvH9 zd`A+S`ZA4vx%waKDZ3h@3exuxZslV&V9Sa1ghgVBhJ5%ewG_MYV@U-LbE-@gc>_0z zLOjZmzZZ7L)s_wqmSOcEr4vGg!}SazRR>30j&M0@x6AXmGb2pii(`^lIoi!_Mtv+U zH-3GXJGu;O4%Oa2T;JTt#Ob-WwzqqEz13a0ZEFR1p?h{9RJVCk0L_#Em4MVkV<_<# z)G9Jc6x1nxmnf)L6gMr%SN@+}rIy8Tkhl1-93=-8sYtdhcMwn}S5Iu_TtX-t-dlj^Un2 zVj3ZHJjAeYZHzey_8$jsu}l(~Sn}bxJ+T`Sm)Jkvat%w@#-07g;fBa1rd?E%gf13+ zIBZYohI4Q66a8{_|2Ak-m8Hm1dOf|B!@=(4U~ar5O@ZzIx8M)8X?s#PWG->-VjCn@ zv21_5wT;FhfgEP=ROUq)tpa`4`|M2{@ql?2%q-u$L`pYvzt3Kb+j+(9&?MH@tRj0N z60LenxK_WuVsJlEB9EZLVM~uonrkvnX@h2+`2R8|L+5xWkvr>&A5rg~=E4MAi+Iy-Wr4ry#}he*r1B ze}fd`Uoj1ZtM*i4v%$WhzV{$g#>RRR@Y4x-A6!M>89q&c2+;{`l72?Pmn5vZZU(k2 z?d}^>z_qAX3=7jWL`TS_K0VT~Uh~+*i~*z09C*WwZ)c+>HWL-pAf&3N#(+}8@d3HU z9JFE(qNS_#1wE>VvD)$r@yt=9_XBw=n+N2pvO%gt2n~%)LESUDaAFu#Za*mv>-mQ( z@q~Pq^X^JNWgmb-*%jg~(uWhm`6E~|fE+gmLEki-jQI1%VT47o5=>!o^bKbSBu?qM z8z0%&HEB#nOrtsoD$l`1w9wnCB6QTCf$stPxH}!X{#cJdnuaK;$%P6>6^d-hjn1MD z#i@He`$?yvVh>bt1QuHJG_2@>`Yxljhm$94Qwzm%K?p;9!tUtmvU^%#(9O z4FyG)38JRKv}Qpu5`;k4)qS0$Qjd3{JeriW$S0T0ucf8Q=cxhPq3gaQ)Z$WPdRws# zL(^&$s%zW^^)dSTa&x=7+SIk#`EFyZTx248BR2g}%m4e zCK}Cfusv3R?8uh*CjzU#WZ+(CO@uHrYE8IY)|Xo~B4TS{V-f_ zJnLr3pfbm$`WzdSNo?<^oswN8?wg#l&+18z#Ju9jsEg2=#%jYjj^(D6zD40d`(J6w zcu9o`14csB66Hp;o*Pwrc=OHD&Ev4-N5(3{C|0@-DPIuF+Pu1KIFMl z5gSw7S?iy~`hmHt_MPH54cVs(D$1o%@;jq%AqP#|QAytriJ>ri$}%HU7F3h*?0b8= zk@>WXIVY!=_AH0gWTxZt?HdhsBWap{I$l$(=+n2H*~q~O9to(*!b7l8MOR^r<5oj5 z0hXUG_w)6;47Tde+)7?J*s1XA-K<9@d{0dC2%ikv`NvDkr}_l&$M1fD?RHaBAMD1) zn~)Lq+4vX@RS7XgXCey8yZ6OK_8aWE8VtM*9n5T2=osi+ z|M_#_^>J%lORf1bVBL#g+R^hIZPROm(a5ar+3o8p_ePwC6K2pN`*y?VmIY-7hGr9? z-J44HDPKr;EPO;v6cc0G8E&SHRNlJ+^S!d+-(fT~pG4kUhajn&-`3JWv+hiyQy;i>!#X3-)2aX5{@jP9HfRZJfwVcouBg3jso59J#7@s z1Ad7z9le4EpgQJ$+p>l`GWcTqGWcd#Q}}$?2$>3FxT!E#`?5TYer(!>oe0#l8q>sK zC{x8zMMwV3H+%oV)(ss7T7RK-v}|3xYxtBJ6(g`s+FHWodNj=|nafCw=ZD~vd-zBv z%}jK&Gk=bV0eU&3-%I#yNy8T#x)q!WKH7{%OXI(lw&(KE3QtD;y>sEWe~(?Gp4mC6 zfV>p682Rf@t1>{dg*rj&Pb6c!?Du16*j!$Kk2(i?Sp+i#B8ReGkKmo_IAE&YqPcyR zJ6JUi0_4OHFWq(nDnmuWdHl&V19J7Ud*RXF*O|O6{SDiZjR#C>H@p{A7N=jJ2}7Lg zEBR-c0z#OJCZ?cGrki{)I$R4u8C%VK$lYAq@Pn3CnP`jHFvXo#d`wr0Rpg1JQ%n%kq4&K5Hd=Ju4!L;!(CARTTA! zf^vQ|I}7Rg>6tRb6Bjj@TY$z><4jhC6Y=}!-VtgLIIB;RIpsow0ZaoPJ<>=xhd5KGt<1(2>-4-HEu9&~Uux)?%{(QZ{k2a;g%hk9yfAW}fF?a!6_fd9! ze6SuP^$+~3p*vP&7m2U=!|kbIn1PG)8hCWW@>)fUu$p39H1>L&q_dPMcR*a)mfiQj z*z2{%i1g{SwU7n<5m{nh_ttk~t9JKGX{tf>rnW<)gZ=%^anZc2PvEa}rWr-TwZzd^ zz_$Ec+4%JE&7{qIpD(YQlSPI*b*Yo4A0aLmu?dSm)wK+G@0B*@=i+2|o)z&RkANM! z7v$H8AhQQick|`q=3>SZ>0&;8&A%Y`ML@11c!sXXlf6aYXE|>@k>u7)j27~EH(mq4t$N= zlPQ1`>D^SFx?53kYTZd&YocCojz`d(C7+AJmv3Gjc1t|6yE9j-Z+|c>>mg;xMA&G8 zxQY{O0n%4oq&K55@vhN$cPUcWItaFo(gj=&`4GfIwkE$}Me%ORD9`zJ)_gowqc%kd zSr00Y=33Z3+Fri=N|^G(3EzqdIt6;>U4cveR?~OiX?JwKhl)@nvi7i+EF3y(Cn8i`~m9JeMtDO+eUtYcIFE*R74cM>@ zBr2I$3Zd99&c8WMH{L6*{&-wV$G&B9v1vS`Z*_*3|6b;)-l#bMtO+l-YASWVW-j%rQUi;4uuM$>|c( z%vn+t*D{#tW)ByB`-#&snBoe1f5x<-#DgA%e(!z@W$^~62za@baQCOi?w`?k|20Z8 z{fqmE0Z>Nozq322j7ILU{$cD2gyn03p}E1LiNgDx)i4YaG>sX2;#0~i8|gNr*`<^; zjwrc{HJXHH+U7C{$LfBvH28dmhRMUp=32t;_szkH?uneewvax~E4ikX#pgLy6 zh$DlhQtf^l=a^rClQo@#wKh$Fg&HgKiRI)dE@?8)E`aCr&Kw|#(w74@o(ZIn=O3E_ z0wD~e{Yq13_7zQ3GAvtg+(lJ8uNeIIyb@xXepna*LH}j9uYH}-8_lvaY?QoY{|wc- zyzX1MVI{0BttTY6X_G2OuGAaWP{lJ~19ESXU?pwcOu(@rx!=?T1(o_qBSFoEpl#et zCqsTxUVX=WyVO40&?OsDGO;rhL}?VtgaHnjB7O?>RQBa*`xhiK+{!JbH1}l4B z>W|kPXRe2Vz1BM=A}o3QY9oDt$Rc3!x;nThUL*8=7epok>uDTjw_9 z8vVL;ixsKLE=c5>)5$r02MVv>Y-L;Skc!*%-t4Nlx2h+vhS<4YCjsZ`sFP3o+dA8j zw}{H=s`p?z=+NjH?{7qWRNSlT`c99w_sEI0jf128yTkMIOE4ZmJ}axuE*W7s45uI5 zXR!g5ko-9a{4mu%kCVE5R$e${Bt~)lPjc|+k{wH8+NN_GRZrCBCpA0t{Rg4x#Qj{3 z^hXXeq_EmmXJ|l+%N8nUH9Ntm^C&KfYr6>{yUwJMD%=}+b*x$U#+895q*|q;xw06# zw0hroQ*zuI?>X?>`)4QRRs8YYhnAbi@AcDa=gL^`dBivJ-p+S-7y0nJJ~-zpTp67r z8YB-o3C+|}zz4p-a>Gcn*cYHNz8Fm0xQK{$tss#LRW;WiqAubnzDai5Uvu*4XW)Hb z3>wHNx8)8SLIJlPwE6N9qw!^TeWD{{`nzzKE_b2l>RelJV8J+&9`TS1dnHuV^3}<> zl=e?Qqs=BiXN8NLS~%R%{_L!KbIAqbfGi&QkFM#5u~n12%aSHLP$J(Sc%I48yzt1G zw>%rf1`&CaJ_G9rs91wJH+VBV3uQ$+nQjGG8IGzU`i(TqSh75eOAzsU4B3j`AQba} z=Wp?FWBeifxis$%7J!Xm?!X5?2A@@!>G)x;Kx+Bbv?)*J7e02B%KD!hkq&#Q2{eIi zT((^===QOgosFKb@8);CPJ57p=)jOf1aK7gs9Wk!HJ&8;qtQWZSV$@Z-3h~+v2GGf zf5da~=12M@wIqClkP>mbu=q&T^`@C%;UyO$ z9E>+<*_ulDa;N&H0(K+OOha3U7M<31gT~gwm~oF$2Yd&z zF(+d4zciHo>f!r}iwq{ZK7fcujKm+kQ<|A;JT{x@OOq=P5-Pdd6QLV(>Nj&nX|6cq z1fy9dB$aEZG2R7&9T8uY!>aI&z)86S1bh8N?#e-xhxth71GSxXoDVgF%Cu+9GI)vv z&BcJ=G=PfV5VQwM?c$oh7z<&&FW;4-!n%%asz?_TQs{@2A&R(iI~;2$84cSK#rS}B z9w7fS!jvN*Uhx!EHebygj#Zrr&)mL@gJP!Q07|x138rVL`psbp;{f;|AUx++(B2S9 zg=bL2^4YJYsgUlK#{QX6=JZ$#nVm|n=g-fJUXxv(meb|+HOw-@m}E~p zc|XF_j#>@3Pih;1IkX{nve}E_Q~fTq*{uQP{Qh^dG5B9?e`h3s_sI8eMq-vG#5#%Z zh_1xYrCE*bRiQcwij5^nYxVvGkXqJy6847o=$de^WuF8M?x4P_=-JDaGkKYL)WLzB~d+pxYTtgqe|h zNiHT=Im|TZmtdV`13hn*R zu{SGQ9#tK#hOHO6t%)znng=DN^^4Lc@3nElYhp#6j@Fy=^Y9mKdL#>nwjzxuxW}e$ zuW1oD)u1+8xJ&Ni#^-Ts3saV*w9-+Tvqry=+b7sbX$MnXEz=wvM8jso$J0+Xa2=%Q zz$JJ#G~5;S)Jat8k+^Pk@(G@?~1_(##H@cptuK54p(Q zCt;~X4_eGct43nr^rC2SSBcId$V2*#AlSBCxPLKo$4ut=4#=wTW`4t!-K#RV2*I^w zvq%%qNpvEb$VOR4m@m>PDSohQ{hDW&%(+u$S!sJ@ttA6@hWPH1wE@NLK%ra|fV`3A z@v;X)=dEb&3$u`ZbeOzt%9e{C&Xx#4+F{`LO$8p8DK!N9vPcdg2JD5Rl9UVu1DMtL zD-XD`UfN}^x=%gdtdFveqpgKIWHr%Av^2KI&F@WGdt*-tQUjK|7^Dp6*^*`X1iS56`YrJY(qy`)8J7707luix=Hc7xc zU$yTT;wK4zG+8kAC!eJF&Wh$aZe&^t6N!Up%pB^a_PM%g&x%{l!Db%C(ql3B6d=w% zn7tcBPrP_b-lnIZHmblhz*>CnsIbp@Fh2ZurLQ_n8#EOP9U~#0fe_bW&zLI5>%W#cPCuc72l1t z%)qL@lTUKJsx#v2NmjgdSf^ytdH8rubMVc`KUL$azk>q2RqMh5>H73TLC zDQ|{VFyu@71d|a{DALVSv9>Z-9EqDmS>d57I$?^_<$cjdQ@9(ZX*6#pMkL0KlWCR} zidudNHnl^m!Bd|OV?mO(N~HgNEESk@A*Yr%HBv{Lmv9&upq#|QIA=iW3s#ijtRS88 zY+NUp%2wLiWXN0ZsyOam+$=dBKgrk^EmpYF%i_Honi zmpgN#dBCnHzRct&8eL(pj7-q|s~4 ztaxIoc2m6Mxf^eDAy|4)j|+xm%NM@d6vqxs7Yb~9si_?ta?t}lQ`+utdA=O`1>HuksIC;>}vH`*y2*Vr18tBUuh_N>wK{Db0 zXxkkhPq$TFpRb3t{KRxqHq6|k)HGQRj~+`n!kk*RUytRo;4go?j~-5HeSE%=f-J^Q z6+%oO&CGdWBK#;u$`OV%v7c%U#^ZsA*wlYEj7zGYhupIU{|2u@m;Te4^x1RJA_33T zRC6NED^=U_yNQOe!m)T<*Ms@r?nax9PYwvim$`JZr4c_pdEisGd2QA+Z3w@au zD-w)b9r52K0r_a>oY5X8Yjb7wUs-V~v%rJ8n{=(y#x)ROA6CM0N#K5fCL^u-M|?POWsWYPxVwGJOb`A zg1Nr~>q^zg+-gE&`;;)2AvDs$cc!!xEyO9B|gT3 zxPv@vVx$aj#%w6gf6)hZ=+o`5k!3RlrvFD!5_g?4krO!|ow)x$C+gAZl`50z94*eOB zGX4i|;=fpi{}M?5!>z=`_*Zsn@_3{^Il%WFz-D}fplvc2MiVE>lnSV7B$ZDgP83l) zzBi1Spwn`o-oFpM7c6udDUM1yumk=`w;6v-_L5a{#0E(gg4 z0oSk3)jP6BpND@?n*sL-1GPhGuy~y!TaURK^VD3zjFoSSp;ZRYK77MGAX^V=iBqxs zOFdrkR1MKjx5@=j+TDKF?DMEeXp=3L8T2vMN-0ntg<_x^rQA@=jw;qUT{|)lOf_=c zJeLJg;B{W#^Y|xN^Ag>kmF51VuTDyN7d-0U!PV!GbK$^9zLYGLvzzI60)IO})JiMF zpk6M=KxU<{b%ks*N_PmqXqgGgOD^%Am%!{$8qdczk1^hJS*Rd=g<^&Kz6tI?9&=cl zE8Hp4$}bfd!;+!s0Xe@o$Njj%FJ=BP51q4o=Fn>T3p=mGfJwCsD>Cf0z(;s}s5Qbo zZZ0^saQUa&x@P!Vsqp%1W8azEvg?kx^g^kzvQajLrLIR?Gik!j<6u_>?_Hm&^0ri>d3qJdF>4 z9`3Pb8Y7`=;DwX%@a4l_Q+Pruige+yMlG_d^puMwQi_#j@PuTGrGo1=r#t`JDwy`RRU0#Yo=VI@F{&ZKUAu zdl~a@ypiaOd^H_Wb|!kvEeu#q8Pj_M#u`q-aoBkbRuN=Q!g?!MQWxO^%`B;Fm=QW= z%q>hD_cZGL>s=&Eu7*62m*+(V^-dy^Vwz-YhV6B=f`^PNWll5-vgoBTbR zjtxNlPCu>6^o$0Sxh}N#Vbdr_NdxzZ&1<{@5gXS=EOll-PPc3Q=9$cuT?#RTMV4Z{ zBGnGx=#Hxxs6{{Z8=~j;hJH8sC+rUbc5+rw7d9n2*-|7`!*tGJ%aF++mlW@vhg|V) z&aX6@v|)Cc%rn795n6iR&><~?@^_;Pq%a2P*LpsXwSm&OQwXmk=_vnUc$AfRm>T}{d#o1RmU&`eL@ z5WLwH`mBkJSR&c?Us4C5FuVi9oj-bUurTB&GGwA>P-db?IQu5uzf+y0*k5NFyVDbk zxtp*6u5jsoE(F`nW&OYsxT*Rhr2|dI2H7I3J;>;k_W5r{Q3T*& zMkwTEqB)|5K1OD#s39it+@1B)o04E7(cF>D92PKcK{L@h*afT#RS2^P%e>g!VzyCj z@$_?kDG=BrOreSkOeIt~ZoxTIR*t5=PMn=e<(hZO%;B{G?wu34;p zt|`5}9k*}ldCZ^EW^*<`>Upk{LS}P@EQ+Eul{uv>{Ul2fr7Wq7N?)vR zTU~*;$y5q#jmmOX4ll@6+eUE91M!50JB@%FG#sd4el_o(41#E?AP9kPZzd>Cor)`s zo`x2aDhmoiKbss6>Oy84m9q>r6zZW)B$ky?pA}V|C+0j9vb<5w%_@wZp2VPX-8x-L z^Laun;AeS$U=e)jgRuZQml&B+wGd0^F7p%VhUU0&|* zvy|#z$;vNus{I>=V|l3Q)Ktohe=1u@pe4dc&O3$BRCPZY4DB9ozpXxJ-7acP2*it- z4G|c?tO%PGJ(yjMtaF@@rJBKaTFx$Ie8pr~nt z_Jmtw*=^xr)aB>P{m;FrHJ}StYs#**mdzmsyBaEEEn3-el-eFcLb9QEkb zSd%*Dm&_7Z31QSGo$p8EomY|e*zB1nV~#L%Gy7_ZKMtjxBcN0i7I|tqa|S6j*FZ<{ z*sUZ31-%_W{xwgdU9>?J1S8JtXc)q3Lcc();yK~JfSXT zl=p`X%ZA~YvCf^5-deWb%NzFF_1&Gdjx{|y1`aH|@G?CG1_~@R1h^j?EGz^BsGmO zV*8OC1^5ixqj)yX{VB>IX^uYx8%ObiML2&Kxa9qP{%?Yazt8{K3EzuQI18pI1Ec{b zKxfDw#s5tZAx&v7p7VDB>)+@9CRq8${2wQn{O1V;(t}+tnV&#Jk&8z~mxXDImg;Rb z=YaufOSlG|rQsH4V;=rfL+4?LID{5RAjCh=2wnW=76sq3)RIh|9G6DS*eceW9eYRC z*X7XD`dAY?mPkgR)Xcx0{?Uy>saeWbvWhkJLRYQVJMON)Q9n-Qm$Py(tEr^nq?VUS z!AmX7lLe8T+Qyxm#yNAD?>3|@SZcKCYzO8HH!hLDN{#5o(KAzqN&TM6r(yjf7+1Td z-?c*0$Bch*@qwxh6gb)@@~2?^A8c0t-!1BYN0YJx>TUm5*OhyM1R%fw@kiBfY}i{F z5MO0Yj4R=42gvnLh>0kXZ`t%U$rop#B=}5JrGfi>D`ETTq>7tP9EXqVr{W&7L<>~T zG3!+N)92LBJw?zzxx?IT-iISdag_EN7kHfJH_F}U_J4uXvXuHJ9Z=Rr8_*~nF`gR! zRBk(c*q<4%X;*EXyjdN063UP@sj4<5Q@3e{29R#2=x1;S##Y+3E!%c-LOi!_lzjzv<0l@c~)V8*mD ztsvW#t(ezOKQLvKwqwC;YDo=W)sPyn2x4JdR}5dVU!3H9iqxGuCoOGAbRrZfrMkpq zS)}3{uaIhKaj>?uOd(bbpNK-syG8Fe)^lP=H!Lqdkrt`A*H^XU!CY8t9bC|ul<`xe@jfRyu-w2JYm*MzNcJgwQ&!C4p8PXKuowF4kBF& zo>8@YBG0>CYBe;fhBV00x8Mrm9fV%^T3Wo3QjfWKS($#W=B^<3;oFN{7su$whoZKd z$Z@)A)nlq;1hW^a3tiy!L{a-w(y4gHhnaZVhaIyZ>w7_IhdlJuiQuE*1|T;X=8W{W z15a@6?Yy+zBWAO{>2oGN(j&dY&Ir_@f4^0C@7o|F~Gza6Sk?Ebkp>#7Nma%#?z%x2G?k4X7G1v(cOS??r^<5^2owu~j+ z!^x6V_A~g>CvK`LG%$&nqhNck|O$zF{NtnYV07`0<4u9x%9kMnWj~G^=9iWx z1LbtVm!8o}^#W`{M&&qF{OObahXrP(YYNBBP4|C!(is4>AOHPPFIO22#SuY#OL+0C zaGwYD=l>1K2ln-}gO+#cW@QOt7~{3t>vCu;C8MV<4x}zAGoOel5AfK#r8 z4S6|Xhi|U0qJ0(!m*ZIzui}a3;ZiqSm#gj~QRiJpCM^GGKNnuz_NbvT?pbhCKtm`l zDS=!|NAqFcRh>3Oqh2r{9O<^`A2HXnM^MIY<^XGxdYG77WxkZ0JGi0RX&fpOU4i~K z)94#ke!i7E?Pxu1?&fBs9>M8mHYd+^IgUGha{v@PL@f~7Ex~{wN1sb-BU?)dEsYoK z8*fCcbEs_{%mG}E)` zgJLJ+2=dh{wWDA^xKjroJkf*!((A)wSH7ZRcf2~eiil#dKHJA>#d5NRhiv5xihDf9 z;U#dWazGMmUvLWcUF_f9V!;UZTiham*G!8DNbl_?Ji+OdVa({I&0gpD{c8un0=wNH zIP)Wzc~Arl9%chL5~&>&2l=NDRMK2GcKKCLpMlI_zmThwX0!ylRTOZ6h#PZ4XI{rP zA}J@L0c|A6AP%yTk02i`AOcUu}FfOj{j_Qb+shTF6B||U$j{A^q<|QavN{Ef8Oo-)CfF^Y?5-%aiqHOsVWKf|~ zqEo3ur7b06o0C6*(k3e}Qivj6Y9L;aZzx`ppH9g^7liunMdAheuCdglvvC@fDVclU z#l&Q6OY$=*Ilcuc{d19+jA>4OK6!zx{7jrY1?90=NxY;CKvf2%Nm6g>LiHT|B0Xw# zK6!;aBTq>}?6Ft{VBR!`q=>vh(KemZ<;P_wZ;yJfe6L2YVvk9)9L2B+Ds}vx;tVt? z<+xQqY^aJSf7TiCI?b!Y2rTTmKSAc-RW^>~rDDa5 z0nEZEfH?RIsjmjZmp#%eiXS^Fq#JAX`_?qPhsvE2vu4>Bb=6C$6eL*%bg(>3N)XG! zK>k#rFsOPC8L)*lbdV@uF5l%sB;R>ukwZic--#a zPPM^gD{;MSFSFr8g#2YlRso1)8@*A^wB}EnNLVwEYQ|84PX2)y_JZm4d7Dsh`yOam ztwte9!mSaJQ}ltt!Eu-q7*(DE#9#M7Zv)slE_%NQyPwxhCA%5IN4QhsVA%-}YvR*x z80FN!VtvJec{Fami)>@+d0*iT6B*o3?elwhjdP9`09BDO%j%ET;|Ha)p)gFuFg|lz zpg8bhTH~W;X8zi~SqG8}3>Oh}#2Z#vn+*(QuWiUYKkh$~&ybr?<*|eZRbif%Ipr;Y z|4M@w`4EVHfXuWjAB(TiJqNg7yYHartx!yUI{`N1dCtIgFc{6q%5+}$^pB+ti_%X5G<~0fB1F>&2uPvu z6Na6@qWFBezJApL)snF4`fa)92W9T;vS$z+g#9&kr&p-;N4IErMSA)D=cznJkt|)nJIaD#weFWtBC;lf%VBfbw5fm_acU`9UzW{)Z@gyCC}-{N|5bGh`Iiu znk-J9zGOH*UUCFG?p-HPEuSSN-d9YDtUzC0OY-5~0Rs1rlW3FU_2nhV?)&w#5SUbB z67}U_$VgM>V#VATmhm6*?}=!K^U);58B0jx9&~H@&cx#Wke~u2CjXSA38}}5Q3LeM zh?BiX3^KBBTk)6!eOok1TSkt*NiM^#v;Yo*`inZaKgE~-Q{D+H%fIo3gY_@)1rUn& z&wyzZQhDzhNMdwy2y=6H?7dih9D*SBp>Z#KURUAWW4sB2q_SL9#gcrBZNzU4TZoi! zXOA>`N0(|airC4r**V~<+RQNOK9p#(JqN_UyrRK5=RcD*)azPPmja{4W?&E0VF-T$ z1wg2K2NXut&&bha&%D-WCs{IUh+rNOm%}N;qA?a~ zRf3>m^vF*NQN|Qb>G;`gAYBHVYbJu9iseuiQ5d8eSL_RuzQXsPkN03{FLlRV99-eHJcwf7bTOJ-5FhIQF-bZ}k-cO#tjn|7kq#V?rRLEAI zoCRMnEOq#$&4OM-D+^vPTI6%zF1sMV!(}m= zmH3-Vq0Av|q10?rkoXbiRL(ou%xKI(hX-~mF-aXO)t)9>WMG2e^yVjWS^3XZD8OA4 zi6r=eC`DFkdOcq3C$ax-Xzy@u1Xn@u@_4?PJuT~n!@ZiVRF+z*e!Zy2b}1|PXzw5L9(Mf61gA+`qtEQB11i1ar9m$}NeYNuP{G7HRaZx{mw8=y*3L1!`d0lE@g@1Lp7TxxtT-#@G zzfpS@lVjKPg~cD6g_L6%z0jkvvQn70htI-+z0MX3crCo|R>!QHd09Oyckd52spBoO#iR;WW8q5{Oo0>(yuN?g`9eNsoJ|S_85=#! zFQcel8f!=No|8=i!61tagY7*R3MvWb!BBukBn_gUT`&Ppa(|JVDPGywT0EU1X&_ft zhan+XCovw#kAx~%ELut^&!&_ej7o@Q8k#DHA_iV-gR&2($FHQ+U7*A<HeR-+W z7BLlEi9=MQ6h}iU%oD|N*WsrL8fD4I^pX=rP_!EK$aK%yY6m_4;I2vN}B4h)nuD zD1RcmDGSG+Kt7DoKH36cFtr zR#g|`te43s0I!!dDbYGSHVOPr#mynI{t>!lF4v3n-INDO-U1rRL?-y#d^AC;VggaC zq67+M{)j-P5)0079n~L$b3}cqI&9B(QDJMV<6h;Q3eKJgu(pwPMBs$ zTwBA7JFaJ6Q#D6>f4Mu&;{0C8-R1s%xW7F+c$tNVAH>>j>gM{qyT6&#<>{6p-7s5H zvrVSWN-9ISX+(2SEb<-3DcUj?g83JP8~Pr$hPP_!VxL~-t#O@U`Y{LhT6yfO>;MX$n=%4YC9R?6bpz-eBw%)El>_B+l@ZFH?%pzu<@CZjc z>}%{3Nn|xi-SQd>JMAcojcB*(l4jQ{HLZ^23k}Zas>d)kC<-wS4A0#aF)w)Q?6xnC zJon2H=kl(P>&`FMW#^sPZt%LAU&*&sh0_dJ61QE|;#48h3@}DdI4Q<#UTXO94)J?w zqp}mnKUn=%9t12_vw7{Zu%3CC$?$x}%~al;!5M@^2Skt3c-SsR7gQ(pl4S|JF}Buw^WH#6EjvQ@Fh=*-P){ zc=u*Oa(uRoeeAM0#9`@lC=g^x6{y7nKf}!fyZ)J%CW7#r8n@qJnnuMAf|Qoh0X>Vb z(6Q^`tL{!&tJ!k{M_I9k5$a=u>VmFjmtj~;5mVh=f{h>q*YkfJMFhyPwLu;(lPAJwKbkkfah}4{PfSmOa5<( z9RGp941ZxgmaD8rlCZ$OC%g%ecakP56W3UEi??Apn_YJs@O#&XA##sx>T#8nrd(v| zlN=PR{yN-Sio`9#QZJ(HEMZEUor(ftDn?2mMT&k~ z`XFapVW~FPk74MeR&1H0#^M|gt*`ZSiN?N#$Z+0SS)r$LaV%HWjCaaTi5?&90(1=0 zhMNh0F5Sok1wsqvz;F4!+uhm@ztKY|)JO+JXebWymM~$R5r6&$Oz2i_+C%lCAoR7G zKw|WI%$*rJ^VxWOG56Uz$uI>0^>G`7qfLw1=S)dGM{np3ttTU-0An!S~!jgrJ- zMZx0{Qjsn+At-gOqF08Px8JrmtsCOWa>_UwZl&Q@IK->I#dXy+WuSyhX$=?DzEm9w z&l}YR8bRFJ-!BL&dG;2Ej~llEmHj;FyYZu z2&M>MUcK4qm}_~XJ#zMY-Y@>~XXB)Q$<2+RnxHiMHq+&D(&OVGby${XX*JZJSl|xY zowgSb!BBB|kDoduAoE*hUna3!q08Kw;hj=<(R7e|N}ya2^|({oA>+6{kqyI7vkTHG zCt9P^3ybpcmQLfB=chR$_I@b(vMqu4d;8}nkgrciN4_i2{}H8!|Ml(q&rv98syCEY zjawUjsjjK!K!DCpQR(pdLzIWe&@HBOp=2T}3RX3{5yOf#i?XLP>y=T@6OQlzeMbCD z=c5wdY;E4Rl4K7kUizjz@7~?xKZDrAzplCrdQG=Yr>l?Mj!YOZHarcUe0`~;GW%VS z$M6r~w2hX;o*3+^8)dbUq)O+oiW1f>ZWYNd%?kIq=q{Jz4Z4{U-axe|rB1CAJKUir z$hY{#Tv1Lqhu79irD|x{UUFRtc}{=_DIu+CPZl8^j7|>Hk-$YIFx_b}_(*g{7Os- zy;569tYh|k{9XPli-Ef3(uN-fLPth~mXE$m)zg)3`!hD(9*M?~O9*B9ZYC8LABIPj zgw>!s@!@-hZO+<8|=cSFSO7vM8K->ouMi{1LRb$tdiPed|Rhh zS^Y~$<$X_-oaiC`Z^77&{6fK4jlQ2?CoT2>=<1-4Z6b@el(tWOf#_jJpwWyx(vH1j zZtIKQC)A;@h0wj*h_M2FrUm=Z^X4_x=kmRoH{eB7#YI>4vXZ@q!gguMS1^0DMTzD< z!4a78qN}2dFiQjVsGwp&(wcZ})0v`ETLpcC88|Gqv{Xp^v2^AKfd~f1@__lbsP!L7 zM>rY(d&SMn^dCy{D%Jm0?E$K74v>_@p#H)*UHJ@Q$hm^M(&!S|EU4+6+QUGUx)j1k zYO)jUB*yEV2vj)5pqD?zpx*tM4D9cn7a%~=w`c;C774RVOQqW8rGii9GiNx7;r4TB z4>`wRR2%DK6)OP1FmjMRv2l(3u%(g#jF? zBpBxlm0!L(>aYdR56CN##mo_}FUkTrn2-p1cTeI@i0R}wQ+Y%lJ_nmiF`ml({H6>d)DFZmS^CxcoqKNNjycL|^JRi9upO5*&3;5fsZ#&#L1o#l&UUpJ@ekoHVy<~O>IGc>@l+=9i zZ+g3b?19Wmjxu?&l_xHt5B4VB_O4<-_C zi)*e_((_}=PA^d$pLg>{^Li+uB=M(%TbTqy)*r4$^AeX!jHc9i>?)7)Qzxq$nDHKc z{}@ij%kih_n7n8DQYVJ+hi~_Gu;sZ%Qg1f2&90jGc#-$!W3vUw$k5ojPFKh^p6PA# zwrtY!=&k5ZVO#}7cKjQ;<#gdyQ3bP_H zuB}DJI{HjIiHJ4(XTD>FOhGd+M_&hGpOob-IZcoqfJ1N43d6apkqghjBG5XyZPQNx9-M5ZYxl-P4fl0yS2I~}R z-yjStIZp6xyl$XfL55^sT`0!Hn@LwC*aHgDDI0f0ngnE5>H_qHf~TDt;q-H0=7jL)SxP6EDDR2%8e}8-a4BzitPFw%k?EX|`q~X9N zT6qN(BWG)b;ryITW(^Rh6{}xS+KE^!tU8|7(f=6sCIX!x-yxd?NGwvJc->&UFI4SB zm(rAmy^Fhn4`RR4jp73ae#8#m!9y3(on6zS`)0%4YVs}`YK9fH`(xu?BmN?RjA`2) zw6%}IWKa{%sTy+64&N3UXG(N@6mll>(4gMBvmXY(eOM!F0N{snE)Vt zT(To=ZDiVhD3bp6pNOUa)tpjyFyXnm7_sFrkwmkJU56S%S;^?R88Kz!Ty#xzZ{)F? zMcHSzujAObI2bjvd53qeqB#j?^dbpoM&Z;^07yG59zRdd@6e#LEnLEIKq5xXSl8k; zq(A*?T)ku>vH65$u?8Te5-q`5tsNL{^};2gVHk;EQvN|ZB#&nLX&27QgSk^bea_&U z#mb-BpPHYK4w}`WaT59B!K2!|hrou`yWu6~0Q^Q^M$q@*QM5x~A+&lPV8Y{8q1qBb ze7{6aC&-k8=Q|hCaj>+fzt?XEF2+M+J4dkpL1Ybih`{Y#KGddYUmImA{uu$#_i(-F zU5)P24-yQcRyXjVa6*Iv3Qk%gsgJD~c}&9R>t|tV_Tr$i_%jKKBj7jlJqBLF$EK*HEop=SMQa-iTc9aJPny*h7kNs$U0vGdgxLY z684AN9ro&87r~M+H&1m%Yvf)zen!a`40B6K>=d46AQv%4A04OQ0v)H=zNxyU$YYlW zY0_~O&Kgta272Jj?}LM-RK54tt1G{cKhKx~zP469ozJ?2%d0(Io$m*yh_`EFXP?nB z**Y2XOYh#_=O(%=A1gP2_BFUM6R{_Bzfe8#ndyW*KiAhIca#PZNf0RyXG^>8`*(1(R~WnnjVV-S*GDCIwz@x$U}>iNFQgwRju+_JyNaX zXW!xN5%aC$DjEwW5C>CO7!wd${jNF^4eS$0xX3=(4DX5V^vgj=>1@tH*z{QA!;JD` zp4}yJnpnliWNMNaw3VV(Y)c=in(C}!=6vWC=HPSf8FY!RZB9u6%lOLHvR4( zj_N}=r=(D+A}tkB6v6e}KseMnlj5zk{=2>ZeDNk5iQtyWnwO9NSD9P6O>m|wydlQu zURMH>VqF@gjuQ4AQ zHB4>}lwZ#jE^GStX-kBkCk=QK8E>B8U zemz{y9hUYq%*IGal9^d+*lq3U{x1J#gYZcyc=3;J;97ul5X{<#pJj!0i?;rerBgpJuH;JbfBe_L*TPYWiX|7 zE3Hgrv?ZV=*PZVNYZiKs?B-A z$ts`h%MH2pmHR~}BoJG`ghe6)BH|Daq70QtWxBzD;fDHarATw&Ks;M$G3*5ueZ;kR z_$URLPf-Tum^cmQgrJ7T)~5ZY_$;pc50b>j;^dElax&c6`4J&Ft}}R4q2glU7mP8* zMn4f!Qry~wC@7qTdJ(EG8AOZ&X$8ybHQX3j-KU{lLApzZz-zgR0F(771npKc&@Er! z^_}&=qjqTgnY^JHLoUQW4H+-w2YF~No!IYJTJf0gm)|sUt~j1C#x1m2-LAcBIyh6c zqHL}1lGk+u%u(@h37@D9(GX42et;#9qu?yyn5P$7Qf;{R!FGDNJ%wE+^#hUZ2YCeu z2>q5~SZ_cA;Ej!y(2bJyhx9p}r{>7$77drxmzC#}enZ1N-gApfHn^{>Qd8NOT}fB#*E1N{(NXt3*;{?;DxQ#3b}@ot%DZb+i zmG#mIBMQ}1*#yb+)Ur4{)p`J3qj+j8-z&x1UTEi!wGIepX7&kNN`6Z``8*xOf`rB( zF#O@d{g-*{$)&7P0he3*^U2-X$<^GFo?NO9-DP{k;`^sR*?RBF_ctSs!8Hun);nkH zhi|Dhewxdn{clrKTkmg2t9_0Z_OzW@rH@+-hgg~9jJa!y4^N#R4}bpb9zJCakyF>=1(YQhnb%mS|#>D}?+1+>Dn|d{E^EUM29ge~m9G4B#3_sPO+YRYN zYV^uj_9Jel|C+FW3Ad}y+U)BJj0R)-g_iJSj4O1{vo z1p2$y!`fYbyKsa)`~7uU{bv*1vf}+FNB`lGer!d|T>Hy+3+lepd7zG}kwsBF2WZp0 z%0>$heu1t=vztS=k^8x@2MNcvEVnZT(u-_2gaDUD?BR=<_e>+&Tvne?p%~jfxQ%sy zM&j@9A8?Fd;~}$u>z&H-kE99zTR+Cc`TtH2SE_f|lK)GH8z34%Lax7K*um?M#C{UR zDwzeXNcOG$?d6(jR(q?ih;DtkQ#hR0+o@PTF-fU!Q4#!Q3my1=#RXpbIWCWbl#y7C zRO&04m^{e?3!92PR*96XMEP!I3a9#4^*hs1E=dZ~FOp?q`e=ztj*>{Pa7C)wrDbep z4yRwlBu*D{8|OQ6PvDr-MdYVfwlU@B{ofIj9W~^W91R5FP~S9EzUDXt{w)5sAb29% z1odNrYT=sS1k7A?HXbIrZgNAr0!s+^^z} zAMqir?M{*|^QIh00f>i?NVFlzh1|>JR)l$+ii!k6A-b`7?xmQV?6EE?1QS6zD&n{1 zY6~+B0!joMpUv}FO-04#+B~LvoehS70W?p%|ncbQ_Cq_sbyeajOWzIA` zvtVj=Y{}4g(s!YIno91t#j z%{!mIgAE`}PJx|qHfKg{*JX>GRw#p0&RUZ%GliWe7a`<$lU(pwC{M}7G*l2A_9+XppLAu6;9+G7LQb_ zO;Oqm&6d>+qf#QGA(0jOq%+SRPFS(ts=TxlCdO-6sq@U~;CQgV_yTo;gQ)AZ^pY{y zzqa~qyICKF&V-h}GBy zt-NYBO9u`v{~P)o3rA!%0rMOTqr5DCui49AJw^_Ld`wC+yMiTdw^XrI0z z$erRNl7YGn`M~7^QxZ};I)N^&>P@;5x3R&F-o9n8M{8YofzLTqDU#neak`4;WiBUo z`|WTNq1ImMJBbpzr$p`cn8I3-3W$E}XU>peyT#bv1o99N*U?XHZzvzG2Vc+U2_T}*dr zgob{Zue|UlbK0<>v&im+QB`n;j3sMSDt@FTkTl1}$&9|S_mPr;Tn36ve0kYz@+l8cmGm0dQ1dV;#g>XjLM5ReI|e zxdbjqsC~^eim&D%cv>)eXXmCIq3KsR&5*HTuU@IuS5p7tF=x!B`?E`cFnp|Ii1!xp zmfbb*WjVR`S0;SS6r95S$c;kpNr0D2oRDb%!(fah$QLrc6$LD16FN*|{%A26Sg`odPhX8W2mNv09b2k6N#Kgh&A0jho zJ#5SdN`8Ozwd&GqY0A{%qFBrbvXg*^B*TKmUu-GGo^@AsH?V_Cx;4qQ$#$JPqPx&F zq2B9Kb&Ff*ZvnBu9KqocUBM9wS_Pqt+IpdFI^dcSH=wl+cLM3$S~xrnV1s~+z`iCd zRY&j$!Akwl;Mk>&0MyjQ6=>>+y4i?k`Ndf)s>j*PR>oiYlrXn#>|3T$U`1E?tn+Q&TYX$yb zj6uRu>IB&C^$%u@!jZbz{$8 zPGJtL*K8H^><)ZPF<83c4(9GANu@ijF$M-!5~D;KbbzitD>`@WZ+t zA!o|YeF9HCCwOoSC$!WJLycLKQsT2(Jk(D+bK0XyS3>Otz)tcFeHXTzHJ*K+zgjo<>pdy%PLbo%lxeAI+|%YU_PBHb4h!# z4*h-@66^Q&U~-B3aDS32U=7J@`F?kB)lwoy)Z^pf%O4e8QJ31Xwe{}ta=X-W0wmhX zjD6iU%DKhVc0p=kqqRy~VyxbA(Raa2djK0!PXSVutQK23h%UqZmO4{7$KWq&p{h*j!C*1fv|bV)rj-Ka?T7^17&-3aXP7I zKKd;CIe~*Njq^@no+0iJaMll5xRzRa;O}$MjIjh!szg_;M0>++H`E{Yw(@A|EuzTp zqr`8&X5?ZXM!+-tFF9*-@ZEO=Ik|>N#Y32oyt9RN#QOF;hf(Pal4|;EOF5x$19X=B%RL80S*uaQAftg*YZTqJPGhTiwC z$~Kszw;>a6E|oE$NH8U_z=v3(?!~Z&XNAuB**HGOEH}0;haFCzJj2p*H+g4ig!IT( zY1LX~pwZtMVu7>=&@B;Nw1{_EuVpT+(?3FsKGJ+*Nj5CavfS9kI9RjytB1 z!x0AVLGv;OOembtebBWEbP?rjo5m7CmT$!}d;1B7Ye)0NT)Vd(O;eSNW38>i*>F&r z?+nPmJ?5M)&J}nx)Kz9j3M|suC9*@VuHRoEhP8Azw?8(4!V*v3$jo7@0#G@q2@tDX zf!CBp)|gI~MLca9Cm$l%t}upbzM=6z$+f`F7V~i&;ihzw(#N#P_rM(*K50XJYeUY^7VG?p6DvAOzCDDVUKYh$W=5ELm0GWoHn|&B z6alItbR@v0Qn&B($-%DrKj0a`@Oz!5{x%f;Goc38e}_V5uKz#{R)Wm9K-A!085Plp z0xEs|UZ?Lv{z#M@q@lDqU*H(gp+@`sh!VhA5_W(Qu>sW?hbNl!qbX9|!d7 ziz`cu6G{P1l(iN@hJHl|O-O_z$GDe-h708HgfYOYhOuI06f8B?@0B0y&EpqmWQ~(r z*x>@h7;YFkt~TfN2c$}&eGwg2Nv7S7VgjqPP-sYilc{gSSSm+q&TBYCXa%s8M;GLj z7qUb7{8%lWaj*n>Dzc(Rt=MM(q71hh@k?#fe|$BGBrWpCi6*!BJ!AAe<88mr;uoXI zEjebD1hZTsRO7XZBHoIUxMffzla)nnT=asz{50?Mc)q)?gw4D)ZQ;j|v8&`sU->0D zWKH1$A)gfRr?IyNraGcoOo^pY1Dh%nO6OlL@>9CvR@Esq2>D^L3j#l};>k-g@VIDN zJ%=>3s6C@`l%?2JGgC?Mh)M*vJr)x_v)ZVR8YwBx-eo8&JL;(`qYbOrDB69ML63*d zUT~-n1brw@O;}Nz+M~8oq=)6CoQNff?7{^667~o;Sp~e=MoZz5^N7_rL)WG}?#%J( ztU*#S)Olx3Q*7r zrUUFw1hWz)c(FEy6)$Ga|b7Wr513GIS7jq?M#|;JhEU%NUUwMXJ zwz^+;3Q;&71-gCTj;`oPdS%DHYi`aEQk0G)(9x|(nbBOC&D}S#{#jD@lpwL#!Lfc>_o*DZ$R7uL>;+~{a6 z9HVJ>rRBY_5PU&mnbD3Ow!8>)#c0|9U%k{XS#ZkTq?cn%K)JmRxsY_<-1Sw@!9nWo zTwU=z)o?>h_0Z#fFuezy?KUHvpKK%6l_X9M(KJ(iXFui>`vY+HktA=2(a|%b(g~Qn z(u|hmxUf-QLGoIcF$-*S>7l8rd~_|Tj|#@NcDql+8RMHV&kiMtF(Cy<CVC+OdT(_Wq@|!<)hqT8~4-# zt6E98=NbUlEYS+o7JsqMFJ{5 z-a}Ko&UvxJeMi`8d)YMNeX;IMmTm4%E09}Ph(sr-n&ZeF$pb~lt|Uw?508cE=gY~e z6>?{ryUTv7{jU>f|7TgWyMT zNYPsaSMmrwh(ItlPpVZgChe_2d_^OzM#+l;iGahcR1CF_GwYDKAi3frg{yeEdGnI^ zUy3PW<=<`ITNp400(G&d_P^-}5}ekuRueWt&tSV~m%5^u=ku%H{QOBcLh^&~n1tK$ zxxny;_OAj8vu`?<)W@II$p(SV^Q$lss*(n=BUS(pYn6f}1>-XHVfYICI#1&!VAR(! z#nDn)W+cn*<_wrRZ2f4}nx71#!WmpVg^@tp%SwISO@C)lFEDg|S$u`S(@Ai;W< zGC09rG_6qej{(%Iq_rcgnw4zPhim<&v|m~~CRpQmrvOmrlZF%!0c!eKknw8%#otal z{EK&Fw2T4gnPcVv-Ul=pVf{)*B;qB}In;YzW~3@zqCc$px43+z?kXvWUG3HJLc{T9 z0G&?$`u*L8QR7kQVfe(#n{mbRXHAZufSZwbn}yqDc*|37TPW3~`{ zxK=(9!#NG$ti>^2Q5uopxVIc3eU#Zg+s4ubX(3UPcP7jfGnBf+WS;dgl6S=}(mx`t z+&t}?Vl5Pbr@Am&TIxe0RO+mUgR&%845<_9v_@)G-ztsNY=E^H-l{YG=Y_^!%%2oqU$fn5(9fYnZ#giBoc~767{?-RNc6 zSKK>V9Siob^LSdyGp!p3O*ulvPFIXMi#KDEu67B3i!Z`wdDhLndiN!BKhXT*=n?RZ zV!3@SU5PvySX=av1p$Le#{=Je)$gOu67|K3EN?a9nyvCTKM#?eDMA- z7aH218#+Aok|`&LLUaVa7Z=X)#B@CT1u=T51$wytNqzinBOE9b!_Kw3-#Z$BhtJ5R z@7TLmJ`vv&@u66l?8?d}3&e(dSI@wU}{DP-^7Ls_J`qPW%_anM?a zvu4`E0W3mi6qadWpazk*I+Kd2qSbWwU?Fzd?ThdI62ZzR6t-3H*W8Mp{u}-?8yHT= z3~)Vdmy752`O*ADVaiZhn?xKyA$4f{aRTU@@P6J@cUZxCPfsWc@g1k zb_pSKm4I?AcyZ_{({lB6;ab{Z1yU2-q*Clkm4FR!!oag#qn&RNGeXt48-O#v)2Wfi zb{1Q%x^c&fZ{A-$eGHcIC&89E?$$Z=TcyCP&5)&MA)&-#vLYCtmm*5}q!P++o#WGQ zhMtyC8;`&W@yEC6q5KzL=X?FF*h`?Xrw-pTXgG8#b`Wa!CEp>?eHVZn2IQ7zwEhyj zV6=Q39z#C9U1siLf_}f_3QlBMS9M>8h35y0iFx0K1$@$vq#C5us&u%^{#=xYQVad< zxaTBye`D>p*ZtN!5a^+vl|3O`oQ-meWmX z$HB>XVdJbn>Qty;FV)f5$o1ua^YHwz=hjm6N!VkkJ^u(L=N+9<8{1gteSg#3 zXT0AW>kTafPQxLqv`7&Ng{q!R+&nz>uf@jt`EYJ_dA-;_zump}-yCl2_1n?7b`+Jl zug^?ER|l%@Sj9HmR}**m&E^$-`yP}6LxRa^A`kS$? z&YFJ*(nUA3Y8HP+S-MScH2Z`5eh6pF5-N`T9oY%y5$vIafEwpzE(Ry+%|aYj%yG=R z&wNw{Qy^IV=$_l)@Os2x)L>HIk0k5UNy9|{U*7;u-*PA{T9i*%Z{LVeVI=DCJ-5TZ z=gI1aNe++=q=`riMg6Z1?SSv+0_~baQ{-6u6#PS|kTj9i&}Bn33nIfv$wHqnI*RxQ z;^s7wUqbtlsDGXb%BJuMxoEk@;noc&J<$xuCALCwa36d^m@fFz`@fu{b$TzJmF#@m zdc4=zLf+^`EXTh~enH5&zaccQS*wqzi@+TWOI^5ah_11@lz z?#Lf7zm=eJKrxY|V#*xXitroYs|u;c=AjB@3MCHJf@mQObu*S6vQbVT+e1IvUE60# zYK4nQv@Ponub2fru`P%^a&lEiWvt0`l+ke=a4`i_rqx~0&>G!SW z%s@>eq8_JC&_Bt1gx>IT0Vw_j%Eyg>h)h?rk^MyxlFT(W{sSlNVUsN$vaeMfXbWQ7 zJry#ITMKI>AHgsqg!(~sym>G*Vy(nOBj%h&#_Zal@}3ck)EU{MdjsH?i!P>wPt)$T zpO%-6>GIea#ag33lLDhLN#{~9NoRkAep;PRZ>nithCD{%7Bh~#R813Yw#3OnX|_f* z(2rEzkA}ezV#H${LaZ|{6}MENMNCgKlZMM>yxj6l`#sOV)INR9g;R0f}duE zG8$(!9!RtJu@_OibBC24g1p4IC%C|7Ev_FhMeaQ3L*Mq6|NYPpMW-I=IRASyqz0Fy zFxb+UrZAnL6wy<_y3JKD+c>w#L)(KOM()^roKUJvkkFE}IJ^!!60#0t+zQ)y0ByB_ z5N9u2B@TWaizyEBOR&zR5JcrDF`VhvJ$r@GCrFTConW{<1N`d#5wbHa!Kgry6 zfnbKb<%fOZP<%weE1|NHvDkq^l~lCek&t(G$8@0_Zp%Da>!JWp=-`Cy($Ld|l@! zQp_ESc>S>)n$tx|s~XG?bLDcfdZvt4)-L|E$rZ!;@%B6&p3EUBAsHbFgF{XUo?nI) z>Sg{(<(jIYCRKFCq|Tz&ER-*nh!0F>Mp4@JY*P*D$gmUm39=#RSMQha?cMm?LyLky zCfR^MNu8}7oCuY!$Qhmi@>lAHLVfXhxV4iRDjD;**_rCNZOFW`CFO9s<2<6AgXr1k zUuf@oIk>FpXsNlX&N%x;sh7vq6GHJb+0Gl2zy0^k$6-gC$K%xZ__^O5(8o~CWkEOd zdy5*Br>}E=R{n&l5pBM`y}!OrR4lpqt#|qOb$i@i-e09+4o+C@d)(+kZ1lB5v8?{C zr(K@#@;ca_=*tY^_+%Gz1N+Gac@@8r%iQUj6u*_r-f8==cj8;Deea?F1b8MS`zKZW zKS=gmXYZpoCq<`32;|aiQEZ6Nh^QO`MgCJnetB8i>n#51it8+3e>HJ#gvuQ38%_yC zSl|Eew=%8vMVWJjZGt}QJb^km(Z^J3$CxNxk!$oZ%loMSR637V3>X50K!oJ*jV33q zBr0>I^GW`%&R$O9Q|)pe+rRJJSx-J(7f-{J5bLA866bBb_se(R2wSb0RE=WNi{fX0 zP0{Fo&s;DyFnk!{WLKCQaIh{gor*O=XUlZKPj~;gnBtVB*?hML93nAr2Ke@^`5ma0 zy;bDs)euf2yMNl-Tf1~rcsq)2qsMQz2W|VZ*++`s&gJZMd^kOkA8S8y7&k$jDa;j%Sp# zcyUErxs%o;D2k9LXoOG;771O|>nzIslq`WZ>Zml{1sqyf1 zN|*s`q6Y}E{Km7$7AQasKuS~-J>D=e1OIKNAVWMAehTkJ&#D!IS(O~*56lp5Pp%^L z4-=^15^Hif+cj{m0Am|QJ+eU73ezm)GUec-Ln~fJM?zS%Y-%i4aLUQh8~kc>%K!AW zNe*_R+_K=a)+T$ShuGPk8XQt(uyLrZQ zV-=T)nW{A<8zCXk9qN7MEQ)l;l&`bY!hWM_VkU>srSV|7-od$${8Nmho@Dz0-SWt7orwQ%M@nFW@TFWXI#IOSMLx zAXc4jJV3dMqBCXvnB0`VxuVjAj^@mGJYCok_d!8^d_Z&aDIr3B?7`Ft6%`=5)ayYO zLlRqKc87s{jQWhY%=}8{Chu`-nP=CnReJ$L&FiS{n1-G<2%dd<@tYIJ3q3tBK5aP& zbOgqy!Uno02dMe1t9G$^ou{MrOc@sa1^Gl)os`)y6@NSGnz4es@*Eqv=(XwwJ1Zr znU-`oFE-_aVj(NKTe&hnJlZDNo&G)3{`{VfsSd`-j%0{`e*y9=gJD5|`1@jnKRd4W$j`q@yIy)5E<@3$&~M8!ir|iAaIQ3%kNm z6D7RRi9u+QLP(m#gyI)yrbowadOrxOhjIlFA4WY=2#v#D$snR|wY|d-uY3-wb+XUC zuXI!Y;>mrMY1<#>c^G?U=2PPUB9lkphu{yX!)P8(tb@jGnS8&SSKVax2i64^Db$$% z3pA<&jOgoV{tGn56)`bKKh3lr3LXF6(CPuW*^e2F8%XGLzG2}^wTSp~C1G^rj{iVY zA}7rsu#O~4S~mI7f^o5!p7>_X_E&LH+&M+m-v+{eq=fn3Rg@rGz5lTM)v+@EtBUd+ z8h5Oo3@;@(gg?oZn0R6>A=D-RrYU7} z8WacHp}8yT0s8req+O_gU@QCn0tVmDKWK1n{V*Cfx(k6|-s7j7x&Ys6iiH*UraGi{;6Sa5enQOFk8!zsT% zwxlX@(Mc8)Cau9$0ixiYm4DOyriMg9q=e+i&|z7dxuO!(b!gjh=IXkD${1Y@nDwc) z9Jy{QXVOkpHfpVYZ`^4w1fbhgCm7ztH`+H2nu`GHfR!_MxU0gJ-J*+`&%k)s&#hLg zkjv|kRyBw38hh+|pDJM56BkpDUn=gx<}^c9fL_q;(aWQYW92hG3>s{(m0uN*PKzt( z{8?$`GT0gyD9ihdy$b+4am^?p!hb!NhJ=9znaXd=h6F1tONUVB2wbJh50H5LH%{?f zp}HuuyPpvwZ05s&jU0Ba0s|hE2exijN+hoiZXXwGEeQXnvJjOC6x{euu=#+YS!>C& zB_+p)HYHIKxPK8}WYqZ7o$0$*$$OU9V+&6&l)+4)TFAl>!jz(~bZuR|gr;V)hK0tm z%@U(oKmIz% z#N?r9@$E?9u_N>B>WY`c(U|AxTa$jT$DrFw?xWvDF5#p2gzx^I+Rh#+k+Wyu4LnW_-J?wMa3R~IH3d8lMJmbgOmMFRv=$7*1T;OQ<%G-(*{sT=$fAs&K7XyJCG~wian7<(?g1|U zFY9Rk0%-0jc`zGnM=#_X_(7-F6aK-%Ug!iJQ*(@I1baw{~;;Ln8D z+w6wPCfT5iI~N#JOroRz@y>2IV0`6xtpS4sv$Gdy{@eKX|5wt&#PT1u!7EgivC{@g zT4uwy3T=oKw`H2M8cy@ zMcFJbIpuS%1XW&fS>X!}MPkyZj0lkB{;y*!`|^ElNtICu85hnH@^V9P<3SfOlEU2t zV%13Kr_Xg@0EtNd1m$}F#IQtixUa%_l95iDilbO}g&Qtnme6QL`}68_RAnxE+gn|= z&`I2x$nU2cqX}Yl+M;v%M~$o*3re0s$@HwK@tBG%1*RwRUUluLeepu77!gB`N)=hX z_n3ha1S;z4v|cu010ExN`=1@d)F3h=5o62?NVu;Gwi;tm6uJA<4RuL2cq>fj_q9T*R$iM0HSa;7oNBTBnL-5bvdehLn1Q^H2 zHc@cC?Y{XNfI8N>^Di^;!d*lJTo`ZekC)eb-bPNIyu4oqdext9ci!AuS}1Xty;>U} z-50WMt!70%)DbhHn$6}%r}===A?LUr% zB{#G=ut;HKXE-;yoVjewOoBcJ!XFmGWlY+JoT2uF;OpLK7tUL@u@t0?b+TgmhUrng z1H8EL;rHVrns|TS1zLyf!*}q1nn2Y)XRU}f)99KoKTw>q)}6~FG2wcli&BD+W9TlW z#=X$RLnd~?NB#j^g#-PpZ_{k2$`M3cGgy|ZPU21H$3e?H{lokFTJCWF+XhN(ON(50 zgJ;|0@LtKD-Y~<|@Djm;T9>a%Y|WX4?}F8b*45?ZK`x5Tx^bud6MUP2w%SkcpckUl zJEY6elK!`ylGrWX_jgbCr`MZ5Ps2B3OZ^*1sYhFK&<^R8gtHTSpSRp@ahmcfd-Xz; zf=&U;!)npptk3F(9?&MxYX2q}8U7lUz1@UrlRM*bm_eE{xq>8nW_+hKtIZ!3<$rKJsJ_=W4VcD z;8^(gWnps?O9R54(AXFqWD7233c14TIpPn=q>K%DK-1(JikWDc#+jtSrJc~$HbpWL zmmer*vj(uzI4Kn%(T~V1jSU4@;^`k(*h0udxI(D?Eo2Q*q_Yy2|2hdmuP(BNUULI~ z8K~myuYdi!_{RE=R5Bd@v(RN>{txl3QeDSR>t8Av$VgEIH1a&yIDM2gz?W}J96%JC zL!`BKV;pY%KaV_x)d#c>$$<+VwYZ7Ap?bT?1Z82j^orCmz;3lv^vw!4wlf|s_yQsO zqzL%)d3Ygb;#&C8U}N?bXih=Y;XaUe|9C4mj47h7o&hxwopM#MaAuHV$6F{m?Vjtz{MhqzW8zMq$DI|$SB^F(i5=X64aA+th=uKnUR3vc7o4G~_g{_4B@7gAi!8lI_U|5r! zlzD&j8Q%9>Hz=D3w`z;Tvv8KHNFeNJ0Do~e6n3xj@j#;gMFJ#M3*@&cR8t|44amQ4#}z}1GwIQJg=+={rm~Ym<70w% zc{F%La@$>^(sOotwY0rJhYl4Iak!R!K3of<-WN}^(^o*6UDL^ z@4@-2n$7U3xLr#U4S)OFT_(B|1OBGKTie>Y?#K1zZ^wXr(veI(B43XAwTX%0lHr~n z+RsWBgl9UydD_JevHW|NE~_ToN_Ly?FZ-O;luI`5+F-P2d-_`!zMF4NUaebnQR_25 zX>d*uqsoAPxQt58^v4|n#-=6_azk$Kg9qU_dqTZje|V4wX`d!3nkHS$tRrKb&-teH z2$asL6AW7tR`jdOK)P5CQ#=9j?W&3VJXf{iH53RmMJ-fKtOqBgLR7UgRS313j&rLr z5~DmNPsz7a$zh|vB0{dhTqotZS=Ood>dDF+Rsm8X&cpX zw>Pmq{w6CmAGm!IMomQJZ7~ZbdAfx7ygBR<6PCO-`*<0 z!at%LE-ks;TqWSw$hPm-S)U^-#Yo9B6d>oC2$Xui{}#+w`Q)-Jt1~=*Jm?_AfI@X- ziW@`wh}YLGQ@GfDEHFNPL8FjhN_ceW<6)Yv$~-l@;&@?i0?$!3nNr9>HjYQaY6B}n zRkPYMVe-pSj^YDlzlj>?TR_%LLYY?>~nZPWX=~fgQHG(_+yr=pbcN!ToaWdY5 zLHoSQ((DXs{d*+3eV1eW6d3mN#cQp*+KxN&((v20^An)88;IidMzihy2UqM7kUAJ0 zv;LgwE>O4ZNR*5-bh9ESwD(lKdi8B=^EftMVs8BH^Y2xaK40Qhfy09Vmx^n3f+0&l zY3kT&nRzUOmEy4KMZK%KXh~&r-5av$(qR!Cv-7bRS&9ZviP_on^G$A5&-Ig0E^PK& z2t2&|j*em5H3mU-OgsZ)*bBYQCBI+i)?1y|Q+?a}t9vZgI}J#~IAk~Tx6%2ZNqhdM zYLb!tKS+CisD6(j;6Ut*uw~p;L(_Lp-mr#|broA|`#@4G3<*k=Zt*n2CX`f3R&Lbt zh*`}1oi(BQ+6A}=tKj&J>kqhv?Ts$^6h~nR-Z(1dFCY!+&XNEIT-gR|@R-t5bsA=v z^}>?|FO81(UrQnsT>Sw7X7k+)EGUN`f-BA|ZzgORLME>)ATJY>R|u7m65?~*CvXRl z*`hzE;@B33xhWB+ViPJSZ4_)kzbVxhdrmVV&UGR1;e$#}6ftvq>Ac_&3W(urk!>6Y zE{HZ(F0(!+KG31|5MWe93NtK)tPNX*I|>#P*-Nj%A7;)Zncuwj z`wfwS{a$ytoZ5T5+B`2|xB3Bm*?62{j)I)A`;7Kc`~RoCFOP?^YacIbv`A4%nMlYw z8-p=+LiRoDm>CRaMl)mI+Z0(MOGK8GBwM5`sU%yn6q0NqTlTH2zkAg4RPWpSyubJP zeBb}x`Fz}SpL4Eroom0ZIk$7pQ}vl!HyCSQb(e61PccMZ&ySi=)59ESj<9$sR&;Tu zCRZR|L00ObGw$YgL1z97ugu)>vidy!`uBP0lH^Fz>1#w$9kIW2RIBSkDxWZjn z@=`bg_KLL&$1#ZPoQpnxTHr2xpG5v#e@Vwlmx%L?bc)_tNoh&hyZ09jqUw7cuFWS8 zJy@$Xzuxf}Io((_pFC__(`jax0;L{w<6F}2-Ym-)&XoIHsOlNo>oqeKy}IZ9VgEO? zg^B|knH%G!%j+VG9#C_f{58#Hx<0?z68Wjhp;~jpg+-f?y2r0ZAcq@J4?yd-o9|XS z145@GefkQag)&p0_AJ!YtWBwmSx?m#<*Igh-5yR05;B`wT-D3{5>Ts``)***=bD;F zQjcE``$$9q0FjHfRg=6Em4V%sZL2l~DS~m;9iE#{N;Wqq1A^WLbv-`Oo$e-I=xI>4 z-&HfqGyUxoo!JDeUEG+#!xY`BKK*WBznHpq@A^1fVT+MDWI}Dao1f%b^u5Z1fpzV# zZ}(NjPr0qkmJEF?87fbijk$NPD5b%V{-yF*Z}1tDhJ)H)%ddR}Y9X}RPXT#U(o+ke zENc3~$8OA%W1~69yhhr8brU}|<9{2U{?-B)=^qPRq-6e0_ITdBKGI5a*IL|0$dx23 z$>4!=SCi?n{X>pl?71}dczz70KNfV8U$%&&qVvA&6>a{mnye&!Bksz#84wNj#ZJrj zPbYG5dO=w#ANR!Wgp)xRZp*ej=S7)4x_r*edqw2I!GW1)g&n-FKAPlkhJt0EDngm9 zIf}O#U8xB<0QIo1(fCr_mMuG)WqAXtoqXoCSkL2YE@PiY_Qs*sc|StlrD!!23@9}! z32rkx&ghKO4z_g+&kkuzNpVbXIP#J+`|&}eJ92dCM@{I{1&$qQV10``z|G!tiEshI z70p`6$fKmtvasPRR(3@dX;v|UVpQi+58|keWnu<)cu;+sj$-9>Pi$6U=TbFWP6)Zv_2su?WSBnSu|@VLi4b*i6%WnApc-B&aC;dXj^q14;YXQ{i}{d%8G>gLmRmWA%&uZt54(F+9;%qDyqO4e_RH#X**#QdPs zuXvfV(OUXrhMh;?I*G1qRbrA_GdDty`H#6Zc{s%1p^blxXTlEplaL z)jD2zvKe?wHd5WFCuzbuxv!S)rPJf!?I^L=SNGnUp2P_*wWzjXZ0@=Hj=n$Frs}yK zV^yq&1N&^J$6-Iz>OommqJ`Fek6-?>fW;u~O z&s2B2%ZDD@?S{fj^ToE8Kb}7$BV6^FDXzT79T95OP!@Q=Jzz*##Pfc6&giWZa%MQM zeHpPipL9&krgP+OsEWAQ=v-J)jpDeqWMjH`E63NUy{G7IZBDMuy)p@Ex!q_ey~qW} z3EZi<7F9mqTNb)9ySe7y1)rIdB9;32NL58mU$pdkAnIJo#LIT-S;?Y$aujbiYVKqR ztk*3$(9a?w_Kw#`<#p*#9PV~O+jTr1HZA8<-w_(mBq%E$M~ruT80keyf4b(>hg5ol zUcYdxrmJ(VY5Q`VZ(qgNrRLQYliAK}*1%nBYV?Y1sV?9#{f%Um0{NRSLmTrG>`)u4 zlhe})Bl&!a0UPu4c72khCxNRA!zb(-yT=vN?K)yhd>N_j;Ke1?+G%3Cd9Ahoo-eFe zKHr|1_(Q!RwjqOPj#|ue_!D#r-x${jHTJKkd#BhD-gsHyGVS+CC7p zO=&W+ckX34*%JCtdT;e&^3_n=4wg*puM0_DKg$NW68P?fKWuRO8Eu{g(AnY_ zJMLz0zYtTzB*KB`aTm_sro0Evt)ar%mUjMq3=59Dk2Lik14acY*=Pwq9%qT^%d&f{;;=-6wDd2^i_VX#lo_~iMzV=cUbZ%($@x0Q^B zAr9@~u@*baBom%?wZ20|z>1B9rz^?c`4V0Vc3%AK#dqhqcpL;AoH>%&RWmQ$zn}r$ zeNh9xd#?c;_$J7m+ZAzfiCsCV}QZyM>3y3oOtk!OU zQu0?a#hfp?!g*mircrD}vj9=w9kLZZl(I6J)fes)QKW0 zPi*2dyqTXD>lkPljOYxQGaK=VY8<{SA$C}ahw+VYq67yov((%Ujh%+Kb=qM}Z{^)s zunf4OxB24hlP~jM2(I_Y9(KZ4GO@i`_9RJ|78}+aiQ>@b5L!Ji?-}x8ms;hey_$7k zetCXAfp_fwmkK6)CKEkBwSO9u(@)Sk9drDEmYJw!|Y=82OTn1!Mdk=(E93p6{XvZy1c&4 zD`>@^-NG(*A$<{NKkL+!5IkGwY7?i`Hx;=rioKD#di3ntH7)tn!vho5l0oN-9*a&L z+4MyOcyDGZI>}y)1UC+LEh&9Kt!>OKS0~ua^o!bNU0d4YBr54_thyT1A9_B1^r)>~v?WC*!v=frVAtza zgH4%k`5+-1|3^d?o2jyjx1rwfWzHhY*(Mv5>WA)~B~_dZej&@t!fVNaH^kQUf%8bj zUmg4H$?UMdx5(qioE`-6Z`ay25`kZhTWjr%ZXE+j^;6@dgU`X_Gs96IGH3y>ysj@H zid)y;P5}1cGJaw-I`kI8h_y6{eB#6D9D7ASx`&DjH};luM+?h9rLJbQr7gf9vrFs4Ve42JBvWu~&(z%mj5@FHb~rHIdg;t`Td#eWc29+l1Pes^0b)BAtn=38 zN#Sv&F-Lew62-Zn;o`YmGu#7^GP-+vc2V6&CT)=Yt2ESkF@6I+_@SHol;BtC#xpNJ z$d0o0C+R1)dZsxt7pdn|@tbsqXLDmP8rml%cIZ6+Xn6Eqdjy-{Q9rtZ;~dhCabfM4 z)*Bd)s#~a})po|uQwhaPUF$hq;^Bi~r{k$d^%&YKt{zh{&qME(wj*5LA!ao0;*+Xl z*0|_`&rX{Z9?vWQ-=myp`VzL%Z(+(O7C)PT%I$Z#%eGTjeDP>|1?Qq+K&V`XOOxPx zQQ$max5}XtrYMhsN*)Q1hhJX#EG0eIh*eO{Rn~YZUSlBUNbiczJ>;>yvRrMGrSib7 z2AB>@UHOZoVhgKqS;&;Z)uy)zihnrvtiZ>Zg6);3yo*HtlH;#yykI43V)#`l?*;LD!#!_7U|$9vMCOP#8^`{4dlZ;XeUx7q z<(#$==WzWp)_$*iIyHCov%$!M?E@a>?$2~*9`@Yko9e|^xt1+D5V5iR=cY9v-;%1G(IQL*2S1JOvwK-3Gxv;KRQTs z>OC(>eoS1QVTIlR5vkDC$Pdk)xjh4KI5WD|%DFzTC~Ts=Tn}~Cm135X>qVOIL~sbe5($_W6?*e9ZzJr6z23Uk9ccWU(N+6F`gaeLl|*QwWGlx}qvB62C?@7dOtrFh z-~W6@8?w!JeE~m8FP^3_ZAW-54a_FXxSGHH z;+W;V|NVY1^?7xe{~66bP00kY#B;UTEj3!+R(bBRGd!ggT3M~REJ9iQnmkTf@xjg7 zS;OjzEVYt+E2}fYF4yCDOoz4Z&MNL$VM#i#Y3gI-QXw02N;@Ky5(89snmA=?m`>!r z&2~uvAPEnQGII!KE+!@V+7Tls$778KBd)I8dh31ccwOx2LDsx4T7}mWp26-t6#HWN zxVYbGQ3H?mMBprDpuU_PP6iB=Ckg2&+45VG#sOq+ExVU19~S5?a@q;a>X4e6Brk|p z7Rf)Q9TYtGP76YBMkK9L^o4ELA3;p3aulOF2hAb&wgcDRSobl1TYxW?f(cCCbAVFo7oAxuh?+px}p z5ksqpg1Vi$xH^fpbtdlM8D6xAoRj_&XP?LdQ!8}j-f-{876ae&at60br3kq+y0FGG^ zKmlTOWjK)I%E0L&{joZ!jn}Zw8>-l}tY{>X?ca9!8HuI9cxnl1?34^VIxA=>D;9kUfWq2J=Aqv^7#pTF!9(%i{XsazswGQP}vlv>JDMu#!CjYBo)73_K?8$sc^?J~DWLT?8<+i8f6nQsibQQVJ9EUoy!Bq1jz3pgD^tUW6Sdso5~uH$hxM>Z#S zm2!$u@89(`@m1eVQqJ-D<&or}ju%nQGUAP1K~F-&`qpK475Sq_oW7zwKZ^BLatg@^ z$KNNBLmj$Sj+buTM$v9%MB?vDI({Vvh%|al=vY3nvTK;@Ss}+y4;m&9HCpw3ux%fj zjc@Nu;U2h0f~-x2)A`R+_zojkXAvRH%u%Yyu-TJ)QqOxQK0U;&uUc{Ssk|?eW%)`c z28r~oSbaY)jy&0aSRu9O=Ir~;fpwv$+xDElee<{B^Y5%SfdPw>zc06w-a1MT29-4N zamPvOJGdBA2`ChWO2(lHyIGxkbK{?dae((;w9D#EGmgtS#@NV<%#msw*F|Pud49x1 zjcs+wY%!Nbtf8+zb>-&KbK)4Id8sA=ufE!0q zd+INVd)0F!Prtm1aJ-TPML2rbbMr#|hj!&1%No9-#NzteUVy!x`@=~W;35!l~#NPK8MZJ}x1%r|{^iN~ggP-^#)b8>95db!avaCUBr>RZ+GQfF$oK1`;&XLx?N zc6j1W_4mv;@~MKN!1=FGObeNZXEJHu)qB1en(?);RR?~_0cQOy*z5>O^1Hgh zX50pQs>xT{8L8C;hcIRX2D__W7A!BQLU$#%I&to)krLBLHbpEvFE~g58h6&5E;eN> zRw?9sm=kO!{P4LMXA5hMS+2l`bk4B*7DA6yMLDd%Y%^;WstC6lWL1e0tsF%Xr$N1X zW3`jkb$+IU?tb<=R!dVQ(eV({T>xRmkuTRyZxtUnw54$@26MzRrDVUmtf=n-Sw6l< zf<4T7F&Gv!-E>Ql{=V*MKc@GIuvOA~AHVg1f$sd9`hjZ)?6-xq#$$hL%|9(8_->$Z z87YXQv4;bdW}gaq%Rp~?5k&j@pZ+>V)+$OtVnM;Z-P4kzy|UrxDhFG-U|F^EXe^U2i()D zCHX;HAXHZceq~w%$U@fuq)H;=KyV3&I7Uhe43dUQfT6N5sH_+WA_bO`l#-Q%$cTfX za!?sLh&1T?habfX+#$(WM>!)kjqk$&B?W$GD%D+1Qqs%IOTr5(K_WXzf@Oi-TL8Wx z5ODxPoZ>^IqP@k56oGF@{=lP#qhNsT9jSN{5k$j_b|86B75MpS1O55=#upaz=Q!>j zWVfwRuoy|48;*b@QYn&P39#g!$br5{EJn_eL?)oAD73q~8yB_S;Z0p6u*8JkEL8%Q9eq{QJ6GI*==PjJ5?zyMK)N+NFw>QDN& zBxZ~|^J^`QL>kL(c$6+i2kj#U(!r6*J|JZYc|`+~o2wYe9Pj3aM-xCMK#i(1$<-N6 z#EXH9(L^eV;*6C1-uVXsG>ludzmBy<@jnwr;}`ow{J#>nMVYc2_1D@TB-4~H=Y}Ra zDe!xXV{wjX4>u}5%7lz2Qrt;oD(Dp22S)}O<1ij%JkW)fCSh}R#0Q8JPw*gs05>6%+{t(}l_nNIb%2om)F@3$TSB0b z`6q?nM@Bp0h!{M85sUN0xslvK1Ux|UmRGw2RsukgzEcMQIrxCI(L@h4VCvwla0CAf zwV~Z8Bp?KS_e#nZ5}Y@cW+@~9Eg)E$5-`qy5&&TWp63Jb=DuZu1P=-=lK!E2?3Tbt zfXc}~$Ok0y1DnKOxzw+|u;o&}cOIHsk?~H>RLZZu^Y=XJH>OOX*%r-m^|l1OWndI4 zp6UT;4QTmcRy0MK0G>u9xsjZFK!9VQLGC}f#CJ5N62=lBO_C?zL9{?hQwAV3v^$Or zbO1~MPuv>qTTc*{L_h;IG~BdCA_?$Y4+j^T{{lM3klcN?nE#`fDG@LHMKvwWm`1=-OOuq%Qf5qMR(604sWc(|hf8$BN zaQCh6Pl>@FY;Q6AQ~ke77=WiI5CVWOL?Zv0IS?rlTgj0G5Md>M0syiHsb8~)pBm)= z^CeR~+=1Zj2n2kT0q|g=B7g4wj}YIV$^U1p07=X5SpXUU(xJbS4phqN;Cui}#Zn~y zkRHvpX$=4qSPu-4;C#yk4S+=M6yAZn6^^AOq>!`(=?BpNH6uW(`g=ya(eD4O0{~i1 zm6lQA)PO$}keb58rC{PVh3LyF)Ab;dU$Uj?1WD^p}4W$gE4GzG= z5@1{x?Fu{$fJ6_xN~f5bz=;vIed zzp(s({4Z!jSNE^=K_L8UTYxxqb>-AZ7!TURLQ4(h0R$8|85l;^5he{42ZQ0V;xGgh zEiMaofQm!V2nPgAMg|AQVYWp3Yu{gG0<>unDL__)!To_328LiD5QvmGRMrt84nu>n z;s{w8oH$wEbfRqNe zMf0!qe@XMVKmz>e0)FpF{`rINC)WnN`@i-1p``!T2ms#y4e}oe_kZI0pSb=b3H(RE z|Ff?DiR(X-z<&h%KkNEG6Bp}`DPv%^NP*uAm=ON$r1Vc)I%weCfXOmR4L4d=4NNp) zagrK%GB8yFhW)vNA@ti;Hq!s!?Cyiec- z)7~h?ApL!PCYM5CleeGfzKNZ97wNNyL-^D6iA?pQ(yk82Tieub`An&ZkPj~UjqzW- zEdjYslDOueTBBsibLR=Co~{XbN84L#(<#@hiNP8v~zhB+;qys)QCCZQJW}>Ej(&L6 zOI16}R`-ieU0lb$RvcJUv_34chqGDZ!(&7b#iq7e@XC2Kw-;{0=cJ8-p-0(Dp?ZD66yv{C=aWtOOpfyV* zp`Kd+-0Wt_~vzB(cO)k@=kX5FLgG%7A&wj2q*w3&Ox&@DODI(PEiZa}e zA4XK=Z`JzKO7(xKR+&HFlYLvZ z`LA?(-dLH~Ew$HQd844zA{Ya#5n>=j^A}uwu4}{<9d7NPURP2KSI;>XXHqq7ME)?; z*2@qk50!`Buxri-T}VzIvpIBPLPLK}nTqjN8j*XH?7vY|xFU7YsVzxuw-w6LY{`H{ z6;pSCrNg|QI%dF7lm}nJ zQAXvZy5f>28@hklqA z;E-(by;8lL-BK7`f}WVz9y@t$G~h-Zy)kQK$l)?OQii%=^|Hs4N_SyGlL@QkwTUEq z9igN!o4sYlZmy=QXM%bL^5c0mgb=gYGK0B&2L@)%pX_4a%-D;1ZfJZ4`6TP9u|q*L zA*<}ZGOU6ycuDkGhkLNwTanIKcKT_ElLRtl;rXni^fvdt0a1;`*p`rni){xeK!{7h}OjcDI zDlH9`l2%4Y!C}g9l#L{9(jEjuz-TY0js9>U1XxDWhy+Y|0u8i~Y9wil_r=jlGT+?F z!U*RmX>J7uK|x@fA8+=Ydp}>ZagH+Nt-U!?rLehPI=1=wYn9t$sbrta4!W!&)>p$z z4ryyViWVhfdfO`WoCQ>OWmXKGZE0Y1(9-(&`kH9g5WLAM?A7a$yD70n)>pEpDYIjp z%WJQ{rp(Lt%^yR~3;I_RWcF)HxpIjgdjd`S z((3WC{GqlZd{+9)OR3T0dcgm)KI%E~_}<0dJ=Yv^td+V*iFL8_=UXdhlF`z~oWvW< zuN{1GsNdMrh%{dvJ47n>fR?+IyA4QS){&RFOZ@sS?ghx%4Q>2He<E{C%?Hebk!n_c$3)NRlBAZF;ZYWk+g zT7PFj>E#!?^rmY`$wM+-HyR|aO+M5Pl+zBq9}}qe!NEzLa*3_FRe1igSJ!@=47-fH zlYjpQ6_$YhBb6Sn?@DGA%iggKZX+)5*QG4Ix_eHLzK~D6boCV+QO^Isxx(i0c+8st zr;9Z`o~5k;8p)GH=!45q7C4KF`EzEM^JIH6wLhIhAJ$tM9ETF(X2N2dS#fLID+ZT3 zROTu%o)_XSe2HJ(jj3wQGTIQo8+C&*=(a&vVscoV-<3SD7#@4A)4YPUtlUKmETsWtaV-= zHQ`fRzpM6D`&z!5;gfyCqQFIctMpZyW2*#QfVRH{Fuv)SF~a8cXgL%J5JGR?y#Gyr c049dWRBtlQaW^X%EWMjmP*7c8V>j#n15Qtnp8x;= literal 0 HcmV?d00001 diff --git a/doc/caldav-proxy.txt b/doc/caldav-proxy.txt new file mode 100644 index 0000000..2d96bfc --- /dev/null +++ b/doc/caldav-proxy.txt @@ -0,0 +1,560 @@ + + + +Calendar Server Extension C. Daboo + Apple Computer + May 3, 2007 + + + Calendar User Proxy Functionality in CalDAV + caldav-cu-proxy-02 + +Abstract + + This specification defines an extension to CalDAV that makes it easy + for clients to setup and manage calendar user proxies, using the + WebDAV Access Control List extension as a basis. + + +Table of Contents + + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 2 + 2. Conventions Used in This Document . . . . . . . . . . . . . . 2 + 3. Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 + 3.1. Server . . . . . . . . . . . . . . . . . . . . . . . . . . 3 + 3.2. Client . . . . . . . . . . . . . . . . . . . . . . . . . . 3 + 4. Open Issues . . . . . . . . . . . . . . . . . . . . . . . . . 4 + 5. New features in CalDAV . . . . . . . . . . . . . . . . . . . . 4 + 5.1. Proxy Principal Resource . . . . . . . . . . . . . . . . . 4 + 5.2. Privilege Provisioning . . . . . . . . . . . . . . . . . . 8 + 6. Security Considerations . . . . . . . . . . . . . . . . . . . 9 + 7. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 9 + 8. Normative References . . . . . . . . . . . . . . . . . . . . . 9 + Appendix A. Acknowledgments . . . . . . . . . . . . . . . . . . . 9 + Appendix B. Change History . . . . . . . . . . . . . . . . . . . 10 + Author's Address . . . . . . . . . . . . . . . . . . . . . . . . . 10 + + + + + + + + + + + + + + + + + + + +Daboo [Page 1] + + CalDAV Proxy May 2007 + + +1. Introduction + + CalDAV [RFC4791] provides a way for calendar users to store calendar + data and exchange this data via scheduling operations. Based on the + WebDAV protocol [RFC2518], it also includes the ability to manage + access to calendar data via the WebDAV ACL extension [RFC3744]. + + It is often common for a calendar user to delegate some form of + responsibility for their calendar and schedules to another calendar + user (e.g., a boss allows an assistant to check a calendar or to send + and accept scheduling invites on his behalf). The user handling the + calendar data on behalf of someone else is often referred to as a + "calendar user proxy". + + Whilst CalDAV does have fine-grained access control features that can + be used to setup complex sharing and management of calendars, often + the proxy behavior required is an "all-or-nothing" approach - i.e. + the proxy has access to all the calendars or to no calendars (in + which case they are of course not a proxy). So a simple way to + manage access to an entire set of calendars and scheduling ability + would be handy. + + In addition, calendar user agents will often want to display to a + user who has proxy access to their calendars, or to whom they are + acting as a proxy. Again, CalDAV's access control discovery and + report features can be used to do that, but with fine-grained control + that exists, it can be hard to tell who is a "real" proxy as opposed + to someone just granted rights to some subset of calendars. Again, a + simple way to discover proxy information would be handy. + + +2. Conventions Used in This Document + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + document are to be interpreted as described in [RFC2119]. + + When XML element types in the namespace "DAV:" are referenced in this + document outside of the context of an XML fragment, the string "DAV:" + will be prefixed to the element type names. + + When XML element types in the namespaces "DAV:" and + "urn:ietf:params:xml:ns:caldav" are referenced in this document + outside of the context of an XML fragment, the string "DAV:" and + "CALDAV:" will be prefixed to the element type names respectively. + + The namespace "http://calendarserver.org/ns/" is used for XML + elements defined in this specification. When XML element types in + + + +Daboo [Page 2] + + CalDAV Proxy May 2007 + + + this namespace are referenced in this document outside of the context + of an XML fragment, the string "CS:" will be prefixed to the element + type names respectively. + + +3. Overview + +3.1. Server + + For each calendar user principal on the server, the server will + generate two group principals - "proxy groups". One is used to hold + the list of principals who have read-only proxy access to the main + principal's calendars, the other holds the list of principals who + have read-write and scheduling proxy access. NB these new group + principals would have no equivalent in Open Directory. + + Privileges on each "proxy group" principal will be set so that the + "owner" has the ability to change property values. + + The "proxy group" principals will be child resources of the user + principal resource with specific resource types and thus are easy to + discover. As a result the user principal resources will also be + collection resources. + + When provisioning the calendar user home collection, the server will: + + a. Add an ACE to the calendar home collection giving the read-only + "proxy group" inheritable read access. + + b. Add an ACE to the calendar home collection giving the read-write + "proxy group" inheritable read-write access. + + c. Add an ACE to each of the calendar Inbox and Outbox collections + giving the CALDAV:schedule privilege + [I-D.desruisseaux-caldav-sched] to the read-write "proxy group". + +3.2. Client + + A client can see who the proxies are for the current principal by + examining the principal resource for the two "proxy group" properties + and then looking at the DAV:group-member-set property of each. + + The client can edit the list of proxies for the current principal by + editing the DAV:group-member-set property on the relevant "proxy + group" principal resource. + + The client can find out who the current principal is a proxy for by + running a DAV:principal-match REPORT on the principal collection. + + + +Daboo [Page 3] + + CalDAV Proxy May 2007 + + + Alternatively, the client can find out who the current principal is a + proxy for by examining the DAV:group-membership property on the + current principal resource looking for membership in other users' + "proxy groups". + + +4. Open Issues + + 1. Do we want to separate read-write access to calendars vs the + ability to schedule as a proxy? + + 2. We may want to restrict changing properties on the proxy group + collections to just the DAV:group-member-set property? + + 3. There is no way for a proxy to be able to manage the list of + proxies. We could allow the main calendar user DAV:write-acl on + their "proxy group" principals, in which case they could grant + others the right to modify the group membership. + + 4. Should the "proxy group" principals also be collections given + that the regular principal resources will be? + + +5. New features in CalDAV + +5.1. Proxy Principal Resource + + Each "regular" principal resource that needs to allow calendar user + proxy support MUST be a collection resource. i.e. in addition to + including the DAV:principal XML element in the DAV:resourcetype + property on the resource, it MUST also include the DAV:collection XML + element. + + Each "regular" principal resource MUST contain two child resources + with names "calendar-proxy-read" and "calendar-proxy-write" (note + that these are only suggested names - the server could choose any + unique name for these). These resources are themselves principal + resources that are groups contain the list of principals for calendar + users who can act as a read-only or read-write proxy respectively. + + The server MUST include the CS:calendar-proxy-read or CS:calendar- + proxy-write XML elements in the DAV:resourcetype property of the + child resources, respectively. This allows clients to discover the + "proxy group" principals by using a PROPFIND, Depth:1 request on the + current user's principal resource and requesting the DAV:resourcetype + property be returned. The element type declarations are: + + + + + +Daboo [Page 4] + + CalDAV Proxy May 2007 + + + + + + + The server MUST allow the "parent" principal to change the DAV:group- + member-set property on each of the "child" "proxy group" principal + resources. When a principal is listed as a member of the "child" + resource, the server MUST include the "child" resource URI in the + DAV:group-membership property on the included principal resource. + Note that this is just "normal" behavior for a group principal. + + An example principal resource layout might be: + + + / + + principals/ + + users/ + + cyrus/ + calendar-proxy-read + calendar-proxy-write + + red/ + calendar-proxy-read + calendar-proxy-write + + wilfredo/ + calendar-proxy-read + calendar-proxy-write + + If the principal "cyrus" wishes to have the principal "red" act as a + calendar user proxy on his behalf and have the ability to change + items on his calendar or schedule meetings on his behalf, then he + would add the principal resource URI for "red" to the DAV:group- + member-set property of the principal resource /principals/users/ + cyrus/calendar-proxy-write, giving: + + + /principals/users/red/ + + + The DAV:group-membership property on the resource /principals/users/ + red/ would be: + + + /principals/users/cyrus/calendar-proxy-write + + + If the principal "red" was also a read-only proxy for the principal + "wilfredo", then the DA:group-membership property on the resource + /principals/users/red/ would be: + + + + +Daboo [Page 5] + + CalDAV Proxy May 2007 + + + + /principals/users/cyrus/calendar-proxy-write + /principals/users/wilfredo/calendar-proxy-read + + + Thus a client can discover to which principals a particular principal + is acting as a calendar user proxy for by examining the DAV:group- + membership property. + + An alternative to discovering which principals a user can proxy as is + to use the WebDAV ACL principal-match report, targeted at the + principal collections available on the server. + + Example: + + >> Request << + + REPORT /principals/ HTTP/1.1 + Host: cal.example.com + Depth: 0 + Content-Type: application/xml; charset="utf-8" + Content-Length: xxxx + Authorization: Digest username="red", + realm="cal.example.com", nonce="...", + uri="/principals/", response="...", opaque="..." + + + + + + + + + + + + + + + + + + + + + + + + + + +Daboo [Page 6] + + CalDAV Proxy May 2007 + + + >> Response << + + HTTP/1.1 207 Multi-Status + Date: Fri, 10 Nov 2006 09:32:12 GMT + Content-Type: application/xml; charset="utf-8" + Content-Length: xxxx + + + + + /principals/users/red/ + + + + + + + + HTTP/1.1 200 OK + + + + /principals/users/cyrus/calendar-proxy-write + + + + + + + + HTTP/1.1 200 OK + + + + /principals/users/wilfredo/calendar-proxy-read + + + + + + + + HTTP/1.1 200 OK + + + + + + + +Daboo [Page 7] + + CalDAV Proxy May 2007 + + +5.2. Privilege Provisioning + + In order for a calendar user proxy to be able to access the calendars + of the user they are proxying for the server MUST ensure that the + privileges on the relevant calendars are setup accordingly: + + The DAV:read privilege MUST be granted for read-only and read- + write calendar user proxy principals + + The DAV:write privilege MUST be granted for read-write calendar + user proxy principals. + + Additionally, the CalDAV scheduling Inbox and Outbox calendar + collections for the user allowing proxy access, MUST have the CALDAV: + schedule privilege [I-D.desruisseaux-caldav-sched] granted for read- + write calendar user proxy principals. + + Note that with a suitable repository layout, a server may be able to + grant the appropriate privileges on a parent collection and ensure + that all the contained collections and resources inherit that. For + example, given the following repository layout: + + + / + + calendars/ + + users/ + + cyrus/ + inbox + outbox + home + work + + red/ + inbox + outbox + work + soccer + + wilfredo/ + inbox + outbox + home + work + flying + + In order for the principal "red" to act as a read-write proxy for the + principal "cyrus", the following WebDAV ACE will need to be granted + on the resource /calendars/users/cyrus/ and all children of that + resource: + + + + + +Daboo [Page 8] + + CalDAV Proxy May 2007 + + + + + /principals/users/cyrus/calendar-proxy-write + + + + + + + +6. Security Considerations + + TBD + + +7. IANA Considerations + + This document does not require any actions on the part of IANA. + + +8. Normative References + + [I-D.desruisseaux-caldav-sched] + Desruisseaux, B., "Scheduling Extensions to CalDAV", + draft-desruisseaux-caldav-sched-03 (work in progress), + January 2007. + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, March 1997. + + [RFC2518] Goland, Y., Whitehead, E., Faizi, A., Carter, S., and D. + Jensen, "HTTP Extensions for Distributed Authoring -- + WEBDAV", RFC 2518, February 1999. + + [RFC3744] Clemm, G., Reschke, J., Sedlar, E., and J. Whitehead, "Web + Distributed Authoring and Versioning (WebDAV) Access + Control Protocol", RFC 3744, May 2004. + + [RFC4791] Daboo, C., Desruisseaux, B., and L. Dusseault, + "Calendaring Extensions to WebDAV (CalDAV)", RFC 4791, + March 2007. + + +Appendix A. Acknowledgments + + This specification is the result of discussions between the Apple + calendar server and client teams. + + + + +Daboo [Page 9] + + CalDAV Proxy May 2007 + + +Appendix B. Change History + + Changes from -00: + + 1. Updated to RFC 4791 reference. + + Changes from -00: + + 1. Added more details on actual CalDAV protocol changes. + + 2. Changed namespace from http://apple.com/ns/calendarserver/ to + http://calendarserver.org/ns/. + + 3. Made "proxy group" principals child resources of their "owner" + principals. + + 4. The "proxy group" principals now have their own resourcetype. + + +Author's Address + + Cyrus Daboo + Apple Computer, Inc. + 1 Infinite Loop + Cupertino, CA 95014 + USA + + Email: cyrus@daboo.name + URI: http://www.apple.com/ + + + + + + + + + + + + + + + + + + + + + + +Daboo [Page 10] + diff --git a/doc/how_davx5_works.svgz b/doc/how_davx5_works.svgz new file mode 100644 index 0000000000000000000000000000000000000000..93b51e46aee4211765607c042282ffe168e52e33 GIT binary patch literal 1971248 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�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

wE50EvoT*9D8k6mK)gnrlY@kEornUN*|1HP?9Bw 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

A{Ea! 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
~ow?l4K?VC$L(2HD^cl{}=c(Ui9CM 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

Y;cSiGfwJ} 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

4JJ0K(O=3d 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

ci6^@C0RyZ0F->nqcI0(v!gV#N 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

A{Ea!sv3g4(SwMTM59}zDiQO;tR4cHCReT_ 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

?;IA6deTk*cMBp`e=d@Sj+>K>=&}dq6+3Fys_r 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 z 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 T&PRAa`a~=DZzlbvOaE?Q>ZG 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

t)Qa>xUwMY~oV4sAa}X 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