From 89f6151f40d915a406bbe864a8db42aa14daf394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Stu=CC=88rmer?= Date: Tue, 2 May 2023 06:36:36 +0200 Subject: [PATCH 01/13] split layer modules by features --- .github/dependabot.yml | 4 - .github/workflows/refreshVersions.yml | 50 ---- .idea/codeStyles/Project.xml | 153 ----------- app/build.gradle.kts | 191 ++++---------- .../network/PhotosNetworkJUnitRunner.kt | 2 +- app/src/main/AndroidManifest.xml | 23 +- .../main/kotlin/photos/network/AppModule.kt | 37 +-- .../kotlin/photos/network/MainActivity.kt | 12 +- .../network/PhotosNetworkApplication.kt | 54 +++- .../main/kotlin/photos/network/home/Home.kt | 78 +++--- .../photos/network/home/HomeViewModel.kt | 2 +- .../kotlin/photos/network/ui/PhotoGrid.kt | 175 ------------- .../kotlin/photos/network/ui/SearchBar.kt | 8 +- .../kotlin/photos/network/ui/TextInput.kt | 18 +- .../kotlin/photos/network/ui/UserAvatar.kt | 42 +-- .../photos/network/user/CurrentUserHost.kt | 4 +- .../network/user/CurrentUserViewModel.kt | 6 +- app/src/main/res/values-night/themes.xml | 25 -- app/src/main/res/values/strings.xml | 22 +- app/src/main/res/values/themes.xml | 25 -- build.gradle.kts | 25 +- common/build.gradle.kts | 99 ++++++++ .../common}/PhotosNetworkMockFileReader.kt | 2 +- .../common/keystore}/FakeAndroidKeyStore.kt | 2 +- .../common/persistence}/SecureStorageTest.kt | 5 +- common/src/main/AndroidManifest.xml | 2 + .../kotlin/photos/network/common/Module.kt | 15 +- .../kotlin/photos/network/common}/Resource.kt | 4 +- .../common/persistence}/PrivacyState.kt | 2 +- .../common/persistence}/SecureStorage.kt | 12 +- .../network/common}/persistence/Settings.kt | 4 +- .../network/common}/persistence/User.kt | 16 +- .../common}/TestCoroutineDispatcherRule.kt | 3 +- data/build.gradle.kts | 185 -------------- data/src/main/AndroidManifest.xml | 19 -- .../kotlin/photos/network/data/DataModule.kt | 158 ------------ .../photos/repository/PhotoRepositoryImpl.kt | 84 ------ .../data/TestCoroutineDispatcherRule.kt | 38 --- database/albums/build.gradle.kts | 59 +++++ database/albums/src/main/AndroidManifest.xml | 2 + database/photos/build.gradle.kts | 71 ++++++ .../1.json | 0 .../2.json | 0 .../network}/PhotoDatabaseMigrationTests.kt | 6 +- database/photos/src/main/AndroidManifest.xml | 2 + .../database/photos/DatabasePhotosModule.kt | 43 ++++ .../photos/network/database/photos}/Photo.kt | 17 +- .../network/database/photos}/PhotoDao.kt | 2 +- .../network/database/photos}/PhotoDatabase.kt | 8 +- database/settings/build.gradle.kts | 64 +++++ .../settings/src/main/AndroidManifest.xml | 2 + .../network/database/settings/Module.kt | 22 +- .../database/settings}/SettingsStorage.kt | 5 +- database/sharing/build.gradle.kts | 66 +++++ database/sharing/src/main/AndroidManifest.xml | 2 + .../photos/network/database/sharing/Module.kt | 26 ++ .../network/database/sharing}/UserStorage.kt | 6 +- domain/albums/build.gradle.kts | 51 ++++ domain/albums/src/main/AndroidManifest.xml | 2 + .../photos/network/domain/albums/Module.kt | 22 ++ domain/build.gradle.kts | 93 ------- domain/folders/build.gradle.kts | 49 ++++ domain/folders/src/main/AndroidManifest.xml | 2 + .../photos/network/domain/folders/Module.kt | 22 ++ domain/photos/build.gradle.kts | 52 ++++ domain/photos/src/main/AndroidManifest.xml | 2 + .../photos/network/domain/photos/Module.kt | 42 +++ .../network/domain/photos/model/Location.kt | 0 .../domain/photos/model/PhotoElement.kt | 6 +- .../network/domain/photos/model/PhotoList.kt | 0 .../domain/photos/model/TechnicalDetails.kt | 4 +- .../domain/photos/usecase/GetPhotoUseCase.kt | 4 +- .../domain/photos/usecase/GetPhotosUseCase.kt | 8 +- .../photos/usecase/StartPhotosSyncUseCase.kt | 5 +- .../domain/photos/model/LocationTests.kt | 2 +- .../photos/usecase/GetPhotoUseCaseTests.kt | 0 .../photos/usecase/GetPhotosUseCaseTests.kt | 2 +- .../usecase/StartPhotosSyncUseCaseTests.kt | 2 +- domain/settings/build.gradle.kts | 52 ++++ domain/settings/src/main/AndroidManifest.xml | 2 + .../photos/network/domain/settings/Module.kt} | 46 +--- .../settings/usecase/GetSettingsUseCase.kt | 6 +- .../settings/usecase/TogglePrivacyUseCase.kt | 4 +- .../settings/usecase/UpdateClientIdUseCase.kt | 4 +- .../settings/usecase/UpdateHostUseCase.kt | 4 +- .../settings/usecase/VerifyClientIdUseCase.kt | 4 +- .../usecase/VerifyServerHostUseCase.kt | 4 +- .../usecase/GetSettingsUseCaseTests.kt | 4 +- .../usecase/TogglePrivacyUseCaseTests.kt | 4 +- .../usecase/UpdateClientIdUseCaseTests.kt | 4 +- .../usecase/UpdateHostUseCaseTests.kt | 4 +- .../usecase/VerifyClientIdUseCaseTests.kt | 2 +- .../usecase/VerifyServerHostUseCaseTests.kt | 3 +- domain/sharing/build.gradle.kts | 50 ++++ .../network/domain/sharing}/FailingTest.kt | 2 +- domain/sharing/src/main/AndroidManifest.xml | 2 + .../photos/network/domain/sharing/Module.kt | 22 ++ .../photos/network/domain/sharing}/Token.kt | 4 +- .../photos/network/domain/sharing}/User.kt | 2 +- .../sharing}/usecase/GetCurrentUserUseCase.kt | 10 +- .../domain/sharing}/usecase/LogoutUseCase.kt | 6 +- .../usecase/RequestAccessTokenUseCase.kt | 6 +- .../usecase/GetCurrentUserUseCaseTests.kt | 6 +- .../sharing}/usecase/LogoutUseCaseTests.kt | 4 +- .../usecase/RequestAccessTokenUseCaseTests.kt | 4 +- domain/src/main/AndroidManifest.xml | 2 - gradle/libs.versions.toml | 239 ++++++++++++++++++ gradle/wrapper/gradle-wrapper.properties | 2 +- network/build.gradle.kts | 46 ++++ .../photos/network/api}/PhotoApiTest.kt | 16 +- .../photos/network/entity/PhotoTest.kt | 4 +- .../photos/network/entity/PhotosTest.kt | 8 +- network/src/main/AndroidManifest.xml | 2 + .../photos/network/network/NetworkModule.kt | 223 ++++++++++++++++ .../photos/network/network/ServerStatus.kt | 8 + .../photos/network/network/photo}/Photo.kt | 2 +- .../photos/network/network/photo}/PhotoApi.kt | 6 +- .../network/network/photo}/PhotoApiImpl.kt | 16 +- .../photos/network/network/photo}/Photos.kt | 2 +- .../photos/network/network/user}/UserApi.kt | 6 +- .../network/network/user}/UserApiImpl.kt | 43 ++-- .../network/user}/model/ApiResponse.kt | 2 +- .../network/user}/model/NetworkUser.kt | 2 +- .../network/network/user}/model/TokenInfo.kt | 2 +- .../photos/network/api}/PhotoApiTests.kt | 16 +- repository/photos/build.gradle.kts | 123 +++++++++ .../photos/src/main/AndroidManifest.xml | 2 + .../network/repository/photos/Module.kt | 60 +++++ .../network/repository/photos}/Photo.kt | 15 +- .../repository/photos}/PhotoRepository.kt | 5 +- .../repository/photos/PhotoRepositoryImpl.kt | 95 +++++-- .../photos/worker/CleanResourcesWorker.kt | 4 +- .../photos/worker/SyncLocalPhotosWorker.kt | 39 +++ .../repository/photos/worker/SyncStatus.kt | 22 ++ .../photos/worker/UploadPhotosWorker.kt | 13 +- .../repository/photos}/PhotoRepositoryTest.kt | 17 +- repository/settings/build.gradle.kts | 74 ++++++ .../settings/src/main/AndroidManifest.xml | 2 + .../network/repository/settings/Module.kt | 30 +++ .../settings}/SettingsRepository.kt | 3 +- .../settings}/SettingsRepositoryImpl.kt | 34 +-- .../settings}/UserRepositoryTests.kt | 11 +- repository/sharing/build.gradle.kts | 81 ++++++ .../sharing/src/main/AndroidManifest.xml | 2 + .../network/repository/sharing/Module.kt | 31 +++ .../network/repository/sharing}/User.kt | 41 ++- .../repository/sharing}/UserRepository.kt | 4 +- .../repository/sharing}/UserRepositoryImpl.kt | 16 +- settings.gradle.kts | 72 +++++- system/account/build.gradle.kts | 43 ++++ system/account/src/main/AndroidManifest.xml | 2 + system/filesystem/build.gradle.kts | 43 ++++ .../filesystem/src/main/AndroidManifest.xml | 2 + system/mediastore/build.gradle.kts | 43 ++++ .../mediastore/src/main/AndroidManifest.xml | 2 + ui/albums/build.gradle.kts | 53 ++++ ui/albums/src/main/AndroidManifest.xml | 2 + .../photos/network/ui}/albums/AlbumsScreen.kt | 19 +- .../kotlin/photos/network/ui/albums/Module.kt | 8 + ui/common/build.gradle.kts | 65 +++++ ui/common/src/main/AndroidManifest.xml | 2 + .../ui/common}/components/ActivityLog.kt | 12 +- .../network/ui/common}/components/AppLogo.kt | 10 +- .../ui/common}/navigation/Destination.kt | 8 +- .../photos/network/ui/common}/theme/Colors.kt | 6 +- .../photos/network/ui/common}/theme/Theme.kt | 6 +- .../network/ui/common}/theme/Typography.kt | 24 +- .../src/main/res/drawable-night/logo.xml | 0 .../main/res/drawable-night/logo_inverted.xml | 0 .../src/main/res/drawable/cloud_lock.xml | 0 .../src/main/res/drawable/cloud_off.xml | 0 .../src/main/res/drawable/cloud_sync.xml | 0 .../common}/src/main/res/drawable/logo.xml | 0 .../src/main/res/drawable/logo_inverted.xml | 0 .../src/main/res/drawable/logo_monochrome.xml | 0 .../common}/src/main/res/font/changa_bold.ttf | Bin .../src/main/res/font/changa_extra_bold.ttf | Bin .../src/main/res/font/changa_extra_light.ttf | Bin .../src/main/res/font/changa_light.ttf | Bin .../src/main/res/font/changa_medium.ttf | Bin .../src/main/res/font/changa_regular.ttf | Bin .../src/main/res/font/changa_semi_bold.ttf | Bin .../main/res/font/jetbrains_mono_light.ttf | Bin .../main/res/font/jetbrains_mono_regular.ttf | Bin .../src/main/res/font/jetbrains_mono_thin.ttf | Bin .../common}/src/main/res/font/roboto.ttf | Bin .../src/main/res/font/roboto_light.ttf | Bin ui/common/src/main/res/values/strings.xml | 21 ++ ui/folders/build.gradle.kts | 53 ++++ ui/folders/src/main/AndroidManifest.xml | 2 + .../network/ui}/folders/FoldersScreen.kt | 19 +- .../photos/network/ui/folders/Module.kt | 8 + ui/photos/build.gradle.kts | 63 +++++ .../photos/network/PhotosNetworkTestUtils.kt | 0 .../network/ui}/photos/PhotosScreenTests.kt | 11 +- ui/photos/src/main/AndroidManifest.xml | 2 + .../kotlin/photos/network/ui/photos/Module.kt | 13 + .../network/ui/photos}/PhotoBottomIcons.kt | 4 +- .../photos/network/ui}/photos/PhotoDetails.kt | 13 +- .../photos/network/ui/photos/PhotoGrid.kt | 184 ++++++++++++++ .../network/ui/photos}/PhotoTopIcons.kt | 4 +- .../photos/network/ui}/photos/PhotosEvent.kt | 2 +- .../photos/network/ui}/photos/PhotosScreen.kt | 42 ++- .../network/ui}/photos/PhotosUiState.kt | 4 +- .../network/ui}/photos/PhotosViewModel.kt | 14 +- .../kotlin/photos/network/ui/photos}/Tag.kt | 17 +- .../photos/network/ui/photos}/TagLines.kt | 7 +- .../main/res/drawable/image_placeholder.xml | 0 ui/photos/src/main/res/values/strings.xml | 3 + .../ui/photos}/photos/PhotosViewModelTests.kt | 18 +- ui/settings/build.gradle.kts | 61 +++++ .../ui}/settings/SettingsScreenTests.kt | 2 +- ui/settings/src/main/AndroidManifest.xml | 2 + .../photos/network/ui/settings/Module.kt | 17 ++ .../network/ui}/settings/SettingsEvent.kt | 2 +- .../network/ui}/settings/SettingsScreen.kt | 80 +++--- .../network/ui}/settings/SettingsUiState.kt | 10 +- .../network/ui}/settings/SettingsViewModel.kt | 16 +- ui/settings/src/main/res/values/strings.xml | 5 + ui/sharing/build.gradle.kts | 62 +++++ ui/sharing/src/main/AndroidManifest.xml | 2 + .../photos/network/ui/sharing/Module.kt | 14 + .../network/ui/sharing}/login/LoginEvent.kt | 2 +- .../network/ui/sharing}/login/LoginScreen.kt | 14 +- .../network/ui/sharing}/login/LoginUiState.kt | 2 +- .../ui/sharing}/login/LoginViewModel.kt | 11 +- versions.properties | 134 ---------- 227 files changed, 3387 insertions(+), 1990 deletions(-) delete mode 100644 .github/workflows/refreshVersions.yml delete mode 100644 .idea/codeStyles/Project.xml delete mode 100644 app/src/main/kotlin/photos/network/ui/PhotoGrid.kt delete mode 100644 app/src/main/res/values-night/themes.xml delete mode 100644 app/src/main/res/values/themes.xml create mode 100644 common/build.gradle.kts rename {data/src/androidTest/kotlin/photos/network/data => common/src/androidTest/kotlin/photos/network/common}/PhotosNetworkMockFileReader.kt (97%) rename {data/src/androidTest/kotlin/photos/network/data => common/src/androidTest/kotlin/photos/network/common/keystore}/FakeAndroidKeyStore.kt (98%) rename {data/src/androidTest/kotlin/photos/network/data => common/src/androidTest/kotlin/photos/network/common/persistence}/SecureStorageTest.kt (96%) create mode 100644 common/src/main/AndroidManifest.xml rename data/src/main/kotlin/photos/network/data/settings/repository/Settings.kt => common/src/main/kotlin/photos/network/common/Module.kt (68%) rename {data/src/main/kotlin/photos/network/data => common/src/main/kotlin/photos/network/common}/Resource.kt (93%) rename {data/src/main/kotlin/photos/network/data/settings/repository => common/src/main/kotlin/photos/network/common/persistence}/PrivacyState.kt (93%) rename {data/src/main/kotlin/photos/network/data => common/src/main/kotlin/photos/network/common/persistence}/SecureStorage.kt (94%) rename {data/src/main/kotlin/photos/network/data/settings => common/src/main/kotlin/photos/network/common}/persistence/Settings.kt (88%) rename {data/src/main/kotlin/photos/network/data/user => common/src/main/kotlin/photos/network/common}/persistence/User.kt (68%) rename {app/src/test/kotlin/photos/network => common/src/test/kotlin/photos/network/common}/TestCoroutineDispatcherRule.kt (97%) delete mode 100644 data/build.gradle.kts delete mode 100644 data/src/main/AndroidManifest.xml delete mode 100644 data/src/main/kotlin/photos/network/data/photos/repository/PhotoRepositoryImpl.kt delete mode 100644 data/src/test/kotlin/photos/network/data/TestCoroutineDispatcherRule.kt create mode 100644 database/albums/build.gradle.kts create mode 100644 database/albums/src/main/AndroidManifest.xml create mode 100644 database/photos/build.gradle.kts rename {data/schemas/photos.network.data.photos.persistence.PhotoDatabase => database/photos/schemas/photos.network.database.photos.PhotoDatabase}/1.json (100%) rename {data/schemas/photos.network.data.photos.persistence.PhotoDatabase => database/photos/schemas/photos.network.database.photos.PhotoDatabase}/2.json (100%) rename {data/src/androidTest/kotlin/photos/network/data/photos/persistence => database/photos/src/androidTest/kotlin/photos/network}/PhotoDatabaseMigrationTests.kt (95%) create mode 100644 database/photos/src/main/AndroidManifest.xml create mode 100644 database/photos/src/main/kotlin/photos/network/database/photos/DatabasePhotosModule.kt rename {data/src/main/kotlin/photos/network/data/photos/persistence => database/photos/src/main/kotlin/photos/network/database/photos}/Photo.kt (67%) rename {data/src/main/kotlin/photos/network/data/photos/persistence => database/photos/src/main/kotlin/photos/network/database/photos}/PhotoDao.kt (96%) rename {data/src/main/kotlin/photos/network/data/photos/persistence => database/photos/src/main/kotlin/photos/network/database/photos}/PhotoDatabase.kt (93%) create mode 100644 database/settings/build.gradle.kts create mode 100644 database/settings/src/main/AndroidManifest.xml rename app/src/main/kotlin/photos/network/presentation/help/HelpScreen.kt => database/settings/src/main/kotlin/photos/network/database/settings/Module.kt (55%) rename {data/src/main/kotlin/photos/network/data/settings/persistence => database/settings/src/main/kotlin/photos/network/database/settings}/SettingsStorage.kt (88%) create mode 100644 database/sharing/build.gradle.kts create mode 100644 database/sharing/src/main/AndroidManifest.xml create mode 100644 database/sharing/src/main/kotlin/photos/network/database/sharing/Module.kt rename {data/src/main/kotlin/photos/network/data/user/persistence => database/sharing/src/main/kotlin/photos/network/database/sharing}/UserStorage.kt (87%) create mode 100644 domain/albums/build.gradle.kts create mode 100644 domain/albums/src/main/AndroidManifest.xml create mode 100644 domain/albums/src/main/kotlin/photos/network/domain/albums/Module.kt delete mode 100644 domain/build.gradle.kts create mode 100644 domain/folders/build.gradle.kts create mode 100644 domain/folders/src/main/AndroidManifest.xml create mode 100644 domain/folders/src/main/kotlin/photos/network/domain/folders/Module.kt create mode 100644 domain/photos/build.gradle.kts create mode 100644 domain/photos/src/main/AndroidManifest.xml create mode 100644 domain/photos/src/main/kotlin/photos/network/domain/photos/Module.kt rename domain/{ => photos}/src/main/kotlin/photos/network/domain/photos/model/Location.kt (100%) rename domain/{ => photos}/src/main/kotlin/photos/network/domain/photos/model/PhotoElement.kt (90%) rename domain/{ => photos}/src/main/kotlin/photos/network/domain/photos/model/PhotoList.kt (100%) rename domain/{ => photos}/src/main/kotlin/photos/network/domain/photos/model/TechnicalDetails.kt (91%) rename domain/{ => photos}/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCase.kt (89%) rename domain/{ => photos}/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCase.kt (85%) rename domain/{ => photos}/src/main/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCase.kt (81%) rename domain/{ => photos}/src/test/kotlin/photos/network/domain/photos/model/LocationTests.kt (97%) rename domain/{ => photos}/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCaseTests.kt (100%) rename domain/{ => photos}/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCaseTests.kt (99%) rename domain/{ => photos}/src/test/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCaseTests.kt (96%) create mode 100644 domain/settings/build.gradle.kts create mode 100644 domain/settings/src/main/AndroidManifest.xml rename domain/{src/main/kotlin/photos/network/domain/DomainModule.kt => settings/src/main/kotlin/photos/network/domain/settings/Module.kt} (65%) rename domain/{ => settings}/src/main/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCase.kt (82%) rename domain/{ => settings}/src/main/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCase.kt (86%) rename domain/{ => settings}/src/main/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCase.kt (86%) rename domain/{ => settings}/src/main/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCase.kt (86%) rename domain/{ => settings}/src/main/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCase.kt (89%) rename domain/{ => settings}/src/main/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCase.kt (89%) rename domain/{ => settings}/src/test/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCaseTests.kt (94%) rename domain/{ => settings}/src/test/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCaseTests.kt (92%) rename domain/{ => settings}/src/test/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCaseTests.kt (92%) rename domain/{ => settings}/src/test/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCaseTests.kt (92%) rename domain/{ => settings}/src/test/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCaseTests.kt (97%) rename domain/{ => settings}/src/test/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCaseTests.kt (95%) create mode 100644 domain/sharing/build.gradle.kts rename domain/{src/androidTest/kotlin/photos/network/domain/photos => sharing/src/androidTest/kotlin/photos/network/domain/sharing}/FailingTest.kt (96%) create mode 100644 domain/sharing/src/main/AndroidManifest.xml create mode 100644 domain/sharing/src/main/kotlin/photos/network/domain/sharing/Module.kt rename domain/{src/main/kotlin/photos/network/domain/user => sharing/src/main/kotlin/photos/network/domain/sharing}/Token.kt (90%) rename domain/{src/main/kotlin/photos/network/domain/user => sharing/src/main/kotlin/photos/network/domain/sharing}/User.kt (95%) rename domain/{src/main/kotlin/photos/network/domain/user => sharing/src/main/kotlin/photos/network/domain/sharing}/usecase/GetCurrentUserUseCase.kt (76%) rename domain/{src/main/kotlin/photos/network/domain/user => sharing/src/main/kotlin/photos/network/domain/sharing}/usecase/LogoutUseCase.kt (83%) rename domain/{src/main/kotlin/photos/network/domain/user => sharing/src/main/kotlin/photos/network/domain/sharing}/usecase/RequestAccessTokenUseCase.kt (83%) rename domain/{src/test/kotlin/photos/network/domain/user => sharing/src/test/kotlin/photos/network/domain/sharing}/usecase/GetCurrentUserUseCaseTests.kt (94%) rename domain/{src/test/kotlin/photos/network/domain/user => sharing/src/test/kotlin/photos/network/domain/sharing}/usecase/LogoutUseCaseTests.kt (94%) rename domain/{src/test/kotlin/photos/network/domain/user => sharing/src/test/kotlin/photos/network/domain/sharing}/usecase/RequestAccessTokenUseCaseTests.kt (95%) delete mode 100644 domain/src/main/AndroidManifest.xml create mode 100644 gradle/libs.versions.toml create mode 100644 network/build.gradle.kts rename {data/src/androidTest/kotlin/photos/network/data/photos/network => network/src/androidTest/kotlin/photos/network/api}/PhotoApiTest.kt (90%) rename {data/src/androidTest/kotlin/photos/network/data => network/src/androidTest/kotlin}/photos/network/entity/PhotoTest.kt (93%) rename {data/src/androidTest/kotlin/photos/network/data => network/src/androidTest/kotlin}/photos/network/entity/PhotosTest.kt (92%) create mode 100644 network/src/main/AndroidManifest.xml create mode 100644 network/src/main/kotlin/photos/network/network/NetworkModule.kt create mode 100644 network/src/main/kotlin/photos/network/network/ServerStatus.kt rename {data/src/main/kotlin/photos/network/data/photos/network => network/src/main/kotlin/photos/network/network/photo}/Photo.kt (95%) rename {data/src/main/kotlin/photos/network/data/photos/network => network/src/main/kotlin/photos/network/network/photo}/PhotoApi.kt (88%) rename {data/src/main/kotlin/photos/network/data/photos/network => network/src/main/kotlin/photos/network/network/photo}/PhotoApiImpl.kt (61%) rename {data/src/main/kotlin/photos/network/data/photos/network => network/src/main/kotlin/photos/network/network/photo}/Photos.kt (95%) rename {data/src/main/kotlin/photos/network/data/user/network => network/src/main/kotlin/photos/network/network/user}/UserApi.kt (93%) rename {data/src/main/kotlin/photos/network/data/user/network => network/src/main/kotlin/photos/network/network/user}/UserApiImpl.kt (77%) rename {data/src/main/kotlin/photos/network/data/user/network => network/src/main/kotlin/photos/network/network/user}/model/ApiResponse.kt (93%) rename {data/src/main/kotlin/photos/network/data/user/network => network/src/main/kotlin/photos/network/network/user}/model/NetworkUser.kt (95%) rename {data/src/main/kotlin/photos/network/data/user/network => network/src/main/kotlin/photos/network/network/user}/model/TokenInfo.kt (95%) rename {data/src/test/kotlin/photos/network/data/photos/network => network/src/test/kotlin/photos/network/api}/PhotoApiTests.kt (92%) create mode 100644 repository/photos/build.gradle.kts create mode 100644 repository/photos/src/main/AndroidManifest.xml create mode 100644 repository/photos/src/main/kotlin/photos/network/repository/photos/Module.kt rename {data/src/main/kotlin/photos/network/data/photos/repository => repository/photos/src/main/kotlin/photos/network/repository/photos}/Photo.kt (72%) rename {data/src/main/kotlin/photos/network/data/photos/repository => repository/photos/src/main/kotlin/photos/network/repository/photos}/PhotoRepository.kt (85%) rename data/src/main/kotlin/photos/network/data/photos/worker/SyncLocalPhotosWorker.kt => repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt (73%) rename {data/src/main/kotlin/photos/network/data => repository/photos/src/main/kotlin/photos/network/repository}/photos/worker/CleanResourcesWorker.kt (92%) create mode 100644 repository/photos/src/main/kotlin/photos/network/repository/photos/worker/SyncLocalPhotosWorker.kt create mode 100644 repository/photos/src/main/kotlin/photos/network/repository/photos/worker/SyncStatus.kt rename {data/src/main/kotlin/photos/network/data => repository/photos/src/main/kotlin/photos/network/repository}/photos/worker/UploadPhotosWorker.kt (77%) rename {data/src/test/kotlin/photos/network/data/photos/repository => repository/photos/src/test/kotlin/photos/network/repository/photos}/PhotoRepositoryTest.kt (93%) create mode 100644 repository/settings/build.gradle.kts create mode 100644 repository/settings/src/main/AndroidManifest.xml create mode 100644 repository/settings/src/main/kotlin/photos/network/repository/settings/Module.kt rename {data/src/main/kotlin/photos/network/data/settings/repository => repository/settings/src/main/kotlin/photos/network/repository/settings}/SettingsRepository.kt (91%) rename {data/src/main/kotlin/photos/network/data/settings/repository => repository/settings/src/main/kotlin/photos/network/repository/settings}/SettingsRepositoryImpl.kt (79%) rename {data/src/test/kotlin/photos/network/data/user/repository => repository/settings/src/test/kotlin/photos/network/repository/settings}/UserRepositoryTests.kt (90%) create mode 100644 repository/sharing/build.gradle.kts create mode 100644 repository/sharing/src/main/AndroidManifest.xml create mode 100644 repository/sharing/src/main/kotlin/photos/network/repository/sharing/Module.kt rename {data/src/main/kotlin/photos/network/data/user/repository => repository/sharing/src/main/kotlin/photos/network/repository/sharing}/User.kt (53%) rename {data/src/main/kotlin/photos/network/data/user/repository => repository/sharing/src/main/kotlin/photos/network/repository/sharing}/UserRepository.kt (90%) rename {data/src/main/kotlin/photos/network/data/user/repository => repository/sharing/src/main/kotlin/photos/network/repository/sharing}/UserRepositoryImpl.kt (87%) create mode 100644 system/account/build.gradle.kts create mode 100644 system/account/src/main/AndroidManifest.xml create mode 100644 system/filesystem/build.gradle.kts create mode 100644 system/filesystem/src/main/AndroidManifest.xml create mode 100644 system/mediastore/build.gradle.kts create mode 100644 system/mediastore/src/main/AndroidManifest.xml create mode 100644 ui/albums/build.gradle.kts create mode 100644 ui/albums/src/main/AndroidManifest.xml rename {app/src/main/kotlin/photos/network/home => ui/albums/src/main/kotlin/photos/network/ui}/albums/AlbumsScreen.kt (83%) create mode 100644 ui/albums/src/main/kotlin/photos/network/ui/albums/Module.kt create mode 100644 ui/common/build.gradle.kts create mode 100644 ui/common/src/main/AndroidManifest.xml rename {app/src/main/kotlin/photos/network/ui => ui/common/src/main/kotlin/photos/network/ui/common}/components/ActivityLog.kt (91%) rename {app/src/main/kotlin/photos/network/ui => ui/common/src/main/kotlin/photos/network/ui/common}/components/AppLogo.kt (95%) rename {app/src/main/kotlin/photos/network => ui/common/src/main/kotlin/photos/network/ui/common}/navigation/Destination.kt (92%) rename {app/src/main/kotlin/photos/network => ui/common/src/main/kotlin/photos/network/ui/common}/theme/Colors.kt (98%) rename {app/src/main/kotlin/photos/network => ui/common/src/main/kotlin/photos/network/ui/common}/theme/Theme.kt (90%) rename {app/src/main/kotlin/photos/network => ui/common/src/main/kotlin/photos/network/ui/common}/theme/Typography.kt (87%) rename {app => ui/common}/src/main/res/drawable-night/logo.xml (100%) rename {app => ui/common}/src/main/res/drawable-night/logo_inverted.xml (100%) rename {app => ui/common}/src/main/res/drawable/cloud_lock.xml (100%) rename {app => ui/common}/src/main/res/drawable/cloud_off.xml (100%) rename {app => ui/common}/src/main/res/drawable/cloud_sync.xml (100%) rename {app => ui/common}/src/main/res/drawable/logo.xml (100%) rename {app => ui/common}/src/main/res/drawable/logo_inverted.xml (100%) rename {app => ui/common}/src/main/res/drawable/logo_monochrome.xml (100%) rename {app => ui/common}/src/main/res/font/changa_bold.ttf (100%) rename {app => ui/common}/src/main/res/font/changa_extra_bold.ttf (100%) rename {app => ui/common}/src/main/res/font/changa_extra_light.ttf (100%) rename {app => ui/common}/src/main/res/font/changa_light.ttf (100%) rename {app => ui/common}/src/main/res/font/changa_medium.ttf (100%) rename {app => ui/common}/src/main/res/font/changa_regular.ttf (100%) rename {app => ui/common}/src/main/res/font/changa_semi_bold.ttf (100%) rename {app => ui/common}/src/main/res/font/jetbrains_mono_light.ttf (100%) rename {app => ui/common}/src/main/res/font/jetbrains_mono_regular.ttf (100%) rename {app => ui/common}/src/main/res/font/jetbrains_mono_thin.ttf (100%) rename {app => ui/common}/src/main/res/font/roboto.ttf (100%) rename {app => ui/common}/src/main/res/font/roboto_light.ttf (100%) create mode 100644 ui/common/src/main/res/values/strings.xml create mode 100644 ui/folders/build.gradle.kts create mode 100644 ui/folders/src/main/AndroidManifest.xml rename {app/src/main/kotlin/photos/network/home => ui/folders/src/main/kotlin/photos/network/ui}/folders/FoldersScreen.kt (83%) create mode 100644 ui/folders/src/main/kotlin/photos/network/ui/folders/Module.kt create mode 100644 ui/photos/build.gradle.kts rename {app => ui/photos}/src/androidTest/kotlin/photos/network/PhotosNetworkTestUtils.kt (100%) rename {app/src/androidTest/kotlin/photos/network/home => ui/photos/src/androidTest/kotlin/photos/network/ui}/photos/PhotosScreenTests.kt (93%) create mode 100644 ui/photos/src/main/AndroidManifest.xml create mode 100644 ui/photos/src/main/kotlin/photos/network/ui/photos/Module.kt rename {app/src/main/kotlin/photos/network/ui => ui/photos/src/main/kotlin/photos/network/ui/photos}/PhotoBottomIcons.kt (91%) rename {app/src/main/kotlin/photos/network/home => ui/photos/src/main/kotlin/photos/network/ui}/photos/PhotoDetails.kt (92%) create mode 100644 ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt rename {app/src/main/kotlin/photos/network/ui => ui/photos/src/main/kotlin/photos/network/ui/photos}/PhotoTopIcons.kt (93%) rename {app/src/main/kotlin/photos/network/home => ui/photos/src/main/kotlin/photos/network/ui}/photos/PhotosEvent.kt (95%) rename {app/src/main/kotlin/photos/network/home => ui/photos/src/main/kotlin/photos/network/ui}/photos/PhotosScreen.kt (88%) rename {app/src/main/kotlin/photos/network/home => ui/photos/src/main/kotlin/photos/network/ui}/photos/PhotosUiState.kt (90%) rename {app/src/main/kotlin/photos/network/home => ui/photos/src/main/kotlin/photos/network/ui}/photos/PhotosViewModel.kt (91%) rename {app/src/main/kotlin/photos/network/ui => ui/photos/src/main/kotlin/photos/network/ui/photos}/Tag.kt (89%) rename {app/src/main/kotlin/photos/network/ui => ui/photos/src/main/kotlin/photos/network/ui/photos}/TagLines.kt (90%) rename {app => ui/photos}/src/main/res/drawable/image_placeholder.xml (100%) create mode 100644 ui/photos/src/main/res/values/strings.xml rename {app/src/test/kotlin/photos/network/home => ui/photos/src/test/kotlin/photos/network/ui/photos}/photos/PhotosViewModelTests.kt (87%) create mode 100644 ui/settings/build.gradle.kts rename {app/src/androidTest/kotlin/photos/network => ui/settings/src/androidTest/kotlin/photos/network/ui}/settings/SettingsScreenTests.kt (97%) create mode 100644 ui/settings/src/main/AndroidManifest.xml create mode 100644 ui/settings/src/main/kotlin/photos/network/ui/settings/Module.kt rename {app/src/main/kotlin/photos/network => ui/settings/src/main/kotlin/photos/network/ui}/settings/SettingsEvent.kt (96%) rename {app/src/main/kotlin/photos/network => ui/settings/src/main/kotlin/photos/network/ui}/settings/SettingsScreen.kt (89%) rename {app/src/main/kotlin/photos/network => ui/settings/src/main/kotlin/photos/network/ui}/settings/SettingsUiState.kt (88%) rename {app/src/main/kotlin/photos/network => ui/settings/src/main/kotlin/photos/network/ui}/settings/SettingsViewModel.kt (86%) create mode 100644 ui/settings/src/main/res/values/strings.xml create mode 100644 ui/sharing/build.gradle.kts create mode 100644 ui/sharing/src/main/AndroidManifest.xml create mode 100644 ui/sharing/src/main/kotlin/photos/network/ui/sharing/Module.kt rename {app/src/main/kotlin/photos/network/presentation => ui/sharing/src/main/kotlin/photos/network/ui/sharing}/login/LoginEvent.kt (94%) rename {app/src/main/kotlin/photos/network/presentation => ui/sharing/src/main/kotlin/photos/network/ui/sharing}/login/LoginScreen.kt (93%) rename {app/src/main/kotlin/photos/network/presentation => ui/sharing/src/main/kotlin/photos/network/ui/sharing}/login/LoginUiState.kt (94%) rename {app/src/main/kotlin/photos/network/presentation => ui/sharing/src/main/kotlin/photos/network/ui/sharing}/login/LoginViewModel.kt (91%) delete mode 100644 versions.properties diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9c47602..192cbc3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,7 +6,3 @@ updates: schedule: interval: daily open-pull-requests-limit: 10 - ignore: - - dependency-name: org.jetbrains.kotlin:kotlin-reflect - versions: - - "> 1.4.32, < 2" diff --git a/.github/workflows/refreshVersions.yml b/.github/workflows/refreshVersions.yml deleted file mode 100644 index 5e9ad0b..0000000 --- a/.github/workflows/refreshVersions.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: RefreshVersions - -on: - workflow_dispatch: - schedule: - - cron: '0 7 * * 1' - -jobs: - "Refresh-Versions": - runs-on: "ubuntu-latest" - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: setup-java - uses: actions/setup-java@v3 - with: - java-version: 17 - distribution: adopt - - - name: create-branch - uses: peterjgrainger/action-create-branch@v2.2.0 - with: - branch: dependency-update - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: gradle refreshVersions - uses: gradle/gradle-build-action@v2 - with: - arguments: refreshVersions - - - name: Commit - uses: EndBug/add-and-commit@v9 - with: - author_name: GitHub Actions - author_email: noreply@github.com - message: Refresh versions.properties - new_branch: dependency-update - push: --force --set-upstream origin dependency-update - - - name: Pull Request - uses: repo-sync/pull-request@v2 - with: - source_branch: dependency-update - destination_branch: main - pr_title: Upgrade gradle dependencies - pr_body: '[refreshVersions](https://github.com/jmfayard/refreshVersions) has found those library updates!' - pr_draft: true - github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index f4f9556..0000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7dbc19b..38934be 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,23 +1,21 @@ import com.github.triplet.gradle.androidpublisher.ResolutionStrategy import com.github.triplet.gradle.androidpublisher.ReleaseStatus -import de.fayard.refreshVersions.core.versionFor plugins { - id("com.android.application") - id("com.diffplug.spotless") - kotlin("android") - kotlin("kapt") - kotlin("plugin.serialization") - id("io.gitlab.arturbosch.detekt") - id("com.github.triplet.play") - id("org.ajoberstar.grgit") - id("jacoco") + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) + alias(libs.plugins.grgit) + alias(libs.plugins.triplet) } spotless { kotlin { target("src/*/kotlin/**/*.kt") - ktlint("0.43.2") + ktlint( libs.versions.ktlint.get()) licenseHeaderFile(rootProject.file("spotless/copyright.kt")) } } @@ -41,75 +39,15 @@ play { releaseStatus.set(ReleaseStatus.COMPLETED) } -jacoco { - toolVersion = "0.8.7" -} - -project.afterEvaluate { - tasks.create(name = "testCoverage") { - dependsOn("testDebugUnitTest") - group = "Reporting" - description = "Generate jacoco coverage reports" - - reports { - html.required.set(true) - xml.required.set(true) - csv.required.set(true) - } - - val excludes = listOf( - // android - "**/R.class", - "**/R$*.class", - "**/BuildConfig.*", - "**/Manifest*.*", - "**/*Test*.*", - "android/**/*.*", - // kotlin - "**/*MapperImpl*.*", - "**/*\$ViewInjector*.*", - "**/*\$ViewBinder*.*", - "**/BuildConfig.*", - "**/*Component*.*", - "**/*BR*.*", - "**/Manifest*.*", - "**/*\$Lambda$*.*", - "**/*Companion*.*", - "**/*Module*.*", - "**/*Dagger*.*", - "**/*Hilt*.*", - "**/*MembersInjector*.*", - "**/*_MembersInjector.class", - "**/*_Factory*.*", - "**/*_Provide*Factory*.*", - "**/*Extensions*.*", - // sealed and data classes - "**/*$Result.*", - "**/*$Result$*.*" - ) - - val kotlinClasses = fileTree(baseDir = "$buildDir/tmp/kotlin-classes/debug") { - exclude(excludes) - } - - classDirectories.setFrom(kotlinClasses) - - val androidTestData = fileTree(baseDir = "$buildDir/outputs/code_coverage/debugAndroidTest/connected/") - - executionData(files( - "${project.buildDir}/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec", - androidTestData - )) - } -} - // https://detekt.dev/gradle.html detekt { config = files("../detekt.yml") } android { - compileSdk = 33 + namespace = "photos.network" + + compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { applicationId = "photos.network" // API 21 | required by: security-crypto library @@ -117,19 +55,13 @@ android { // API 24 | required by: networkSecurityConfig // API 26 | required by: Java 8 Time API minSdk = 26 - targetSdk = 31 + targetSdk = libs.versions.compileSdk.get().toInt() versionCode = grgit.log().size versionName = "0.1.0-${grgit.head().abbreviatedId}" testInstrumentationRunner = "photos.network.PhotosNetworkJUnitRunner" } - testCoverage { - // needed to force the jacoco version - jacocoVersion = "0.8.7" - version = "0.8.7" - } - signingConfigs { named("debug") { storeFile = file("../android_debug.keystore") @@ -163,7 +95,6 @@ android { "proguard-rules-debug.pro", ) signingConfig = signingConfigs.getByName("debug") - isTestCoverageEnabled = true } release { isMinifyEnabled = true @@ -182,28 +113,31 @@ android { resValues = false shaders = false } + composeOptions { - kotlinCompilerExtensionVersion = versionFor(AndroidX.compose.compiler) + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } + compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } + kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "11" freeCompilerArgs = freeCompilerArgs + "-Xallow-unstable-dependencies" - freeCompilerArgs = freeCompilerArgs + "-Xopt-in=coil.annotation.ExperimentalCoilApi" - freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlin.RequiresOptIn" - freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlin.Experimental" - freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" - freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi" - freeCompilerArgs = freeCompilerArgs + "-Xopt-in=androidx.compose.animation.ExperimentalAnimationApi" - freeCompilerArgs = freeCompilerArgs + "-Xopt-in=androidx.compose.foundation.ExperimentalFoundationApi" - freeCompilerArgs = freeCompilerArgs + "-Xopt-in=androidx.compose.material.ExperimentalMaterialApi" - freeCompilerArgs = freeCompilerArgs + "-Xopt-in=androidx.compose.material3.ExperimentalMaterial3Api" - freeCompilerArgs = freeCompilerArgs + "-Xopt-in=androidx.compose.ui.ExperimentalComposeUiApi" - freeCompilerArgs = freeCompilerArgs + "-Xopt-in=com.google.accompanist.pager.ExperimentalPagerApi" - freeCompilerArgs = freeCompilerArgs + "-Xopt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi" + freeCompilerArgs = freeCompilerArgs + "-opt-in=coil.annotation.ExperimentalCoilApi" + freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn" + freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.Experimental" + freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlinx.serialization.ExperimentalSerializationApi" + freeCompilerArgs = freeCompilerArgs + "-opt-in=androidx.compose.animation.ExperimentalAnimationApi" + freeCompilerArgs = freeCompilerArgs + "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi" + freeCompilerArgs = freeCompilerArgs + "-opt-in=androidx.compose.material.ExperimentalMaterialApi" + freeCompilerArgs = freeCompilerArgs + "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api" + freeCompilerArgs = freeCompilerArgs + "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi" + freeCompilerArgs = freeCompilerArgs + "-opt-in=com.google.accompanist.pager.ExperimentalPagerApi" + freeCompilerArgs = freeCompilerArgs + "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi" } testOptions { @@ -222,58 +156,25 @@ android { } } -repositories { - google() - mavenCentral() -} - dependencies { - api(project(":domain")) - testImplementation(project(":data", "testArtifacts")) - androidTestImplementation(project(":data", "androidTestArtifacts")) - - // Compose - implementation(AndroidX.activity.compose) - implementation(AndroidX.compose.runtime.liveData) - implementation(AndroidX.compose.ui) - implementation(AndroidX.compose.material3) - implementation(AndroidX.compose.material) - implementation(AndroidX.compose.ui.toolingPreview) - implementation(AndroidX.navigation.compose) - implementation(AndroidX.constraintLayout.compose) - implementation(AndroidX.compose.material.icons.extended) - implementation(AndroidX.paging.compose) - implementation(AndroidX.paging.commonKtx) - androidTestApi(AndroidX.compose.ui.test) - androidTestApi(AndroidX.compose.ui.testJunit4) - debugImplementation(AndroidX.compose.ui.testManifest) - debugApi(AndroidX.compose.ui.tooling) - - // accompanist - implementation(Google.accompanist.navigationAnimation) - implementation(Google.accompanist.systemUiController) - implementation(Google.accompanist.placeholder) - implementation(Google.accompanist.flowLayout) - implementation(Google.accompanist.insets) - implementation(Google.accompanist.pager) - implementation(Google.accompanist.swipeRefresh) - implementation(Google.accompanist.permissions) + implementation(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) - // design - implementation(Google.android.material) + implementation(projects.ui.albums) + implementation(projects.ui.folders) + implementation(projects.ui.photos) + implementation(projects.ui.settings) + implementation(projects.ui.sharing) - // Coil - implementation(COIL) - implementation(COIL.compose) + implementation(projects.ui.common) - // retrofit - implementation(Square.retrofit2) - implementation(Square.okHttp3.loggingInterceptor) + // Compose Activity + implementation(platform(libs.compose.bom)) + implementation(libs.activity.compose) - // serialization - implementation(KotlinX.serialization.json) - implementation(JakeWharton.retrofit2.converter.kotlinxSerialization) + implementation(libs.bundles.accompanist) // leakCanary - debugImplementation(Square.leakCanary.android) + debugImplementation(libs.leakcanary.android) } diff --git a/app/src/androidTest/kotlin/photos/network/PhotosNetworkJUnitRunner.kt b/app/src/androidTest/kotlin/photos/network/PhotosNetworkJUnitRunner.kt index ad4dc6e..48e2dbc 100644 --- a/app/src/androidTest/kotlin/photos/network/PhotosNetworkJUnitRunner.kt +++ b/app/src/androidTest/kotlin/photos/network/PhotosNetworkJUnitRunner.kt @@ -28,7 +28,7 @@ class PhotosNetworkJUnitRunner : AndroidJUnitRunner() { override fun newApplication( cl: ClassLoader?, className: String?, - context: Context? + context: Context?, ): Application { return super.newApplication(cl, PhotosNetworkApplication::class.java.name, context) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7969881..303de94 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools"> @@ -15,7 +14,8 @@ - + + @@ -38,26 +38,27 @@ android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.PhotosNetwork" + android:theme="@style/Theme.AppCompat.NoActionBar" tools:targetApi="tiramisu"> + android:exported="true"> + - - - + + + + + + diff --git a/app/src/main/kotlin/photos/network/AppModule.kt b/app/src/main/kotlin/photos/network/AppModule.kt index 3e434c2..042a79a 100644 --- a/app/src/main/kotlin/photos/network/AppModule.kt +++ b/app/src/main/kotlin/photos/network/AppModule.kt @@ -17,47 +17,24 @@ package photos.network import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module -import photos.network.domain.photos.usecase.StartPhotosSyncUseCase import photos.network.home.HomeViewModel -import photos.network.home.photos.PhotosViewModel -import photos.network.presentation.login.LoginViewModel -import photos.network.settings.SettingsViewModel +import photos.network.ui.settings.SettingsViewModel +import photos.network.ui.sharing.login.LoginViewModel import photos.network.user.CurrentUserViewModel val appModule = module { viewModel { CurrentUserViewModel( - userRepository = get() - ) - } - viewModel { - PhotosViewModel( - getPhotosUseCase = get(), - startPhotosSyncUseCase = StartPhotosSyncUseCase( - photoRepository = get() - ), - ) - } - viewModel { - LoginViewModel( - requestAccessTokenUseCase = get(), - settingsUseCase = get() + userRepository = get(), ) } + + viewModel { HomeViewModel( getSettingsUseCase = get(), - togglePrivacyStateUseCase = get() - ) - } - viewModel { - SettingsViewModel( - application = get(), - getSettingsUseCase = get(), - updateHostUseCase = get(), - updateClientIdUseCase = get(), - verifyServerHostUseCase = get(), - verifyClientIdUseCase = get(), + togglePrivacyStateUseCase = get(), ) } + } diff --git a/app/src/main/kotlin/photos/network/MainActivity.kt b/app/src/main/kotlin/photos/network/MainActivity.kt index 5a55b35..ddf9881 100644 --- a/app/src/main/kotlin/photos/network/MainActivity.kt +++ b/app/src/main/kotlin/photos/network/MainActivity.kt @@ -16,8 +16,8 @@ package photos.network import android.os.Bundle +import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable @@ -33,14 +33,14 @@ import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.google.accompanist.systemuicontroller.SystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController import photos.network.home.Home -import photos.network.navigation.Destination -import photos.network.theme.AppTheme +import photos.network.ui.common.navigation.Destination +import photos.network.ui.common.theme.AppTheme import photos.network.user.CurrentUserHost /** * Main entry point, handling navigation events. */ -class MainActivity : AppCompatActivity() { +class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -64,7 +64,7 @@ fun PhotosApp( SideEffect { systemUiController.setSystemBarsColor( color = Color.Transparent, - darkIcons = useDarkIcons + darkIcons = useDarkIcons, ) } @@ -73,7 +73,7 @@ fun PhotosApp( CurrentUserHost { Home( modifier = Modifier.fillMaxSize(), - orientation = LocalConfiguration.current.orientation + orientation = LocalConfiguration.current.orientation, ) } } diff --git a/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt b/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt index 7ad17d7..3c1a171 100644 --- a/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt +++ b/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt @@ -24,9 +24,23 @@ import org.koin.androidx.workmanager.koin.workManagerFactory import org.koin.core.component.KoinComponent import org.koin.core.context.startKoin import org.koin.core.logger.Level -import photos.network.data.BuildConfig -import photos.network.data.dataModule -import photos.network.domain.domainModule +import photos.network.database.photos.databasePhotosModule +import photos.network.database.settings.databaseSettingsModule +import photos.network.database.sharing.databaseSharingModule +import photos.network.domain.albums.domainAlbumsModule +import photos.network.domain.folders.domainFoldersModule +import photos.network.domain.photos.domainPhotosModule +import photos.network.domain.settings.domainSettingsModule +import photos.network.domain.sharing.domainSharingModule +import photos.network.network.networkModule +import photos.network.repository.photos.repositoryPhotosModule +import photos.network.repository.settings.repositorySettingsModule +import photos.network.repository.sharing.repositorySharingModule +import photos.network.ui.albums.uiAlbumsModule +import photos.network.ui.folders.uiFoldersModule +import photos.network.ui.photos.uiPhotosModule +import photos.network.ui.settings.uiSettingsModule +import photos.network.ui.sharing.uiSharingModule /** * Android Application subclass to manually initialize logging and dependency injection. @@ -38,7 +52,7 @@ open class PhotosNetworkApplication : Application(), KoinComponent { // setup logging AndroidLogcatLogger.installOnDebuggableApp( this@PhotosNetworkApplication, - minPriority = LogPriority.VERBOSE + minPriority = LogPriority.VERBOSE, ) // setup dependency injection @@ -54,10 +68,36 @@ open class PhotosNetworkApplication : Application(), KoinComponent { // use modules modules( listOf( - dataModule, - domainModule, appModule, - ) + + // albums + uiAlbumsModule, + domainAlbumsModule, + + // folders + uiFoldersModule, + domainFoldersModule, + + // photos + uiPhotosModule, + domainPhotosModule, + repositoryPhotosModule, + databasePhotosModule, + + // settings + uiSettingsModule, + domainSettingsModule, + repositorySettingsModule, + databaseSettingsModule, + + // sharing + uiSharingModule, + domainSharingModule, + repositorySharingModule, + databaseSharingModule, + + networkModule, + ), ) } } diff --git a/app/src/main/kotlin/photos/network/home/Home.kt b/app/src/main/kotlin/photos/network/home/Home.kt index 06f08cd..d98eebf 100644 --- a/app/src/main/kotlin/photos/network/home/Home.kt +++ b/app/src/main/kotlin/photos/network/home/Home.kt @@ -19,7 +19,9 @@ import android.content.res.Configuration import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Shield import androidx.compose.material.icons.outlined.Shield @@ -36,6 +38,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource @@ -45,20 +48,12 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import com.google.accompanist.insets.navigationBarsPadding -import com.google.accompanist.insets.statusBarsPadding import org.koin.androidx.compose.getViewModel import photos.network.R -import photos.network.home.albums.AlbumsScreen -import photos.network.home.folders.FoldersScreen -import photos.network.home.photos.PhotosScreen -import photos.network.navigation.Destination -import photos.network.presentation.help.HelpScreen -import photos.network.presentation.login.LoginScreen -import photos.network.settings.ServerStatus -import photos.network.settings.SettingsScreen -import photos.network.theme.AppTheme -import photos.network.ui.components.AppLogo +import photos.network.network.ServerStatus +import photos.network.ui.common.navigation.Destination +import photos.network.ui.common.theme.AppTheme +import photos.network.ui.common.components.AppLogo /** * Default app screen containing a searchbar, photos grid, albums tab and more. @@ -66,12 +61,14 @@ import photos.network.ui.components.AppLogo @Composable fun Home( modifier: Modifier = Modifier, - orientation: Int + orientation: Int, ) { val navController = rememberNavController() val navBackStackEntry = navController.currentBackStackEntryAsState() - val currentDestination by derivedStateOf { - Destination.fromString(navBackStackEntry.value?.destination?.route) + val currentDestination by remember { + derivedStateOf { + Destination.fromString(navBackStackEntry.value?.destination?.route) + } } val viewmodel: HomeViewModel = getViewModel() @@ -102,7 +99,7 @@ fun Home( }, size = 32.dp, statusSize = 16.dp, - serverStatus = ServerStatus.UNAVAILABLE + serverStatus = ServerStatus.UNAVAILABLE, ) }, actions = { @@ -110,25 +107,25 @@ fun Home( IconButton( onClick = { viewmodel.handleEvent(HomeEvent.TogglePrivacyEvent) - } + }, ) { if (viewmodel.uiState.collectAsState().value.isPrivacyEnabled) { Icon( imageVector = Icons.Default.Shield, contentDescription = stringResource(id = R.string.privacy_filter_enabled_description), - tint = MaterialTheme.colorScheme.onPrimary + tint = MaterialTheme.colorScheme.onPrimary, ) } else { Icon( imageVector = Icons.Outlined.Shield, contentDescription = stringResource(id = R.string.privacy_filter_disabled_description), - tint = MaterialTheme.colorScheme.onPrimary + tint = MaterialTheme.colorScheme.onPrimary, ) } } }, colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = MaterialTheme.colorScheme.primary + containerColor = MaterialTheme.colorScheme.primary, ), ) } @@ -143,7 +140,7 @@ fun Home( selected = currentDestination == Destination.Photos, onClick = { navController.navigate(Destination.Photos.route) - } + }, ) // Albums @@ -153,7 +150,7 @@ fun Home( selected = currentDestination == Destination.Albums, onClick = { navController.navigate(Destination.Albums.route) - } + }, ) // Folders @@ -163,7 +160,7 @@ fun Home( selected = currentDestination == Destination.Folders, onClick = { navController.navigate(Destination.Folders.route) - } + }, ) } } @@ -174,15 +171,34 @@ fun Home( navController = navController, startDestination = Destination.Photos.route, ) { - composable(route = Destination.Photos.route) { PhotosScreen(navController = navController) } - composable(route = Destination.Albums.route) { AlbumsScreen(navController = navController) } - composable(route = Destination.Folders.route) { FoldersScreen(navController = navController) } - composable(route = Destination.Account.route) { SettingsScreen(navController = navController) } - composable(route = Destination.Login.route) { LoginScreen(navController = navController) } - composable(route = Destination.Help.route) { HelpScreen(navController = navController) } + composable(route = Destination.Photos.route) { + photos.network.ui.photos.PhotosScreen( + navController = navController, + ) + } + composable(route = Destination.Albums.route) { + photos.network.ui.albums.AlbumsScreen( + navController = navController, + ) + } + composable(route = Destination.Folders.route) { + photos.network.ui.folders.FoldersScreen( + navController = navController, + ) + } + composable(route = Destination.Account.route) { + photos.network.ui.settings.SettingsScreen( + navController = navController, + ) + } + composable(route = Destination.Login.route) { + photos.network.ui.sharing.login.LoginScreen( + navController = navController, + ) + } } } - } + }, ) } @@ -193,7 +209,7 @@ fun PreviewHomeScreen() { AppTheme { Home( modifier = Modifier.fillMaxSize(), - orientation = Configuration.ORIENTATION_LANDSCAPE + orientation = Configuration.ORIENTATION_LANDSCAPE, ) } } diff --git a/app/src/main/kotlin/photos/network/home/HomeViewModel.kt b/app/src/main/kotlin/photos/network/home/HomeViewModel.kt index 0a31358..f408818 100644 --- a/app/src/main/kotlin/photos/network/home/HomeViewModel.kt +++ b/app/src/main/kotlin/photos/network/home/HomeViewModel.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import photos.network.data.settings.repository.PrivacyState +import photos.network.common.persistence.PrivacyState import photos.network.domain.settings.usecase.GetSettingsUseCase import photos.network.domain.settings.usecase.TogglePrivacyUseCase diff --git a/app/src/main/kotlin/photos/network/ui/PhotoGrid.kt b/app/src/main/kotlin/photos/network/ui/PhotoGrid.kt deleted file mode 100644 index b0b3911..0000000 --- a/app/src/main/kotlin/photos/network/ui/PhotoGrid.kt +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright 2020-2022 Photos.network developers - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package photos.network.ui - -import android.icu.text.DateFormatSymbols -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.GridItemSpan -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.rememberLazyGridState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import coil.compose.rememberImagePainter -import photos.network.R -import photos.network.data.photos.repository.Photo -import photos.network.home.photos.PhotoDetails -import photos.network.theme.AppTheme -import java.time.Instant -import java.time.ZoneOffset - -@Composable -fun PhotoGrid( - modifier: Modifier = Modifier, - photos: List, - selectedIndex: Int? = null, - selectedPhoto: Photo? = null, - onSelectItem: (index: Int?) -> Unit, - selectPreviousPhoto: () -> Unit = {}, - selectNextPhoto: () -> Unit = {}, -) { - val lazyListState = rememberLazyGridState() - - Box { - LazyVerticalGrid( - state = lazyListState, - modifier = modifier - .fillMaxSize() - .padding(4.dp), - columns = GridCells.Adaptive(90.dp), - ) { - // group by year - val groupedByYear = photos.groupBy { - it.dateAdded.atZone(ZoneOffset.UTC).year - } - - groupedByYear.forEach { (_, photos) -> - val yearOfFirst = photos[0].dateAdded.atZone(ZoneOffset.UTC).year - val yearNow = Instant.now().atZone(ZoneOffset.UTC).year - - // add year header if necessary - if (yearOfFirst != yearNow) { - item(span = { GridItemSpan(maxCurrentLineSpan) }) { - Text( - text = yearOfFirst.toString(), - style = MaterialTheme.typography.bodyMedium - ) - } - } - - // group by month - val groupedByMonth = photos.groupBy { - it.dateAdded.atZone(ZoneOffset.UTC).month - } - - groupedByMonth.forEach { (month, photos) -> - // add year if not matching with current year - val title = if (yearOfFirst == yearNow) { - DateFormatSymbols().months[month.value - 1] - } else { - "${DateFormatSymbols().months[month.value - 1]} $yearOfFirst" - } - - // month header - item(span = { GridItemSpan(maxCurrentLineSpan) }) { - Text(text = title, style = MaterialTheme.typography.bodyLarge) - } - - items(photos.size) { index: Int -> - // TODO: show always local uri? - val data = if (photos[index].uri != null) { - photos[index].uri - } else { - photos[index].imageUrl - } - - Box( - modifier = Modifier - .aspectRatio(1.0f) - .size(128.dp) - .clip(RoundedCornerShape(2.dp)) - .clickable { - onSelectItem(index) - } - ) { - Image( - painter = rememberImagePainter( - data = data, - builder = { - crossfade(true) - placeholder(R.drawable.image_placeholder) - } - ), - contentDescription = null, - contentScale = ContentScale.None, - modifier = Modifier.padding(1.dp), - ) - } - } - } - } - } - - if (selectedPhoto != null) { - PhotoDetails( - modifier = Modifier - .testTag("PHOTO_DETAILS") - .background(Color.Black.copy(alpha = 0.9f)) - .fillMaxSize(), - selectedIndex = selectedIndex, - selectNextPhoto = selectNextPhoto, - selectPreviousPhoto = selectPreviousPhoto, - selectedPhoto = selectedPhoto, - onSelectItem = onSelectItem - ) - } - } -} - -@Preview -@Composable -internal fun PreviewPhotoGrid() { - val list = (0..15).map { - Photo( - filename = it.toString(), - imageUrl = "", - dateAdded = Instant.parse("2022-01-01T13:37:00.123Z"), - dateTaken = Instant.parse("2022-01-01T13:37:00.123Z") - ) - } - AppTheme { - PhotoGrid( - photos = list, - onSelectItem = {} - ) - } -} diff --git a/app/src/main/kotlin/photos/network/ui/SearchBar.kt b/app/src/main/kotlin/photos/network/ui/SearchBar.kt index 0fef54e..d666d4e 100644 --- a/app/src/main/kotlin/photos/network/ui/SearchBar.kt +++ b/app/src/main/kotlin/photos/network/ui/SearchBar.kt @@ -54,7 +54,7 @@ class SearchContentProvider : PreviewParameterProvider { showSystemUi = false, locale = "ar", uiMode = UI_MODE_NIGHT_YES, - device = Devices.PIXEL_4 + device = Devices.PIXEL_4, ) @Preview(name = "Day", locale = "ko-rKR", showSystemUi = false, device = Devices.PIXEL_4) @Preview(name = "Day, small screen", showSystemUi = true, device = Devices.NEXUS_5) @@ -68,7 +68,7 @@ fun SearchViewPreview(@PreviewParameter(SearchContentProvider::class) search: St value = textState.value, onValueChange = { newTextFieldValue -> textState.value = newTextFieldValue }, onSearch = {}, - hint = stringResource(id = R.string.search_hint) + hint = stringResource(id = R.string.search_hint), ) } } @@ -84,12 +84,12 @@ fun Searchbar( keyboardOptions: KeyboardOptions = KeyboardOptions.Default.copy( imeAction = ImeAction.Search, keyboardType = KeyboardType.Text, - capitalization = KeyboardCapitalization.Sentences + capitalization = KeyboardCapitalization.Sentences, ), keyboardActions: KeyboardActions = KeyboardActions(onSearch = { onSearch() }), ) { Box( - modifier = modifier.testTag("Searchbar") + modifier = modifier.testTag("Searchbar"), ) { val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current diff --git a/app/src/main/kotlin/photos/network/ui/TextInput.kt b/app/src/main/kotlin/photos/network/ui/TextInput.kt index ed6a23d..faaf9df 100644 --- a/app/src/main/kotlin/photos/network/ui/TextInput.kt +++ b/app/src/main/kotlin/photos/network/ui/TextInput.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import photos.network.theme.AppTheme +import photos.network.ui.common.theme.AppTheme @Composable fun TextInput( @@ -42,17 +42,17 @@ fun TextInput( labelColor: Color = MaterialTheme.colorScheme.secondary, valueColor: Color = MaterialTheme.colorScheme.secondary, errroColor: Color = MaterialTheme.colorScheme.error, - isValid: () -> (Boolean) = { true } + isValid: () -> (Boolean) = { true }, ) { Column( - modifier = modifier + modifier = modifier, ) { label?.let { Text( modifier = Modifier.padding(all = 0.dp), text = it, style = MaterialTheme.typography.labelSmall, - color = labelColor + color = labelColor, ) } BasicTextField( @@ -61,17 +61,17 @@ fun TextInput( .border( BorderStroke( 1.dp, - if (isValid()) valueColor else errroColor - ) + if (isValid()) valueColor else errroColor, + ), ) .padding(horizontal = 8.dp), value = value, maxLines = 2, textStyle = TextStyle( color = valueColor, - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, ), - onValueChange = onValueChanged + onValueChange = onValueChanged, ) } } @@ -84,7 +84,7 @@ fun PreviewTextInput() { TextInput( label = "Label", value = "content", - onValueChanged = {} + onValueChanged = {}, ) } } diff --git a/app/src/main/kotlin/photos/network/ui/UserAvatar.kt b/app/src/main/kotlin/photos/network/ui/UserAvatar.kt index f82c33e..429375b 100644 --- a/app/src/main/kotlin/photos/network/ui/UserAvatar.kt +++ b/app/src/main/kotlin/photos/network/ui/UserAvatar.kt @@ -32,15 +32,18 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext 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 coil.compose.LocalImageLoader -import coil.compose.rememberImagePainter +import coil.compose.rememberAsyncImagePainter +import coil.imageLoader +import coil.request.ImageRequest import photos.network.R -import photos.network.domain.user.User -import photos.network.theme.AppTheme +import photos.network.repository.sharing.User +import photos.network.ui.common.theme.AppTheme /** * Rounded user avatar with initials if no profile image url is available or set @@ -49,28 +52,29 @@ import photos.network.theme.AppTheme fun UserAvatar( modifier: Modifier = Modifier, user: User?, - color: Color = MaterialTheme.colorScheme.primary + color: Color = MaterialTheme.colorScheme.primary, ) { Box( modifier = modifier .aspectRatio(1f) .wrapContentSize(Alignment.Center) .clip(CircleShape) - .background(color) + .background(color), ) { if (user != null) { if (user.profileImageUrl.isNotBlank()) { Image( - painter = rememberImagePainter( - data = user.profileImageUrl, - imageLoader = LocalImageLoader.current, - builder = { - crossfade(true) - placeholder(R.drawable.bob_ross_head_200x200) - } + painter = rememberAsyncImagePainter( + ImageRequest.Builder(LocalContext.current).data(data = user.profileImageUrl) + .apply(block = fun ImageRequest.Builder.() { + crossfade(true) + placeholder(R.drawable.bob_ross_head_200x200) + }).build(), imageLoader = LocalContext.current.imageLoader ), contentDescription = stringResource(id = R.string.icon_user_profile), - modifier = Modifier.aspectRatio(1f).clip(CircleShape), + modifier = Modifier + .aspectRatio(1f) + .clip(CircleShape), contentScale = ContentScale.FillBounds, ) } else { @@ -88,7 +92,7 @@ fun UserAvatar( textAlign = TextAlign.Center, text = textContent, color = MaterialTheme.colorScheme.onPrimary, - style = MaterialTheme.typography.headlineSmall + style = MaterialTheme.typography.headlineSmall, ) } } else { @@ -100,7 +104,7 @@ fun UserAvatar( textAlign = TextAlign.Center, text = "--", color = MaterialTheme.colorScheme.onPrimary, - style = MaterialTheme.typography.headlineSmall + style = MaterialTheme.typography.headlineSmall, ) } } @@ -114,8 +118,8 @@ fun UserProfileImagePreview() { user = User( firstname = "Bob", lastname = "Ross", - profileImageUrl = "https://boardgaming.com/wp-content/uploads/2017/12/bob-ross-head-200x200.jpg" - ) + profileImageUrl = "https://boardgaming.com/wp-content/uploads/2017/12/bob-ross-head-200x200.jpg", + ), ) } } @@ -126,7 +130,7 @@ fun UserProfileImagePreview() { fun UserProfileImagePreviewNoImage() { AppTheme { UserAvatar( - user = User(firstname = "Bob", lastname = "Ross", profileImageUrl = "") + user = User(firstname = "Bob", lastname = "Ross", profileImageUrl = ""), ) } } @@ -136,7 +140,7 @@ fun UserProfileImagePreviewNoImage() { fun UserProfileImagePreviewNoName() { AppTheme { UserAvatar( - user = User(firstname = "", lastname = "", profileImageUrl = "") + user = User(firstname = "", lastname = "", profileImageUrl = ""), ) } } diff --git a/app/src/main/kotlin/photos/network/user/CurrentUserHost.kt b/app/src/main/kotlin/photos/network/user/CurrentUserHost.kt index 7529d4b..116f0f0 100644 --- a/app/src/main/kotlin/photos/network/user/CurrentUserHost.kt +++ b/app/src/main/kotlin/photos/network/user/CurrentUserHost.kt @@ -19,7 +19,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.staticCompositionLocalOf import org.koin.androidx.compose.viewModel -import photos.network.domain.user.User +import photos.network.repository.sharing.User val LocalCurrentUser = staticCompositionLocalOf { error("LocalCurrentUser not provided") @@ -30,7 +30,7 @@ val LocalCurrentUser = staticCompositionLocalOf { */ @Composable fun CurrentUserHost( - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { val viewModel: CurrentUserViewModel by viewModel() diff --git a/app/src/main/kotlin/photos/network/user/CurrentUserViewModel.kt b/app/src/main/kotlin/photos/network/user/CurrentUserViewModel.kt index a843880..4d76423 100644 --- a/app/src/main/kotlin/photos/network/user/CurrentUserViewModel.kt +++ b/app/src/main/kotlin/photos/network/user/CurrentUserViewModel.kt @@ -16,11 +16,11 @@ package photos.network.user import androidx.lifecycle.ViewModel -import photos.network.data.user.repository.UserRepository -import photos.network.domain.user.User +import photos.network.repository.sharing.User +import photos.network.repository.sharing.UserRepository class CurrentUserViewModel( - private val userRepository: UserRepository + private val userRepository: UserRepository, ) : ViewModel() { val currentUser: User? get() { diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml deleted file mode 100644 index 8fa1299..0000000 --- a/app/src/main/res/values-night/themes.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 63905af..000cde2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,35 +1,17 @@ Photos - Photos.network - An open source project for self hosted photo management. Host Client ID Client Secret Next User profile icon - tags icon + Could not load photos - Home - Login - Setup - Help - Photos - Albums - Details - Search - Folders - Account + application logo - Communication with the configured photos.network instance is fine. - The configured photos.network instance is currently not available. - Data is being transmitted with the configured photos.network instance. - Communication to the configured photos.network instance is not authorized! All items labeled as private are hidden Hide items labeled as private in this view - Setup server instance - Change server setup - Version copied into clipboard diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml deleted file mode 100644 index e1c8921..0000000 --- a/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - diff --git a/build.gradle.kts b/build.gradle.kts index be7218c..7b46354 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,18 +1,9 @@ -buildscript { - repositories { - mavenCentral() - google() - } - - dependencies { - classpath(Android.tools.build.gradlePlugin) - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:_") - } -} - -subprojects { - repositories { - google() - mavenCentral() - } +plugins { + // this is necessary to avoid the plugins to be loaded multiple times + // in each subproject's classloader + kotlin("android") apply false + id("com.android.application") apply false + id("com.android.library") apply false + // kotlin("kapt") apply false + id("com.google.devtools.ksp") apply false } diff --git a/common/build.gradle.kts b/common/build.gradle.kts new file mode 100644 index 0000000..01a7449 --- /dev/null +++ b/common/build.gradle.kts @@ -0,0 +1,99 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.common" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 23 | android.security.keystore + minSdk = 23 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + } +} + +configurations { + create("testArtifacts"){ + extendsFrom(configurations.testApi.get()) + } + create("androidTestArtifacts"){ + extendsFrom(configurations.androidTestApi.get()) + } +} + +dependencies { + api(libs.androidx.core.core.ktx) + testApi(libs.androidx.test.core.ktx) + + // Coroutines + api(libs.kotlinx.coroutines.core) + api(libs.kotlinx.coroutines.android) + testApi(libs.kotlinx.coroutines.test) + + // Coroutine Lifecycle Scopes + api(libs.lifecycle.runtime.ktx) + api(libs.lifecycle.viewmodel.ktx) + + // Koin dependency injection + api(libs.bundles.koin) + testApi(libs.koin.test) + + // logging + api(libs.logcat) + + // serialization + api(libs.kotlin.serialization) + api(libs.kotlinx.serialization.json) + + // workmanager + api(libs.work.runtime.ktx) + androidTestApi(libs.work.testing) + + // httpclient + api(libs.bundles.ktor) + testApi(libs.ktor.client.mock.jvm) + + // exifinterface + api(libs.exifinterface) + + // security crypto + api(libs.security.crypto) + + // testing + testApi(libs.mockk) + testApi(libs.com.google.truth.truth) + androidTestApi(libs.androidx.test.core) + androidTestApi(libs.androidx.test.ext.junit) + androidTestApi(libs.androidx.test.ext.junit) + androidTestApi(libs.androidx.test.ext.truth) + androidTestApi(libs.androidx.test.monitor) + androidTestApi(libs.androidx.test.runner) + androidTestApi(libs.androidx.test.rules) + androidTestApi(libs.androidx.test.orchestrator) + androidTestApi(libs.androidx.test.services) +} diff --git a/data/src/androidTest/kotlin/photos/network/data/PhotosNetworkMockFileReader.kt b/common/src/androidTest/kotlin/photos/network/common/PhotosNetworkMockFileReader.kt similarity index 97% rename from data/src/androidTest/kotlin/photos/network/data/PhotosNetworkMockFileReader.kt rename to common/src/androidTest/kotlin/photos/network/common/PhotosNetworkMockFileReader.kt index 4d9bcd7..e1a927f 100644 --- a/data/src/androidTest/kotlin/photos/network/data/PhotosNetworkMockFileReader.kt +++ b/common/src/androidTest/kotlin/photos/network/common/PhotosNetworkMockFileReader.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data +package photos.network.common import androidx.test.platform.app.InstrumentationRegistry import java.io.IOException diff --git a/data/src/androidTest/kotlin/photos/network/data/FakeAndroidKeyStore.kt b/common/src/androidTest/kotlin/photos/network/common/keystore/FakeAndroidKeyStore.kt similarity index 98% rename from data/src/androidTest/kotlin/photos/network/data/FakeAndroidKeyStore.kt rename to common/src/androidTest/kotlin/photos/network/common/keystore/FakeAndroidKeyStore.kt index 9e76165..6204508 100644 --- a/data/src/androidTest/kotlin/photos/network/data/FakeAndroidKeyStore.kt +++ b/common/src/androidTest/kotlin/photos/network/common/keystore/FakeAndroidKeyStore.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data +package photos.network.common.keystore import java.io.InputStream import java.io.OutputStream diff --git a/data/src/androidTest/kotlin/photos/network/data/SecureStorageTest.kt b/common/src/androidTest/kotlin/photos/network/common/persistence/SecureStorageTest.kt similarity index 96% rename from data/src/androidTest/kotlin/photos/network/data/SecureStorageTest.kt rename to common/src/androidTest/kotlin/photos/network/common/persistence/SecureStorageTest.kt index 6fedf0d..0086da3 100644 --- a/data/src/androidTest/kotlin/photos/network/data/SecureStorageTest.kt +++ b/common/src/androidTest/kotlin/photos/network/common/persistence/SecureStorageTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data +package photos.network.common.persistence import android.content.Context import androidx.security.crypto.EncryptedFile @@ -25,6 +25,7 @@ import org.junit.Before import org.junit.BeforeClass import org.junit.Test import org.junit.runner.RunWith +import photos.network.common.keystore.FakeAndroidKeyStore import java.io.ByteArrayOutputStream import java.io.File import java.nio.charset.StandardCharsets @@ -46,7 +47,7 @@ class SecureStorageTest { File(context.filesDir, "fileToPersist"), context, masterKeyAlias, - EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB + EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB, ).build() @Test diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/common/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/data/src/main/kotlin/photos/network/data/settings/repository/Settings.kt b/common/src/main/kotlin/photos/network/common/Module.kt similarity index 68% rename from data/src/main/kotlin/photos/network/data/settings/repository/Settings.kt rename to common/src/main/kotlin/photos/network/common/Module.kt index 3b5c87b..d17994b 100644 --- a/data/src/main/kotlin/photos/network/data/settings/repository/Settings.kt +++ b/common/src/main/kotlin/photos/network/common/Module.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,11 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.settings.repository +package photos.network.common -data class Settings( - val host: String = "", - val port: Int = 443, - val clientId: String = "", - val privacyState: PrivacyState = PrivacyState.NONE, -) +import org.koin.dsl.module + +val commonModule = module { + +} diff --git a/data/src/main/kotlin/photos/network/data/Resource.kt b/common/src/main/kotlin/photos/network/common/Resource.kt similarity index 93% rename from data/src/main/kotlin/photos/network/data/Resource.kt rename to common/src/main/kotlin/photos/network/common/Resource.kt index 5fd0564..05210d2 100644 --- a/data/src/main/kotlin/photos/network/data/Resource.kt +++ b/common/src/main/kotlin/photos/network/common/Resource.kt @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data +package photos.network.common /** * Network loading resource */ sealed class Resource( val data: T? = null, - val message: String? = null + val message: String? = null, ) { class Success(data: T) : Resource(data) class Error(message: String, data: T? = null, val isNetworkError: Boolean = false) : Resource(data, message) diff --git a/data/src/main/kotlin/photos/network/data/settings/repository/PrivacyState.kt b/common/src/main/kotlin/photos/network/common/persistence/PrivacyState.kt similarity index 93% rename from data/src/main/kotlin/photos/network/data/settings/repository/PrivacyState.kt rename to common/src/main/kotlin/photos/network/common/persistence/PrivacyState.kt index cff7ca5..580b810 100644 --- a/data/src/main/kotlin/photos/network/data/settings/repository/PrivacyState.kt +++ b/common/src/main/kotlin/photos/network/common/persistence/PrivacyState.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.settings.repository +package photos.network.common.persistence enum class PrivacyState { NONE, diff --git a/data/src/main/kotlin/photos/network/data/SecureStorage.kt b/common/src/main/kotlin/photos/network/common/persistence/SecureStorage.kt similarity index 94% rename from data/src/main/kotlin/photos/network/data/SecureStorage.kt rename to common/src/main/kotlin/photos/network/common/persistence/SecureStorage.kt index 786adf9..a21640a 100644 --- a/data/src/main/kotlin/photos/network/data/SecureStorage.kt +++ b/common/src/main/kotlin/photos/network/common/persistence/SecureStorage.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data +package photos.network.common.persistence import android.content.Context import android.security.keystore.KeyGenParameterSpec @@ -24,9 +24,10 @@ import java.io.ByteArrayOutputStream import java.io.File import java.nio.charset.StandardCharsets +@Suppress("TooGenericExceptionCaught", "SwallowedException") abstract class SecureStorage( context: Context, - filename: String + filename: String, ) { private val secureFile = File(context.filesDir, filename) private lateinit var masterKey: MasterKey @@ -45,6 +46,7 @@ abstract class SecureStorage( abstract fun encodeData(data: T): String + @Suppress("SwallowedException") open fun read(): T? { try { val inputStream = encryptedFile.openFileInput() @@ -82,7 +84,7 @@ abstract class SecureStorage( val keyGenParameterSpec = KeyGenParameterSpec .Builder( MasterKey.DEFAULT_MASTER_KEY_ALIAS, - KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT, ) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) @@ -97,11 +99,11 @@ abstract class SecureStorage( context.applicationContext, secureFile, masterKey, - EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB + EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB, ).build() } - internal fun delete() { + fun delete() { if (secureFile.exists()) { secureFile.deleteRecursively() } diff --git a/data/src/main/kotlin/photos/network/data/settings/persistence/Settings.kt b/common/src/main/kotlin/photos/network/common/persistence/Settings.kt similarity index 88% rename from data/src/main/kotlin/photos/network/data/settings/persistence/Settings.kt rename to common/src/main/kotlin/photos/network/common/persistence/Settings.kt index d04cdf2..f9a76b7 100644 --- a/data/src/main/kotlin/photos/network/data/settings/persistence/Settings.kt +++ b/common/src/main/kotlin/photos/network/common/persistence/Settings.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.settings.persistence +package photos.network.common.persistence import kotlinx.serialization.Serializable @@ -22,5 +22,5 @@ class Settings( val host: String? = null, val port: Int = 443, val clientId: String? = null, - val privacyState: String = "NONE", + val privacyState: PrivacyState = PrivacyState.NONE, ) diff --git a/data/src/main/kotlin/photos/network/data/user/persistence/User.kt b/common/src/main/kotlin/photos/network/common/persistence/User.kt similarity index 68% rename from data/src/main/kotlin/photos/network/data/user/persistence/User.kt rename to common/src/main/kotlin/photos/network/common/persistence/User.kt index 686403b..7168007 100644 --- a/data/src/main/kotlin/photos/network/data/user/persistence/User.kt +++ b/common/src/main/kotlin/photos/network/common/persistence/User.kt @@ -13,10 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.user.persistence +package photos.network.common.persistence import kotlinx.serialization.Serializable -import photos.network.data.user.repository.User as DomainUser @Serializable class User( @@ -26,15 +25,4 @@ class User( val profileImageUrl: String, val accessToken: String? = null, val refreshToken: String? = null, -) { - fun toDomain(): DomainUser { - return DomainUser( - id = id, - lastname = lastname, - firstname = firstname, - profileImageUrl = profileImageUrl, - accessToken = accessToken, - refreshToken = refreshToken, - ) - } -} +) diff --git a/app/src/test/kotlin/photos/network/TestCoroutineDispatcherRule.kt b/common/src/test/kotlin/photos/network/common/TestCoroutineDispatcherRule.kt similarity index 97% rename from app/src/test/kotlin/photos/network/TestCoroutineDispatcherRule.kt rename to common/src/test/kotlin/photos/network/common/TestCoroutineDispatcherRule.kt index 88c1eba..be0242e 100644 --- a/app/src/test/kotlin/photos/network/TestCoroutineDispatcherRule.kt +++ b/common/src/test/kotlin/photos/network/common/TestCoroutineDispatcherRule.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network +package photos.network.common import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.StandardTestDispatcher @@ -23,7 +23,6 @@ import org.junit.rules.TestWatcher import org.junit.runner.Description class TestCoroutineDispatcherRule : TestWatcher() { - private val dispatcher = StandardTestDispatcher() override fun starting(description: Description?) { diff --git a/data/build.gradle.kts b/data/build.gradle.kts deleted file mode 100644 index c509561..0000000 --- a/data/build.gradle.kts +++ /dev/null @@ -1,185 +0,0 @@ -plugins { - id("com.android.library") - id("com.diffplug.spotless") - kotlin("android") - kotlin("kapt") - kotlin("plugin.serialization") - id("jacoco") -} - -spotless { - kotlin { - target("src/*/kotlin/**/*.kt") - ktlint("0.43.2") - licenseHeaderFile(rootProject.file("spotless/copyright.kt")) - } -} - -jacoco { - toolVersion = "0.8.7" -} - -project.afterEvaluate { - tasks.create(name = "testCoverage") { - dependsOn("testDebugUnitTest") - group = "Reporting" - description = "Generate jacoco coverage reports" - - reports { - html.required.set(true) - xml.required.set(true) - csv.required.set(true) - } - - val excludes = listOf( - ) - - val kotlinClasses = fileTree(baseDir = "$buildDir/tmp/kotlin-classes/debug") { - exclude(excludes) - } - - classDirectories.setFrom(kotlinClasses) - - val androidTestData = fileTree(baseDir = "$buildDir/outputs/code_coverage/debugAndroidTest/connected/") - - executionData(files( - "${project.buildDir}/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec", - androidTestData - )) - } -} - -android { - compileSdk = 31 - defaultConfig { - minSdk = 26 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - - javaCompileOptions { - annotationProcessorOptions { - arguments( - mapOf( - "room.schemaLocation" to "$projectDir/schemas", - "room.incremental" to "true", - "room.expandProjection" to "true" - ) - ) - } - } - } - - testCoverage { - // needed to force the jacoco version - jacocoVersion = "0.8.7" - version = "0.8.7" - } - - buildTypes { - debug { - isTestCoverageEnabled = true - } - } - - sourceSets { - // Adds exported schema location as test app assets. - getByName("androidTest").assets.srcDir("$projectDir/schemas") - } - - kotlinOptions { - jvmTarget = "1.8" - freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" - } - packagingOptions { - resources.excludes += "META-INF/AL2.0" - resources.excludes += "META-INF/LGPL2.1" - resources.excludes += "META-INF/licenses/ASM" - resources.pickFirsts.add("win32-x86-64/attach_hotspot_windows.dll") - resources.pickFirsts.add("win32-x86/attach_hotspot_windows.dll") - } -} - -configurations { - create("testArtifacts"){ - extendsFrom(configurations.testApi.get()) - } - create("androidTestArtifacts"){ - extendsFrom(configurations.androidTestApi.get()) - } -} - -repositories { - google() - mavenCentral() -} - -dependencies { - api(AndroidX.core.ktx) - - // Coroutines - api(KotlinX.coroutines.core) - api(KotlinX.coroutines.android) - - // Coroutine Lifecycle Scopes - api(AndroidX.lifecycle.runtime.ktx) - api(AndroidX.lifecycle.viewModelKtx) - - // Koin dependency injection - api(Koin.core) - testApi(Koin.test) - api(Koin.android) - api(Koin.workManager) - api(Koin.navigation) - api(Koin.compose) - - // Persistence - api(AndroidX.room.runtime) - api(AndroidX.room.ktx) - kapt(AndroidX.room.compiler) - androidTestImplementation(AndroidX.room.testing) - - // workmanager - api(AndroidX.work.runtimeKtx) - androidTestApi(AndroidX.work.testing) - - // exifinterface - api(AndroidX.exifInterface) - - // httpclient - implementation(Ktor.client.core) - implementation(Ktor.client.cio) - implementation(Ktor.client.cio) - implementation(Ktor.client.auth) - implementation(Ktor.client.serialization) - implementation(Ktor.client.contentNegotiation) - implementation(Ktor.plugins.serialization.kotlinx.json) - implementation("io.ktor:ktor-client-logging-jvm:_") - implementation("io.ktor:ktor-client-mock-jvm:_") - - // logging - api(Square.logcat) - - // serialization - api(KotlinX.serialization.json) - api(AndroidX.security.crypto) - - // testing - testApi(AndroidX.test.ext.junit.ktx) - testApi(Testing.junit4) - testApi("com.google.truth:truth:_") - testApi(Testing.mockK) - testApi(KotlinX.coroutines.test) - testApi(AndroidX.archCore.testing) - - androidTestApi(AndroidX.test.core) - androidTestApi(AndroidX.test.coreKtx) - androidTestApi(AndroidX.test.ext.junit) - androidTestApi(AndroidX.test.ext.junit.ktx) - androidTestApi(AndroidX.test.ext.truth) - androidTestApi(AndroidX.test.monitor) - androidTestApi(AndroidX.test.orchestrator) - androidTestApi(AndroidX.test.runner) - androidTestApi(AndroidX.test.rules) - androidTestApi(AndroidX.test.services) - androidTestApi(Testing.mockK) -} diff --git a/data/src/main/AndroidManifest.xml b/data/src/main/AndroidManifest.xml deleted file mode 100644 index 3167ee8..0000000 --- a/data/src/main/AndroidManifest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - diff --git a/data/src/main/kotlin/photos/network/data/DataModule.kt b/data/src/main/kotlin/photos/network/data/DataModule.kt index 15e7fc2..040537a 100644 --- a/data/src/main/kotlin/photos/network/data/DataModule.kt +++ b/data/src/main/kotlin/photos/network/data/DataModule.kt @@ -60,25 +60,6 @@ import photos.network.data.user.repository.UserRepository import photos.network.data.user.repository.UserRepositoryImpl val dataModule = module { - single { - UserApiImpl( - httpClient = get(), - settingsRepository = get(), - userStorage = get(), - ) - } - - single { UserStorage(context = get()) } - - factory { WorkManager.getInstance(androidApplication()) } - - worker { - SyncLocalPhotosWorker( - application = get(), - workerParameters = get(), - photoRepository = get(), - ) - } single { UserRepositoryImpl( @@ -87,28 +68,6 @@ val dataModule = module { ) } - single { - provideKtorClient( - userStorage = get(), - settingsStore = get(), - ) - } - - single { - PhotoApiImpl( - httpClient = get(), - settingsRepository = get(), - ) - } - - single { - PhotoRepositoryImpl( - applicationContext = get(), - photoApi = get(), - photoDao = get(), - workManager = get(), - ) - } single { providePhotoDatabase(get()) } factory { providePhotoDao(get()) } @@ -122,120 +81,3 @@ val dataModule = module { ) } } - -private fun providePhotoDatabase(context: Context): PhotoDatabase { - return Room.databaseBuilder( - context, - PhotoDatabase::class.java, "photos.db" - ) - .addMigrations(MIGRATION_1_2) - .build() -} - -private fun providePhotoDao(photoDatabase: PhotoDatabase): PhotoDao { - return photoDatabase.photoDao() -} - -private fun provideKtorClient( - userStorage: UserStorage, - settingsStore: SettingsStorage, -): HttpClient { - val client = HttpClient(CIO) { - expectSuccess = false - followRedirects = true - - engine { - threadsCount = 1_000 - } - - install(Logging) { - logger = object : Logger { - override fun log(message: String) { - logcat(LogPriority.INFO) { message } - } - } - level = LogLevel.ALL - } - - install(ResponseObserver) { - onResponse { - logcat(LogPriority.INFO) { "<== ${it.status} ${it.request.url}" } - } - } - - install(ContentNegotiation) { - json( - Json { - prettyPrint = true - isLenient = true - ignoreUnknownKeys = true - } - ) - } - - install(Auth) { - bearer { - loadTokens { - val accessToken = userStorage.read()?.accessToken ?: "" - val refreshToken = userStorage.read()?.refreshToken ?: "" - - BearerTokens( - accessToken = accessToken, - refreshToken = refreshToken - ) - } - - // called after receiving a 401 (Unauthorized) response with the WWW-Authenticate header - refreshTokens { - val refreshToken = userStorage.read()?.refreshToken ?: "" - val host = settingsStore.read()?.host ?: "" - val clientId = settingsStore.read()?.clientId ?: "" - - /** - * OAuth refresh token request based on [RFC6749](https://tools.ietf.org/html/rfc6749#section-6) - * - * @param refreshToken The refresh token issued to the client. - * @param clientId The client identifier issued by the authorization server. - * @param scope list of case-sensitive strings to grant access based on. - */ - val refreshTokenInfo: TokenInfo = client.submitForm( - url = "$host/api/oauth/token", - formParameters = Parameters.build { - append("grant_type", "refresh_token") - append("refresh_token", refreshToken) - append("client_id", clientId) - append("scope", "openid profile email phone library:read library:write") - }, - ).body() - - logcat(LogPriority.ERROR) { "refreshTokenInfo=${refreshTokenInfo.accessToken}" } - - val user = userStorage.read() - user?.let { - val tmpUser = User( - id = it.id, - lastname = it.lastname, - firstname = it.firstname, - profileImageUrl = it.profileImageUrl, - accessToken = refreshTokenInfo.accessToken, - refreshToken = refreshTokenInfo.refreshToken - ) - userStorage.save(tmpUser) - } - - BearerTokens( - accessToken = refreshTokenInfo.accessToken, - refreshToken = refreshTokenInfo.refreshToken - ) - } - - // always send credentials when predicate fulfilled - sendWithoutRequest { request -> - request.url.encodedPath.endsWith("/protected") - } - } - } - } - - return client -} diff --git a/data/src/main/kotlin/photos/network/data/photos/repository/PhotoRepositoryImpl.kt b/data/src/main/kotlin/photos/network/data/photos/repository/PhotoRepositoryImpl.kt deleted file mode 100644 index 10cf0ab..0000000 --- a/data/src/main/kotlin/photos/network/data/photos/repository/PhotoRepositoryImpl.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2020-2022 Photos.network developers - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package photos.network.data.photos.repository - -import android.content.Context -import androidx.work.Constraints -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.PeriodicWorkRequest -import androidx.work.WorkManager -import androidx.work.WorkRequest -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.mapNotNull -import photos.network.data.photos.network.PhotoApi -import photos.network.data.photos.persistence.PhotoDao -import photos.network.data.photos.worker.SyncLocalPhotosWorker -import java.util.concurrent.TimeUnit - -class PhotoRepositoryImpl( - private val applicationContext: Context, - private val photoApi: PhotoApi, - private val photoDao: PhotoDao, - private val workManager: WorkManager, -) : PhotoRepository { - // TODO: user should be able to define the required network type in the app settings. - private val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.UNMETERED) - .build() - - private val periodicWorkRequest = PeriodicWorkRequest.Builder( - SyncLocalPhotosWorker::class.java, 2, TimeUnit.HOURS - ) - .setConstraints(constraints) - .addTag("photosSyncWorker") - .build() - - override fun syncPhotos() { - val syncLocalPhotosWorkRequest: WorkRequest = OneTimeWorkRequestBuilder().build() - - WorkManager.getInstance(applicationContext) - .enqueue(syncLocalPhotosWorkRequest) - } - - fun startPersiodicSync() { - workManager.enqueueUniquePeriodicWork( - "photosSyncWorker", - ExistingPeriodicWorkPolicy.REPLACE, - periodicWorkRequest - ) - - // TODO: observe sync state (at least errors) - workManager.getWorkInfosByTag("photosSyncWorker") - } - - override fun getPhotos(): Flow> = photoDao.getPhotos().mapNotNull { photos -> - photos.sortedByDescending { - it.dateTaken ?: it.dateAdded - }.map { photo -> - photo.toPhoto() - } - } - - override fun getPhoto(identifier: String): Flow = photoDao.getPhoto(identifier).mapNotNull { - it?.toPhoto() - } - - override suspend fun addPhoto(photo: Photo) { - photoDao.insertAll(photos = arrayOf(photo.toDatabasePhoto())) - } -} diff --git a/data/src/test/kotlin/photos/network/data/TestCoroutineDispatcherRule.kt b/data/src/test/kotlin/photos/network/data/TestCoroutineDispatcherRule.kt deleted file mode 100644 index 412c115..0000000 --- a/data/src/test/kotlin/photos/network/data/TestCoroutineDispatcherRule.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2020-2022 Photos.network developers - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package photos.network.data - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain -import org.junit.rules.TestWatcher -import org.junit.runner.Description - -class TestCoroutineDispatcherRule : TestWatcher() { - - private val dispatcher = StandardTestDispatcher() - - override fun starting(description: Description?) { - super.starting(description) - Dispatchers.setMain(dispatcher) - } - - override fun finished(description: Description?) { - super.finished(description) - Dispatchers.resetMain() - } -} diff --git a/database/albums/build.gradle.kts b/database/albums/build.gradle.kts new file mode 100644 index 0000000..01059b7 --- /dev/null +++ b/database/albums/build.gradle.kts @@ -0,0 +1,59 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.database.albums" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + javaCompileOptions { + annotationProcessorOptions { + arguments( + mapOf( + "room.schemaLocation" to "$projectDir/schemas", + "room.incremental" to "true", + "room.expandProjection" to "true" + ) + ) + } + } + } + + sourceSets { + // Adds exported schema location as test app assets. + getByName("androidTest").assets.srcDir("$projectDir/schemas") + } + + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + // Persistence + implementation(libs.bundles.room) + androidTestImplementation(libs.room.testing) +} diff --git a/database/albums/src/main/AndroidManifest.xml b/database/albums/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/database/albums/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/database/photos/build.gradle.kts b/database/photos/build.gradle.kts new file mode 100644 index 0000000..5579d41 --- /dev/null +++ b/database/photos/build.gradle.kts @@ -0,0 +1,71 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +// alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) + alias(libs.plugins.kotlin.ksp) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.database.photos" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + javaCompileOptions { + annotationProcessorOptions { + arguments( + mapOf( + "room.schemaLocation" to "$projectDir/schemas", + "room.incremental" to "true", + "room.expandProjection" to "true" + ) + ) + } + } + } + + sourceSets { + // Adds exported schema location as test app assets. + getByName("androidTest").assets.srcDir("$projectDir/schemas") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + // Persistence + implementation(libs.bundles.room) +// annotationProcessor(libs.room.compiler) +// kapt(libs.room.compiler) + // To use Kotlin Symbol Processing (KSP) + ksp(libs.room.compiler) + + androidTestImplementation(libs.room.testing) +} diff --git a/data/schemas/photos.network.data.photos.persistence.PhotoDatabase/1.json b/database/photos/schemas/photos.network.database.photos.PhotoDatabase/1.json similarity index 100% rename from data/schemas/photos.network.data.photos.persistence.PhotoDatabase/1.json rename to database/photos/schemas/photos.network.database.photos.PhotoDatabase/1.json diff --git a/data/schemas/photos.network.data.photos.persistence.PhotoDatabase/2.json b/database/photos/schemas/photos.network.database.photos.PhotoDatabase/2.json similarity index 100% rename from data/schemas/photos.network.data.photos.persistence.PhotoDatabase/2.json rename to database/photos/schemas/photos.network.database.photos.PhotoDatabase/2.json diff --git a/data/src/androidTest/kotlin/photos/network/data/photos/persistence/PhotoDatabaseMigrationTests.kt b/database/photos/src/androidTest/kotlin/photos/network/PhotoDatabaseMigrationTests.kt similarity index 95% rename from data/src/androidTest/kotlin/photos/network/data/photos/persistence/PhotoDatabaseMigrationTests.kt rename to database/photos/src/androidTest/kotlin/photos/network/PhotoDatabaseMigrationTests.kt index 91e5de9..64e8751 100644 --- a/data/src/androidTest/kotlin/photos/network/data/photos/persistence/PhotoDatabaseMigrationTests.kt +++ b/database/photos/src/androidTest/kotlin/photos/network/PhotoDatabaseMigrationTests.kt @@ -27,6 +27,8 @@ import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import photos.network.database.photos.MIGRATION_1_2 +import photos.network.database.photos.PhotoDatabase import java.io.IOException @RunWith(AndroidJUnit4::class) @@ -67,9 +69,9 @@ class PhotoDatabaseMigrationTests { val databaseNew = Room.databaseBuilder( InstrumentationRegistry.getInstrumentation().targetContext, PhotoDatabase::class.java, - TEST_DB + TEST_DB, ).addMigrations( - MIGRATION_1_2 + MIGRATION_1_2, ).build().apply { openHelper.writableDatabase close() diff --git a/database/photos/src/main/AndroidManifest.xml b/database/photos/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/database/photos/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/database/photos/src/main/kotlin/photos/network/database/photos/DatabasePhotosModule.kt b/database/photos/src/main/kotlin/photos/network/database/photos/DatabasePhotosModule.kt new file mode 100644 index 0000000..edfa2b5 --- /dev/null +++ b/database/photos/src/main/kotlin/photos/network/database/photos/DatabasePhotosModule.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2020-2022 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.database.photos + +import android.content.Context +import androidx.room.Room +import org.koin.dsl.module + +val databasePhotosModule = module { + single { + providePhotoDatabase(context = get()) + } + factory { + providePhotoDao(photoDatabase = get()) + } +} + +private fun providePhotoDatabase(context: Context): PhotoDatabase { + return Room.databaseBuilder( + context, + PhotoDatabase::class.java, + "photos.db", + ) + .addMigrations(MIGRATION_1_2) + .build() +} + +private fun providePhotoDao(photoDatabase: PhotoDatabase): PhotoDao { + return photoDatabase.photoDao() +} diff --git a/data/src/main/kotlin/photos/network/data/photos/persistence/Photo.kt b/database/photos/src/main/kotlin/photos/network/database/photos/Photo.kt similarity index 67% rename from data/src/main/kotlin/photos/network/data/photos/persistence/Photo.kt rename to database/photos/src/main/kotlin/photos/network/database/photos/Photo.kt index 1c52082..e030107 100644 --- a/data/src/main/kotlin/photos/network/data/photos/persistence/Photo.kt +++ b/database/photos/src/main/kotlin/photos/network/database/photos/Photo.kt @@ -13,13 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.photos.persistence +package photos.network.database.photos -import android.net.Uri import androidx.room.Entity import androidx.room.PrimaryKey -import java.time.Instant -import photos.network.data.photos.repository.Photo as RepositoryPhoto @Entity(tableName = "photos") data class Photo( @@ -31,14 +28,4 @@ data class Photo( val dateModified: Long? = null, val thumbnailFileUri: String? = null, val originalFileUri: String? = null, -) { - fun toPhoto(): RepositoryPhoto { - return RepositoryPhoto( - filename = filename, - imageUrl = imageUrl, - dateAdded = Instant.ofEpochMilli(dateAdded), - dateTaken = Instant.ofEpochMilli(dateTaken ?: 0L), - uri = originalFileUri?.let { Uri.parse(it) }, - ) - } -} +) diff --git a/data/src/main/kotlin/photos/network/data/photos/persistence/PhotoDao.kt b/database/photos/src/main/kotlin/photos/network/database/photos/PhotoDao.kt similarity index 96% rename from data/src/main/kotlin/photos/network/data/photos/persistence/PhotoDao.kt rename to database/photos/src/main/kotlin/photos/network/database/photos/PhotoDao.kt index 9c867a2..a321916 100644 --- a/data/src/main/kotlin/photos/network/data/photos/persistence/PhotoDao.kt +++ b/database/photos/src/main/kotlin/photos/network/database/photos/PhotoDao.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.photos.persistence +package photos.network.database.photos import androidx.room.Dao import androidx.room.Insert diff --git a/data/src/main/kotlin/photos/network/data/photos/persistence/PhotoDatabase.kt b/database/photos/src/main/kotlin/photos/network/database/photos/PhotoDatabase.kt similarity index 93% rename from data/src/main/kotlin/photos/network/data/photos/persistence/PhotoDatabase.kt rename to database/photos/src/main/kotlin/photos/network/database/photos/PhotoDatabase.kt index 218d761..b15e5c9 100644 --- a/data/src/main/kotlin/photos/network/data/photos/persistence/PhotoDatabase.kt +++ b/database/photos/src/main/kotlin/photos/network/database/photos/PhotoDatabase.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.photos.persistence +package photos.network.database.photos import androidx.room.Database import androidx.room.RoomDatabase @@ -25,7 +25,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase Photo::class, ], version = 2, - exportSchema = true + exportSchema = true, ) abstract class PhotoDatabase : RoomDatabase() { abstract fun photoDao(): PhotoDao @@ -45,14 +45,14 @@ val MIGRATION_1_2 = object : Migration(1, 2) { `thumbnailFileUri` TEXT, `originalFileUri` TEXT ) - """.trimIndent() + """.trimIndent(), ) database.execSQL( """ INSERT INTO photos_new (uuid, filename, imageUrl, dateTaken, dateAdded, dateModified, thumbnailFileUri, originalFileUri) SELECT uuid, filename, imageUrl, dateTaken, strftime('%s', 'now'), NULL, thumbnailFileUri, originalFileUri FROM photos - """.trimIndent() + """.trimIndent(), ) database.execSQL("DROP TABLE photos") database.execSQL("ALTER TABLE photos_new RENAME TO photos") diff --git a/database/settings/build.gradle.kts b/database/settings/build.gradle.kts new file mode 100644 index 0000000..0f68dfe --- /dev/null +++ b/database/settings/build.gradle.kts @@ -0,0 +1,64 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.database.settings" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + javaCompileOptions { + annotationProcessorOptions { + arguments( + mapOf( + "room.schemaLocation" to "$projectDir/schemas", + "room.incremental" to "true", + "room.expandProjection" to "true" + ) + ) + } + } + } + + sourceSets { + // Adds exported schema location as test app assets. + getByName("androidTest").assets.srcDir("$projectDir/schemas") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + // Persistence + implementation(libs.bundles.room) + androidTestImplementation(libs.room.testing) +} diff --git a/database/settings/src/main/AndroidManifest.xml b/database/settings/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/database/settings/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/app/src/main/kotlin/photos/network/presentation/help/HelpScreen.kt b/database/settings/src/main/kotlin/photos/network/database/settings/Module.kt similarity index 55% rename from app/src/main/kotlin/photos/network/presentation/help/HelpScreen.kt rename to database/settings/src/main/kotlin/photos/network/database/settings/Module.kt index f59d57b..e183a75 100644 --- a/app/src/main/kotlin/photos/network/presentation/help/HelpScreen.kt +++ b/database/settings/src/main/kotlin/photos/network/database/settings/Module.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,18 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.presentation.help +package photos.network.database.settings -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation.NavController -import androidx.navigation.compose.rememberNavController +import org.koin.dsl.module +import photos.network.common.persistence.SecureStorage +import photos.network.common.persistence.Settings -@Composable -fun HelpScreen( - modifier: Modifier = Modifier, - navController: NavController = rememberNavController(), -) { - Text(text = "Help") +val databaseSettingsModule = module { + single> { + SettingsStorage(context = get()) + } } diff --git a/data/src/main/kotlin/photos/network/data/settings/persistence/SettingsStorage.kt b/database/settings/src/main/kotlin/photos/network/database/settings/SettingsStorage.kt similarity index 88% rename from data/src/main/kotlin/photos/network/data/settings/persistence/SettingsStorage.kt rename to database/settings/src/main/kotlin/photos/network/database/settings/SettingsStorage.kt index f505154..3c2229f 100644 --- a/data/src/main/kotlin/photos/network/data/settings/persistence/SettingsStorage.kt +++ b/database/settings/src/main/kotlin/photos/network/database/settings/SettingsStorage.kt @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.settings.persistence +package photos.network.database.settings import android.content.Context import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import photos.network.data.SecureStorage +import photos.network.common.persistence.SecureStorage +import photos.network.common.persistence.Settings /** * Read/Write settings encrypted into internal storage diff --git a/database/sharing/build.gradle.kts b/database/sharing/build.gradle.kts new file mode 100644 index 0000000..2783772 --- /dev/null +++ b/database/sharing/build.gradle.kts @@ -0,0 +1,66 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.database.sharing" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + javaCompileOptions { + annotationProcessorOptions { + arguments( + mapOf( + "room.schemaLocation" to "$projectDir/schemas", + "room.incremental" to "true", + "room.expandProjection" to "true" + ) + ) + } + } + } + + sourceSets { + // Adds exported schema location as test app assets. + getByName("androidTest").assets.srcDir("$projectDir/schemas") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + implementation(projects.network) + + // Persistence + implementation(libs.bundles.room) + androidTestImplementation(libs.room.testing) +} diff --git a/database/sharing/src/main/AndroidManifest.xml b/database/sharing/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/database/sharing/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/database/sharing/src/main/kotlin/photos/network/database/sharing/Module.kt b/database/sharing/src/main/kotlin/photos/network/database/sharing/Module.kt new file mode 100644 index 0000000..77017c9 --- /dev/null +++ b/database/sharing/src/main/kotlin/photos/network/database/sharing/Module.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.database.sharing + +import org.koin.dsl.module +import photos.network.common.persistence.SecureStorage +import photos.network.common.persistence.User + +val databaseSharingModule = module { + single> { + UserStorage(context = get()) + } +} diff --git a/data/src/main/kotlin/photos/network/data/user/persistence/UserStorage.kt b/database/sharing/src/main/kotlin/photos/network/database/sharing/UserStorage.kt similarity index 87% rename from data/src/main/kotlin/photos/network/data/user/persistence/UserStorage.kt rename to database/sharing/src/main/kotlin/photos/network/database/sharing/UserStorage.kt index f6f91f0..eb66657 100644 --- a/data/src/main/kotlin/photos/network/data/user/persistence/UserStorage.kt +++ b/database/sharing/src/main/kotlin/photos/network/database/sharing/UserStorage.kt @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.user.persistence +package photos.network.database.sharing import android.content.Context import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import photos.network.data.SecureStorage -import photos.network.data.user.persistence.User as DatabaseUser +import photos.network.common.persistence.SecureStorage +import photos.network.common.persistence.User as DatabaseUser class UserStorage(context: Context) : SecureStorage(context, "user_storage.txt") { override fun decodeData(data: String): DatabaseUser { diff --git a/domain/albums/build.gradle.kts b/domain/albums/build.gradle.kts new file mode 100644 index 0000000..c8b1dfb --- /dev/null +++ b/domain/albums/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.domain.albums" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + } + + packagingOptions { + resources.excludes += "META-INF/AL2.0" + resources.excludes += "META-INF/LGPL2.1" + } +} + +dependencies { + api(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + implementation(projects.repository.settings) +} diff --git a/domain/albums/src/main/AndroidManifest.xml b/domain/albums/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/domain/albums/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/domain/albums/src/main/kotlin/photos/network/domain/albums/Module.kt b/domain/albums/src/main/kotlin/photos/network/domain/albums/Module.kt new file mode 100644 index 0000000..63f74f0 --- /dev/null +++ b/domain/albums/src/main/kotlin/photos/network/domain/albums/Module.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.domain.albums + +import org.koin.dsl.module + +val domainAlbumsModule = module { + +} diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts deleted file mode 100644 index 14cd647..0000000 --- a/domain/build.gradle.kts +++ /dev/null @@ -1,93 +0,0 @@ -plugins { - id("com.android.library") - id("com.diffplug.spotless") - kotlin("android") - kotlin("kapt") - kotlin("plugin.serialization") - id("jacoco") -} - -spotless { - kotlin { - target("src/*/kotlin/**/*.kt") - ktlint("0.43.2") - licenseHeaderFile(rootProject.file("spotless/copyright.kt")) - } -} - -jacoco { - toolVersion = "0.8.7" -} - -project.afterEvaluate { - tasks.create(name = "testCoverage") { - dependsOn("testDebugUnitTest") - group = "Reporting" - description = "Generate jacoco coverage reports" - - reports { - html.required.set(true) - xml.required.set(true) - csv.required.set(true) - } - - val excludes = listOf( - "**/*\$*\$*.class", - "**/DomainModule*", - ) - - val kotlinClasses = fileTree(baseDir = "$buildDir/tmp/kotlin-classes/debug") { - exclude(excludes) - } - - classDirectories.setFrom(kotlinClasses) - - val androidTestData = fileTree(baseDir = "$buildDir/outputs/code_coverage/debugAndroidTest/connected/") - - executionData(files( - "${project.buildDir}/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec", - androidTestData - )) - } -} - -android { - compileSdk = 31 - defaultConfig { - minSdk = 26 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - - testCoverage { - // needed to force the jacoco version - jacocoVersion = "0.8.7" - version = "0.8.7" - } - - buildTypes { - debug { - isTestCoverageEnabled = true - } - } - - kotlinOptions { - jvmTarget = "1.8" - freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" - } - packagingOptions { - resources.excludes += "META-INF/AL2.0" - resources.excludes += "META-INF/LGPL2.1" - } -} - -repositories { - google() - mavenCentral() -} - -dependencies { - api(project(":data")) - testImplementation(project(":data", "testArtifacts")) - androidTestImplementation(project(":data", "androidTestArtifacts")) -} diff --git a/domain/folders/build.gradle.kts b/domain/folders/build.gradle.kts new file mode 100644 index 0000000..91775de --- /dev/null +++ b/domain/folders/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.domain.folders" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + } + + packagingOptions { + resources.excludes += "META-INF/AL2.0" + resources.excludes += "META-INF/LGPL2.1" + } +} + +dependencies { + api(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) +} diff --git a/domain/folders/src/main/AndroidManifest.xml b/domain/folders/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/domain/folders/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/domain/folders/src/main/kotlin/photos/network/domain/folders/Module.kt b/domain/folders/src/main/kotlin/photos/network/domain/folders/Module.kt new file mode 100644 index 0000000..b496916 --- /dev/null +++ b/domain/folders/src/main/kotlin/photos/network/domain/folders/Module.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.domain.folders + +import org.koin.dsl.module + +val domainFoldersModule = module { + +} diff --git a/domain/photos/build.gradle.kts b/domain/photos/build.gradle.kts new file mode 100644 index 0000000..7d9e5fb --- /dev/null +++ b/domain/photos/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.domain.photos" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + } + + packagingOptions { + resources.excludes += "META-INF/AL2.0" + resources.excludes += "META-INF/LGPL2.1" + } +} + +dependencies { + implementation(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + api(projects.repository.photos) + implementation(projects.repository.settings) +} diff --git a/domain/photos/src/main/AndroidManifest.xml b/domain/photos/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/domain/photos/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/domain/photos/src/main/kotlin/photos/network/domain/photos/Module.kt b/domain/photos/src/main/kotlin/photos/network/domain/photos/Module.kt new file mode 100644 index 0000000..46c5b7a --- /dev/null +++ b/domain/photos/src/main/kotlin/photos/network/domain/photos/Module.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.domain.photos + +import org.koin.dsl.module +import photos.network.domain.photos.usecase.GetPhotoUseCase +import photos.network.domain.photos.usecase.GetPhotosUseCase +import photos.network.domain.photos.usecase.StartPhotosSyncUseCase + +val domainPhotosModule = module { + factory { + GetPhotosUseCase( + photoRepository = get(), + settingsRepository = get(), + ) + } + + factory { + GetPhotoUseCase( + photoRepository = get(), + ) + } + + factory { + StartPhotosSyncUseCase( + photoRepository = get(), + ) + } +} diff --git a/domain/src/main/kotlin/photos/network/domain/photos/model/Location.kt b/domain/photos/src/main/kotlin/photos/network/domain/photos/model/Location.kt similarity index 100% rename from domain/src/main/kotlin/photos/network/domain/photos/model/Location.kt rename to domain/photos/src/main/kotlin/photos/network/domain/photos/model/Location.kt diff --git a/domain/src/main/kotlin/photos/network/domain/photos/model/PhotoElement.kt b/domain/photos/src/main/kotlin/photos/network/domain/photos/model/PhotoElement.kt similarity index 90% rename from domain/src/main/kotlin/photos/network/domain/photos/model/PhotoElement.kt rename to domain/photos/src/main/kotlin/photos/network/domain/photos/model/PhotoElement.kt index 4fbd0fc..4f1119c 100644 --- a/domain/src/main/kotlin/photos/network/domain/photos/model/PhotoElement.kt +++ b/domain/photos/src/main/kotlin/photos/network/domain/photos/model/PhotoElement.kt @@ -19,10 +19,10 @@ data class PhotoElement( val id: String? = null, val name: String, val owner: String?, - val created_at: String?, - val modified_at: String?, + val createdAt: String?, + val modifiedAt: String?, val details: TechnicalDetails?, val tags: List?, val location: Location?, - val image_url: String + val imageUrl: String, ) diff --git a/domain/src/main/kotlin/photos/network/domain/photos/model/PhotoList.kt b/domain/photos/src/main/kotlin/photos/network/domain/photos/model/PhotoList.kt similarity index 100% rename from domain/src/main/kotlin/photos/network/domain/photos/model/PhotoList.kt rename to domain/photos/src/main/kotlin/photos/network/domain/photos/model/PhotoList.kt diff --git a/domain/src/main/kotlin/photos/network/domain/photos/model/TechnicalDetails.kt b/domain/photos/src/main/kotlin/photos/network/domain/photos/model/TechnicalDetails.kt similarity index 91% rename from domain/src/main/kotlin/photos/network/domain/photos/model/TechnicalDetails.kt rename to domain/photos/src/main/kotlin/photos/network/domain/photos/model/TechnicalDetails.kt index 603775e..5c8060f 100644 --- a/domain/src/main/kotlin/photos/network/domain/photos/model/TechnicalDetails.kt +++ b/domain/photos/src/main/kotlin/photos/network/domain/photos/model/TechnicalDetails.kt @@ -18,8 +18,8 @@ package photos.network.domain.photos.model data class TechnicalDetails( val exposure: String? = null, val camera: String? = null, - val focal_length: String? = null, + val focalLength: String? = null, val iso: String? = null, val lens: String? = null, - val shutter_speed: String? = null + val shutterSpeed: String? = null, ) diff --git a/domain/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCase.kt b/domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCase.kt similarity index 89% rename from domain/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCase.kt rename to domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCase.kt index 9574a94..96c3b72 100644 --- a/domain/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCase.kt +++ b/domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCase.kt @@ -16,8 +16,8 @@ package photos.network.domain.photos.usecase import kotlinx.coroutines.flow.Flow -import photos.network.data.photos.repository.Photo -import photos.network.data.photos.repository.PhotoRepository +import photos.network.repository.photos.Photo +import photos.network.repository.photos.PhotoRepository /** * Load a list of phots from persistency. diff --git a/domain/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCase.kt b/domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCase.kt similarity index 85% rename from domain/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCase.kt rename to domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCase.kt index db1b601..e3e1951 100644 --- a/domain/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCase.kt +++ b/domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCase.kt @@ -17,10 +17,10 @@ package photos.network.domain.photos.usecase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import photos.network.data.photos.repository.Photo -import photos.network.data.photos.repository.PhotoRepository -import photos.network.data.settings.repository.PrivacyState -import photos.network.data.settings.repository.SettingsRepository +import photos.network.common.persistence.PrivacyState +import photos.network.repository.photos.Photo +import photos.network.repository.photos.PhotoRepository +import photos.network.repository.settings.SettingsRepository /** * Load a list of photos, filtered based on the users privacy filter. diff --git a/domain/src/main/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCase.kt b/domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCase.kt similarity index 81% rename from domain/src/main/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCase.kt rename to domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCase.kt index 9fc183f..68aceaf 100644 --- a/domain/src/main/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCase.kt +++ b/domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCase.kt @@ -15,7 +15,8 @@ */ package photos.network.domain.photos.usecase -import photos.network.data.photos.repository.PhotoRepository +import photos.network.repository.photos.PhotoRepository +import photos.network.repository.photos.worker.SyncStatus /** * Start synchronisation of local images with photos.network instance. @@ -23,5 +24,5 @@ import photos.network.data.photos.repository.PhotoRepository class StartPhotosSyncUseCase( private val photoRepository: PhotoRepository, ) { - operator fun invoke(): Unit = photoRepository.syncPhotos() + suspend operator fun invoke(): SyncStatus = photoRepository.syncPhotos() } diff --git a/domain/src/test/kotlin/photos/network/domain/photos/model/LocationTests.kt b/domain/photos/src/test/kotlin/photos/network/domain/photos/model/LocationTests.kt similarity index 97% rename from domain/src/test/kotlin/photos/network/domain/photos/model/LocationTests.kt rename to domain/photos/src/test/kotlin/photos/network/domain/photos/model/LocationTests.kt index fc0eafd..3bd8ec8 100644 --- a/domain/src/test/kotlin/photos/network/domain/photos/model/LocationTests.kt +++ b/domain/photos/src/test/kotlin/photos/network/domain/photos/model/LocationTests.kt @@ -33,7 +33,7 @@ class LocationTests { val location = Location( longitude = 180f, latitude = 90f, - altitude = 200 + altitude = 200, ) // then Truth.assertThat(location.longitude).isEqualTo(180f) diff --git a/domain/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCaseTests.kt b/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCaseTests.kt similarity index 100% rename from domain/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCaseTests.kt rename to domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCaseTests.kt diff --git a/domain/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCaseTests.kt b/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCaseTests.kt similarity index 99% rename from domain/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCaseTests.kt rename to domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCaseTests.kt index 172eb8e..9fd54cf 100644 --- a/domain/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCaseTests.kt +++ b/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCaseTests.kt @@ -115,7 +115,7 @@ class GetPhotosUseCaseTests { dateAdded = Instant.parse("2020-02-02T20:20:20Z"), isPrivate = true, uri = null, - ) + ), ) } } diff --git a/domain/src/test/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCaseTests.kt b/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCaseTests.kt similarity index 96% rename from domain/src/test/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCaseTests.kt rename to domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCaseTests.kt index f745b72..ee8be68 100644 --- a/domain/src/test/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCaseTests.kt +++ b/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCaseTests.kt @@ -33,7 +33,7 @@ class StartPhotosSyncUseCaseTests { private val startPhotosUseCase by lazy { StartPhotosSyncUseCase( - photoRepository = photoRepository + photoRepository = photoRepository, ) } diff --git a/domain/settings/build.gradle.kts b/domain/settings/build.gradle.kts new file mode 100644 index 0000000..2ca81ff --- /dev/null +++ b/domain/settings/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.domain.settings" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + } + + packagingOptions { + resources.excludes += "META-INF/AL2.0" + resources.excludes += "META-INF/LGPL2.1" + } +} + +dependencies { + api(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + api(projects.repository.settings) + api(projects.repository.sharing) +} diff --git a/domain/settings/src/main/AndroidManifest.xml b/domain/settings/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/domain/settings/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/domain/src/main/kotlin/photos/network/domain/DomainModule.kt b/domain/settings/src/main/kotlin/photos/network/domain/settings/Module.kt similarity index 65% rename from domain/src/main/kotlin/photos/network/domain/DomainModule.kt rename to domain/settings/src/main/kotlin/photos/network/domain/settings/Module.kt index 04890b3..f1c893a 100644 --- a/domain/src/main/kotlin/photos/network/domain/DomainModule.kt +++ b/domain/settings/src/main/kotlin/photos/network/domain/settings/Module.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,72 +13,50 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.domain +package photos.network.domain.settings import org.koin.dsl.module -import photos.network.domain.photos.usecase.GetPhotoUseCase -import photos.network.domain.photos.usecase.GetPhotosUseCase import photos.network.domain.settings.usecase.GetSettingsUseCase import photos.network.domain.settings.usecase.TogglePrivacyUseCase import photos.network.domain.settings.usecase.UpdateClientIdUseCase import photos.network.domain.settings.usecase.UpdateHostUseCase import photos.network.domain.settings.usecase.VerifyClientIdUseCase import photos.network.domain.settings.usecase.VerifyServerHostUseCase -import photos.network.domain.user.usecase.RequestAccessTokenUseCase -val domainModule = module { +val domainSettingsModule = module { factory { - GetPhotosUseCase( - photoRepository = get(), + GetSettingsUseCase( settingsRepository = get(), ) } factory { - RequestAccessTokenUseCase( - userRepository = get(), - ) - } - - factory { - GetPhotoUseCase( - photoRepository = get() - ) - } - - factory { - GetSettingsUseCase( - settingsRepository = get() + TogglePrivacyUseCase( + settingsRepository = get(), ) } factory { - TogglePrivacyUseCase( - settingsRepository = get() + UpdateClientIdUseCase( + settingsRepository = get(), ) } factory { UpdateHostUseCase( - settingsRepository = get() + settingsRepository = get(), ) } factory { - UpdateClientIdUseCase( - settingsRepository = get() + VerifyClientIdUseCase( + userRepository = get(), ) } factory { VerifyServerHostUseCase( - userRepository = get() - ) - } - - factory { - VerifyClientIdUseCase( - userRepository = get() + userRepository = get(), ) } } diff --git a/domain/src/main/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCase.kt b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCase.kt similarity index 82% rename from domain/src/main/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCase.kt rename to domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCase.kt index 02a1ae9..5e135d9 100644 --- a/domain/src/main/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCase.kt +++ b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCase.kt @@ -16,11 +16,11 @@ package photos.network.domain.settings.usecase import kotlinx.coroutines.flow.Flow -import photos.network.data.settings.repository.Settings -import photos.network.data.settings.repository.SettingsRepository +import photos.network.common.persistence.Settings +import photos.network.repository.settings.SettingsRepository class GetSettingsUseCase( - private val settingsRepository: SettingsRepository + private val settingsRepository: SettingsRepository, ) { operator fun invoke(): Flow { return settingsRepository.settings diff --git a/domain/src/main/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCase.kt b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCase.kt similarity index 86% rename from domain/src/main/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCase.kt rename to domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCase.kt index 963b734..26a45c0 100644 --- a/domain/src/main/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCase.kt +++ b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCase.kt @@ -15,13 +15,13 @@ */ package photos.network.domain.settings.usecase -import photos.network.data.settings.repository.SettingsRepository +import photos.network.repository.settings.SettingsRepository /** * Toggle privacy setting */ class TogglePrivacyUseCase( - private val settingsRepository: SettingsRepository + private val settingsRepository: SettingsRepository, ) { suspend operator fun invoke(): Unit = settingsRepository.togglePrivacy() } diff --git a/domain/src/main/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCase.kt b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCase.kt similarity index 86% rename from domain/src/main/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCase.kt rename to domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCase.kt index cc4fdee..f766be2 100644 --- a/domain/src/main/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCase.kt +++ b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCase.kt @@ -15,10 +15,10 @@ */ package photos.network.domain.settings.usecase -import photos.network.data.settings.repository.SettingsRepository +import photos.network.repository.settings.SettingsRepository class UpdateClientIdUseCase( - private val settingsRepository: SettingsRepository + private val settingsRepository: SettingsRepository, ) { suspend operator fun invoke(clientId: String) { settingsRepository.updateClientId(clientId) diff --git a/domain/src/main/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCase.kt b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCase.kt similarity index 86% rename from domain/src/main/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCase.kt rename to domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCase.kt index 079472c..4cb87f0 100644 --- a/domain/src/main/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCase.kt +++ b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCase.kt @@ -15,10 +15,10 @@ */ package photos.network.domain.settings.usecase -import photos.network.data.settings.repository.SettingsRepository +import photos.network.repository.settings.SettingsRepository class UpdateHostUseCase( - private val settingsRepository: SettingsRepository + private val settingsRepository: SettingsRepository, ) { suspend operator fun invoke(host: String) { settingsRepository.updateHost(host) diff --git a/domain/src/main/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCase.kt b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCase.kt similarity index 89% rename from domain/src/main/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCase.kt rename to domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCase.kt index 3d0d5a3..170833f 100644 --- a/domain/src/main/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCase.kt +++ b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCase.kt @@ -15,10 +15,10 @@ */ package photos.network.domain.settings.usecase -import photos.network.data.user.repository.UserRepository +import photos.network.repository.sharing.UserRepository class VerifyClientIdUseCase( - private val userRepository: UserRepository + private val userRepository: UserRepository, ) { suspend operator fun invoke(clientId: String): Boolean { return if (clientId.length > 10) { diff --git a/domain/src/main/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCase.kt b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCase.kt similarity index 89% rename from domain/src/main/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCase.kt rename to domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCase.kt index f0fb3ce..9fb8844 100644 --- a/domain/src/main/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCase.kt +++ b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCase.kt @@ -15,10 +15,10 @@ */ package photos.network.domain.settings.usecase -import photos.network.data.user.repository.UserRepository +import photos.network.repository.sharing.UserRepository class VerifyServerHostUseCase( - private val userRepository: UserRepository + private val userRepository: UserRepository, ) { suspend operator fun invoke(host: String): Boolean { return if (host.length > 8) { diff --git a/domain/src/test/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCaseTests.kt b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCaseTests.kt similarity index 94% rename from domain/src/test/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCaseTests.kt rename to domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCaseTests.kt index 5df6b75..2721623 100644 --- a/domain/src/test/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCaseTests.kt +++ b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCaseTests.kt @@ -26,7 +26,7 @@ import org.junit.Rule import org.junit.Test import photos.network.data.settings.repository.PrivacyState import photos.network.data.settings.repository.Settings -import photos.network.data.settings.repository.SettingsRepository +import photos.network.repository.settings.SettingsRepository class GetSettingsUseCaseTests { @Rule @@ -37,7 +37,7 @@ class GetSettingsUseCaseTests { private val getSettingsUseCase by lazy { GetSettingsUseCase( - settingsRepository = settingsRepository + settingsRepository = settingsRepository, ) } diff --git a/domain/src/test/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCaseTests.kt b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCaseTests.kt similarity index 92% rename from domain/src/test/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCaseTests.kt rename to domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCaseTests.kt index 949eff0..0b1bada 100644 --- a/domain/src/test/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCaseTests.kt +++ b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCaseTests.kt @@ -22,7 +22,7 @@ import io.mockk.mockk import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test -import photos.network.data.settings.repository.SettingsRepository +import photos.network.repository.settings.SettingsRepository class TogglePrivacyUseCaseTests { @Rule @@ -33,7 +33,7 @@ class TogglePrivacyUseCaseTests { private val togglePrivacyUseCase by lazy { TogglePrivacyUseCase( - settingsRepository = settingsRepository + settingsRepository = settingsRepository, ) } diff --git a/domain/src/test/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCaseTests.kt b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCaseTests.kt similarity index 92% rename from domain/src/test/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCaseTests.kt rename to domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCaseTests.kt index a368792..704d3b6 100644 --- a/domain/src/test/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCaseTests.kt +++ b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCaseTests.kt @@ -22,7 +22,7 @@ import io.mockk.mockk import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test -import photos.network.data.settings.repository.SettingsRepository +import photos.network.repository.settings.SettingsRepository class UpdateClientIdUseCaseTests { @Rule @@ -33,7 +33,7 @@ class UpdateClientIdUseCaseTests { private val updateClientIdUseCase by lazy { UpdateClientIdUseCase( - settingsRepository = settingsRepository + settingsRepository = settingsRepository, ) } diff --git a/domain/src/test/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCaseTests.kt b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCaseTests.kt similarity index 92% rename from domain/src/test/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCaseTests.kt rename to domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCaseTests.kt index a073438..9aff38e 100644 --- a/domain/src/test/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCaseTests.kt +++ b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCaseTests.kt @@ -22,7 +22,7 @@ import io.mockk.mockk import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test -import photos.network.data.settings.repository.SettingsRepository +import photos.network.repository.settings.SettingsRepository class UpdateHostUseCaseTests { @Rule @@ -33,7 +33,7 @@ class UpdateHostUseCaseTests { private val updateHostUseCase by lazy { UpdateHostUseCase( - settingsRepository = settingsRepository + settingsRepository = settingsRepository, ) } diff --git a/domain/src/test/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCaseTests.kt b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCaseTests.kt similarity index 97% rename from domain/src/test/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCaseTests.kt rename to domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCaseTests.kt index 0bfaf69..1edd2de 100644 --- a/domain/src/test/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCaseTests.kt +++ b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCaseTests.kt @@ -33,7 +33,7 @@ class VerifyClientIdUseCaseTests { private val verifyClientIdUseCase by lazy { VerifyClientIdUseCase( - userRepository = userRepository + userRepository = userRepository, ) } diff --git a/domain/src/test/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCaseTests.kt b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCaseTests.kt similarity index 95% rename from domain/src/test/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCaseTests.kt rename to domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCaseTests.kt index 605a2ea..83ee7a6 100644 --- a/domain/src/test/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCaseTests.kt +++ b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCaseTests.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test import photos.network.data.user.repository.UserRepository +import photos.network.repository.sharing.UserRepository class VerifyServerHostUseCaseTests { @Rule @@ -33,7 +34,7 @@ class VerifyServerHostUseCaseTests { private val verifyServerHostUseCase by lazy { VerifyServerHostUseCase( - userRepository = userRepository + userRepository = userRepository, ) } diff --git a/domain/sharing/build.gradle.kts b/domain/sharing/build.gradle.kts new file mode 100644 index 0000000..67426cb --- /dev/null +++ b/domain/sharing/build.gradle.kts @@ -0,0 +1,50 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.domain.sharing" + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + } + + packagingOptions { + resources.excludes += "META-INF/AL2.0" + resources.excludes += "META-INF/LGPL2.1" + } +} + +dependencies { + api(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + api(projects.repository.sharing) +} diff --git a/domain/src/androidTest/kotlin/photos/network/domain/photos/FailingTest.kt b/domain/sharing/src/androidTest/kotlin/photos/network/domain/sharing/FailingTest.kt similarity index 96% rename from domain/src/androidTest/kotlin/photos/network/domain/photos/FailingTest.kt rename to domain/sharing/src/androidTest/kotlin/photos/network/domain/sharing/FailingTest.kt index 29990ce..842ee49 100644 --- a/domain/src/androidTest/kotlin/photos/network/domain/photos/FailingTest.kt +++ b/domain/sharing/src/androidTest/kotlin/photos/network/domain/sharing/FailingTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.domain.photos +package photos.network.domain.sharing import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.runBlocking diff --git a/domain/sharing/src/main/AndroidManifest.xml b/domain/sharing/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/domain/sharing/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/domain/sharing/src/main/kotlin/photos/network/domain/sharing/Module.kt b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/Module.kt new file mode 100644 index 0000000..98ba3ef --- /dev/null +++ b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/Module.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.domain.sharing + +import org.koin.dsl.module + +val domainSharingModule = module { + +} diff --git a/domain/src/main/kotlin/photos/network/domain/user/Token.kt b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/Token.kt similarity index 90% rename from domain/src/main/kotlin/photos/network/domain/user/Token.kt rename to domain/sharing/src/main/kotlin/photos/network/domain/sharing/Token.kt index 5635326..0601a48 100644 --- a/domain/src/main/kotlin/photos/network/domain/user/Token.kt +++ b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/Token.kt @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.domain.user +package photos.network.domain.sharing class Token( val accessToken: String, - val refreshToken: String + val refreshToken: String, ) diff --git a/domain/src/main/kotlin/photos/network/domain/user/User.kt b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/User.kt similarity index 95% rename from domain/src/main/kotlin/photos/network/domain/user/User.kt rename to domain/sharing/src/main/kotlin/photos/network/domain/sharing/User.kt index 89668a5..533b020 100644 --- a/domain/src/main/kotlin/photos/network/domain/user/User.kt +++ b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/User.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.domain.user +package photos.network.domain.sharing import java.util.UUID diff --git a/domain/src/main/kotlin/photos/network/domain/user/usecase/GetCurrentUserUseCase.kt b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCase.kt similarity index 76% rename from domain/src/main/kotlin/photos/network/domain/user/usecase/GetCurrentUserUseCase.kt rename to domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCase.kt index f7a13ff..4b8ed2f 100644 --- a/domain/src/main/kotlin/photos/network/domain/user/usecase/GetCurrentUserUseCase.kt +++ b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCase.kt @@ -13,22 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.domain.user.usecase +package photos.network.domain.sharing.usecase import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -import photos.network.data.user.repository.UserRepository -import photos.network.data.user.persistence.User as DatabaseUser +import photos.network.repository.sharing.User +import photos.network.repository.sharing.UserRepository /** * Get currently logged in User if available. */ class GetCurrentUserUseCase( - private val userRepository: UserRepository + private val userRepository: UserRepository, ) { - suspend operator fun invoke(): Flow = flow { + suspend operator fun invoke(): Flow = flow { emit(userRepository.currentUser()) }.flowOn(Dispatchers.IO) } diff --git a/domain/src/main/kotlin/photos/network/domain/user/usecase/LogoutUseCase.kt b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/LogoutUseCase.kt similarity index 83% rename from domain/src/main/kotlin/photos/network/domain/user/usecase/LogoutUseCase.kt rename to domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/LogoutUseCase.kt index 1711b04..f4802ae 100644 --- a/domain/src/main/kotlin/photos/network/domain/user/usecase/LogoutUseCase.kt +++ b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/LogoutUseCase.kt @@ -13,15 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.domain.user.usecase +package photos.network.domain.sharing.usecase -import photos.network.data.user.repository.UserRepository +import photos.network.repository.sharing.UserRepository /** * Invalidate authorization and logout current user. */ class LogoutUseCase( - private val userRepository: UserRepository + private val userRepository: UserRepository, ) { suspend operator fun invoke() = userRepository.invalidateAuthorization() } diff --git a/domain/src/main/kotlin/photos/network/domain/user/usecase/RequestAccessTokenUseCase.kt b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/RequestAccessTokenUseCase.kt similarity index 83% rename from domain/src/main/kotlin/photos/network/domain/user/usecase/RequestAccessTokenUseCase.kt rename to domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/RequestAccessTokenUseCase.kt index 5de43ed..fa728f0 100644 --- a/domain/src/main/kotlin/photos/network/domain/user/usecase/RequestAccessTokenUseCase.kt +++ b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/RequestAccessTokenUseCase.kt @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.domain.user.usecase +package photos.network.domain.sharing.usecase -import photos.network.data.user.repository.UserRepository +import photos.network.repository.sharing.UserRepository class RequestAccessTokenUseCase( - private val userRepository: UserRepository + private val userRepository: UserRepository, ) { suspend operator fun invoke(authCode: String): Boolean { return userRepository.accessTokenRequest(authCode) diff --git a/domain/src/test/kotlin/photos/network/domain/user/usecase/GetCurrentUserUseCaseTests.kt b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCaseTests.kt similarity index 94% rename from domain/src/test/kotlin/photos/network/domain/user/usecase/GetCurrentUserUseCaseTests.kt rename to domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCaseTests.kt index 0df727a..6af024b 100644 --- a/domain/src/test/kotlin/photos/network/domain/user/usecase/GetCurrentUserUseCaseTests.kt +++ b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCaseTests.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.domain.user.usecase +package photos.network.domain.sharing.usecase import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.google.common.truth.Truth @@ -35,7 +35,7 @@ class GetCurrentUserUseCaseTests { private val getCurrentUserUseCase by lazy { GetCurrentUserUseCase( - userRepository = userRepository + userRepository = userRepository, ) } @@ -47,7 +47,7 @@ class GetCurrentUserUseCaseTests { lastname = "Norris", firstname = "Carlos Ray", profileImageUrl = "http://localhost/image/chuck_norris.jpg", - accessToken = "access_token" + accessToken = "access_token", ) every { userRepository.currentUser() } answers { diff --git a/domain/src/test/kotlin/photos/network/domain/user/usecase/LogoutUseCaseTests.kt b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/LogoutUseCaseTests.kt similarity index 94% rename from domain/src/test/kotlin/photos/network/domain/user/usecase/LogoutUseCaseTests.kt rename to domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/LogoutUseCaseTests.kt index 5d6964f..4bba8b4 100644 --- a/domain/src/test/kotlin/photos/network/domain/user/usecase/LogoutUseCaseTests.kt +++ b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/LogoutUseCaseTests.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.domain.user.usecase +package photos.network.domain.sharing.usecase import androidx.arch.core.executor.testing.InstantTaskExecutorRule import io.mockk.coEvery @@ -33,7 +33,7 @@ class LogoutUseCaseTests { private val logoutUseCase by lazy { LogoutUseCase( - userRepository = userRepository + userRepository = userRepository, ) } diff --git a/domain/src/test/kotlin/photos/network/domain/user/usecase/RequestAccessTokenUseCaseTests.kt b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/RequestAccessTokenUseCaseTests.kt similarity index 95% rename from domain/src/test/kotlin/photos/network/domain/user/usecase/RequestAccessTokenUseCaseTests.kt rename to domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/RequestAccessTokenUseCaseTests.kt index 76935ed..b2e3d71 100644 --- a/domain/src/test/kotlin/photos/network/domain/user/usecase/RequestAccessTokenUseCaseTests.kt +++ b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/RequestAccessTokenUseCaseTests.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.domain.user.usecase +package photos.network.domain.sharing.usecase import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.google.common.truth.Truth @@ -33,7 +33,7 @@ class RequestAccessTokenUseCaseTests { private val requestAccessTokenUseCase by lazy { RequestAccessTokenUseCase( - userRepository = userRepository + userRepository = userRepository, ) } diff --git a/domain/src/main/AndroidManifest.xml b/domain/src/main/AndroidManifest.xml deleted file mode 100644 index e71b6b9..0000000 --- a/domain/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..29206b4 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,239 @@ +[versions] +compileSdk = "33" +kotlin = "1.8.10" +ksp = "1.8.10-1.0.9" +androidGradlePlugin = "7.4.0" +compose-bom = "2023.03.00" +compose-compiler = "1.4.2" +detekt = "1.22.0" +kover = "0.6.1" +spotless = "6.18.0" +grgit = "5.0.0" +tripletPlugin = "3.8.1" +ktlint = "0.48.2" + + + + +androidx-compose-ui = "1.2.1" +leakcanary = "2.9.1" +androidx-activity = "1.5.1" +androidx-compose-runtime = "1.2.1" +androidx-compose-material3 = "1.0.0-alpha06" +androidx-compose-material = "1.2.1" +androidx-navigation = "2.5.2" +androidx-constraintlayout-compose = "1.0.1" +androidx-paging-compose = "1.0.0-alpha14" +androidx-paging = "3.1.1" +google-accompanist = "0.25.1" +google-android-material = "1.6.1" +coil-kt = "2.2.1" +retrofit2 = "2.9.0" +okhttp3 = "4.10.0" +kotlinx-serialization = "1.4.0" +jakewharton-retrofit2-kotlinx-serialization-converter = "0.8.0" +androidx-compose-compiler = "1.2.0" +androidx-room = "2.4.3" +androidx-work = "2.7.1" +androidx-test-core = "1.4.0" +androidx-test-ext-junit = "1.1.3" +androidx-test-ext-truth = "1.4.0" +androidx-test-monitor = "1.5.0" +androidx-test-orchestrator = "1.4.1" +androidx-test-runner = "1.4.0" +androidx-test-rules = "1.4.0" +androidx-test-services = "1.4.1" +mockk = "1.12.5" +androidx-core = "1.8.0" +kotlinx-coroutines = "1.6.4" +androidx-lifecycle = "2.5.1" +koin = "3.1.6" +androidx-exifinterface = "1.3.3" +logcat = "0.1" +androidx-security-crypto = "1.1.0-alpha03" +ktor = "2.1.1" +junit-junit = "4.13.2" +androidx-arch-core = "2.1.0" + + +[libraries] +accompanist-navigation-animation = { group = "com.google.accompanist", name = "accompanist-navigation-animation", version.ref = "google-accompanist" } +accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "google-accompanist" } +accompanist-placeholder = { group = "com.google.accompanist", name = "accompanist-placeholder", version.ref = "google-accompanist" } +accompanist-flowlayout = { group = "com.google.accompanist", name = "accompanist-flowlayout", version.ref = "google-accompanist" } +accompanist-insets = { group = "com.google.accompanist", name = "accompanist-insets", version.ref = "google-accompanist" } +accompanist-pager = { group = "com.google.accompanist", name = "accompanist-pager", version.ref = "google-accompanist" } +accompanist-swiperefresh = { group = "com.google.accompanist", name = "accompanist-swiperefresh", version.ref = "google-accompanist" } +accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "google-accompanist" } + +androidx-core-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" } +androidx-test-core-ktx = { group = "androidx.test", name = "core-ktx", version.ref = "androidx-test-core" } + +androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidx-test-core" } +androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } +androidx-test-ext-truth = { group = "androidx.test.ext", name = "truth", version.ref = "androidx-test-ext-truth" } +androidx-test-monitor = { group = "androidx.test", name = "monitor", version.ref = "androidx-test-monitor" } +androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidx-test-runner" } +androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidx-test-rules" } +androidx-test-orchestrator = { group = "androidx.test", name = "orchestrator", version.ref = "androidx-test-orchestrator" } +androidx-test-services = { group = "androidx.test.services", name = "test-services", version.ref = "androidx-test-services" } + +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } + +lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } +lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } + +koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" } +koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" } +koin-androidx-workmanager = { group = "io.insert-koin", name = "koin-androidx-workmanager", version.ref = "koin" } +koin-androidx-navigation = { group = "io.insert-koin", name = "koin-androidx-navigation", version.ref = "koin" } +koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" } +koin-test = { group = "io.insert-koin", name = "koin-test", version.ref = "koin" } + +logcat = { group = "com.squareup.logcat", name = "logcat", version.ref = "logcat" } + +kotlin-serialization = { group = "org.jetbrains.kotlin", name = "kotlin-serialization", version.ref = "kotlin" } +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" } + +mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } +com-google-truth-truth = "com.google.truth:truth:1.1.3" + + +room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidx-room" } +room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "androidx-room" } +room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidx-room" } +room-testing = { group = "androidx.room", name = "room-testing", version.ref = "androidx-room" } + +work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidx-work" } +work-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidx-work" } + +ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } +ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" } +ktor-client-auth = { group = "io.ktor", name = "ktor-client-auth", version.ref = "ktor" } +ktor-client-serialization = { group = "io.ktor", name = "ktor-client-serialization", version.ref = "ktor" } +ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-client-logging-jvm = { group = "io.ktor", name = "ktor-client-logging-jvm", version.ref = "ktor" } +ktor-client-mock-jvm = { group = "io.ktor", name = "ktor-client-mock-jvm", version.ref = "ktor" } + +exifinterface = { group = "androidx.exifinterface", name = "exifinterface", version.ref = "androidx-exifinterface" } + + + +compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } +compose-foundation = { module = "androidx.compose.foundation:foundation" } +compose-material = { module = "androidx.compose.material:material" } +compose-material-icons-core = { module = "androidx.compose.material:material-icons-core" } +compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } +compose-material3 = { module = "androidx.compose.material3:material3" } +compose-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +compose-tooling = { module = "androidx.compose.ui:ui-tooling" } +compose-test = { group = "androidx.compose.ui", name = "ui-test", version.ref = "androidx-compose-ui" } +compose-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "androidx-compose-ui" } +compose-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "androidx-compose-ui" } + +constraintlayout-compose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "androidx-constraintlayout-compose" } + +#ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidx-compose-ui" } +#ui = { group = "androidx.compose.ui", name = "ui", version.ref = "androidx-compose-ui" } +#runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "androidx-compose-runtime" } +#material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "androidx-compose-material" } +#androidx-compose-compiler-compiler = { group = "androidx.compose.compiler", name = "compiler", version.ref = "androidx-compose-compiler" } + +leakcanary-android = { group = "com.squareup.leakcanary", name = "leakcanary-android", version.ref = "leakcanary" } +activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity" } +com-google-android-material = { group = "com.google.android.material", name = "material", version.ref = "google-android-material" } + +security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "androidx-security-crypto" } + +coil = { group = "io.coil-kt", name = "coil", version.ref = "coil-kt" } +coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil-kt" } + +#com-android-tools-build-gradle = "com.android.tools.build:gradle:7.0.4" +#kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } +#org-jacoco-ant = "org.jacoco:org.jacoco.ant:0.8.7" +#navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" } +#paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "androidx-paging-compose" } +#paging-common-ktx = { group = "androidx.paging", name = "paging-common-ktx", version.ref = "androidx-paging" } +#logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp3" } +#retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit2" } +#retrofit2-kotlinx-serialization-converter = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "jakewharton-retrofit2-kotlinx-serialization-converter" } +#org-jacoco-agent = "org.jacoco:org.jacoco.agent:0.8.7" +#kotlin-annotation-processing-gradle = { group = "org.jetbrains.kotlin", name = "kotlin-annotation-processing-gradle", version.ref = "kotlin" } +#kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" } +#lint-gradle = "com.android.tools.lint:lint-gradle:30.0.4" + +#junit-junit = { group = "junit", name = "junit", version.ref = "junit-junit" } + +#core-testing = { group = "androidx.arch.core", name = "core-testing", version.ref = "androidx-arch-core" } + + + +[bundles] +coil = [ + "coil", + "coil-compose" +] +koin = [ + "koin-core", + "koin-android", + "koin-androidx-workmanager", + "koin-androidx-navigation", + "koin-androidx-compose" +] +room = ["room-ktx", "room-runtime"] +compose = [ + "compose-foundation", + "compose-material", + "compose-material-icons-core", + "compose-material-icons-extended", + "compose-material3", + "compose-tooling-preview", + "compose-tooling", +] + +accompanist = [ + "accompanist-flowlayout", + "accompanist-navigation-animation", + "accompanist-systemuicontroller", + "accompanist-permissions", + "accompanist-insets", +] +ktor = [ + "ktor-client-core", + "ktor-client-cio", + "ktor-client-auth", + "ktor-client-logging-jvm", + "ktor-client-serialization", + "ktor-serialization-kotlinx-json", + "ktor-client-content-negotiation" +] + + + +[plugins] +android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } +android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } +kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } + +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } +grgit = { id = "org.ajoberstar.grgit", version.ref = "grgit" } +triplet = { id = "com.github.triplet.play", version.ref = "tripletPlugin" } + + + +# legacy +#gradle = { id = "gradle", version = "7.0.4" } +#com-diffplug-spotless = { id = "com.diffplug.spotless", version = "6.7.0" } +#org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version = "1.7.0" } +#org-jetbrains-kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version = "1.7.0" } +#org-jetbrains-kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "1.7.0" } +#com-github-triplet-play = { id = "com.github.triplet.play", version = "3.7.0" } +#org-ajoberstar-grgit = { id = "org.ajoberstar.grgit", version = "5.0.0" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ae04661..59bc51a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/network/build.gradle.kts b/network/build.gradle.kts new file mode 100644 index 0000000..23d9900 --- /dev/null +++ b/network/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.network" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + // httpclient + implementation(libs.bundles.ktor) +} diff --git a/data/src/androidTest/kotlin/photos/network/data/photos/network/PhotoApiTest.kt b/network/src/androidTest/kotlin/photos/network/api/PhotoApiTest.kt similarity index 90% rename from data/src/androidTest/kotlin/photos/network/data/photos/network/PhotoApiTest.kt rename to network/src/androidTest/kotlin/photos/network/api/PhotoApiTest.kt index f749e0c..5e9cc70 100644 --- a/data/src/androidTest/kotlin/photos/network/data/photos/network/PhotoApiTest.kt +++ b/network/src/androidTest/kotlin/photos/network/api/PhotoApiTest.kt @@ -31,9 +31,11 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Test import org.junit.runner.RunWith -import photos.network.data.PhotosNetworkMockFileReader +import photos.network.common.PhotosNetworkMockFileReader import photos.network.data.settings.persistence.SettingsStorage import photos.network.data.settings.repository.SettingsRepositoryImpl +import photos.network.network.photo.PhotoApiImpl +import photos.network.network.photo.Photos /** * Test the REST interface to the photos.network core instance. @@ -43,8 +45,8 @@ class PhotoApiTest { private val settingsRepository = SettingsRepositoryImpl( SettingsStorage( - context = InstrumentationRegistry.getInstrumentation().context - ) + context = InstrumentationRegistry.getInstrumentation().context, + ), ) @Test @@ -55,12 +57,12 @@ class PhotoApiTest { respond( content = fakeResponse, status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()), ) } val photoApi = PhotoApiImpl( httpClient = createHttpClient(mockEngine), - settingsRepository = settingsRepository + settingsRepository = settingsRepository, ) // when @@ -86,12 +88,12 @@ class PhotoApiTest { respond( content = "{}", status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()), ) } val photoApi = PhotoApiImpl( httpClient = createHttpClient(mockEngine), - settingsRepository = settingsRepository + settingsRepository = settingsRepository, ) // when diff --git a/data/src/androidTest/kotlin/photos/network/data/photos/network/entity/PhotoTest.kt b/network/src/androidTest/kotlin/photos/network/entity/PhotoTest.kt similarity index 93% rename from data/src/androidTest/kotlin/photos/network/data/photos/network/entity/PhotoTest.kt rename to network/src/androidTest/kotlin/photos/network/entity/PhotoTest.kt index 4da37ba..97cdabe 100644 --- a/data/src/androidTest/kotlin/photos/network/data/photos/network/entity/PhotoTest.kt +++ b/network/src/androidTest/kotlin/photos/network/entity/PhotoTest.kt @@ -23,8 +23,8 @@ import org.junit.Assert.assertEquals import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith -import photos.network.data.PhotosNetworkMockFileReader -import photos.network.data.photos.network.Photo +import photos.network.common.PhotosNetworkMockFileReader +import photos.network.network.photo.Photo /** * Test deserializing photo object. diff --git a/data/src/androidTest/kotlin/photos/network/data/photos/network/entity/PhotosTest.kt b/network/src/androidTest/kotlin/photos/network/entity/PhotosTest.kt similarity index 92% rename from data/src/androidTest/kotlin/photos/network/data/photos/network/entity/PhotosTest.kt rename to network/src/androidTest/kotlin/photos/network/entity/PhotosTest.kt index 685aa7e..b4919a0 100644 --- a/data/src/androidTest/kotlin/photos/network/data/photos/network/entity/PhotosTest.kt +++ b/network/src/androidTest/kotlin/photos/network/entity/PhotosTest.kt @@ -22,9 +22,9 @@ import kotlinx.serialization.json.Json import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith -import photos.network.data.PhotosNetworkMockFileReader -import photos.network.data.photos.network.Photo -import photos.network.data.photos.network.Photos +import photos.network.common.PhotosNetworkMockFileReader +import photos.network.network.photo.Photo +import photos.network.network.photo.Photos /** * Test (de)serializing photos responses. @@ -50,7 +50,7 @@ class PhotosTest { Photo("a1", "xy", "https://photos.network/foo.raw", null, null), Photo("b2", "yz", "https://photos.network/bar.raw", null, null), ), - response.results + response.results, ) } // diff --git a/network/src/main/AndroidManifest.xml b/network/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/network/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/network/src/main/kotlin/photos/network/network/NetworkModule.kt b/network/src/main/kotlin/photos/network/network/NetworkModule.kt new file mode 100644 index 0000000..7e30306 --- /dev/null +++ b/network/src/main/kotlin/photos/network/network/NetworkModule.kt @@ -0,0 +1,223 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.network + +import android.app.Application +import android.content.pm.PackageInfo +import android.os.Build +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.HttpSend +import io.ktor.client.plugins.auth.Auth +import io.ktor.client.plugins.auth.providers.BearerTokens +import io.ktor.client.plugins.auth.providers.bearer +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.plugins.observer.ResponseObserver +import io.ktor.client.plugins.plugin +import io.ktor.client.request.forms.submitForm +import io.ktor.client.request.headers +import io.ktor.client.request.port +import io.ktor.client.request.url +import io.ktor.client.statement.request +import io.ktor.http.Parameters +import io.ktor.http.encodedPath +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json +import logcat.LogPriority +import logcat.logcat +import org.koin.dsl.module +import photos.network.common.persistence.SecureStorage +import photos.network.common.persistence.Settings +import photos.network.common.persistence.User +import photos.network.network.photo.PhotoApi +import photos.network.network.photo.PhotoApiImpl +import photos.network.network.user.UserApi +import photos.network.network.user.UserApiImpl +import photos.network.network.user.model.TokenInfo + +val networkModule = module { + single { + provideKtorClient( + application = get(), + userStorage = get(), + settingsStore = get(), + ) + } + + single { + UserApiImpl( + httpClient = get(), + settingsStorage = get(), + userStorage = get(), + ) + } + + single { + PhotoApiImpl(httpClient = get()) + } +} + +@Suppress("LongMethod") +private fun provideKtorClient( + application: Application, + userStorage: SecureStorage, + settingsStore: SecureStorage, +): HttpClient { + val client = HttpClient(CIO) { + expectSuccess = false + followRedirects = true + + defaultRequest { + headers { + val context = application.applicationContext + + @Suppress("DEPRECATION") + val packageInfo: PackageInfo = context.packageManager.getPackageInfo(context.packageName, 0) + val version: String = packageInfo.versionName + + @Suppress("DEPRECATION") + val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + packageInfo.longVersionCode + } else { + packageInfo.versionCode + } + + append( + "User-Agent", + "PhotosNetwork-Android/$version (Build $versionCode) Android/${Build.VERSION.RELEASE}", + ) + } + } + + engine { + @Suppress("MagicNumber") + threadsCount = 1_000 + } + + install(Logging) { + logger = object : Logger { + override fun log(message: String) { + logcat(LogPriority.INFO) { message } + } + } + level = LogLevel.ALL + } + + install(ResponseObserver) { + onResponse { + logcat(LogPriority.INFO) { "<== ${it.status} ${it.request.url}" } + } + } + + install(ContentNegotiation) { + json( + Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + }, + ) + } + + install(Auth) { + bearer { + loadTokens { + val accessToken = userStorage.read()?.accessToken ?: "" + val refreshToken = userStorage.read()?.refreshToken ?: "" + + BearerTokens( + accessToken = accessToken, + refreshToken = refreshToken, + ) + } + + // called after receiving a 401 (Unauthorized) response with the WWW-Authenticate header + refreshTokens { + val refreshToken = userStorage.read()?.refreshToken ?: "" + val host = settingsStore.read()?.host ?: "" + val clientId = settingsStore.read()?.clientId ?: "" + + /** + * OAuth refresh token request based on [RFC6749](https://tools.ietf.org/html/rfc6749#section-6) + * + * @param refreshToken The refresh token issued to the client. + * @param clientId The client identifier issued by the authorization server. + * @param scope list of case-sensitive strings to grant access based on. + */ + /** + * OAuth refresh token request based on [RFC6749](https://tools.ietf.org/html/rfc6749#section-6) + * + * @param refreshToken The refresh token issued to the client. + * @param clientId The client identifier issued by the authorization server. + * @param scope list of case-sensitive strings to grant access based on. + */ + val refreshTokenInfo: TokenInfo = client.submitForm( + url = "$host/api/oauth/token", + formParameters = Parameters.build { + append("grant_type", "refresh_token") + append("refresh_token", refreshToken) + append("client_id", clientId) + append("scope", "openid profile email phone library:read library:write") + }, + ).body() + + logcat(LogPriority.ERROR) { "refreshTokenInfo=${refreshTokenInfo.accessToken}" } + + val user = userStorage.read() + user?.let { + val tmpUser = User( + id = it.id, + lastname = it.lastname, + firstname = it.firstname, + profileImageUrl = it.profileImageUrl, + accessToken = refreshTokenInfo.accessToken, + refreshToken = refreshTokenInfo.refreshToken, + ) + userStorage.save(tmpUser) + } + + BearerTokens( + accessToken = refreshTokenInfo.accessToken, + refreshToken = refreshTokenInfo.refreshToken, + ) + } + + // always send credentials when predicate fulfilled + sendWithoutRequest { request -> + request.url.encodedPath.endsWith("/protected") + } + } + } + } + + client.plugin(HttpSend).intercept { request -> + // replace port and host for each call + @Suppress("MagicNumber") + request.port = settingsStore.read()?.port ?: 443 + request.url(settingsStore.read()?.host ?: "") + + val originalCall = execute(request) + + originalCall + } + + return client +} diff --git a/network/src/main/kotlin/photos/network/network/ServerStatus.kt b/network/src/main/kotlin/photos/network/network/ServerStatus.kt new file mode 100644 index 0000000..7dd6d75 --- /dev/null +++ b/network/src/main/kotlin/photos/network/network/ServerStatus.kt @@ -0,0 +1,8 @@ +package photos.network.network + +enum class ServerStatus { + AVAILABLE(), + UNAVAILABLE(), + PROGRESS(), + UNAUTHORIZED(), +} diff --git a/data/src/main/kotlin/photos/network/data/photos/network/Photo.kt b/network/src/main/kotlin/photos/network/network/photo/Photo.kt similarity index 95% rename from data/src/main/kotlin/photos/network/data/photos/network/Photo.kt rename to network/src/main/kotlin/photos/network/network/photo/Photo.kt index 880b5a2..afa68ef 100644 --- a/data/src/main/kotlin/photos/network/data/photos/network/Photo.kt +++ b/network/src/main/kotlin/photos/network/network/photo/Photo.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.photos.network +package photos.network.network.photo import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/data/src/main/kotlin/photos/network/data/photos/network/PhotoApi.kt b/network/src/main/kotlin/photos/network/network/photo/PhotoApi.kt similarity index 88% rename from data/src/main/kotlin/photos/network/data/photos/network/PhotoApi.kt rename to network/src/main/kotlin/photos/network/network/photo/PhotoApi.kt index 072dad6..4543643 100644 --- a/data/src/main/kotlin/photos/network/data/photos/network/PhotoApi.kt +++ b/network/src/main/kotlin/photos/network/network/photo/PhotoApi.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.photos.network +package photos.network.network.photo interface PhotoApi { /** @@ -22,12 +22,12 @@ interface PhotoApi { suspend fun getPhotos( offset: Int = 0, limit: Int = 0, - ): Photos + ): Result /** * Load detailed information for a single photo * * @param photoId Identifier of the photo details to return */ - suspend fun getPhoto(photoId: String): Photo + suspend fun getPhoto(photoId: String): Result } diff --git a/data/src/main/kotlin/photos/network/data/photos/network/PhotoApiImpl.kt b/network/src/main/kotlin/photos/network/network/photo/PhotoApiImpl.kt similarity index 61% rename from data/src/main/kotlin/photos/network/data/photos/network/PhotoApiImpl.kt rename to network/src/main/kotlin/photos/network/network/photo/PhotoApiImpl.kt index 93bb6cf..07f3266 100644 --- a/data/src/main/kotlin/photos/network/data/photos/network/PhotoApiImpl.kt +++ b/network/src/main/kotlin/photos/network/network/photo/PhotoApiImpl.kt @@ -13,27 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.photos.network +package photos.network.network.photo import io.ktor.client.HttpClient import io.ktor.client.call.body +import io.ktor.client.request.get import io.ktor.client.request.parameter -import io.ktor.client.request.request -import kotlinx.coroutines.flow.first -import photos.network.data.settings.repository.SettingsRepository class PhotoApiImpl( private val httpClient: HttpClient, - private val settingsRepository: SettingsRepository, ) : PhotoApi { - override suspend fun getPhotos(offset: Int, limit: Int): Photos { - val host = settingsRepository.settings.first().host - return httpClient.request(urlString = "$host/api/photos") { + override suspend fun getPhotos(offset: Int, limit: Int): Result { + return httpClient.get(urlString = "/api/photos") { parameter("offset", offset) parameter("limit", limit) }.body() } - override suspend fun getPhoto(photoId: String): Photo = - httpClient.request(urlString = "/api/photo/$photoId").body() + override suspend fun getPhoto(photoId: String): Result = + httpClient.get(urlString = "/api/photo/$photoId").body() } diff --git a/data/src/main/kotlin/photos/network/data/photos/network/Photos.kt b/network/src/main/kotlin/photos/network/network/photo/Photos.kt similarity index 95% rename from data/src/main/kotlin/photos/network/data/photos/network/Photos.kt rename to network/src/main/kotlin/photos/network/network/photo/Photos.kt index 96c4ef3..9b620a9 100644 --- a/data/src/main/kotlin/photos/network/data/photos/network/Photos.kt +++ b/network/src/main/kotlin/photos/network/network/photo/Photos.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.photos.network +package photos.network.network.photo import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/data/src/main/kotlin/photos/network/data/user/network/UserApi.kt b/network/src/main/kotlin/photos/network/network/user/UserApi.kt similarity index 93% rename from data/src/main/kotlin/photos/network/data/user/network/UserApi.kt rename to network/src/main/kotlin/photos/network/network/user/UserApi.kt index 14423a1..eecf2f9 100644 --- a/data/src/main/kotlin/photos/network/data/user/network/UserApi.kt +++ b/network/src/main/kotlin/photos/network/network/user/UserApi.kt @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.user.network +package photos.network.network.user -import photos.network.data.user.network.model.NetworkUser +import photos.network.network.user.model.NetworkUser interface UserApi { /** @@ -53,7 +53,7 @@ interface UserApi { */ suspend fun revocationRequest( token: String, - tokenTypeHint: String? + tokenTypeHint: String?, ) suspend fun getUser(): NetworkUser? diff --git a/data/src/main/kotlin/photos/network/data/user/network/UserApiImpl.kt b/network/src/main/kotlin/photos/network/network/user/UserApiImpl.kt similarity index 77% rename from data/src/main/kotlin/photos/network/data/user/network/UserApiImpl.kt rename to network/src/main/kotlin/photos/network/network/user/UserApiImpl.kt index f865d32..e33a271 100644 --- a/data/src/main/kotlin/photos/network/data/user/network/UserApiImpl.kt +++ b/network/src/main/kotlin/photos/network/network/user/UserApiImpl.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.user.network +package photos.network.network.user import io.ktor.client.HttpClient import io.ktor.client.call.body @@ -26,22 +26,22 @@ import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode import io.ktor.http.Parameters import io.ktor.http.isSuccess -import kotlinx.coroutines.flow.first import logcat.LogPriority import logcat.asLog import logcat.logcat -import photos.network.data.settings.repository.SettingsRepository -import photos.network.data.user.network.model.ApiResponse -import photos.network.data.user.network.model.NetworkUser -import photos.network.data.user.network.model.TokenInfo -import photos.network.data.user.persistence.User -import photos.network.data.user.persistence.UserStorage +import photos.network.common.persistence.SecureStorage +import photos.network.common.persistence.Settings +import photos.network.common.persistence.User +import photos.network.network.user.model.ApiResponse +import photos.network.network.user.model.NetworkUser +import photos.network.network.user.model.TokenInfo class UserApiImpl( private val httpClient: HttpClient, - private val userStorage: UserStorage, - private val settingsRepository: SettingsRepository, + private val userStorage: SecureStorage, + private val settingsStorage: SecureStorage, ) : UserApi { + @Suppress("TooGenericExceptionCaught", "ReturnCount") override suspend fun verifyServerHost(host: String): Boolean { try { val response: HttpResponse = httpClient.request("$host/api") { @@ -63,8 +63,7 @@ class UserApiImpl( } override suspend fun verifyClientId(clientId: String): Boolean { - val host = settingsRepository.settings.first().host - val response: HttpResponse = httpClient.request("$host/api/oauth/authorize") { + val response: HttpResponse = httpClient.request("/api/oauth/authorize") { method = HttpMethod.Get parameter("client_id", clientId) parameter("redirect_uri", "photosapp://authenticate") @@ -74,10 +73,9 @@ class UserApiImpl( } override suspend fun accessTokenRequest(authCode: String): Boolean { - val host = settingsRepository.settings.first().host - val clientId = settingsRepository.settings.first().clientId + val clientId = settingsStorage.read()?.clientId ?: "" - val url = "$host/api/oauth/token" + val url = "/api/oauth/token" val tokenInfo: TokenInfo = httpClient.submitForm( url = url, formParameters = Parameters.build { @@ -85,7 +83,7 @@ class UserApiImpl( append("code", authCode) append("redirect_uri", "photosapp://authenticate") append("client_id", clientId) - } + }, ).body() if (tokenInfo.accessToken.isEmpty() || tokenInfo.refreshToken.isEmpty()) { @@ -120,25 +118,22 @@ class UserApiImpl( override suspend fun revocationRequest( token: String, - tokenTypeHint: String? + tokenTypeHint: String?, ) { - val host = settingsRepository.settings.first().host - httpClient.submitForm( - url = "$host/api/revoke", + url = "/api/revoke", formParameters = Parameters.build { append("token", token) - } + }, ) { this.port = port } } + @Suppress("TooGenericExceptionCaught") override suspend fun getUser(): NetworkUser? { try { - val host = settingsRepository.settings.first().host - - return httpClient.request(urlString = "$host/api/user/") { + return httpClient.request(urlString = "/api/user/") { method = HttpMethod.Get }.body() } catch (exception: Exception) { diff --git a/data/src/main/kotlin/photos/network/data/user/network/model/ApiResponse.kt b/network/src/main/kotlin/photos/network/network/user/model/ApiResponse.kt similarity index 93% rename from data/src/main/kotlin/photos/network/data/user/network/model/ApiResponse.kt rename to network/src/main/kotlin/photos/network/network/user/model/ApiResponse.kt index f93a6db..b9ad7c6 100644 --- a/data/src/main/kotlin/photos/network/data/user/network/model/ApiResponse.kt +++ b/network/src/main/kotlin/photos/network/network/user/model/ApiResponse.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.user.network.model +package photos.network.network.user.model import kotlinx.serialization.Serializable diff --git a/data/src/main/kotlin/photos/network/data/user/network/model/NetworkUser.kt b/network/src/main/kotlin/photos/network/network/user/model/NetworkUser.kt similarity index 95% rename from data/src/main/kotlin/photos/network/data/user/network/model/NetworkUser.kt rename to network/src/main/kotlin/photos/network/network/user/model/NetworkUser.kt index 5161d97..96843e1 100644 --- a/data/src/main/kotlin/photos/network/data/user/network/model/NetworkUser.kt +++ b/network/src/main/kotlin/photos/network/network/user/model/NetworkUser.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.user.network.model +package photos.network.network.user.model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/data/src/main/kotlin/photos/network/data/user/network/model/TokenInfo.kt b/network/src/main/kotlin/photos/network/network/user/model/TokenInfo.kt similarity index 95% rename from data/src/main/kotlin/photos/network/data/user/network/model/TokenInfo.kt rename to network/src/main/kotlin/photos/network/network/user/model/TokenInfo.kt index 00b3ea6..39d1abb 100644 --- a/data/src/main/kotlin/photos/network/data/user/network/model/TokenInfo.kt +++ b/network/src/main/kotlin/photos/network/network/user/model/TokenInfo.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.user.network.model +package photos.network.network.user.model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/data/src/test/kotlin/photos/network/data/photos/network/PhotoApiTests.kt b/network/src/test/kotlin/photos/network/api/PhotoApiTests.kt similarity index 92% rename from data/src/test/kotlin/photos/network/data/photos/network/PhotoApiTests.kt rename to network/src/test/kotlin/photos/network/api/PhotoApiTests.kt index 37d9abd..08595b9 100644 --- a/data/src/test/kotlin/photos/network/data/photos/network/PhotoApiTests.kt +++ b/network/src/test/kotlin/photos/network/api/PhotoApiTests.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.photos.network +package photos.network.api import com.google.common.truth.Truth import io.ktor.client.HttpClient @@ -36,6 +36,7 @@ import org.junit.Test import photos.network.data.settings.repository.PrivacyState import photos.network.data.settings.repository.Settings import photos.network.data.settings.repository.SettingsRepository +import photos.network.network.photo.PhotoApiImpl class PhotoApiTests { private val settingsRepository = mockk() @@ -46,8 +47,8 @@ class PhotoApiTests { flowOf( Settings( host = "http://localhost", - privacyState = PrivacyState.NONE - ) + privacyState = PrivacyState.NONE, + ), ) } } @@ -73,12 +74,12 @@ class PhotoApiTests { } ] } - """.trimIndent() + """.trimIndent(), ), status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/json") + headers = headersOf(HttpHeaders.ContentType, "application/json"), ) - } + }, ) { install(ContentNegotiation) { json( @@ -87,11 +88,10 @@ class PhotoApiTests { isLenient = true ignoreUnknownKeys = true }, - contentType = ContentType.Application.Json + contentType = ContentType.Application.Json, ) } }, - settingsRepository = settingsRepository ) // when diff --git a/repository/photos/build.gradle.kts b/repository/photos/build.gradle.kts new file mode 100644 index 0000000..6be9142 --- /dev/null +++ b/repository/photos/build.gradle.kts @@ -0,0 +1,123 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.repository.photos" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + } + + packagingOptions { + resources.excludes += "META-INF/AL2.0" + resources.excludes += "META-INF/LGPL2.1" + resources.excludes += "META-INF/licenses/ASM" + resources.pickFirsts.add("win32-x86-64/attach_hotspot_windows.dll") + resources.pickFirsts.add("win32-x86/attach_hotspot_windows.dll") + } +} + +dependencies { + implementation(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + // workmanager + api(libs.work.runtime.ktx) + androidTestApi(libs.work.testing) + + implementation(projects.network) + api(projects.database.photos) + +// api(AndroidX.core.ktx) + + // Coroutines +// api(KotlinX.coroutines.core) +// api(KotlinX.coroutines.android) + + // Coroutine Lifecycle Scopes +// api(AndroidX.lifecycle.runtime.ktx) +// api(AndroidX.lifecycle.viewModelKtx) + + // Koin dependency injection +// api(Koin.core) +// testApi(Koin.test) +// api(Koin.android) +// api(Koin.workManager) +// api(Koin.navigation) +// api(Koin.compose) + + // Persistence +// api(AndroidX.room.runtime) +// api(AndroidX.room.ktx) +// androidTestImplementation(AndroidX.room.testing) + + // exifinterface +// api(AndroidX.exifInterface) + + // httpclient +// implementation(Ktor.client.core) +// implementation(Ktor.client.cio) +// implementation(Ktor.client.cio) +// implementation(Ktor.client.auth) +// implementation(Ktor.client.serialization) +// implementation(Ktor.client.contentNegotiation) +// implementation(Ktor.plugins.serialization.kotlinx.json) +// implementation(libs.ktor.client.logging.jvm) +// implementation(libs.ktor.client.mock.jvm) + + // logging +// api(Square.logcat) + + // serialization +// api(KotlinX.serialization.json) +// api(AndroidX.security.crypto) + + // testing +// testApi(AndroidX.test.ext.junit.ktx) +// testApi(Testing.junit4) +// testApi(libs.com.google.truth.truth) +// testApi(Testing.mockK) +// testApi(KotlinX.coroutines.test) +// testApi(AndroidX.archCore.testing) + +// androidTestApi(AndroidX.test.core) +// androidTestApi(AndroidX.test.coreKtx) +// androidTestApi(AndroidX.test.ext.junit) +// androidTestApi(AndroidX.test.ext.junit.ktx) +// androidTestApi(AndroidX.test.ext.truth) +// androidTestApi(AndroidX.test.monitor) +// androidTestApi(AndroidX.test.orchestrator) +// androidTestApi(AndroidX.test.runner) +// androidTestApi(AndroidX.test.rules) +// androidTestApi(AndroidX.test.services) +// androidTestApi(Testing.mockK) +} diff --git a/repository/photos/src/main/AndroidManifest.xml b/repository/photos/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/repository/photos/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/repository/photos/src/main/kotlin/photos/network/repository/photos/Module.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/Module.kt new file mode 100644 index 0000000..4d1dea6 --- /dev/null +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/Module.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.repository.photos + +import androidx.work.WorkManager +import org.koin.android.ext.koin.androidApplication +import org.koin.androidx.workmanager.dsl.worker +import org.koin.dsl.module +import photos.network.repository.photos.worker.CleanResourcesWorker +import photos.network.repository.photos.worker.SyncLocalPhotosWorker +import photos.network.repository.photos.worker.UploadPhotosWorker + +val repositoryPhotosModule = module { + factory { WorkManager.getInstance(androidApplication()) } + + worker { + SyncLocalPhotosWorker( + application = get(), + workerParameters = get(), + repository = get(), + ) + } + + worker { + CleanResourcesWorker( + application = get(), + workerParameters = get(), + ) + } + + worker { + UploadPhotosWorker( + application = get(), + workerParameters = get(), + getPhotos = get(), + ) + } + + single { + PhotoRepositoryImpl( + applicationContext = get(), + photoApi = get(), + photoDao = get(), + workManager = get(), + ) + } +} diff --git a/data/src/main/kotlin/photos/network/data/photos/repository/Photo.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/Photo.kt similarity index 72% rename from data/src/main/kotlin/photos/network/data/photos/repository/Photo.kt rename to repository/photos/src/main/kotlin/photos/network/repository/photos/Photo.kt index 420ad3f..8b11079 100644 --- a/data/src/main/kotlin/photos/network/data/photos/repository/Photo.kt +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/Photo.kt @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.photos.repository +package photos.network.repository.photos import android.net.Uri import java.time.Instant -import photos.network.data.photos.persistence.Photo as DatabasePhoto +import photos.network.database.photos.Photo as DatabasePhoto data class Photo( val filename: String, @@ -28,6 +28,17 @@ data class Photo( val isPrivate: Boolean = false, val uri: Uri? = null, ) { + /** + * create from database photo + */ + constructor(photo: DatabasePhoto) : this( + filename = photo.filename, + imageUrl = photo.imageUrl, + dateAdded = Instant.ofEpochMilli(photo.dateAdded), + dateTaken = Instant.ofEpochMilli(photo.dateTaken ?: 0L), + uri = photo.originalFileUri?.let { Uri.parse(it) }, + ) + fun toDatabasePhoto(): DatabasePhoto = DatabasePhoto( filename = filename, imageUrl = imageUrl, diff --git a/data/src/main/kotlin/photos/network/data/photos/repository/PhotoRepository.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepository.kt similarity index 85% rename from data/src/main/kotlin/photos/network/data/photos/repository/PhotoRepository.kt rename to repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepository.kt index 9146fc8..335663e 100644 --- a/data/src/main/kotlin/photos/network/data/photos/repository/PhotoRepository.kt +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepository.kt @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.photos.repository +package photos.network.repository.photos import kotlinx.coroutines.flow.Flow +import photos.network.repository.photos.worker.SyncStatus interface PhotoRepository { - fun syncPhotos() + suspend fun syncPhotos(): SyncStatus fun getPhotos(): Flow> fun getPhoto(identifier: String): Flow diff --git a/data/src/main/kotlin/photos/network/data/photos/worker/SyncLocalPhotosWorker.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt similarity index 73% rename from data/src/main/kotlin/photos/network/data/photos/worker/SyncLocalPhotosWorker.kt rename to repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt index 02ab593..574f62d 100644 --- a/data/src/main/kotlin/photos/network/data/photos/worker/SyncLocalPhotosWorker.kt +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt @@ -13,43 +13,96 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.photos.worker +package photos.network.repository.photos -import android.app.Application +import android.content.Context import android.net.Uri import android.os.Build import android.provider.MediaStore -import androidx.work.CoroutineWorker -import androidx.work.WorkerParameters +import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapNotNull import logcat.LogPriority import logcat.logcat -import photos.network.data.photos.repository.Photo -import photos.network.data.photos.repository.PhotoRepository +import photos.network.database.photos.PhotoDao +import photos.network.network.photo.PhotoApi +import photos.network.repository.photos.worker.SyncLocalPhotosWorker +import photos.network.repository.photos.worker.SyncStatus import java.time.Instant +import java.util.concurrent.TimeUnit -/** - * Synchronizes all local photos from androids media store with the local database. - */ -class SyncLocalPhotosWorker( - application: Application, - workerParameters: WorkerParameters, - private val photoRepository: PhotoRepository, -) : CoroutineWorker(application.applicationContext, workerParameters) { - override suspend fun doWork(): Result { +class PhotoRepositoryImpl( + private val applicationContext: Context, + private val photoApi: PhotoApi, + private val photoDao: PhotoDao, + private val workManager: WorkManager, +) : PhotoRepository { + // TODO: user should be able to define the required network type in the app settings. + private val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.UNMETERED) + .build() + + private val periodicWorkRequest = PeriodicWorkRequest.Builder( + SyncLocalPhotosWorker::class.java, + 2, + TimeUnit.HOURS, + ) + .setConstraints(constraints) + .addTag("photosSyncWorker") + .build() + + override suspend fun syncPhotos(): SyncStatus { val photos = queryLocalMediaStore() logcat(LogPriority.VERBOSE) { "Found ${photos.size} photos." } photos.forEach { - photoRepository.addPhoto(it) + addPhoto(it) } - return Result.success() + // TODO: move to somewhere else +// val syncLocalPhotosWorkRequest: WorkRequest = OneTimeWorkRequestBuilder().build() +// +// WorkManager.getInstance(applicationContext) +// .enqueue(syncLocalPhotosWorkRequest) + + return SyncStatus.SyncSucceeded + } + + fun startPersiodicSync() { + workManager.enqueueUniquePeriodicWork( + "photosSyncWorker", + ExistingPeriodicWorkPolicy.REPLACE, + periodicWorkRequest, + ) + + // TODO: observe sync state (at least errors) + workManager.getWorkInfosByTag("photosSyncWorker") + } + + override fun getPhotos(): Flow> = photoDao.getPhotos().mapNotNull { photos -> + photos.sortedByDescending { + it.dateTaken ?: it.dateAdded + }.map { photo -> + Photo(photo) + } + } + + override fun getPhoto(identifier: String): Flow = photoDao.getPhoto(identifier).mapNotNull { + it?.let { it1 -> Photo(it1) } + } + + override suspend fun addPhoto(photo: Photo) { + photoDao.insertAll(photos = arrayOf(photo.toDatabasePhoto())) } private fun generateContentUri(): Uri { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { MediaStore.Images.Media.getContentUri( - MediaStore.VOLUME_EXTERNAL + MediaStore.VOLUME_EXTERNAL, ) } else { MediaStore.Images.Media.EXTERNAL_CONTENT_URI @@ -95,7 +148,7 @@ class SyncLocalPhotosWorker( generateProjection(), selection, selectionArgs, - sortOrder + sortOrder, )?.use { cursor -> val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID) val nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME) @@ -136,7 +189,7 @@ class SyncLocalPhotosWorker( while (cursor.moveToNext()) { var photoUri = Uri.withAppendedPath( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - cursor.getString(idColumn) + cursor.getString(idColumn), ) // Get values of columns for a given Image. @@ -185,7 +238,7 @@ class SyncLocalPhotosWorker( if (latColumn != -1 && longColumn != -1) { latLong = floatArrayOf( cursor.getFloat(latColumn), - cursor.getFloat(longColumn) + cursor.getFloat(longColumn), ) } } diff --git a/data/src/main/kotlin/photos/network/data/photos/worker/CleanResourcesWorker.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/CleanResourcesWorker.kt similarity index 92% rename from data/src/main/kotlin/photos/network/data/photos/worker/CleanResourcesWorker.kt rename to repository/photos/src/main/kotlin/photos/network/repository/photos/worker/CleanResourcesWorker.kt index 618d316..265abcf 100644 --- a/data/src/main/kotlin/photos/network/data/photos/worker/CleanResourcesWorker.kt +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/CleanResourcesWorker.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.photos.worker +package photos.network.repository.photos.worker import android.app.Application import androidx.work.CoroutineWorker @@ -24,7 +24,7 @@ import androidx.work.WorkerParameters */ class CleanResourcesWorker( application: Application, - workerParameters: WorkerParameters + workerParameters: WorkerParameters, ) : CoroutineWorker(application.applicationContext, workerParameters) { override suspend fun doWork(): Result { // TODO: Not implemented yet diff --git a/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/SyncLocalPhotosWorker.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/SyncLocalPhotosWorker.kt new file mode 100644 index 0000000..4fab81a --- /dev/null +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/SyncLocalPhotosWorker.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2020-2022 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.repository.photos.worker + +import android.app.Application +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import photos.network.repository.photos.PhotoRepository + +/** + * Synchronizes all local photos from androids media store with the local database. + */ +class SyncLocalPhotosWorker( + application: Application, + workerParameters: WorkerParameters, + private val repository: PhotoRepository, +) : CoroutineWorker(application.applicationContext, workerParameters) { + override suspend fun doWork(): Result { + // Start sync inside the repository + return when (repository.syncPhotos()) { + is SyncStatus.SyncFailed -> Result.failure() + SyncStatus.SyncSkipped -> Result.success() + SyncStatus.SyncSucceeded -> Result.success() + } + } +} diff --git a/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/SyncStatus.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/SyncStatus.kt new file mode 100644 index 0000000..6dfbee8 --- /dev/null +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/SyncStatus.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.repository.photos.worker + +sealed interface SyncStatus { + object SyncSucceeded : SyncStatus + object SyncSkipped : SyncStatus + class SyncFailed(val message: String) : SyncStatus +} diff --git a/data/src/main/kotlin/photos/network/data/photos/worker/UploadPhotosWorker.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/UploadPhotosWorker.kt similarity index 77% rename from data/src/main/kotlin/photos/network/data/photos/worker/UploadPhotosWorker.kt rename to repository/photos/src/main/kotlin/photos/network/repository/photos/worker/UploadPhotosWorker.kt index e6ed5bc..dd8861e 100644 --- a/data/src/main/kotlin/photos/network/data/photos/worker/UploadPhotosWorker.kt +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/UploadPhotosWorker.kt @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.photos.worker +package photos.network.repository.photos.worker import android.app.Application import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -import photos.network.data.photos.repository.PhotoRepository +import photos.network.database.photos.Photo /** * Uploading non-synced photos from the device to a photos.network instance. @@ -26,12 +26,13 @@ import photos.network.data.photos.repository.PhotoRepository class UploadPhotosWorker( application: Application, workerParameters: WorkerParameters, - private val photoRepository: PhotoRepository, + private val getPhotos: () -> List, ) : CoroutineWorker(application.applicationContext, workerParameters) { override suspend fun doWork(): Result { - // TODO: iterate through all local photos without an uuid - // TODO: upload file - // TODO: update file in database (add uuid) + getPhotos().filterNot { it.uuid == null }.forEach { + // TODO: upload file + // TODO: update file in database (add uuid) + } return Result.failure() } diff --git a/data/src/test/kotlin/photos/network/data/photos/repository/PhotoRepositoryTest.kt b/repository/photos/src/test/kotlin/photos/network/repository/photos/PhotoRepositoryTest.kt similarity index 93% rename from data/src/test/kotlin/photos/network/data/photos/repository/PhotoRepositoryTest.kt rename to repository/photos/src/test/kotlin/photos/network/repository/photos/PhotoRepositoryTest.kt index 361869d..87fee76 100644 --- a/data/src/test/kotlin/photos/network/data/photos/repository/PhotoRepositoryTest.kt +++ b/repository/photos/src/test/kotlin/photos/network/repository/photos/PhotoRepositoryTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.photos.repository +package photos.network.repository.photos import android.content.Context import androidx.work.WorkManager @@ -26,9 +26,8 @@ import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test import photos.network.data.TestCoroutineDispatcherRule -import photos.network.data.photos.network.PhotoApi -import photos.network.data.photos.persistence.Photo -import photos.network.data.photos.persistence.PhotoDao +import photos.network.database.photos.PhotoDao +import photos.network.network.photo.PhotoApi /** * Test photo repository @@ -59,8 +58,8 @@ class PhotoRepositoryTest { flowOf( listOf( createFakePhoto(filename = "001", dateTaken = 1580671220), - createFakePhoto(filename = "002", dateTaken = 1580671221) - ) + createFakePhoto(filename = "002", dateTaken = 1580671221), + ), ) } @@ -80,7 +79,7 @@ class PhotoRepositoryTest { createFakePhoto(filename = "002", dateTaken = 1580671221), createFakePhoto(filename = "001", dateTaken = 1580671220), createFakePhoto(filename = "003", dateTaken = 1580671223), - ) + ), ) } @@ -102,7 +101,7 @@ class PhotoRepositoryTest { createFakePhoto(filename = "002", dateTaken = null, dateAdded = 1580671221), createFakePhoto(filename = "001", dateTaken = null, dateAdded = 1580671220), createFakePhoto(filename = "003", dateTaken = null, dateAdded = 1580671223), - ) + ), ) } @@ -123,7 +122,7 @@ class PhotoRepositoryTest { dateTaken: Long? = null, dateModified: Long? = null, thumbnailFileUri: String? = null, - originalFileUri: String? = null + originalFileUri: String? = null, ): Photo { return Photo( uuid = uuid, diff --git a/repository/settings/build.gradle.kts b/repository/settings/build.gradle.kts new file mode 100644 index 0000000..2ffef8e --- /dev/null +++ b/repository/settings/build.gradle.kts @@ -0,0 +1,74 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +// alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.repository.settings" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + javaCompileOptions { + annotationProcessorOptions { + arguments( + mapOf( + "room.schemaLocation" to "$projectDir/schemas", + "room.incremental" to "true", + "room.expandProjection" to "true" + ) + ) + } + } + } + + sourceSets { + // Adds exported schema location as test app assets. + getByName("androidTest").assets.srcDir("$projectDir/schemas") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + } + + packagingOptions { + resources.excludes += "META-INF/AL2.0" + resources.excludes += "META-INF/LGPL2.1" + resources.excludes += "META-INF/licenses/ASM" + resources.pickFirsts.add("win32-x86-64/attach_hotspot_windows.dll") + resources.pickFirsts.add("win32-x86/attach_hotspot_windows.dll") + } +} + +dependencies { + api(projects.common) +// testApi(projects.common) +// androidTestApi(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + api(projects.database.settings) +} diff --git a/repository/settings/src/main/AndroidManifest.xml b/repository/settings/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/repository/settings/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/repository/settings/src/main/kotlin/photos/network/repository/settings/Module.kt b/repository/settings/src/main/kotlin/photos/network/repository/settings/Module.kt new file mode 100644 index 0000000..779263b --- /dev/null +++ b/repository/settings/src/main/kotlin/photos/network/repository/settings/Module.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.repository.settings + +import androidx.work.WorkManager +import org.koin.android.ext.koin.androidApplication +import org.koin.dsl.module + +val repositorySettingsModule = module { + factory { WorkManager.getInstance(androidApplication()) } + + single { + SettingsRepositoryImpl( + settingsStore = get(), + ) + } +} diff --git a/data/src/main/kotlin/photos/network/data/settings/repository/SettingsRepository.kt b/repository/settings/src/main/kotlin/photos/network/repository/settings/SettingsRepository.kt similarity index 91% rename from data/src/main/kotlin/photos/network/data/settings/repository/SettingsRepository.kt rename to repository/settings/src/main/kotlin/photos/network/repository/settings/SettingsRepository.kt index 23a2e52..b2df70b 100644 --- a/data/src/main/kotlin/photos/network/data/settings/repository/SettingsRepository.kt +++ b/repository/settings/src/main/kotlin/photos/network/repository/settings/SettingsRepository.kt @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.settings.repository +package photos.network.repository.settings import kotlinx.coroutines.flow.Flow +import photos.network.common.persistence.Settings interface SettingsRepository { val settings: Flow diff --git a/data/src/main/kotlin/photos/network/data/settings/repository/SettingsRepositoryImpl.kt b/repository/settings/src/main/kotlin/photos/network/repository/settings/SettingsRepositoryImpl.kt similarity index 79% rename from data/src/main/kotlin/photos/network/data/settings/repository/SettingsRepositoryImpl.kt rename to repository/settings/src/main/kotlin/photos/network/repository/settings/SettingsRepositoryImpl.kt index d289671..ca57ee0 100644 --- a/data/src/main/kotlin/photos/network/data/settings/repository/SettingsRepositoryImpl.kt +++ b/repository/settings/src/main/kotlin/photos/network/repository/settings/SettingsRepositoryImpl.kt @@ -13,33 +13,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.settings.repository +package photos.network.repository.settings import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext -import photos.network.data.settings.persistence.SettingsStorage -import photos.network.data.settings.persistence.Settings as PersistenceSettings +import photos.network.common.persistence.PrivacyState +import photos.network.common.persistence.SecureStorage +import photos.network.common.persistence.Settings class SettingsRepositoryImpl( - private val settingsStore: SettingsStorage, + private val settingsStore: SecureStorage, ) : SettingsRepository { - private var currentSettings: PersistenceSettings? = null + private var currentSettings: Settings? = null override val settings: Flow = flow { while (true) { loadSettings() currentSettings?.let { dto -> - val privacyState = PrivacyState.valueOf(dto.privacyState) emit( Settings( host = dto.host ?: "", clientId = dto.clientId ?: "", - privacyState = privacyState, - ) + privacyState = dto.privacyState, + ), ) } delay(POLL_INTERVAL) @@ -52,10 +52,10 @@ class SettingsRepositoryImpl( override suspend fun updateSettings(newSettings: Settings) { withContext(Dispatchers.IO) { - currentSettings = PersistenceSettings( + currentSettings = Settings( host = newSettings.host, clientId = newSettings.clientId, - privacyState = newSettings.privacyState.toString() + privacyState = newSettings.privacyState, ) saveSettings() } @@ -70,12 +70,12 @@ class SettingsRepositoryImpl( override suspend fun togglePrivacy() { currentSettings?.let { - val newValue = if (it.privacyState == PrivacyState.NONE.toString()) { - PrivacyState.ACTIVE.toString() + val newValue = if (it.privacyState == PrivacyState.NONE) { + PrivacyState.ACTIVE } else { - PrivacyState.NONE.toString() + PrivacyState.NONE } - val new = PersistenceSettings( + val new = Settings( host = it.host, clientId = it.clientId, privacyState = newValue, @@ -89,7 +89,7 @@ class SettingsRepositoryImpl( override suspend fun updateHost(newHost: String) { withContext(Dispatchers.IO) { currentSettings?.let { - val new = PersistenceSettings( + val new = Settings( host = newHost, clientId = it.clientId, privacyState = it.privacyState, @@ -104,7 +104,7 @@ class SettingsRepositoryImpl( override suspend fun updateClientId(newClientId: String) { withContext(Dispatchers.IO) { currentSettings?.let { - val new = PersistenceSettings( + val new = Settings( host = it.host, clientId = newClientId, privacyState = it.privacyState, @@ -124,7 +124,7 @@ class SettingsRepositoryImpl( } if (currentSettings == null) { - currentSettings = PersistenceSettings() + currentSettings = Settings() } } diff --git a/data/src/test/kotlin/photos/network/data/user/repository/UserRepositoryTests.kt b/repository/settings/src/test/kotlin/photos/network/repository/settings/UserRepositoryTests.kt similarity index 90% rename from data/src/test/kotlin/photos/network/data/user/repository/UserRepositoryTests.kt rename to repository/settings/src/test/kotlin/photos/network/repository/settings/UserRepositoryTests.kt index d11450f..b2933e1 100644 --- a/data/src/test/kotlin/photos/network/data/user/repository/UserRepositoryTests.kt +++ b/repository/settings/src/test/kotlin/photos/network/repository/settings/UserRepositoryTests.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.user.repository +package photos.network.repository.settings import com.google.common.truth.Truth import io.mockk.coEvery @@ -22,11 +22,11 @@ import io.mockk.mockk import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test -import photos.network.data.TestCoroutineDispatcherRule +import photos.network.common.TestCoroutineDispatcherRule import photos.network.data.user.network.UserApi import photos.network.data.user.network.model.NetworkUser import photos.network.data.user.persistence.UserStorage -import photos.network.data.user.persistence.User as DatabaseUser +import photos.network.common.persistence.User as DatabaseUser class UserRepositoryTests { @get:Rule @@ -39,7 +39,7 @@ class UserRepositoryTests { private val userRepository by lazy { UserRepositoryImpl( userApi = userApi, - userStorage = userStorage + userStorage = userStorage, ) } @@ -57,6 +57,7 @@ class UserRepositoryTests { Truth.assertThat(user).isNotNull() } + @Suppress("LongParameterList") private fun fakeUser( id: String = "123-abc-456-789", lastname: String = "Done", @@ -79,7 +80,7 @@ class UserRepositoryTests { lastname: String = "Jane", firstname: String = "http://localhost/foo/bar/jane.jpg", lastSeen: String = "", - ): NetworkUser = NetworkUser( + ): User = User( id = id, email = email, lastname = lastname, diff --git a/repository/sharing/build.gradle.kts b/repository/sharing/build.gradle.kts new file mode 100644 index 0000000..c6725d3 --- /dev/null +++ b/repository/sharing/build.gradle.kts @@ -0,0 +1,81 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.repository.sharing" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + javaCompileOptions { + annotationProcessorOptions { + arguments( + mapOf( + "room.schemaLocation" to "$projectDir/schemas", + "room.incremental" to "true", + "room.expandProjection" to "true" + ) + ) + } + } + } + + sourceSets { + // Adds exported schema location as test app assets. + getByName("androidTest").assets.srcDir("$projectDir/schemas") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + } + + packagingOptions { + resources.excludes += "META-INF/AL2.0" + resources.excludes += "META-INF/LGPL2.1" + resources.excludes += "META-INF/licenses/ASM" + resources.pickFirsts.add("win32-x86-64/attach_hotspot_windows.dll") + resources.pickFirsts.add("win32-x86/attach_hotspot_windows.dll") + } +} + +configurations { + create("testArtifacts"){ + extendsFrom(configurations.testApi.get()) + } + create("androidTestArtifacts"){ + extendsFrom(configurations.androidTestApi.get()) + } +} + +dependencies { + api(project(":common")) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + api(projects.network) + api(projects.database.sharing) +} diff --git a/repository/sharing/src/main/AndroidManifest.xml b/repository/sharing/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/repository/sharing/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/repository/sharing/src/main/kotlin/photos/network/repository/sharing/Module.kt b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/Module.kt new file mode 100644 index 0000000..f48368d --- /dev/null +++ b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/Module.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.repository.sharing + +import androidx.work.WorkManager +import org.koin.android.ext.koin.androidApplication +import org.koin.dsl.module + +val repositorySharingModule = module { + factory { WorkManager.getInstance(androidApplication()) } + + single { + UserRepositoryImpl( + userApi = get(), + userStorage = get(), + ) + } +} diff --git a/data/src/main/kotlin/photos/network/data/user/repository/User.kt b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/User.kt similarity index 53% rename from data/src/main/kotlin/photos/network/data/user/repository/User.kt rename to repository/sharing/src/main/kotlin/photos/network/repository/sharing/User.kt index 2a80e8a..7391d03 100644 --- a/data/src/main/kotlin/photos/network/data/user/repository/User.kt +++ b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/User.kt @@ -13,34 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.user.repository - -import photos.network.data.user.persistence.User as PersistenceUser -import photos.network.data.user.repository.User as DomainUser +package photos.network.repository.sharing data class User( - val id: String, + val id: String? = null, val lastname: String, val firstname: String, val profileImageUrl: String, val accessToken: String? = null, val refreshToken: String? = null, ) { - fun toDatabaseUser(): PersistenceUser = PersistenceUser( - id = id, - lastname = lastname, - firstname = firstname, - profileImageUrl = profileImageUrl, - accessToken = accessToken, - refreshToken = refreshToken, - ) - - fun toDomain(): DomainUser = DomainUser( - id = id, - lastname = lastname, - firstname = firstname, - profileImageUrl = profileImageUrl, - accessToken = accessToken, - refreshToken = refreshToken, - ) +// fun toDatabaseUser(): PersistenceUser = PersistenceUser( +// id = id, +// lastname = lastname, +// firstname = firstname, +// profileImageUrl = profileImageUrl, +// accessToken = accessToken, +// refreshToken = refreshToken, +// ) +// +// fun toDomain(): DomainUser = DomainUser( +// id = id, +// lastname = lastname, +// firstname = firstname, +// profileImageUrl = profileImageUrl, +// accessToken = accessToken, +// refreshToken = refreshToken, +// ) } diff --git a/data/src/main/kotlin/photos/network/data/user/repository/UserRepository.kt b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepository.kt similarity index 90% rename from data/src/main/kotlin/photos/network/data/user/repository/UserRepository.kt rename to repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepository.kt index de68f95..1f50a70 100644 --- a/data/src/main/kotlin/photos/network/data/user/repository/UserRepository.kt +++ b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepository.kt @@ -13,9 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.user.repository - -import photos.network.data.user.persistence.User +package photos.network.repository.sharing interface UserRepository { fun currentUser(): User? diff --git a/data/src/main/kotlin/photos/network/data/user/repository/UserRepositoryImpl.kt b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepositoryImpl.kt similarity index 87% rename from data/src/main/kotlin/photos/network/data/user/repository/UserRepositoryImpl.kt rename to repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepositoryImpl.kt index ab7d4c9..0e40baf 100644 --- a/data/src/main/kotlin/photos/network/data/user/repository/UserRepositoryImpl.kt +++ b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepositoryImpl.kt @@ -13,24 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.user.repository +package photos.network.repository.sharing import io.ktor.client.plugins.ServerResponseException import kotlinx.coroutines.runBlocking import logcat.LogPriority import logcat.logcat -import photos.network.data.user.network.UserApi -import photos.network.data.user.persistence.UserStorage +import photos.network.common.persistence.SecureStorage +import photos.network.network.user.UserApi import java.net.ConnectException -import photos.network.data.user.persistence.User as DatabaseUser class UserRepositoryImpl( private val userApi: UserApi, - private val userStorage: UserStorage + private val userStorage: SecureStorage, ) : UserRepository { - private var currentUser: DatabaseUser? = null + private var currentUser: User? = null - override fun currentUser(): DatabaseUser? { + override fun currentUser(): User? { // from memory if (currentUser != null) { return currentUser @@ -46,7 +45,7 @@ class UserRepositoryImpl( logcat(LogPriority.INFO) { "userApi.getUser(): ${userApi.getUser()}" } userApi.getUser()?.let { logcat(LogPriority.INFO) { "userApi.getUser()?.let: $it" } - val user = DatabaseUser( + val user = User( id = it.id, lastname = it.lastname, firstname = it.firstname, @@ -74,6 +73,7 @@ class UserRepositoryImpl( return userApi.accessTokenRequest(authCode) } + @Suppress("SwallowedException") override suspend fun verifyServerHost(host: String): Boolean { try { return userApi.verifyServerHost(host) diff --git a/settings.gradle.kts b/settings.gradle.kts index dd0504b..fd62b9c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,16 +1,68 @@ -import de.fayard.refreshVersions.core.StabilityLevel -rootProject.name = "PhotosNetwork" +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") -plugins { - id("de.fayard.refreshVersions") version "0.50.1" -} +pluginManagement { + repositories { + gradlePluginPortal() + mavenLocal() + mavenCentral() + google() + } + dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } + } -refreshVersions { - rejectVersionIf { - candidate.stabilityLevel != StabilityLevel.Stable + plugins { + kotlin("android").version("1.8.10") + id("com.android.application").version("7.4.0") + id("com.android.library").version("7.4.0") + // kotlin("kapt").version("1.8.10") + id("com.google.devtools.ksp").version("1.8.10-1.0.9") } } +rootProject.name = "PhotosNetwork" + include(":app") -include(":domain") -include(":data") + +include(":ui:albums") +include(":ui:folders") +include(":ui:photos") +include(":ui:settings") +include(":ui:sharing") + +include(":ui:common") + +include(":domain:albums") +include(":domain:folders") +include(":domain:photos") +include(":domain:settings") +include(":domain:sharing") + +include(":repository:photos") +include(":repository:settings") +include(":repository:sharing") + +// Persist albums & photos enriched with user info or from backend +include(":database:albums") +include(":database:photos") +include(":database:settings") +include(":database:sharing") + +// communication via REST with core instance +include(":network") + +// instance and account +include(":system:account") + +// folders via Android Filesystem +include(":system:filesystem") + +// media items via Android Media Store +include(":system:mediastore") + +// shared code +include(":common") diff --git a/system/account/build.gradle.kts b/system/account/build.gradle.kts new file mode 100644 index 0000000..2d5fd14 --- /dev/null +++ b/system/account/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.system.account" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + api(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) +} diff --git a/system/account/src/main/AndroidManifest.xml b/system/account/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/system/account/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/system/filesystem/build.gradle.kts b/system/filesystem/build.gradle.kts new file mode 100644 index 0000000..07cde3b --- /dev/null +++ b/system/filesystem/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.system.filesystem" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + api(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) +} diff --git a/system/filesystem/src/main/AndroidManifest.xml b/system/filesystem/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/system/filesystem/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/system/mediastore/build.gradle.kts b/system/mediastore/build.gradle.kts new file mode 100644 index 0000000..b1bf699 --- /dev/null +++ b/system/mediastore/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.system.mediastore" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + api(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) +} diff --git a/system/mediastore/src/main/AndroidManifest.xml b/system/mediastore/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/system/mediastore/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/ui/albums/build.gradle.kts b/ui/albums/build.gradle.kts new file mode 100644 index 0000000..a78d127 --- /dev/null +++ b/ui/albums/build.gradle.kts @@ -0,0 +1,53 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.ui.albums" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() + } +} + +dependencies { + implementation(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + api(projects.ui.common) + api(projects.domain.albums) +} diff --git a/ui/albums/src/main/AndroidManifest.xml b/ui/albums/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/ui/albums/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/app/src/main/kotlin/photos/network/home/albums/AlbumsScreen.kt b/ui/albums/src/main/kotlin/photos/network/ui/albums/AlbumsScreen.kt similarity index 83% rename from app/src/main/kotlin/photos/network/home/albums/AlbumsScreen.kt rename to ui/albums/src/main/kotlin/photos/network/ui/albums/AlbumsScreen.kt index 5ec4027..83d8e0b 100644 --- a/app/src/main/kotlin/photos/network/home/albums/AlbumsScreen.kt +++ b/ui/albums/src/main/kotlin/photos/network/ui/albums/AlbumsScreen.kt @@ -13,13 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.home.albums +package photos.network.ui.albums import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PhotoAlbum import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -30,8 +32,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController -import photos.network.navigation.Destination -import photos.network.theme.AppTheme +import photos.network.ui.common.theme.AppTheme @Composable fun AlbumsScreen( @@ -51,22 +52,22 @@ fun AlbumsContent( .padding(16.dp) .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.Center, ) { Icon( - imageVector = Destination.Albums.icon, + imageVector = Icons.Filled.PhotoAlbum, contentDescription = null, tint = MaterialTheme.colorScheme.primary, ) Text( text = "Coming soon", color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.displaySmall + style = MaterialTheme.typography.displaySmall, ) Text( text = "Here you'll be able to group images into albums", color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.bodyMedium + style = MaterialTheme.typography.bodyMedium, ) } } @@ -75,13 +76,13 @@ fun AlbumsContent( name = "Albums", showSystemUi = true, showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_NO + uiMode = Configuration.UI_MODE_NIGHT_NO, ) @Preview( name = "Albums · DARK", showSystemUi = true, showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_YES + uiMode = Configuration.UI_MODE_NIGHT_YES, ) @Composable internal fun PreviewAlbumContent() { diff --git a/ui/albums/src/main/kotlin/photos/network/ui/albums/Module.kt b/ui/albums/src/main/kotlin/photos/network/ui/albums/Module.kt new file mode 100644 index 0000000..14dbe3e --- /dev/null +++ b/ui/albums/src/main/kotlin/photos/network/ui/albums/Module.kt @@ -0,0 +1,8 @@ +package photos.network.ui.albums + +import org.koin.dsl.module + +val uiAlbumsModule = module { +// viewModel { +// } +} diff --git a/ui/common/build.gradle.kts b/ui/common/build.gradle.kts new file mode 100644 index 0000000..2ce3a0c --- /dev/null +++ b/ui/common/build.gradle.kts @@ -0,0 +1,65 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.ui.common" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() + } +} + +dependencies { + implementation(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + implementation(projects.network) + + // Compose + api(platform(libs.compose.bom)) + api(libs.bundles.compose) + + api(libs.constraintlayout.compose) + + // accompanist + api(libs.bundles.accompanist) + + + // coil image loading + api(libs.bundles.coil) +} diff --git a/ui/common/src/main/AndroidManifest.xml b/ui/common/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/ui/common/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/app/src/main/kotlin/photos/network/ui/components/ActivityLog.kt b/ui/common/src/main/kotlin/photos/network/ui/common/components/ActivityLog.kt similarity index 91% rename from app/src/main/kotlin/photos/network/ui/components/ActivityLog.kt rename to ui/common/src/main/kotlin/photos/network/ui/common/components/ActivityLog.kt index 3ab5b59..e5dee08 100644 --- a/app/src/main/kotlin/photos/network/ui/components/ActivityLog.kt +++ b/ui/common/src/main/kotlin/photos/network/ui/common/components/ActivityLog.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.ui.components +package photos.network.ui.common.components import androidx.compose.material.MaterialTheme import androidx.compose.material.TextField @@ -35,7 +35,7 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp -import photos.network.theme.JetbrainsMono +import photos.network.ui.common.theme.JetbrainsMono @Composable fun ActivityLog( @@ -44,7 +44,7 @@ fun ActivityLog( TextField( modifier = modifier, value = TextFieldValue( - text = "15:40:46 Lorem ipsum dolor sit amet" + text = "15:40:46 Lorem ipsum dolor sit amet", ), singleLine = false, maxLines = Int.MAX_VALUE, @@ -67,7 +67,7 @@ fun ActivityLog( trailingIcon = null, isError = false, visualTransformation = FormattedTextTransformation(), - onValueChange = {} + onValueChange = {}, ) } @@ -80,13 +80,13 @@ class FormattedTextTransformation : VisualTransformation { originalText.substring(0, 8) } append( - originalText.substring(9) + originalText.substring(9), ) } return TransformedText( text = formattedText, - offsetMapping = OffsetMapping.Identity + offsetMapping = OffsetMapping.Identity, ) } } diff --git a/app/src/main/kotlin/photos/network/ui/components/AppLogo.kt b/ui/common/src/main/kotlin/photos/network/ui/common/components/AppLogo.kt similarity index 95% rename from app/src/main/kotlin/photos/network/ui/components/AppLogo.kt rename to ui/common/src/main/kotlin/photos/network/ui/common/components/AppLogo.kt index 8e5ba52..851075c 100644 --- a/app/src/main/kotlin/photos/network/ui/components/AppLogo.kt +++ b/ui/common/src/main/kotlin/photos/network/ui/common/components/AppLogo.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.ui.components +package photos.network.ui.common.components import android.content.res.Configuration import androidx.compose.foundation.Image @@ -37,9 +37,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout -import photos.network.R -import photos.network.settings.ServerStatus -import photos.network.theme.AppTheme +import photos.network.network.ServerStatus +import photos.network.ui.common.R +import photos.network.ui.common.theme.AppTheme @Composable fun AppLogo( @@ -75,7 +75,7 @@ fun AppLogo( .background(Color(0xFF5DA6E3), CircleShape) .padding(10.dp), painter = image, - contentDescription = stateDescription + contentDescription = stateDescription, ) val imageVector: Painter? = when (serverStatus) { diff --git a/app/src/main/kotlin/photos/network/navigation/Destination.kt b/ui/common/src/main/kotlin/photos/network/ui/common/navigation/Destination.kt similarity index 92% rename from app/src/main/kotlin/photos/network/navigation/Destination.kt rename to ui/common/src/main/kotlin/photos/network/ui/common/navigation/Destination.kt index 4f05ef9..5f8bb4a 100644 --- a/app/src/main/kotlin/photos/network/navigation/Destination.kt +++ b/ui/common/src/main/kotlin/photos/network/ui/common/navigation/Destination.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.navigation +package photos.network.ui.common.navigation import android.os.Bundle import androidx.annotation.StringRes @@ -27,7 +27,7 @@ import androidx.compose.material.icons.filled.Photo import androidx.compose.material.icons.filled.PhotoAlbum import androidx.compose.ui.graphics.vector.ImageVector import androidx.core.os.bundleOf -import photos.network.R +import photos.network.ui.common.R /** * Navigation destinations with titles and icons @@ -35,7 +35,7 @@ import photos.network.R sealed class Destination( val route: String, @StringRes val resourceId: Int, - val icon: ImageVector + val icon: ImageVector, ) { object Home : Destination("home", R.string.home_title, Icons.Filled.House) object Photos : Destination("photos", R.string.photos_title, Icons.Filled.Photo) @@ -43,7 +43,6 @@ sealed class Destination( object Account : Destination("account", R.string.account_title, Icons.Filled.People) object Folders : Destination("folders", R.string.folders_title, Icons.Filled.Folder) object Login : Destination("login", R.string.login_title, Icons.Filled.Lock) - object Help : Destination("help", R.string.help_title, Icons.Filled.Help) fun isRootDestination(): Boolean { return this == Photos || this == Albums || this == Folders @@ -62,7 +61,6 @@ sealed class Destination( Account.route -> Account Folders.route -> Folders Login.route -> Login - Help.route -> Help else -> Home } } diff --git a/app/src/main/kotlin/photos/network/theme/Colors.kt b/ui/common/src/main/kotlin/photos/network/ui/common/theme/Colors.kt similarity index 98% rename from app/src/main/kotlin/photos/network/theme/Colors.kt rename to ui/common/src/main/kotlin/photos/network/ui/common/theme/Colors.kt index b5623b6..0578d5a 100644 --- a/app/src/main/kotlin/photos/network/theme/Colors.kt +++ b/ui/common/src/main/kotlin/photos/network/ui/common/theme/Colors.kt @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.theme +@file:Suppress("MagicNumber") + +package photos.network.ui.common.theme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color val colorLightPrimary = Color(0xFF0062a2) diff --git a/app/src/main/kotlin/photos/network/theme/Theme.kt b/ui/common/src/main/kotlin/photos/network/ui/common/theme/Theme.kt similarity index 90% rename from app/src/main/kotlin/photos/network/theme/Theme.kt rename to ui/common/src/main/kotlin/photos/network/ui/common/theme/Theme.kt index 18fc880..6c04a75 100644 --- a/app/src/main/kotlin/photos/network/theme/Theme.kt +++ b/ui/common/src/main/kotlin/photos/network/ui/common/theme/Theme.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.theme +package photos.network.ui.common.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme @@ -22,7 +22,7 @@ import androidx.compose.runtime.Composable @Composable fun AppTheme( useDarkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { val colors = if (!useDarkTheme) { lightColors @@ -33,6 +33,6 @@ fun AppTheme( MaterialTheme( colorScheme = colors, typography = AppTypography, - content = content + content = content, ) } diff --git a/app/src/main/kotlin/photos/network/theme/Typography.kt b/ui/common/src/main/kotlin/photos/network/ui/common/theme/Typography.kt similarity index 87% rename from app/src/main/kotlin/photos/network/theme/Typography.kt rename to ui/common/src/main/kotlin/photos/network/ui/common/theme/Typography.kt index 3a44cf7..9338d00 100644 --- a/app/src/main/kotlin/photos/network/theme/Typography.kt +++ b/ui/common/src/main/kotlin/photos/network/ui/common/theme/Typography.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.theme +package photos.network.ui.common.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle @@ -21,17 +21,17 @@ import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp -import photos.network.R +import photos.network.ui.common.R val Roboto = FontFamily.Default val Changa = FontFamily( Font(R.font.changa_bold, FontWeight.Bold), -// Font(R.font.changa_extra_bold, FontWeight.ExtraBold), -// Font(R.font.changa_extra_light, FontWeight.ExtraLight), -// Font(R.font.changa_light, FontWeight.Light), -// Font(R.font.changa_medium, FontWeight.Medium), -// Font(R.font.changa_regular, FontWeight.Normal), -// Font(R.font.changa_semi_bold, FontWeight.Medium), + Font(R.font.changa_extra_bold, FontWeight.ExtraBold), + Font(R.font.changa_extra_light, FontWeight.ExtraLight), + Font(R.font.changa_light, FontWeight.Light), + Font(R.font.changa_medium, FontWeight.Medium), + Font(R.font.changa_regular, FontWeight.Normal), + Font(R.font.changa_semi_bold, FontWeight.Medium), ) val JetbrainsMono = FontFamily( @@ -46,7 +46,7 @@ val AppTypography = Typography( fontWeight = FontWeight.W400, fontSize = 57.sp, lineHeight = 64.sp, - letterSpacing = -0.25.sp, + letterSpacing = (-0.25).sp, ), displayMedium = TextStyle( fontFamily = Roboto, @@ -65,12 +65,12 @@ val AppTypography = Typography( headlineLarge = TextStyle( fontFamily = Changa, fontWeight = FontWeight.Bold, - fontSize = 24.sp + fontSize = 24.sp, ), headlineMedium = TextStyle( fontFamily = Changa, fontWeight = FontWeight.Bold, - fontSize = 16.sp + fontSize = 16.sp, ), headlineSmall = TextStyle( fontFamily = Roboto, diff --git a/app/src/main/res/drawable-night/logo.xml b/ui/common/src/main/res/drawable-night/logo.xml similarity index 100% rename from app/src/main/res/drawable-night/logo.xml rename to ui/common/src/main/res/drawable-night/logo.xml diff --git a/app/src/main/res/drawable-night/logo_inverted.xml b/ui/common/src/main/res/drawable-night/logo_inverted.xml similarity index 100% rename from app/src/main/res/drawable-night/logo_inverted.xml rename to ui/common/src/main/res/drawable-night/logo_inverted.xml diff --git a/app/src/main/res/drawable/cloud_lock.xml b/ui/common/src/main/res/drawable/cloud_lock.xml similarity index 100% rename from app/src/main/res/drawable/cloud_lock.xml rename to ui/common/src/main/res/drawable/cloud_lock.xml diff --git a/app/src/main/res/drawable/cloud_off.xml b/ui/common/src/main/res/drawable/cloud_off.xml similarity index 100% rename from app/src/main/res/drawable/cloud_off.xml rename to ui/common/src/main/res/drawable/cloud_off.xml diff --git a/app/src/main/res/drawable/cloud_sync.xml b/ui/common/src/main/res/drawable/cloud_sync.xml similarity index 100% rename from app/src/main/res/drawable/cloud_sync.xml rename to ui/common/src/main/res/drawable/cloud_sync.xml diff --git a/app/src/main/res/drawable/logo.xml b/ui/common/src/main/res/drawable/logo.xml similarity index 100% rename from app/src/main/res/drawable/logo.xml rename to ui/common/src/main/res/drawable/logo.xml diff --git a/app/src/main/res/drawable/logo_inverted.xml b/ui/common/src/main/res/drawable/logo_inverted.xml similarity index 100% rename from app/src/main/res/drawable/logo_inverted.xml rename to ui/common/src/main/res/drawable/logo_inverted.xml diff --git a/app/src/main/res/drawable/logo_monochrome.xml b/ui/common/src/main/res/drawable/logo_monochrome.xml similarity index 100% rename from app/src/main/res/drawable/logo_monochrome.xml rename to ui/common/src/main/res/drawable/logo_monochrome.xml diff --git a/app/src/main/res/font/changa_bold.ttf b/ui/common/src/main/res/font/changa_bold.ttf similarity index 100% rename from app/src/main/res/font/changa_bold.ttf rename to ui/common/src/main/res/font/changa_bold.ttf diff --git a/app/src/main/res/font/changa_extra_bold.ttf b/ui/common/src/main/res/font/changa_extra_bold.ttf similarity index 100% rename from app/src/main/res/font/changa_extra_bold.ttf rename to ui/common/src/main/res/font/changa_extra_bold.ttf diff --git a/app/src/main/res/font/changa_extra_light.ttf b/ui/common/src/main/res/font/changa_extra_light.ttf similarity index 100% rename from app/src/main/res/font/changa_extra_light.ttf rename to ui/common/src/main/res/font/changa_extra_light.ttf diff --git a/app/src/main/res/font/changa_light.ttf b/ui/common/src/main/res/font/changa_light.ttf similarity index 100% rename from app/src/main/res/font/changa_light.ttf rename to ui/common/src/main/res/font/changa_light.ttf diff --git a/app/src/main/res/font/changa_medium.ttf b/ui/common/src/main/res/font/changa_medium.ttf similarity index 100% rename from app/src/main/res/font/changa_medium.ttf rename to ui/common/src/main/res/font/changa_medium.ttf diff --git a/app/src/main/res/font/changa_regular.ttf b/ui/common/src/main/res/font/changa_regular.ttf similarity index 100% rename from app/src/main/res/font/changa_regular.ttf rename to ui/common/src/main/res/font/changa_regular.ttf diff --git a/app/src/main/res/font/changa_semi_bold.ttf b/ui/common/src/main/res/font/changa_semi_bold.ttf similarity index 100% rename from app/src/main/res/font/changa_semi_bold.ttf rename to ui/common/src/main/res/font/changa_semi_bold.ttf diff --git a/app/src/main/res/font/jetbrains_mono_light.ttf b/ui/common/src/main/res/font/jetbrains_mono_light.ttf similarity index 100% rename from app/src/main/res/font/jetbrains_mono_light.ttf rename to ui/common/src/main/res/font/jetbrains_mono_light.ttf diff --git a/app/src/main/res/font/jetbrains_mono_regular.ttf b/ui/common/src/main/res/font/jetbrains_mono_regular.ttf similarity index 100% rename from app/src/main/res/font/jetbrains_mono_regular.ttf rename to ui/common/src/main/res/font/jetbrains_mono_regular.ttf diff --git a/app/src/main/res/font/jetbrains_mono_thin.ttf b/ui/common/src/main/res/font/jetbrains_mono_thin.ttf similarity index 100% rename from app/src/main/res/font/jetbrains_mono_thin.ttf rename to ui/common/src/main/res/font/jetbrains_mono_thin.ttf diff --git a/app/src/main/res/font/roboto.ttf b/ui/common/src/main/res/font/roboto.ttf similarity index 100% rename from app/src/main/res/font/roboto.ttf rename to ui/common/src/main/res/font/roboto.ttf diff --git a/app/src/main/res/font/roboto_light.ttf b/ui/common/src/main/res/font/roboto_light.ttf similarity index 100% rename from app/src/main/res/font/roboto_light.ttf rename to ui/common/src/main/res/font/roboto_light.ttf diff --git a/ui/common/src/main/res/values/strings.xml b/ui/common/src/main/res/values/strings.xml new file mode 100644 index 0000000..b71da54 --- /dev/null +++ b/ui/common/src/main/res/values/strings.xml @@ -0,0 +1,21 @@ + + Photos.network + An open source project for self hosted photo management. + + Home + Login + Setup + Photos + Albums + Details + Search + Folders + Account + + Communication with the configured photos.network instance is fine. + The configured photos.network instance is currently not available. + Data is being transmitted with the configured photos.network instance. + Communication to the configured photos.network instance is not authorized! + + diff --git a/ui/folders/build.gradle.kts b/ui/folders/build.gradle.kts new file mode 100644 index 0000000..6034154 --- /dev/null +++ b/ui/folders/build.gradle.kts @@ -0,0 +1,53 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.ui.folders" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() + } +} + +dependencies { + implementation(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + api(projects.domain.folders) + api(projects.ui.common) +} diff --git a/ui/folders/src/main/AndroidManifest.xml b/ui/folders/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/ui/folders/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/app/src/main/kotlin/photos/network/home/folders/FoldersScreen.kt b/ui/folders/src/main/kotlin/photos/network/ui/folders/FoldersScreen.kt similarity index 83% rename from app/src/main/kotlin/photos/network/home/folders/FoldersScreen.kt rename to ui/folders/src/main/kotlin/photos/network/ui/folders/FoldersScreen.kt index 547e463..baaefb5 100644 --- a/app/src/main/kotlin/photos/network/home/folders/FoldersScreen.kt +++ b/ui/folders/src/main/kotlin/photos/network/ui/folders/FoldersScreen.kt @@ -13,13 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.home.folders +package photos.network.ui.folders import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Folder import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -30,8 +32,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController -import photos.network.navigation.Destination -import photos.network.theme.AppTheme +import photos.network.ui.common.theme.AppTheme @Composable fun FoldersScreen( @@ -51,22 +52,22 @@ fun FoldersContent( .padding(16.dp) .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.Center, ) { Icon( - imageVector = Destination.Folders.icon, + imageVector = Icons.Filled.Folder, contentDescription = null, tint = MaterialTheme.colorScheme.primary, ) Text( text = "Coming soon", color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.displaySmall + style = MaterialTheme.typography.displaySmall, ) Text( text = "Here you'll be able to browse folders on this device", color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.bodyMedium + style = MaterialTheme.typography.bodyMedium, ) } } @@ -75,13 +76,13 @@ fun FoldersContent( name = "Folders", showSystemUi = true, showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_NO + uiMode = Configuration.UI_MODE_NIGHT_NO, ) @Preview( name = "Folders · DARK", showSystemUi = true, showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_YES + uiMode = Configuration.UI_MODE_NIGHT_YES, ) @Composable internal fun PreviewAlbumContent() { diff --git a/ui/folders/src/main/kotlin/photos/network/ui/folders/Module.kt b/ui/folders/src/main/kotlin/photos/network/ui/folders/Module.kt new file mode 100644 index 0000000..c053360 --- /dev/null +++ b/ui/folders/src/main/kotlin/photos/network/ui/folders/Module.kt @@ -0,0 +1,8 @@ +package photos.network.ui.folders + +import org.koin.dsl.module + +val uiFoldersModule = module { +// viewModel { +// } +} diff --git a/ui/photos/build.gradle.kts b/ui/photos/build.gradle.kts new file mode 100644 index 0000000..59e3577 --- /dev/null +++ b/ui/photos/build.gradle.kts @@ -0,0 +1,63 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.ui.photos" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi" + freeCompilerArgs = freeCompilerArgs + "-opt-in=androidx.compose.material.ExperimentalMaterialApi" + } + + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() + } +} + +dependencies { + implementation(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + api(projects.domain.photos) + implementation(projects.ui.common) +// implementation(projects.repository.photos) + + // Compose + implementation(platform(libs.compose.bom)) + implementation(libs.activity.compose) + + // accompanist + implementation(libs.bundles.accompanist) +} diff --git a/app/src/androidTest/kotlin/photos/network/PhotosNetworkTestUtils.kt b/ui/photos/src/androidTest/kotlin/photos/network/PhotosNetworkTestUtils.kt similarity index 100% rename from app/src/androidTest/kotlin/photos/network/PhotosNetworkTestUtils.kt rename to ui/photos/src/androidTest/kotlin/photos/network/PhotosNetworkTestUtils.kt diff --git a/app/src/androidTest/kotlin/photos/network/home/photos/PhotosScreenTests.kt b/ui/photos/src/androidTest/kotlin/photos/network/ui/photos/PhotosScreenTests.kt similarity index 93% rename from app/src/androidTest/kotlin/photos/network/home/photos/PhotosScreenTests.kt rename to ui/photos/src/androidTest/kotlin/photos/network/ui/photos/PhotosScreenTests.kt index 0dca119..bdad9ba 100644 --- a/app/src/androidTest/kotlin/photos/network/home/photos/PhotosScreenTests.kt +++ b/ui/photos/src/androidTest/kotlin/photos/network/ui/photos/PhotosScreenTests.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.home.photos +package photos.network.ui.photos import androidx.activity.compose.setContent import androidx.compose.ui.geometry.Offset @@ -27,7 +27,7 @@ import org.junit.Rule import org.junit.Test import photos.network.MainActivity import photos.network.generateTestPhoto -import photos.network.theme.AppTheme +import photos.network.ui.common.theme.AppTheme class PhotosScreenTests { @get:Rule @@ -72,7 +72,7 @@ class PhotosScreenTests { AppTheme { PhotosContent( uiState = uiState, - handleEvent = eventHandler + handleEvent = eventHandler, ) } } @@ -106,7 +106,10 @@ class PhotosScreenTests { // when composeTestRule.activity.setContent { AppTheme { - PhotosContent(uiState = uiState, handleEvent = eventHandler) + PhotosContent( + uiState = uiState, + handleEvent = eventHandler, + ) } } composeTestRule.onNodeWithTag("PHOTO_DETAILS").performTouchInput { diff --git a/ui/photos/src/main/AndroidManifest.xml b/ui/photos/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/ui/photos/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/Module.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/Module.kt new file mode 100644 index 0000000..7e31580 --- /dev/null +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/Module.kt @@ -0,0 +1,13 @@ +package photos.network.ui.photos + +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val uiPhotosModule = module { + viewModel { + PhotosViewModel( + getPhotosUseCase = get(), + startPhotosSyncUseCase = get(), + ) + } +} diff --git a/app/src/main/kotlin/photos/network/ui/PhotoBottomIcons.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoBottomIcons.kt similarity index 91% rename from app/src/main/kotlin/photos/network/ui/PhotoBottomIcons.kt rename to ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoBottomIcons.kt index e2de88a..314304c 100644 --- a/app/src/main/kotlin/photos/network/ui/PhotoBottomIcons.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoBottomIcons.kt @@ -18,11 +18,11 @@ package photos.network.ui import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import photos.network.theme.AppTheme +import photos.network.ui.common.theme.AppTheme @Composable fun PhotoBottomIcons( - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { } diff --git a/app/src/main/kotlin/photos/network/home/photos/PhotoDetails.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoDetails.kt similarity index 92% rename from app/src/main/kotlin/photos/network/home/photos/PhotoDetails.kt rename to ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoDetails.kt index 0e4fb74..6bfc531 100644 --- a/app/src/main/kotlin/photos/network/home/photos/PhotoDetails.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoDetails.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.home.photos +package photos.network.ui.photos import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -29,8 +29,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import coil.compose.rememberImagePainter -import photos.network.R -import photos.network.data.photos.repository.Photo +import photos.network.repository.photos.Photo import photos.network.ui.PhotoBottomIcons import photos.network.ui.PhotoTopIcons @@ -61,7 +60,7 @@ fun PhotoDetails( true } false - } + }, ) Box(modifier = modifier) { @@ -71,7 +70,7 @@ fun PhotoDetails( state = swipeableState, anchors = mapOf(0f to 0, 1f to 1), thresholds = { _, _ -> FractionalThreshold(0.3f) }, - orientation = Orientation.Horizontal + orientation = Orientation.Horizontal, ) .fillMaxSize(), painter = rememberImagePainter( @@ -79,7 +78,7 @@ fun PhotoDetails( builder = { crossfade(true) placeholder(R.drawable.image_placeholder) - } + }, ), contentDescription = null, ) @@ -89,7 +88,7 @@ fun PhotoDetails( .background(Color.White) .fillMaxWidth() .align(Alignment.TopStart), - onBackPressed = { onSelectItem(null) } + onBackPressed = { onSelectItem(null) }, ) PhotoBottomIcons( diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt new file mode 100644 index 0000000..7bfa1eb --- /dev/null +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt @@ -0,0 +1,184 @@ +/* + * Copyright 2020-2022 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.ui.photos + +import android.icu.text.DateFormatSymbols +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.rememberImagePainter +import photos.network.repository.photos.Photo +import photos.network.ui.common.theme.AppTheme +import photos.network.ui.photos.PhotoDetails +import java.time.Instant +import java.time.ZoneOffset + +@Composable +fun PhotoGrid( + modifier: Modifier = Modifier, + photos: List, + selectedIndex: Int? = null, + selectedPhoto: Photo? = null, + onSelectItem: (index: Int?) -> Unit, + selectPreviousPhoto: () -> Unit = {}, + selectNextPhoto: () -> Unit = {}, +) { + val lazyListState = rememberLazyGridState() + + if (photos.isEmpty()) { + Text( + modifier = Modifier.testTag("LOADING_SPINNER"), + text = "There are no photos to show right now.", + ) + } else { + // TODO: add fast-scroll + + Box { + LazyVerticalGrid( + state = lazyListState, + modifier = modifier + .fillMaxSize() + .padding(4.dp), + columns = GridCells.Adaptive(90.dp), + ) { + // group by year + val groupedByYear = photos.groupBy { + it.dateAdded.atZone(ZoneOffset.UTC).year + } + + groupedByYear.forEach { (_, photos) -> + val yearOfFirst = photos[0].dateAdded.atZone(ZoneOffset.UTC).year + val yearNow = Instant.now().atZone(ZoneOffset.UTC).year + + // add year header if necessary + if (yearOfFirst != yearNow) { + + item(span = { GridItemSpan(maxCurrentLineSpan) }) { + Text( + text = yearOfFirst.toString(), + style = MaterialTheme.typography.bodyMedium, + ) + } + } + + // group by month + val groupedByMonth = photos.groupBy { + it.dateAdded.atZone(ZoneOffset.UTC).month + } + + groupedByMonth.forEach { (month, photos) -> + // add year if not matching with current year + val title = if (yearOfFirst == yearNow) { + DateFormatSymbols().months[month.value - 1] + } else { + "${DateFormatSymbols().months[month.value - 1]} $yearOfFirst" + } + + // month header + item(span = { GridItemSpan(maxCurrentLineSpan) }) { + Text(text = title, style = MaterialTheme.typography.bodyLarge) + } + + items(photos.size) { index: Int -> + // TODO: show always local uri? + val data = if (photos[index].uri != null) { + photos[index].uri + } else { + photos[index].imageUrl + } + + Box( + modifier = Modifier + .aspectRatio(1.0f) + .size(128.dp) + .clip(RoundedCornerShape(2.dp)) + .clickable { + onSelectItem(index) + }, + ) { + Image( + painter = rememberImagePainter( + data = data, + builder = { + crossfade(true) + placeholder(R.drawable.image_placeholder) + }, + ), + contentDescription = null, + contentScale = ContentScale.None, + modifier = Modifier.padding(1.dp), + ) + } + } + } + } + } + + if (selectedPhoto != null) { + PhotoDetails( + modifier = Modifier + .testTag("PHOTO_DETAILS") + .background(Color.Black.copy(alpha = 0.9f)) + .fillMaxSize(), + selectedIndex = selectedIndex, + selectNextPhoto = selectNextPhoto, + selectPreviousPhoto = selectPreviousPhoto, + selectedPhoto = selectedPhoto, + onSelectItem = onSelectItem, + ) + } + } + } +} + +@Preview +@Composable +internal fun PreviewPhotoGrid() { + val list = (0..15).map { + Photo( + filename = it.toString(), + imageUrl = "", + dateAdded = Instant.parse("2022-01-01T13:37:00.123Z"), + dateTaken = Instant.parse("2022-01-01T13:37:00.123Z"), + ) + } + AppTheme { + PhotoGrid( + photos = list, + onSelectItem = {}, + ) + } +} diff --git a/app/src/main/kotlin/photos/network/ui/PhotoTopIcons.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoTopIcons.kt similarity index 93% rename from app/src/main/kotlin/photos/network/ui/PhotoTopIcons.kt rename to ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoTopIcons.kt index 5f0e285..08b7440 100644 --- a/app/src/main/kotlin/photos/network/ui/PhotoTopIcons.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoTopIcons.kt @@ -23,7 +23,7 @@ import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import photos.network.theme.AppTheme +import photos.network.ui.common.theme.AppTheme @Composable fun PhotoTopIcons( @@ -32,7 +32,7 @@ fun PhotoTopIcons( ) { Row(modifier = modifier) { IconButton( - onClick = { onBackPressed() } + onClick = { onBackPressed() }, ) { Icon(Icons.Filled.ArrowBack, null) } diff --git a/app/src/main/kotlin/photos/network/home/photos/PhotosEvent.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosEvent.kt similarity index 95% rename from app/src/main/kotlin/photos/network/home/photos/PhotosEvent.kt rename to ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosEvent.kt index 898ea11..df32d3d 100644 --- a/app/src/main/kotlin/photos/network/home/photos/PhotosEvent.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosEvent.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.home.photos +package photos.network.ui.photos sealed interface PhotosEvent { object StartLocalPhotoSyncEvent : PhotosEvent diff --git a/app/src/main/kotlin/photos/network/home/photos/PhotosScreen.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosScreen.kt similarity index 88% rename from app/src/main/kotlin/photos/network/home/photos/PhotosScreen.kt rename to ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosScreen.kt index df9673d..a136bfb 100644 --- a/app/src/main/kotlin/photos/network/home/photos/PhotosScreen.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosScreen.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.home.photos +package photos.network.ui.photos import android.content.Context import android.content.Intent @@ -33,8 +33,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner @@ -52,10 +50,7 @@ import androidx.navigation.compose.rememberNavController import com.google.accompanist.permissions.PermissionStatus import com.google.accompanist.permissions.rememberPermissionState import org.koin.androidx.compose.getViewModel -import photos.network.data.photos.repository.Photo -import photos.network.theme.AppTheme -import photos.network.ui.PhotoGrid -import java.time.Instant +import photos.network.ui.common.theme.AppTheme @Composable fun PhotosScreen( @@ -76,7 +71,7 @@ fun PhotosScreen( Text( modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, - text = "To show images stored on this device, the permission to read external storage is mandatory. Please grant the permission." + text = "To show images stored on this device, the permission to read external storage is mandatory. Please grant the permission.", ) Spacer(modifier = Modifier.height(8.dp)) Button(onClick = { permissionState.launchPermissionRequest() }) { @@ -101,7 +96,7 @@ fun PhotosScreen( private fun navigateToPermissionSettings(context: Context) { val intent = Intent( ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.parse("package:${context.packageName}") + Uri.parse("package:${context.packageName}"), ).apply { addCategory(Intent.CATEGORY_DEFAULT) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) @@ -141,7 +136,7 @@ fun PhotosContent( if (uiState.isLoading) { Text( modifier = Modifier.testTag("LOADING_SPINNER"), - text = "Loading" + text = "Loading", ) } @@ -158,22 +153,21 @@ fun PhotosContent( }, selectPreviousPhoto = { handleEvent(PhotosEvent.SelectPreviousPhoto) - } + }, ) - // TODO: add fast-scroll } @Preview( "Photos", showSystemUi = true, showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_YES + uiMode = Configuration.UI_MODE_NIGHT_YES, ) @Preview( "Photos • Dark", showSystemUi = true, showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_NO + uiMode = Configuration.UI_MODE_NIGHT_NO, ) @Composable private fun PreviewDashboard( @@ -182,7 +176,7 @@ private fun PreviewDashboard( AppTheme { PhotosContent( uiState = uiState, - handleEvent = {} + handleEvent = {}, ) } } @@ -192,16 +186,16 @@ internal class PreviewPhotosProvider : PreviewParameterProvider { PhotosUiState(photos = emptyList(), isLoading = true, hasError = false), PhotosUiState(photos = emptyList(), isLoading = false, hasError = true), PhotosUiState( - photos = listOf( - Photo( - filename = "0L", - imageUrl = "", - dateAdded = Instant.parse("2022-01-01T13:37:00.123Z"), - dateTaken = Instant.parse("2022-01-01T13:37:00.123Z") - ) - ), +// photos = listOf( +// PhotoElement( +// filename = "0L", +// imageUrl = "", +// dateAdded = Instant.parse("2022-01-01T13:37:00.123Z"), +// dateTaken = Instant.parse("2022-01-01T13:37:00.123Z"), +// ), +// ), isLoading = false, - hasError = false + hasError = false, ), ) override val count: Int = values.count() diff --git a/app/src/main/kotlin/photos/network/home/photos/PhotosUiState.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosUiState.kt similarity index 90% rename from app/src/main/kotlin/photos/network/home/photos/PhotosUiState.kt rename to ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosUiState.kt index 7fc3f9a..c76fb92 100644 --- a/app/src/main/kotlin/photos/network/home/photos/PhotosUiState.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosUiState.kt @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.home.photos +package photos.network.ui.photos -import photos.network.data.photos.repository.Photo +import photos.network.repository.photos.Photo data class PhotosUiState( val photos: List = emptyList(), diff --git a/app/src/main/kotlin/photos/network/home/photos/PhotosViewModel.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosViewModel.kt similarity index 91% rename from app/src/main/kotlin/photos/network/home/photos/PhotosViewModel.kt rename to ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosViewModel.kt index a8a2ee6..dfe4741 100644 --- a/app/src/main/kotlin/photos/network/home/photos/PhotosViewModel.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosViewModel.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.home.photos +package photos.network.ui.photos import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -52,7 +52,7 @@ class PhotosViewModel( uiState.update { it.copy( photos = photos, - isLoading = false + isLoading = false, ) } } @@ -60,7 +60,9 @@ class PhotosViewModel( } private fun startLocalPhotoSync() { - startPhotosSyncUseCase() + viewModelScope.launch(Dispatchers.IO) { + startPhotosSyncUseCase() + } } private fun selectPreviousPhoto() { @@ -74,7 +76,7 @@ class PhotosViewModel( uiState.update { it.copy( selectedPhoto = photo, - selectedIndex = newIndex + selectedIndex = newIndex, ) } } @@ -94,7 +96,7 @@ class PhotosViewModel( uiState.update { it.copy( selectedPhoto = photo, - selectedIndex = newIndex + selectedIndex = newIndex, ) } } @@ -114,7 +116,7 @@ class PhotosViewModel( uiState.update { it.copy( selectedPhoto = photo, - selectedIndex = index + selectedIndex = index, ) } } diff --git a/app/src/main/kotlin/photos/network/ui/Tag.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/Tag.kt similarity index 89% rename from app/src/main/kotlin/photos/network/ui/Tag.kt rename to ui/photos/src/main/kotlin/photos/network/ui/photos/Tag.kt index 371a9e6..cf36cd1 100644 --- a/app/src/main/kotlin/photos/network/ui/Tag.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/Tag.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.ui +package photos.network.ui.photos import android.content.res.Configuration import androidx.compose.foundation.background @@ -33,8 +33,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import photos.network.R -import photos.network.theme.AppTheme +import photos.network.ui.common.theme.AppTheme /** * Tag used for categories etc. @@ -42,17 +41,17 @@ import photos.network.theme.AppTheme @Composable fun Tag( tag: String, - onClickTag: (String) -> Unit + onClickTag: (String) -> Unit, ) { Row( modifier = Modifier .background( color = MaterialTheme.colorScheme.secondaryContainer, - shape = RoundedCornerShape(topStartPercent = 50, bottomStartPercent = 1) + shape = RoundedCornerShape(topStartPercent = 50, bottomStartPercent = 1), ) .clip(RoundedCornerShape(topStartPercent = 50)) .clickable(onClick = { onClickTag(tag) }) - .padding(horizontal = 8.dp) + .padding(horizontal = 8.dp), ) { Icon( @@ -60,13 +59,13 @@ fun Tag( .size(24.dp) .padding(vertical = 4.dp), imageVector = Icons.Default.Bookmarks, - contentDescription = stringResource(id = R.string.icon_tags) + contentDescription = stringResource(id = R.string.icon_tags), ) Text( modifier = Modifier.padding(top = 4.dp, bottom = 4.dp), text = tag, fontSize = MaterialTheme.typography.labelSmall.fontSize, - color = MaterialTheme.colorScheme.onSecondary + color = MaterialTheme.colorScheme.onSecondary, ) } } @@ -78,7 +77,7 @@ fun TagPreview() { AppTheme { Tag( tag = "Landscape", - onClickTag = {} + onClickTag = {}, ) } } diff --git a/app/src/main/kotlin/photos/network/ui/TagLines.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/TagLines.kt similarity index 90% rename from app/src/main/kotlin/photos/network/ui/TagLines.kt rename to ui/photos/src/main/kotlin/photos/network/ui/photos/TagLines.kt index 0a6496f..c7a2c55 100644 --- a/app/src/main/kotlin/photos/network/ui/TagLines.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/TagLines.kt @@ -23,7 +23,8 @@ import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import photos.network.theme.AppTheme +import photos.network.ui.common.theme.AppTheme +import photos.network.ui.photos.Tag @Preview(name = "Tags") @Preview(name = "Tags · DARK", uiMode = Configuration.UI_MODE_NIGHT_YES) @@ -32,7 +33,7 @@ fun TagLinePreview() { AppTheme { TagLines( tags = listOf("Landscape", "Architectur"), - onClickTag = {} + onClickTag = {}, ) } } @@ -40,7 +41,7 @@ fun TagLinePreview() { @Composable fun TagLines( tags: List, - onClickTag: (String) -> Unit = {} + onClickTag: (String) -> Unit = {}, ) { LazyRow { items(tags) { tag -> diff --git a/app/src/main/res/drawable/image_placeholder.xml b/ui/photos/src/main/res/drawable/image_placeholder.xml similarity index 100% rename from app/src/main/res/drawable/image_placeholder.xml rename to ui/photos/src/main/res/drawable/image_placeholder.xml diff --git a/ui/photos/src/main/res/values/strings.xml b/ui/photos/src/main/res/values/strings.xml new file mode 100644 index 0000000..e1c1763 --- /dev/null +++ b/ui/photos/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + tags icon + diff --git a/app/src/test/kotlin/photos/network/home/photos/PhotosViewModelTests.kt b/ui/photos/src/test/kotlin/photos/network/ui/photos/photos/PhotosViewModelTests.kt similarity index 87% rename from app/src/test/kotlin/photos/network/home/photos/PhotosViewModelTests.kt rename to ui/photos/src/test/kotlin/photos/network/ui/photos/photos/PhotosViewModelTests.kt index 0f8fdf5..49da1c8 100644 --- a/app/src/test/kotlin/photos/network/home/photos/PhotosViewModelTests.kt +++ b/ui/photos/src/test/kotlin/photos/network/ui/photos/photos/PhotosViewModelTests.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.home.photos +package photos.network.ui.photos.photos import com.google.common.truth.Truth import io.mockk.every @@ -37,9 +37,9 @@ class PhotosViewModelTests { private val getPhotosUseCase = mockk() private val startPhotosSyncUseCase = mockk() private val viewmodel by lazy { - PhotosViewModel( + photos.network.ui.photos.PhotosViewModel( getPhotosUseCase = getPhotosUseCase, - startPhotosSyncUseCase = startPhotosSyncUseCase + startPhotosSyncUseCase = startPhotosSyncUseCase, ) } private val photo1 = Photo( @@ -48,7 +48,7 @@ class PhotosViewModelTests { dateTaken = Instant.parse("2022-02-02T20:20:20Z"), dateAdded = Instant.parse("2022-02-02T20:20:20Z"), uri = null, - isPrivate = true + isPrivate = true, ) private val photo2 = Photo( filename = "filename2", @@ -56,7 +56,7 @@ class PhotosViewModelTests { dateTaken = Instant.parse("2022-02-02T21:21:20Z"), dateAdded = Instant.parse("2022-02-02T21:21:20Z"), uri = null, - isPrivate = false + isPrivate = false, ) @Before @@ -79,11 +79,11 @@ class PhotosViewModelTests { // then Truth.assertThat(viewmodel.uiState.value).isEqualTo( - PhotosUiState( + photos.network.ui.photos.PhotosUiState( photos = listOf(photo1, photo2), isLoading = false, - hasError = false - ) + hasError = false, + ), ) } @@ -94,7 +94,7 @@ class PhotosViewModelTests { every { getPhotosUseCase() } answers { flowOf(emptyList()) } // when - viewmodel.handleEvent(PhotosEvent.StartLocalPhotoSyncEvent) + viewmodel.handleEvent(photos.network.ui.photos.PhotosEvent.StartLocalPhotoSyncEvent) // then verify(atLeast = 1) { startPhotosSyncUseCase.invoke() } diff --git a/ui/settings/build.gradle.kts b/ui/settings/build.gradle.kts new file mode 100644 index 0000000..45fb1ff --- /dev/null +++ b/ui/settings/build.gradle.kts @@ -0,0 +1,61 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.ui.settings" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() + } +} + +dependencies { + implementation(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + api(projects.ui.common) + api(projects.domain.settings) + + // Compose + implementation(platform(libs.compose.bom)) + implementation(libs.activity.compose) + + // accompanist + implementation(libs.bundles.accompanist) + +} diff --git a/app/src/androidTest/kotlin/photos/network/settings/SettingsScreenTests.kt b/ui/settings/src/androidTest/kotlin/photos/network/ui/settings/SettingsScreenTests.kt similarity index 97% rename from app/src/androidTest/kotlin/photos/network/settings/SettingsScreenTests.kt rename to ui/settings/src/androidTest/kotlin/photos/network/ui/settings/SettingsScreenTests.kt index 833a1cf..870c85f 100644 --- a/app/src/androidTest/kotlin/photos/network/settings/SettingsScreenTests.kt +++ b/ui/settings/src/androidTest/kotlin/photos/network/ui/settings/SettingsScreenTests.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.settings +package photos.network.ui.settings import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule diff --git a/ui/settings/src/main/AndroidManifest.xml b/ui/settings/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/ui/settings/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/ui/settings/src/main/kotlin/photos/network/ui/settings/Module.kt b/ui/settings/src/main/kotlin/photos/network/ui/settings/Module.kt new file mode 100644 index 0000000..9e028d3 --- /dev/null +++ b/ui/settings/src/main/kotlin/photos/network/ui/settings/Module.kt @@ -0,0 +1,17 @@ +package photos.network.ui.settings + +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val uiSettingsModule = module { + viewModel { + SettingsViewModel( + application = get(), + getSettingsUseCase = get(), + updateHostUseCase = get(), + updateClientIdUseCase = get(), + verifyServerHostUseCase = get(), + verifyClientIdUseCase = get(), + ) + } +} diff --git a/app/src/main/kotlin/photos/network/settings/SettingsEvent.kt b/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsEvent.kt similarity index 96% rename from app/src/main/kotlin/photos/network/settings/SettingsEvent.kt rename to ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsEvent.kt index 6066cbd..c8cf833 100644 --- a/app/src/main/kotlin/photos/network/settings/SettingsEvent.kt +++ b/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsEvent.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.settings +package photos.network.ui.settings sealed class SettingsEvent { object ToggleServerSetup : SettingsEvent() diff --git a/app/src/main/kotlin/photos/network/settings/SettingsScreen.kt b/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsScreen.kt similarity index 89% rename from app/src/main/kotlin/photos/network/settings/SettingsScreen.kt rename to ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsScreen.kt index 9243b09..ce6417c 100644 --- a/app/src/main/kotlin/photos/network/settings/SettingsScreen.kt +++ b/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsScreen.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.settings +package photos.network.ui.settings import android.content.res.Configuration import android.widget.Toast @@ -65,10 +65,10 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import org.koin.androidx.compose.getViewModel -import photos.network.R -import photos.network.navigation.Destination -import photos.network.theme.AppTheme -import photos.network.ui.components.AppLogo +import photos.network.network.ServerStatus +import photos.network.ui.common.components.AppLogo +import photos.network.ui.common.navigation.Destination +import photos.network.ui.common.theme.AppTheme @Composable fun SettingsScreen( @@ -81,7 +81,7 @@ fun SettingsScreen( modifier = modifier, uiState = viewmodel.uiState.collectAsState().value, handleEvent = viewmodel::handleEvent, - navigateToLogin = { navController.navigate(Destination.Login.route) } + navigateToLogin = { navController.navigate(Destination.Login.route) }, ) } @@ -90,16 +90,15 @@ fun SettingsContent( modifier: Modifier = Modifier, uiState: SettingsUiState, handleEvent: (event: SettingsEvent) -> Unit, - navigateToLogin: () -> Unit = {} + navigateToLogin: () -> Unit = {}, ) { val verticalScrollState = rememberScrollState(0) Column( modifier = modifier .verticalScroll(verticalScrollState) - .fillMaxSize() + .fillMaxSize(), ) { - SettingsHeader(serverStatus = uiState.serverStatus) ServerSetupItem( @@ -116,7 +115,7 @@ fun SettingsContent( onClientIdUpdated = { handleEvent(SettingsEvent.ClientIdChanged(it)) }, - isClientIdVerified = uiState.isClientVerified + isClientIdVerified = uiState.isClientVerified, ) Divider() @@ -135,6 +134,7 @@ fun SettingsContent( } } +@Suppress("MagicNumber") @Composable internal fun SettingsHeader( modifier: Modifier = Modifier, @@ -142,7 +142,7 @@ internal fun SettingsHeader( ) { // header + icon Box( - modifier = modifier.background(MaterialTheme.colorScheme.surface) + modifier = modifier.background(MaterialTheme.colorScheme.surface), ) { // header gradient Box( @@ -154,10 +154,10 @@ internal fun SettingsHeader( brush = Brush.verticalGradient( colors = listOf( Color(0x55000000), - Color(0x00000000) - ) - ) - ) + Color(0x00000000), + ), + ), + ), ) // app name @@ -169,7 +169,7 @@ internal fun SettingsHeader( text = stringResource(id = R.string.app_name_full), style = MaterialTheme.typography.headlineLarge, textAlign = TextAlign.Center, - color = Color.White + color = Color.White, ) // logo with status indicator @@ -206,28 +206,28 @@ fun ServerSetupItem( Surface( modifier = modifier .clickable( - onClickLabel = serverSetupLabel + onClickLabel = serverSetupLabel, ) { onServerSetupClicked() - } + }, ) { Row( modifier = Modifier - .padding(16.dp) + .padding(16.dp), ) { Text( modifier = Modifier.weight(1f), - text = serverSetupLabel + text = serverSetupLabel, ) if (isExpanded) { Icon( imageVector = Icons.Default.KeyboardArrowDown, - contentDescription = null + contentDescription = null, ) } else { Icon( imageVector = Icons.Default.KeyboardArrowRight, - contentDescription = null + contentDescription = null, ) } } @@ -248,7 +248,7 @@ fun ServerSetupItem( onValueChanged = { onServerHostUpdated(it) }, - showTrailingIcon = isHostVerified + showTrailingIcon = isHostVerified, ) } @@ -265,7 +265,7 @@ fun ServerSetupItem( onValueChanged = { onClientIdUpdated(it) }, - showTrailingIcon = isClientIdVerified + showTrailingIcon = isClientIdVerified, ) } } @@ -278,7 +278,7 @@ fun FormInput( value: String = "", onValueChanged: (String) -> Unit = {}, hint: String = "", - showTrailingIcon: Boolean = false + showTrailingIcon: Boolean = false, ) { Surface(modifier = modifier) { var text by remember { mutableStateOf(value) } @@ -307,7 +307,7 @@ fun FormInput( Icon( imageVector = Icons.Default.Check, tint = Color(0xFF4CAF50), - contentDescription = null + contentDescription = null, ) } }, @@ -320,14 +320,14 @@ fun FormInput( @Composable fun SectionSpacer( - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Box( modifier = modifier .height(48.dp) .background( - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.12f) - ) + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.12f), + ), ) } @@ -345,16 +345,16 @@ fun AppVersionItem( Toast .makeText(context, R.string.settings_copied_to_clipboard, Toast.LENGTH_SHORT) .show() - } + }, ) { Row( modifier = Modifier .semantics(mergeDescendants = true) {} - .padding(16.dp) + .padding(16.dp), ) { Text( modifier = Modifier.weight(1f), - text = "App Version" + text = "App Version", ) Text( text = version, @@ -377,18 +377,18 @@ fun AccountSetupItem( Surface( modifier = modifier .clickable( - onClickLabel = clickLabel + onClickLabel = clickLabel, ) { onAccountSetupClicked() - } + }, ) { Row( modifier = Modifier - .padding(16.dp) + .padding(16.dp), ) { Text( modifier = Modifier.weight(1f), - text = clickLabel + text = clickLabel, ) } } @@ -398,13 +398,13 @@ fun AccountSetupItem( "Account", showSystemUi = true, showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_NO + uiMode = Configuration.UI_MODE_NIGHT_NO, ) @Preview( "Account • Dark", showSystemUi = true, showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_YES + uiMode = Configuration.UI_MODE_NIGHT_YES, ) @Composable private fun PreviewAccount( @@ -413,7 +413,7 @@ private fun PreviewAccount( AppTheme { SettingsContent( uiState = uiState, - handleEvent = {} + handleEvent = {}, ) } } @@ -423,7 +423,7 @@ internal class PreviewAccountProvider : PreviewParameterProvider + Setup server instance + Change server setup + Version copied into clipboard + diff --git a/ui/sharing/build.gradle.kts b/ui/sharing/build.gradle.kts new file mode 100644 index 0000000..12e9f8e --- /dev/null +++ b/ui/sharing/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.ui.sharing" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() + } +} + +dependencies { + implementation(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + api(projects.ui.common) + api(projects.domain.sharing) + api(projects.domain.settings) + + // Compose + implementation(platform(libs.compose.bom)) + implementation(libs.activity.compose) + + // accompanist + implementation(libs.bundles.accompanist) + +} diff --git a/ui/sharing/src/main/AndroidManifest.xml b/ui/sharing/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/ui/sharing/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/ui/sharing/src/main/kotlin/photos/network/ui/sharing/Module.kt b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/Module.kt new file mode 100644 index 0000000..1d35bdd --- /dev/null +++ b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/Module.kt @@ -0,0 +1,14 @@ +package photos.network.ui.sharing + +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module +import photos.network.ui.sharing.login.LoginViewModel + +val uiSharingModule = module { + viewModel { + LoginViewModel( + requestAccessTokenUseCase = get(), + settingsUseCase = get(), + ) + } +} diff --git a/app/src/main/kotlin/photos/network/presentation/login/LoginEvent.kt b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginEvent.kt similarity index 94% rename from app/src/main/kotlin/photos/network/presentation/login/LoginEvent.kt rename to ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginEvent.kt index 360e071..73d84b6 100644 --- a/app/src/main/kotlin/photos/network/presentation/login/LoginEvent.kt +++ b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginEvent.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.presentation.login +package photos.network.ui.sharing.login sealed class LoginEvent { class VerifyAuthCode(val authCode: String) : LoginEvent() diff --git a/app/src/main/kotlin/photos/network/presentation/login/LoginScreen.kt b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginScreen.kt similarity index 93% rename from app/src/main/kotlin/photos/network/presentation/login/LoginScreen.kt rename to ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginScreen.kt index f80a2d4..a8b9e3d 100644 --- a/app/src/main/kotlin/photos/network/presentation/login/LoginScreen.kt +++ b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginScreen.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.presentation.login +package photos.network.ui.sharing.login import android.webkit.WebChromeClient import android.webkit.WebResourceRequest @@ -34,7 +34,7 @@ import androidx.navigation.compose.rememberNavController import logcat.LogPriority import logcat.logcat import org.koin.androidx.compose.viewModel -import photos.network.navigation.Destination +import photos.network.ui.common.navigation.Destination /** * app screen to enter user credentials to authenticate @@ -57,7 +57,7 @@ fun LoginScreen( inclusive = true } } - } + }, ) } @@ -66,7 +66,7 @@ fun LoginScreen( modifier: Modifier = Modifier, uiState: LoginUiState, handleEvent: (event: LoginEvent) -> Unit, - navigateToHome: () -> Unit = {} + navigateToHome: () -> Unit = {}, ) { if (uiState.loginSucceded) { navigateToHome() @@ -74,13 +74,13 @@ fun LoginScreen( Column( modifier = modifier.padding(8.dp), verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { AndroidView( factory = { viewBlockContext -> WebView(viewBlockContext) }, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) { webView -> val redirectUri = "photosapp://authenticate" @@ -88,7 +88,7 @@ fun LoginScreen( webViewClient = object : WebViewClient() { override fun shouldOverrideUrlLoading( view: WebView?, - request: WebResourceRequest? + request: WebResourceRequest?, ): Boolean { request?.let { logcat(LogPriority.ERROR) { "url=$redirectUri" } diff --git a/app/src/main/kotlin/photos/network/presentation/login/LoginUiState.kt b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginUiState.kt similarity index 94% rename from app/src/main/kotlin/photos/network/presentation/login/LoginUiState.kt rename to ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginUiState.kt index d323b14..f982a6e 100644 --- a/app/src/main/kotlin/photos/network/presentation/login/LoginUiState.kt +++ b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginUiState.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.presentation.login +package photos.network.ui.sharing.login data class LoginUiState( val loginSucceded: Boolean = false, diff --git a/app/src/main/kotlin/photos/network/presentation/login/LoginViewModel.kt b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginViewModel.kt similarity index 91% rename from app/src/main/kotlin/photos/network/presentation/login/LoginViewModel.kt rename to ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginViewModel.kt index f1778d6..8083d86 100644 --- a/app/src/main/kotlin/photos/network/presentation/login/LoginViewModel.kt +++ b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginViewModel.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.presentation.login +package photos.network.ui.sharing.login import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -25,7 +25,7 @@ import kotlinx.coroutines.withContext import logcat.LogPriority import logcat.logcat import photos.network.domain.settings.usecase.GetSettingsUseCase -import photos.network.domain.user.usecase.RequestAccessTokenUseCase +import photos.network.domain.sharing.usecase.RequestAccessTokenUseCase class LoginViewModel( private val requestAccessTokenUseCase: RequestAccessTokenUseCase, @@ -44,7 +44,7 @@ class LoginViewModel( uiState.update { it.copy( host = it.host, - clientId = it.clientId + clientId = it.clientId, ) } } @@ -67,6 +67,7 @@ class LoginViewModel( /** * generate random nonce for EACH request to prevent replay attacs */ + @Suppress("MagicNumber") private fun generateRandomNonce() { viewModelScope.launch(Dispatchers.IO) { val chars = ('a'..'z') + ('A'..'Z') + ('0'..'9') @@ -78,7 +79,7 @@ class LoginViewModel( withContext(Dispatchers.Main) { uiState.update { it.copy( - nonce = tmpNonce + nonce = tmpNonce, ) } } @@ -91,7 +92,7 @@ class LoginViewModel( withContext(Dispatchers.Main) { uiState.update { it.copy( - loginSucceded = true + loginSucceded = true, ) } } diff --git a/versions.properties b/versions.properties deleted file mode 100644 index c2d5262..0000000 --- a/versions.properties +++ /dev/null @@ -1,134 +0,0 @@ -#### Dependencies and Plugin versions with their available updates. -#### Generated by `./gradlew refreshVersions` version 0.50.1 -#### -#### Don't manually edit or split the comments that start with four hashtags (####), -#### they will be overwritten by refreshVersions. -#### -#### suppress inspection "SpellCheckingInspection" for whole file -#### suppress inspection "UnusedProperty" for whole file -#### -#### NOTE: Some versions are filtered by the rejectVersionsIf predicate. See the settings.gradle.kts file. - -plugin.android=7.0.4 -## # available=7.1.0 -## # available=7.1.1 -## # available=7.1.2 -## # available=7.1.3 -## # available=7.2.0 -## # available=7.2.1 -## # available=7.2.2 - -plugin.com.diffplug.spotless=6.7.0 -## # available=6.7.1 -## # available=6.7.2 -## # available=6.8.0 -## # available=6.9.0 -## # available=6.9.1 -## # available=6.10.0 - -plugin.com.github.triplet.play=3.7.0 - -plugin.io.gitlab.arturbosch.detekt=1.21.0 - -plugin.org.ajoberstar.grgit=5.0.0 - -version.androidx.compose.compiler=1.2.0 -## # available=1.3.0 -## # available=1.3.1 - -version.androidx.navigation=2.5.2 - - version.koin=3.1.6 -### available=3.2.0 - -version.retrofit2=2.9.0 - -## unused -version.org.jacoco..org.jacoco.ant=0.8.7 - -## unused -version.org.jacoco..org.jacoco.agent=0.8.7 - -version.okhttp3=4.10.0 - -version.mockk=1.12.5 -### available=1.12.6 -### available=1.12.7 - -version.logcat=0.1 - -version.leakcanary=2.9.1 - - version.ktor=2.1.1 - -version.kotlinx.serialization=1.4.0 - -version.kotlinx.coroutines=1.6.4 - -version.junit.junit=4.13.2 - -version.jakewharton.retrofit2-kotlinx-serialization-converter=0.8.0 - -version.google.android.material=1.6.1 - -version.google.accompanist=0.25.1 - -version.com.google.truth..truth=1.1.3 - -## unused -version.com.github.triplet.gradle..play-publisher=3.6.0 - -version.coil-kt=2.2.1 - -version.androidx.work=2.7.1 - -version.androidx.test.services=1.4.1 - -version.androidx.test.runner=1.4.0 - -version.androidx.test.rules=1.4.0 - -version.androidx.test.orchestrator=1.4.1 - -version.androidx.test.monitor=1.5.0 - -version.androidx.test.ext.truth=1.4.0 - -version.androidx.test.ext.junit=1.1.3 - -version.androidx.test.core=1.4.0 - -version.androidx.security-crypto=1.1.0-alpha03 - -version.androidx.room=2.4.3 - -version.androidx.paging-compose=1.0.0-alpha14 - -version.androidx.paging=3.1.1 - -## unused -version.androidx.navigation-compose=2.5.2 - -version.androidx.lifecycle=2.5.1 - -version.androidx.exifinterface=1.3.3 - -version.androidx.core=1.8.0 -## # available=1.9.0 - -version.androidx.constraintlayout-compose=1.0.1 - -version.androidx.compose.ui=1.2.1 - -version.androidx.compose.runtime=1.2.1 - -version.androidx.compose.material3=1.0.0-alpha06 - -version.androidx.compose.material=1.2.1 - -version.androidx.arch.core=2.1.0 - -version.androidx.activity=1.5.1 - -version.kotlin=1.7.0 -## # available=1.7.10 From ef5e0b241d93de3563cdd1df259e5d03d338f8cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Stu=CC=88rmer?= Date: Fri, 5 May 2023 07:21:37 +0200 Subject: [PATCH 02/13] extract, split and move more layers --- .../continuous-delivery-pipeline.yml | 14 +- {network => api}/build.gradle.kts | 17 +- {network => api}/src/main/AndroidManifest.xml | 0 .../kotlin/photos/network/api/ApiModule.kt | 14 +- .../kotlin/photos/network/api/ServerStatus.kt | 23 ++ .../photos/network/api}/photo/PhotoApi.kt | 8 +- .../photos/network/api}/photo/PhotoApiImpl.kt | 7 +- .../photos/network/api}/photo/Photos.kt | 3 +- .../photos/network/api/photo/entity}/Photo.kt | 10 +- .../photos/network/api/status/StatusApi.kt | 31 +++ .../network/api/status/StatusApiImpl.kt | 34 +++ .../network/api/status/entity/Status.kt | 4 +- .../photos/network/api}/user/UserApi.kt | 4 +- .../photos/network/api}/user/UserApiImpl.kt | 10 +- .../network/api/user/entity}/NetworkUser.kt | 4 +- .../network/api/user/entity}/TokenInfo.kt | 2 +- .../photos/network/api/photo/PhotoApiTests.kt | 202 ++++++++++++++++++ .../network/api/photo}/entity/PhotoTest.kt | 31 +-- .../network/api/status/StatusApiTests.kt | 83 ++++--- .../photos/network/api/user/UserApiTests.kt | 166 ++++++++++++++ .../api/user/entity/NetworkUserTests.kt | 71 ++++++ .../network/api/user/entity/TokenInfoTests.kt | 47 ++++ app/build.gradle.kts | 3 + app/src/debug/AndroidManifest.xml | 5 + .../main/kotlin/photos/network/AppModule.kt | 4 - .../kotlin/photos/network/MainActivity.kt | 6 +- .../network/PhotosNetworkApplication.kt | 4 +- .../main/kotlin/photos/network/home/Home.kt | 37 +++- .../kotlin/photos/network/ui/UserAvatar.kt | 4 +- .../network/CurrentUserViewModelTests.kt | 2 +- common/build.gradle.kts | 3 +- .../kotlin/photos/network/common/Module.kt | 1 - database/photos/build.gradle.kts | 6 +- database/sharing/build.gradle.kts | 2 +- .../photos/network/domain/albums/Module.kt | 1 - .../photos/network/domain/folders/Module.kt | 1 - domain/photos/build.gradle.kts | 5 + .../photos/usecase/GetPhotoUseCaseTests.kt | 4 +- .../photos/usecase/GetPhotosUseCaseTests.kt | 10 +- .../usecase/StartPhotosSyncUseCaseTests.kt | 11 +- domain/settings/build.gradle.kts | 5 + .../settings/usecase/VerifyClientIdUseCase.kt | 4 +- .../usecase/VerifyServerHostUseCase.kt | 4 +- .../usecase/VerifyServerHostUseCaseTests.kt | 1 - domain/sharing/build.gradle.kts | 5 + .../photos/network/domain/sharing/Module.kt | 1 - gradle/libs.versions.toml | 8 +- .../kotlin/photos/network/api/PhotoApiTest.kt | 114 ---------- .../photos/network/entity/PhotosTest.kt | 75 ------- .../photos/network/network/ServerStatus.kt | 8 - repository/photos/build.gradle.kts | 74 +------ .../repository/photos/PhotoRepositoryImpl.kt | 2 +- .../repository/photos/PhotoRepositoryTest.kt | 46 ++-- repository/settings/build.gradle.kts | 6 +- repository/sharing/build.gradle.kts | 11 +- .../repository/sharing/UserRepositoryImpl.kt | 2 +- .../sharing}/UserRepositoryTests.kt | 14 +- run_tests | 7 +- settings.gradle.kts | 4 +- ui/albums/build.gradle.kts | 4 + .../kotlin/photos/network/ui/albums/Module.kt | 15 ++ ui/common/build.gradle.kts | 6 +- .../network/ui/common/components/AppLogo.kt | 2 +- .../ui/common/navigation/Destination.kt | 1 - .../src/main/res/values-ar/strings.xml | 0 .../src/main/res/values-fr/strings.xml | 0 .../src/main/res/values-ko-rKR/strings.xml | 0 .../src/main/res/values-ko/strings.xml | 0 ui/folders/build.gradle.kts | 4 + .../photos/network/ui/folders/Module.kt | 15 ++ ui/photos/build.gradle.kts | 10 +- .../kotlin/photos/network/ui/photos/Module.kt | 15 ++ .../photos/network/ui/photos/PhotoGrid.kt | 2 - .../main/res/drawable/image_placeholder.xml | 4 +- ui/settings/build.gradle.kts | 4 + .../photos/network/ui/settings/Module.kt | 15 ++ .../network/ui/settings/SettingsScreen.kt | 2 +- .../network/ui/settings/SettingsUiState.kt | 3 +- ui/sharing/build.gradle.kts | 4 + .../photos/network/ui/sharing/Module.kt | 15 ++ 80 files changed, 961 insertions(+), 445 deletions(-) rename {network => api}/build.gradle.kts (71%) rename {network => api}/src/main/AndroidManifest.xml (100%) rename network/src/main/kotlin/photos/network/network/NetworkModule.kt => api/src/main/kotlin/photos/network/api/ApiModule.kt (96%) create mode 100644 api/src/main/kotlin/photos/network/api/ServerStatus.kt rename {network/src/main/kotlin/photos/network/network => api/src/main/kotlin/photos/network/api}/photo/PhotoApi.kt (86%) rename {network/src/main/kotlin/photos/network/network => api/src/main/kotlin/photos/network/api}/photo/PhotoApiImpl.kt (83%) rename {network/src/main/kotlin/photos/network/network => api/src/main/kotlin/photos/network/api}/photo/Photos.kt (92%) rename {network/src/main/kotlin/photos/network/network/photo => api/src/main/kotlin/photos/network/api/photo/entity}/Photo.kt (74%) create mode 100644 api/src/main/kotlin/photos/network/api/status/StatusApi.kt create mode 100644 api/src/main/kotlin/photos/network/api/status/StatusApiImpl.kt rename network/src/main/kotlin/photos/network/network/user/model/ApiResponse.kt => api/src/main/kotlin/photos/network/api/status/entity/Status.kt (91%) rename {network/src/main/kotlin/photos/network/network => api/src/main/kotlin/photos/network/api}/user/UserApi.kt (95%) rename {network/src/main/kotlin/photos/network/network => api/src/main/kotlin/photos/network/api}/user/UserApiImpl.kt (94%) rename {network/src/main/kotlin/photos/network/network/user/model => api/src/main/kotlin/photos/network/api/user/entity}/NetworkUser.kt (90%) rename {network/src/main/kotlin/photos/network/network/user/model => api/src/main/kotlin/photos/network/api/user/entity}/TokenInfo.kt (95%) create mode 100644 api/src/test/kotlin/photos/network/api/photo/PhotoApiTests.kt rename {network/src/androidTest/kotlin/photos/network => api/src/test/kotlin/photos/network/api/photo}/entity/PhotoTest.kt (58%) rename network/src/test/kotlin/photos/network/api/PhotoApiTests.kt => api/src/test/kotlin/photos/network/api/status/StatusApiTests.kt (52%) create mode 100644 api/src/test/kotlin/photos/network/api/user/UserApiTests.kt create mode 100644 api/src/test/kotlin/photos/network/api/user/entity/NetworkUserTests.kt create mode 100644 api/src/test/kotlin/photos/network/api/user/entity/TokenInfoTests.kt create mode 100644 app/src/debug/AndroidManifest.xml delete mode 100644 network/src/androidTest/kotlin/photos/network/api/PhotoApiTest.kt delete mode 100644 network/src/androidTest/kotlin/photos/network/entity/PhotosTest.kt delete mode 100644 network/src/main/kotlin/photos/network/network/ServerStatus.kt rename repository/{settings/src/test/kotlin/photos/network/repository/settings => sharing/src/test/kotlin/photos/network/repository/sharing}/UserRepositoryTests.kt (87%) rename {app => ui/common}/src/main/res/values-ar/strings.xml (100%) rename {app => ui/common}/src/main/res/values-fr/strings.xml (100%) rename {app => ui/common}/src/main/res/values-ko-rKR/strings.xml (100%) rename {app => ui/common}/src/main/res/values-ko/strings.xml (100%) diff --git a/.github/workflows/continuous-delivery-pipeline.yml b/.github/workflows/continuous-delivery-pipeline.yml index 655c4c3..bf2d573 100644 --- a/.github/workflows/continuous-delivery-pipeline.yml +++ b/.github/workflows/continuous-delivery-pipeline.yml @@ -137,15 +137,25 @@ jobs: - name: Run debug unit tests run: | - ./gradlew --console=plain testDebugUnitTest --stacktrace + ./gradlew --console=plain koverXmlReport --stacktrace - name: Upload test reports if: always() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: tests path: app/build/reports/tests + - name: Add coverage report to PR + uses: mi-kas/kover-report@v1 + with: + path: ${{ github.workspace }}/app/build/reports/kover/xml/report.xml + token: ${{ secrets.GITHUB_TOKEN }} + title: App Coverage + update-comment: true + min-coverage-overall: 20 + min-coverage-changed-files: 50 + android_tests: name: Tests on Android (API level ${{ matrix.api-level }}) needs: [ detekt, lint ] diff --git a/network/build.gradle.kts b/api/build.gradle.kts similarity index 71% rename from network/build.gradle.kts rename to api/build.gradle.kts index 23d9900..8962b43 100644 --- a/network/build.gradle.kts +++ b/api/build.gradle.kts @@ -16,7 +16,7 @@ spotless { } android { - namespace = "photos.network.network" + namespace = "photos.network.api" compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { @@ -36,6 +36,16 @@ android { } } +kover { + filters { + classes { + excludes += "photos.network.api.ApiModule*" + excludes += "photos.network.api.BuildConfig" + excludes += "photos.network.api.ServerStatus" + } + } +} + dependencies { implementation(projects.common) testImplementation(project(":common", "testArtifacts")) @@ -43,4 +53,9 @@ dependencies { // httpclient implementation(libs.bundles.ktor) + testImplementation(libs.ktor.client.mock.jvm) + + testImplementation(libs.mockk) + testImplementation(libs.junit.junit) + testImplementation(libs.truth) } diff --git a/network/src/main/AndroidManifest.xml b/api/src/main/AndroidManifest.xml similarity index 100% rename from network/src/main/AndroidManifest.xml rename to api/src/main/AndroidManifest.xml diff --git a/network/src/main/kotlin/photos/network/network/NetworkModule.kt b/api/src/main/kotlin/photos/network/api/ApiModule.kt similarity index 96% rename from network/src/main/kotlin/photos/network/network/NetworkModule.kt rename to api/src/main/kotlin/photos/network/api/ApiModule.kt index 7e30306..f0314b2 100644 --- a/network/src/main/kotlin/photos/network/network/NetworkModule.kt +++ b/api/src/main/kotlin/photos/network/api/ApiModule.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.network +package photos.network.api import android.app.Application import android.content.pm.PackageInfo @@ -44,16 +44,16 @@ import kotlinx.serialization.json.Json import logcat.LogPriority import logcat.logcat import org.koin.dsl.module +import photos.network.api.photo.PhotoApi +import photos.network.api.photo.PhotoApiImpl +import photos.network.api.user.UserApi +import photos.network.api.user.UserApiImpl +import photos.network.api.user.entity.TokenInfo import photos.network.common.persistence.SecureStorage import photos.network.common.persistence.Settings import photos.network.common.persistence.User -import photos.network.network.photo.PhotoApi -import photos.network.network.photo.PhotoApiImpl -import photos.network.network.user.UserApi -import photos.network.network.user.UserApiImpl -import photos.network.network.user.model.TokenInfo -val networkModule = module { +val apiModule = module { single { provideKtorClient( application = get(), diff --git a/api/src/main/kotlin/photos/network/api/ServerStatus.kt b/api/src/main/kotlin/photos/network/api/ServerStatus.kt new file mode 100644 index 0000000..23ada4f --- /dev/null +++ b/api/src/main/kotlin/photos/network/api/ServerStatus.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.api + +enum class ServerStatus { + AVAILABLE(), + UNAVAILABLE(), + PROGRESS(), + UNAUTHORIZED(), +} diff --git a/network/src/main/kotlin/photos/network/network/photo/PhotoApi.kt b/api/src/main/kotlin/photos/network/api/photo/PhotoApi.kt similarity index 86% rename from network/src/main/kotlin/photos/network/network/photo/PhotoApi.kt rename to api/src/main/kotlin/photos/network/api/photo/PhotoApi.kt index 4543643..56caffb 100644 --- a/network/src/main/kotlin/photos/network/network/photo/PhotoApi.kt +++ b/api/src/main/kotlin/photos/network/api/photo/PhotoApi.kt @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.network.photo +package photos.network.api.photo + +import photos.network.api.photo.entity.Photo interface PhotoApi { /** @@ -22,12 +24,12 @@ interface PhotoApi { suspend fun getPhotos( offset: Int = 0, limit: Int = 0, - ): Result + ): Photos /** * Load detailed information for a single photo * * @param photoId Identifier of the photo details to return */ - suspend fun getPhoto(photoId: String): Result + suspend fun getPhoto(photoId: String): Photo } diff --git a/network/src/main/kotlin/photos/network/network/photo/PhotoApiImpl.kt b/api/src/main/kotlin/photos/network/api/photo/PhotoApiImpl.kt similarity index 83% rename from network/src/main/kotlin/photos/network/network/photo/PhotoApiImpl.kt rename to api/src/main/kotlin/photos/network/api/photo/PhotoApiImpl.kt index 07f3266..f684749 100644 --- a/network/src/main/kotlin/photos/network/network/photo/PhotoApiImpl.kt +++ b/api/src/main/kotlin/photos/network/api/photo/PhotoApiImpl.kt @@ -13,23 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.network.photo +package photos.network.api.photo import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.get import io.ktor.client.request.parameter +import photos.network.api.photo.entity.Photo class PhotoApiImpl( private val httpClient: HttpClient, ) : PhotoApi { - override suspend fun getPhotos(offset: Int, limit: Int): Result { + override suspend fun getPhotos(offset: Int, limit: Int): Photos { return httpClient.get(urlString = "/api/photos") { parameter("offset", offset) parameter("limit", limit) }.body() } - override suspend fun getPhoto(photoId: String): Result = + override suspend fun getPhoto(photoId: String): Photo = httpClient.get(urlString = "/api/photo/$photoId").body() } diff --git a/network/src/main/kotlin/photos/network/network/photo/Photos.kt b/api/src/main/kotlin/photos/network/api/photo/Photos.kt similarity index 92% rename from network/src/main/kotlin/photos/network/network/photo/Photos.kt rename to api/src/main/kotlin/photos/network/api/photo/Photos.kt index 9b620a9..ead3b7e 100644 --- a/network/src/main/kotlin/photos/network/network/photo/Photos.kt +++ b/api/src/main/kotlin/photos/network/api/photo/Photos.kt @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.network.photo +package photos.network.api.photo import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import photos.network.api.photo.entity.Photo @Serializable data class Photos( diff --git a/network/src/main/kotlin/photos/network/network/photo/Photo.kt b/api/src/main/kotlin/photos/network/api/photo/entity/Photo.kt similarity index 74% rename from network/src/main/kotlin/photos/network/network/photo/Photo.kt rename to api/src/main/kotlin/photos/network/api/photo/entity/Photo.kt index afa68ef..0f8e809 100644 --- a/network/src/main/kotlin/photos/network/network/photo/Photo.kt +++ b/api/src/main/kotlin/photos/network/api/photo/entity/Photo.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.network.photo +package photos.network.api.photo.entity import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -22,7 +22,11 @@ import kotlinx.serialization.Serializable data class Photo( @SerialName("id") val id: String, @SerialName("name") val name: String, - @SerialName("image_url") val imageUrl: String, + @SerialName("owner") val owner: String? = null, @SerialName("date_added") val dateAdded: String? = null, - @SerialName("date_taken") val dateTaken: Long? = null, + @SerialName("date_taken") val dateTaken: String? = null, + @SerialName("image_url") val imageUrl: String, + @SerialName("details") val details: String? = null, + @SerialName("tags") val tags: String? = null, + @SerialName("location") val location: String? = null, ) diff --git a/api/src/main/kotlin/photos/network/api/status/StatusApi.kt b/api/src/main/kotlin/photos/network/api/status/StatusApi.kt new file mode 100644 index 0000000..ff4c72d --- /dev/null +++ b/api/src/main/kotlin/photos/network/api/status/StatusApi.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.api.status + +import photos.network.api.status.entity.Status + +interface StatusApi { + + /** + * Chack the availibility of the CORE instance + */ + suspend fun headStatus(): Status + + /** + * Check the status of the CORE instance + */ + suspend fun getStatus(): Status +} diff --git a/api/src/main/kotlin/photos/network/api/status/StatusApiImpl.kt b/api/src/main/kotlin/photos/network/api/status/StatusApiImpl.kt new file mode 100644 index 0000000..64222a3 --- /dev/null +++ b/api/src/main/kotlin/photos/network/api/status/StatusApiImpl.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2022 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.api.status + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import photos.network.api.status.entity.Status + +class StatusApiImpl( + private val httpClient: HttpClient, +) : StatusApi { + override suspend fun headStatus(): Status { + return httpClient.get(urlString = "/api/").body() + } + + override suspend fun getStatus(): Status { + return httpClient.get(urlString = "/api/") { + }.body() + } +} diff --git a/network/src/main/kotlin/photos/network/network/user/model/ApiResponse.kt b/api/src/main/kotlin/photos/network/api/status/entity/Status.kt similarity index 91% rename from network/src/main/kotlin/photos/network/network/user/model/ApiResponse.kt rename to api/src/main/kotlin/photos/network/api/status/entity/Status.kt index b9ad7c6..f838ead 100644 --- a/network/src/main/kotlin/photos/network/network/user/model/ApiResponse.kt +++ b/api/src/main/kotlin/photos/network/api/status/entity/Status.kt @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.network.user.model +package photos.network.api.status.entity import kotlinx.serialization.Serializable @Serializable -data class ApiResponse( +data class Status( val message: String, ) diff --git a/network/src/main/kotlin/photos/network/network/user/UserApi.kt b/api/src/main/kotlin/photos/network/api/user/UserApi.kt similarity index 95% rename from network/src/main/kotlin/photos/network/network/user/UserApi.kt rename to api/src/main/kotlin/photos/network/api/user/UserApi.kt index eecf2f9..962133b 100644 --- a/network/src/main/kotlin/photos/network/network/user/UserApi.kt +++ b/api/src/main/kotlin/photos/network/api/user/UserApi.kt @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.network.user +package photos.network.api.user -import photos.network.network.user.model.NetworkUser +import photos.network.api.user.entity.NetworkUser interface UserApi { /** diff --git a/network/src/main/kotlin/photos/network/network/user/UserApiImpl.kt b/api/src/main/kotlin/photos/network/api/user/UserApiImpl.kt similarity index 94% rename from network/src/main/kotlin/photos/network/network/user/UserApiImpl.kt rename to api/src/main/kotlin/photos/network/api/user/UserApiImpl.kt index e33a271..711d758 100644 --- a/network/src/main/kotlin/photos/network/network/user/UserApiImpl.kt +++ b/api/src/main/kotlin/photos/network/api/user/UserApiImpl.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.network.user +package photos.network.api.user import io.ktor.client.HttpClient import io.ktor.client.call.body @@ -29,12 +29,12 @@ import io.ktor.http.isSuccess import logcat.LogPriority import logcat.asLog import logcat.logcat +import photos.network.api.status.entity.Status +import photos.network.api.user.entity.NetworkUser +import photos.network.api.user.entity.TokenInfo import photos.network.common.persistence.SecureStorage import photos.network.common.persistence.Settings import photos.network.common.persistence.User -import photos.network.network.user.model.ApiResponse -import photos.network.network.user.model.NetworkUser -import photos.network.network.user.model.TokenInfo class UserApiImpl( private val httpClient: HttpClient, @@ -52,7 +52,7 @@ class UserApiImpl( return false } - val body = response.body() + val body = response.body() return body.message.contains("API running") } catch (exception: Exception) { diff --git a/network/src/main/kotlin/photos/network/network/user/model/NetworkUser.kt b/api/src/main/kotlin/photos/network/api/user/entity/NetworkUser.kt similarity index 90% rename from network/src/main/kotlin/photos/network/network/user/model/NetworkUser.kt rename to api/src/main/kotlin/photos/network/api/user/entity/NetworkUser.kt index 96843e1..db78c4e 100644 --- a/network/src/main/kotlin/photos/network/network/user/model/NetworkUser.kt +++ b/api/src/main/kotlin/photos/network/api/user/entity/NetworkUser.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.network.user.model +package photos.network.api.user.entity import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -24,5 +24,5 @@ data class NetworkUser( @SerialName("email") val email: String, @SerialName("lastname") val lastname: String, @SerialName("firstname") val firstname: String, - @SerialName("lastSeen") val lastSeen: String, + @SerialName("last_seen") val lastSeen: String, ) diff --git a/network/src/main/kotlin/photos/network/network/user/model/TokenInfo.kt b/api/src/main/kotlin/photos/network/api/user/entity/TokenInfo.kt similarity index 95% rename from network/src/main/kotlin/photos/network/network/user/model/TokenInfo.kt rename to api/src/main/kotlin/photos/network/api/user/entity/TokenInfo.kt index 39d1abb..b7d73cc 100644 --- a/network/src/main/kotlin/photos/network/network/user/model/TokenInfo.kt +++ b/api/src/main/kotlin/photos/network/api/user/entity/TokenInfo.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.network.user.model +package photos.network.api.user.entity import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/api/src/test/kotlin/photos/network/api/photo/PhotoApiTests.kt b/api/src/test/kotlin/photos/network/api/photo/PhotoApiTests.kt new file mode 100644 index 0000000..7c36578 --- /dev/null +++ b/api/src/test/kotlin/photos/network/api/photo/PhotoApiTests.kt @@ -0,0 +1,202 @@ +/* + * Copyright 2020-2022 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.api.photo + +import com.google.common.truth.Truth +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.headersOf +import io.ktor.serialization.JsonConvertException +import io.ktor.serialization.kotlinx.json.json +import io.ktor.utils.io.ByteReadChannel +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import org.junit.Before +import org.junit.Test +import photos.network.common.persistence.PrivacyState +import photos.network.common.persistence.SecureStorage +import photos.network.common.persistence.Settings + +/** + * Test API endpoints for photos with static fake data + */ +class PhotoApiTests { + private val settingsStore = mockk>() + + @Before + fun setup() { + coEvery { settingsStore.read()?.host } answers { "http://localhost" } + coEvery { settingsStore.read()?.port } answers { 443 } + coEvery { settingsStore.read()?.clientId } answers { "TEST-CLIENTID" } + coEvery { settingsStore.read()?.privacyState } answers { PrivacyState.NONE } + } + + @Test + fun `valid photos response should return list of photos`() = runBlocking { + // given + val photoApi = PhotoApiImpl( + httpClient = HttpClient( + MockEngine { + respond( + content = ByteReadChannel( + """ + { + "offset": 0, + "limit": 50, + "size": 1, + "results": [ + { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "name": "filename.ext", + "image_url": "string" + } + ] + } + """.trimIndent(), + ), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + }, + ) { + install(ContentNegotiation) { + json( + Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + }, + contentType = ContentType.Application.Json, + ) + } + }, + ) + + // when + val result = photoApi.getPhotos() + + // then + Truth.assertThat(result.size).isEqualTo(1) + } + + @Test(expected = JsonConvertException::class) + fun `invalid photos response should fail`() = runBlocking { + // given + val photoApi = PhotoApiImpl( + httpClient = HttpClient( + MockEngine { + respond( + content = ByteReadChannel( + """ + { + } + """.trimIndent(), + ), + status = HttpStatusCode.InternalServerError, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + }, + ) { + install(ContentNegotiation) { json() } + }, + ) + + // when + val result = photoApi.getPhotos() + + // then + Truth.assertThat(result.size).isEqualTo(0) + } + + @Test + fun `valid photo response should return a single photos`() = runBlocking { + // given + val photoApi = PhotoApiImpl( + httpClient = HttpClient( + MockEngine { + respond( + content = ByteReadChannel( + """ + { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "name": "filename.ext", + "owner": "lastname, firstname", + "date_added": "2023-05-02T05:18:45.130Z", + "date_taken": "2023-05-02T05:18:45.130Z", + "image_url": "string" + } + """.trimIndent(), + ), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + }, + ) { + install(ContentNegotiation) { + json( + Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + }, + contentType = ContentType.Application.Json, + ) + } + }, + ) + + // when + val result = photoApi.getPhoto("3fa85f64-5717-4562-b3fc-2c963f66afa6") + + // then + Truth.assertThat(result.id).isEqualTo("3fa85f64-5717-4562-b3fc-2c963f66afa6") + } + + @Test(expected = JsonConvertException::class) + fun `invalid photo response should fail`() = runBlocking { + // given + val photoApi = PhotoApiImpl( + httpClient = HttpClient( + MockEngine { + respond( + content = ByteReadChannel( + """ + {} + """.trimIndent(), + ), + status = HttpStatusCode.InternalServerError, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + }, + ) { + install(ContentNegotiation) { json() } + }, + ) + + // when + val result = photoApi.getPhoto("3fa85f64-5717-4562-b3fc-2c963f66afa6") + + // then + Truth.assertThat(result.id).isEqualTo("3fa85f64-5717-4562-b3fc-2c963f66afa6") + } +} diff --git a/network/src/androidTest/kotlin/photos/network/entity/PhotoTest.kt b/api/src/test/kotlin/photos/network/api/photo/entity/PhotoTest.kt similarity index 58% rename from network/src/androidTest/kotlin/photos/network/entity/PhotoTest.kt rename to api/src/test/kotlin/photos/network/api/photo/entity/PhotoTest.kt index 97cdabe..cb6e3b9 100644 --- a/network/src/androidTest/kotlin/photos/network/entity/PhotoTest.kt +++ b/api/src/test/kotlin/photos/network/api/photo/entity/PhotoTest.kt @@ -13,39 +13,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.photos.network.entity +package photos.network.api.photo.entity -import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.runBlocking import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import org.junit.Assert.assertEquals -import org.junit.Ignore import org.junit.Test -import org.junit.runner.RunWith -import photos.network.common.PhotosNetworkMockFileReader -import photos.network.network.photo.Photo /** * Test deserializing photo object. */ -@RunWith(AndroidJUnit4::class) class PhotoTest { - - @Ignore("Not fully implemented") @Test fun testDeserialization() = runBlocking { // given - val jsonString = PhotosNetworkMockFileReader.readStringFromFile("photo_object.json") + val jsonString = """ + { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "name": "filename.ext", + "owner": "lastname, firstname", + "date_added": "2023-01-01T01:01:01.130Z", + "date_taken": "2023-01-01T02:02:02.130Z", + "image_url": "string" + } + """.trimIndent() // when val response = Json.decodeFromString(jsonString) // then - assertEquals(response.id, "photoIdentifier") - assertEquals(response.name, "photoName") - assertEquals(response.imageUrl, "") - assertEquals(response.dateAdded, "") - assertEquals(response.dateTaken, "") + assertEquals(response.id, "3fa85f64-5717-4562-b3fc-2c963f66afa6") + assertEquals(response.name, "filename.ext") + assertEquals(response.imageUrl, "string") + assertEquals(response.dateAdded, "2023-01-01T01:01:01.130Z") + assertEquals(response.dateTaken, "2023-01-01T02:02:02.130Z") } } diff --git a/network/src/test/kotlin/photos/network/api/PhotoApiTests.kt b/api/src/test/kotlin/photos/network/api/status/StatusApiTests.kt similarity index 52% rename from network/src/test/kotlin/photos/network/api/PhotoApiTests.kt rename to api/src/test/kotlin/photos/network/api/status/StatusApiTests.kt index 08595b9..f5c67a2 100644 --- a/network/src/test/kotlin/photos/network/api/PhotoApiTests.kt +++ b/api/src/test/kotlin/photos/network/api/status/StatusApiTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.api +package photos.network.api.status import com.google.common.truth.Truth import io.ktor.client.HttpClient @@ -24,56 +24,45 @@ import io.ktor.http.ContentType import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode import io.ktor.http.headersOf +import io.ktor.serialization.JsonConvertException import io.ktor.serialization.kotlinx.json.json import io.ktor.utils.io.ByteReadChannel import io.mockk.coEvery import io.mockk.mockk -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import org.junit.Before import org.junit.Test -import photos.network.data.settings.repository.PrivacyState -import photos.network.data.settings.repository.Settings -import photos.network.data.settings.repository.SettingsRepository -import photos.network.network.photo.PhotoApiImpl +import photos.network.common.persistence.PrivacyState +import photos.network.common.persistence.SecureStorage +import photos.network.common.persistence.Settings -class PhotoApiTests { - private val settingsRepository = mockk() +/** + * Test status endpoints with static fake data + */ +class StatusApiTests { + private val settingsStore = mockk>() @Before fun setup() { - coEvery { settingsRepository.settings } answers { - flowOf( - Settings( - host = "http://localhost", - privacyState = PrivacyState.NONE, - ), - ) - } + coEvery { settingsStore.read()?.host } answers { "http://localhost" } + coEvery { settingsStore.read()?.port } answers { 443 } + coEvery { settingsStore.read()?.clientId } answers { "TEST-CLIENTID" } + coEvery { settingsStore.read()?.privacyState } answers { PrivacyState.NONE } } @Test - fun `get photos should return photos for the current user`() = runBlocking { + fun `valid status response should succeed`() = runBlocking { // given - val photoApi = PhotoApiImpl( + val photoApi = StatusApiImpl( httpClient = HttpClient( MockEngine { respond( content = ByteReadChannel( """ -{ - "offset": 0, - "limit": 50, - "size": 1, - "results": [ - { - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "filename.ext", - "image_url": "string" - } - ] -} + { + "message": "API running." + } """.trimIndent(), ), status = HttpStatusCode.OK, @@ -95,9 +84,37 @@ class PhotoApiTests { ) // when - val result = photoApi.getPhotos() + val result = photoApi.getStatus() + + // then + Truth.assertThat(result.message).isEqualTo("API running.") + } + + @Test(expected = JsonConvertException::class) + fun `invalid status response should fail`() = runBlocking { + // given + val photoApi = StatusApiImpl( + httpClient = HttpClient( + MockEngine { + respond( + content = ByteReadChannel( + """ + {} + """.trimIndent(), + ), + status = HttpStatusCode.InternalServerError, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + }, + ) { + install(ContentNegotiation) { json() } + }, + ) + + // when + val result = photoApi.headStatus() // then - Truth.assertThat(result.size).isEqualTo(1) + Truth.assertThat(result.message).isEqualTo("") } } diff --git a/api/src/test/kotlin/photos/network/api/user/UserApiTests.kt b/api/src/test/kotlin/photos/network/api/user/UserApiTests.kt new file mode 100644 index 0000000..0265030 --- /dev/null +++ b/api/src/test/kotlin/photos/network/api/user/UserApiTests.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2020-2022 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.api.user + +import com.google.common.truth.Truth +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.headersOf +import io.ktor.serialization.kotlinx.json.json +import io.ktor.utils.io.ByteReadChannel +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import org.junit.Before +import org.junit.Test +import photos.network.common.persistence.PrivacyState +import photos.network.common.persistence.SecureStorage +import photos.network.common.persistence.Settings +import photos.network.common.persistence.User + +/** + * Test API endpoints for photos with static fake data + */ +class UserApiTests { + private val settingsStorage = mockk>() + private val userStorage = mockk>() + + @Before + fun setup() { + coEvery { userStorage.read()?.id } answers { "" } + coEvery { userStorage.read()?.lastname } answers { "" } + coEvery { userStorage.read()?.firstname } answers { "" } + coEvery { userStorage.read()?.profileImageUrl } answers { "" } + coEvery { userStorage.read()?.accessToken } answers { "" } + coEvery { userStorage.read()?.refreshToken } answers { "" } + + coEvery { settingsStorage.read()?.host } answers { "http://localhost" } + coEvery { settingsStorage.read()?.port } answers { 443 } + coEvery { settingsStorage.read()?.clientId } answers { "TEST-CLIENTID" } + coEvery { settingsStorage.read()?.privacyState } answers { PrivacyState.NONE } + } + + @Test + fun `valid verify server response should return true`() = runBlocking { + // given + val userApi = UserApiImpl( + userStorage = userStorage, + settingsStorage = settingsStorage, + httpClient = HttpClient( + MockEngine { + respond( + content = ByteReadChannel( + """ + { + "message": "API running." + } + """.trimIndent(), + ), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + }, + ) { + install(ContentNegotiation) { + json( + Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + }, + contentType = ContentType.Application.Json, + ) + } + }, + ) + + // when + val result = userApi.verifyServerHost("http://localhost") + + // then + Truth.assertThat(result).isTrue() + } + + @Test + fun `invalid verify server response should return false`() = runBlocking { + // given + val userApi = UserApiImpl( + userStorage = userStorage, + settingsStorage = settingsStorage, + httpClient = HttpClient( + MockEngine { + respond( + content = ByteReadChannel( + """ + Content-Length: 68137 + Content-Type: multipart/form-data; boundary=--------------------------- + 974767299852498929531610575 + + -----------------------------974767299852498929531610575 + Content-Disposition: form-data; name="description" + """.trimIndent(), + ), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "text/html; charset=utf-8"), + ) + }, + ) { + install(ContentNegotiation) { json() } + }, + ) + + // when + val result = userApi.verifyServerHost("http://localhost") + + // then + Truth.assertThat(result).isFalse() + } + + @Test + fun `empty verify server response should return false`() = runBlocking { + // given + val userApi = UserApiImpl( + userStorage = userStorage, + settingsStorage = settingsStorage, + httpClient = HttpClient( + MockEngine { + respond( + content = ByteReadChannel( + """ + """.trimIndent(), + ), + status = HttpStatusCode.NotFound, + headers = headersOf(HttpHeaders.ContentType, "text/plain; charset=utf-8"), + ) + }, + ) { + install(ContentNegotiation) { json() } + }, + ) + + // when + val result = userApi.verifyServerHost("http://localhost") + + // then + Truth.assertThat(result).isFalse() + } +} diff --git a/api/src/test/kotlin/photos/network/api/user/entity/NetworkUserTests.kt b/api/src/test/kotlin/photos/network/api/user/entity/NetworkUserTests.kt new file mode 100644 index 0000000..aa47011 --- /dev/null +++ b/api/src/test/kotlin/photos/network/api/user/entity/NetworkUserTests.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.api.user.entity + +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.junit.Assert.assertEquals +import org.junit.Test + +class NetworkUserTests { + @Test + fun testDeserialization() = runBlocking { + // given + val jsonString = """ + { + "id": "c18b3495-e537-46c1-b2e4-ed5ea9bb4c6f", + "email": "max.mustermann@photos.network", + "lastname": "Mustermann", + "firstname": "Max", + "last_seen": "2023-01-01T01:01:01Z" + } + """.trimIndent() + + // when + val response = Json.decodeFromString(jsonString) + + // then + assertEquals(response.id, "c18b3495-e537-46c1-b2e4-ed5ea9bb4c6f") + assertEquals(response.email, "max.mustermann@photos.network") + assertEquals(response.firstname, "Max") + assertEquals(response.lastname, "Mustermann") + assertEquals(response.lastSeen, "2023-01-01T01:01:01Z") + } + + @Suppress("MaxLineLength") + @Test + fun testSerialization() = runBlocking { + // given + val networkUser = NetworkUser( + id = "c18b3495-e537-46c1-b2e4-ed5ea9bb4c6f", + email = "max.mustermann@photos.network", + firstname = "Max", + lastname = "Mustermann", + lastSeen = "2023-01-01T01:01:01Z", + ) + + // when + val jsonString = Json.encodeToString(networkUser) + + // then + assertEquals( + jsonString, + """{"id":"c18b3495-e537-46c1-b2e4-ed5ea9bb4c6f","email":"max.mustermann@photos.network","lastname":"Mustermann","firstname":"Max","last_seen":"2023-01-01T01:01:01Z"}""".trimIndent(), + ) + } +} diff --git a/api/src/test/kotlin/photos/network/api/user/entity/TokenInfoTests.kt b/api/src/test/kotlin/photos/network/api/user/entity/TokenInfoTests.kt new file mode 100644 index 0000000..c39cea5 --- /dev/null +++ b/api/src/test/kotlin/photos/network/api/user/entity/TokenInfoTests.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.api.user.entity + +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import org.junit.Assert.assertEquals +import org.junit.Test + +class TokenInfoTests { + @Suppress("MaxLineLength") + @Test + fun testDeserialization() = runBlocking { + // given + val jsonString = """ + { + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTMyMzM4Mjh9.8U4oXtAHEkYgZldFMduANu-ryhTN5RX69XslPzU7pnQ", + "token_type": "Bearer", + "expires_in": 3600, + "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTMyMzA4ODN9.4kFQD33F7-xQPUWSM9IxsDYqv30zAEa7WS7jpk8NtFU" + } + """.trimIndent() + + // when + val response = Json.decodeFromString(jsonString) + + // then + assertEquals(response.accessToken, "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTMyMzM4Mjh9.8U4oXtAHEkYgZldFMduANu-ryhTN5RX69XslPzU7pnQ") + assertEquals(response.expiresIn, 3600) + assertEquals(response.refreshToken, "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTMyMzA4ODN9.4kFQD33F7-xQPUWSM9IxsDYqv30zAEa7WS7jpk8NtFU") + assertEquals(response.tokenType, "Bearer") + } +} diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 38934be..3d3a00c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -177,4 +177,7 @@ dependencies { // leakCanary debugImplementation(libs.leakcanary.android) + + testImplementation(libs.core.testing) + testImplementation(libs.mockk) } diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..84ab813 --- /dev/null +++ b/app/src/debug/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/kotlin/photos/network/AppModule.kt b/app/src/main/kotlin/photos/network/AppModule.kt index 042a79a..a30f88b 100644 --- a/app/src/main/kotlin/photos/network/AppModule.kt +++ b/app/src/main/kotlin/photos/network/AppModule.kt @@ -18,8 +18,6 @@ package photos.network import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module import photos.network.home.HomeViewModel -import photos.network.ui.settings.SettingsViewModel -import photos.network.ui.sharing.login.LoginViewModel import photos.network.user.CurrentUserViewModel val appModule = module { @@ -29,12 +27,10 @@ val appModule = module { ) } - viewModel { HomeViewModel( getSettingsUseCase = get(), togglePrivacyStateUseCase = get(), ) } - } diff --git a/app/src/main/kotlin/photos/network/MainActivity.kt b/app/src/main/kotlin/photos/network/MainActivity.kt index ddf9881..d95b571 100644 --- a/app/src/main/kotlin/photos/network/MainActivity.kt +++ b/app/src/main/kotlin/photos/network/MainActivity.kt @@ -28,12 +28,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration import androidx.core.view.WindowCompat -import androidx.navigation.NavHostController -import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.google.accompanist.systemuicontroller.SystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController import photos.network.home.Home -import photos.network.ui.common.navigation.Destination import photos.network.ui.common.theme.AppTheme import photos.network.user.CurrentUserHost @@ -45,6 +42,7 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) + setContent { PhotosApp() } @@ -55,8 +53,6 @@ val LocalAppVersion = staticCompositionLocalOf { "Unknown" } @Composable fun PhotosApp( - startDestination: String = Destination.Home.route, - navController: NavHostController = rememberAnimatedNavController(), systemUiController: SystemUiController = rememberSystemUiController(), ) { val useDarkIcons = !isSystemInDarkTheme() diff --git a/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt b/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt index 3c1a171..403127c 100644 --- a/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt +++ b/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt @@ -24,6 +24,7 @@ import org.koin.androidx.workmanager.koin.workManagerFactory import org.koin.core.component.KoinComponent import org.koin.core.context.startKoin import org.koin.core.logger.Level +import photos.network.api.apiModule import photos.network.database.photos.databasePhotosModule import photos.network.database.settings.databaseSettingsModule import photos.network.database.sharing.databaseSharingModule @@ -32,7 +33,6 @@ import photos.network.domain.folders.domainFoldersModule import photos.network.domain.photos.domainPhotosModule import photos.network.domain.settings.domainSettingsModule import photos.network.domain.sharing.domainSharingModule -import photos.network.network.networkModule import photos.network.repository.photos.repositoryPhotosModule import photos.network.repository.settings.repositorySettingsModule import photos.network.repository.sharing.repositorySharingModule @@ -96,7 +96,7 @@ open class PhotosNetworkApplication : Application(), KoinComponent { repositorySharingModule, databaseSharingModule, - networkModule, + apiModule, ), ) } diff --git a/app/src/main/kotlin/photos/network/home/Home.kt b/app/src/main/kotlin/photos/network/home/Home.kt index d98eebf..a104483 100644 --- a/app/src/main/kotlin/photos/network/home/Home.kt +++ b/app/src/main/kotlin/photos/network/home/Home.kt @@ -16,8 +16,10 @@ package photos.network.home import android.content.res.Configuration +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding @@ -31,8 +33,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold -import androidx.compose.material3.SmallTopAppBar import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -40,9 +42,11 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag 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 androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -50,10 +54,10 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import org.koin.androidx.compose.getViewModel import photos.network.R -import photos.network.network.ServerStatus +import photos.network.api.ServerStatus +import photos.network.ui.common.components.AppLogo import photos.network.ui.common.navigation.Destination import photos.network.ui.common.theme.AppTheme -import photos.network.ui.common.components.AppLogo /** * Default app screen containing a searchbar, photos grid, albums tab and more. @@ -76,9 +80,9 @@ fun Home( Scaffold( modifier = modifier .fillMaxSize() - .statusBarsPadding() .navigationBarsPadding() - .testTag("HomeScreenTag"), + .testTag("HomeScreenTag") + .border(1.dp, Color.Magenta), snackbarHost = { // SnackbarHost( // hostState = it, @@ -87,9 +91,12 @@ fun Home( }, topBar = { if (currentDestination.isRootDestination()) { - SmallTopAppBar( - modifier = Modifier.padding(top = 36.dp), + // privacy + TopAppBar( title = {}, + modifier = Modifier + .statusBarsPadding() + .padding(top = 36.dp), navigationIcon = { AppLogo( modifier = Modifier @@ -123,10 +130,9 @@ fun Home( ) } } - }, - colors = TopAppBarDefaults.smallTopAppBarColors( + }, colors = TopAppBarDefaults.smallTopAppBarColors( containerColor = MaterialTheme.colorScheme.primary, - ), + ) ) } }, @@ -166,7 +172,16 @@ fun Home( } }, content = { innerPadding -> - Box(modifier = Modifier.padding(bottom = innerPadding.calculateBottomPadding())) { + val topPadding: Dp = if (currentDestination.isRootDestination()) { + innerPadding.calculateTopPadding() + } else { + 0.dp + } + Box(modifier = Modifier + .padding(bottom = innerPadding.calculateBottomPadding()) + .padding(top = topPadding) + .border(2.dp, Color.Green) + ) { NavHost( navController = navController, startDestination = Destination.Photos.route, diff --git a/app/src/main/kotlin/photos/network/ui/UserAvatar.kt b/app/src/main/kotlin/photos/network/ui/UserAvatar.kt index 429375b..6615bfe 100644 --- a/app/src/main/kotlin/photos/network/ui/UserAvatar.kt +++ b/app/src/main/kotlin/photos/network/ui/UserAvatar.kt @@ -37,7 +37,6 @@ 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 coil.compose.LocalImageLoader import coil.compose.rememberAsyncImagePainter import coil.imageLoader import coil.request.ImageRequest @@ -69,7 +68,8 @@ fun UserAvatar( .apply(block = fun ImageRequest.Builder.() { crossfade(true) placeholder(R.drawable.bob_ross_head_200x200) - }).build(), imageLoader = LocalContext.current.imageLoader + }).build(), + imageLoader = LocalContext.current.imageLoader, ), contentDescription = stringResource(id = R.string.icon_user_profile), modifier = Modifier diff --git a/app/src/test/kotlin/photos/network/CurrentUserViewModelTests.kt b/app/src/test/kotlin/photos/network/CurrentUserViewModelTests.kt index fc13034..f3705cf 100644 --- a/app/src/test/kotlin/photos/network/CurrentUserViewModelTests.kt +++ b/app/src/test/kotlin/photos/network/CurrentUserViewModelTests.kt @@ -20,7 +20,7 @@ import io.mockk.mockk import org.junit.Ignore import org.junit.Rule import org.junit.Test -import photos.network.domain.user.usecase.GetCurrentUserUseCase +import photos.network.domain.sharing.usecase.GetCurrentUserUseCase class CurrentUserViewModelTests { @get:Rule diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 01a7449..b1ba1de 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -86,7 +86,8 @@ dependencies { // testing testApi(libs.mockk) - testApi(libs.com.google.truth.truth) + testApi(libs.truth) + testApi(libs.junit.junit) androidTestApi(libs.androidx.test.core) androidTestApi(libs.androidx.test.ext.junit) androidTestApi(libs.androidx.test.ext.junit) diff --git a/common/src/main/kotlin/photos/network/common/Module.kt b/common/src/main/kotlin/photos/network/common/Module.kt index d17994b..81c6bca 100644 --- a/common/src/main/kotlin/photos/network/common/Module.kt +++ b/common/src/main/kotlin/photos/network/common/Module.kt @@ -18,5 +18,4 @@ package photos.network.common import org.koin.dsl.module val commonModule = module { - } diff --git a/database/photos/build.gradle.kts b/database/photos/build.gradle.kts index 5579d41..afe2de1 100644 --- a/database/photos/build.gradle.kts +++ b/database/photos/build.gradle.kts @@ -62,10 +62,6 @@ dependencies { // Persistence implementation(libs.bundles.room) -// annotationProcessor(libs.room.compiler) -// kapt(libs.room.compiler) - // To use Kotlin Symbol Processing (KSP) - ksp(libs.room.compiler) - + ksp(libs.room.compiler) androidTestImplementation(libs.room.testing) } diff --git a/database/sharing/build.gradle.kts b/database/sharing/build.gradle.kts index 2783772..af2454d 100644 --- a/database/sharing/build.gradle.kts +++ b/database/sharing/build.gradle.kts @@ -58,7 +58,7 @@ dependencies { testImplementation(project(":common", "testArtifacts")) androidTestImplementation(project(":common", "androidTestArtifacts")) - implementation(projects.network) + implementation(projects.api) // Persistence implementation(libs.bundles.room) diff --git a/domain/albums/src/main/kotlin/photos/network/domain/albums/Module.kt b/domain/albums/src/main/kotlin/photos/network/domain/albums/Module.kt index 63f74f0..64d1ebd 100644 --- a/domain/albums/src/main/kotlin/photos/network/domain/albums/Module.kt +++ b/domain/albums/src/main/kotlin/photos/network/domain/albums/Module.kt @@ -18,5 +18,4 @@ package photos.network.domain.albums import org.koin.dsl.module val domainAlbumsModule = module { - } diff --git a/domain/folders/src/main/kotlin/photos/network/domain/folders/Module.kt b/domain/folders/src/main/kotlin/photos/network/domain/folders/Module.kt index b496916..da27bfd 100644 --- a/domain/folders/src/main/kotlin/photos/network/domain/folders/Module.kt +++ b/domain/folders/src/main/kotlin/photos/network/domain/folders/Module.kt @@ -18,5 +18,4 @@ package photos.network.domain.folders import org.koin.dsl.module val domainFoldersModule = module { - } diff --git a/domain/photos/build.gradle.kts b/domain/photos/build.gradle.kts index 7d9e5fb..eb5dd64 100644 --- a/domain/photos/build.gradle.kts +++ b/domain/photos/build.gradle.kts @@ -49,4 +49,9 @@ dependencies { api(projects.repository.photos) implementation(projects.repository.settings) + + testImplementation(libs.mockk) + testImplementation(libs.junit.junit) + testImplementation(libs.truth) + testImplementation(libs.core.testing) } diff --git a/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCaseTests.kt b/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCaseTests.kt index fc49e3c..dafb645 100644 --- a/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCaseTests.kt +++ b/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCaseTests.kt @@ -24,8 +24,8 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test -import photos.network.data.photos.repository.Photo -import photos.network.data.photos.repository.PhotoRepository +import photos.network.repository.photos.Photo +import photos.network.repository.photos.PhotoRepository import java.time.Instant class GetPhotoUseCaseTests { diff --git a/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCaseTests.kt b/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCaseTests.kt index 9fd54cf..9a1026e 100644 --- a/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCaseTests.kt +++ b/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCaseTests.kt @@ -24,12 +24,12 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test -import photos.network.data.photos.repository.Photo -import photos.network.data.photos.repository.PhotoRepository -import photos.network.data.settings.repository.PrivacyState -import photos.network.data.settings.repository.Settings -import photos.network.data.settings.repository.SettingsRepository import java.time.Instant +import photos.network.common.persistence.PrivacyState +import photos.network.common.persistence.Settings +import photos.network.repository.photos.Photo +import photos.network.repository.photos.PhotoRepository +import photos.network.repository.settings.SettingsRepository class GetPhotosUseCaseTests { @Rule diff --git a/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCaseTests.kt b/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCaseTests.kt index ee8be68..aafb3e8 100644 --- a/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCaseTests.kt +++ b/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCaseTests.kt @@ -16,13 +16,14 @@ package photos.network.domain.photos.usecase import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import io.mockk.every +import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.mockk -import io.mockk.verify import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test -import photos.network.data.photos.repository.PhotoRepository +import photos.network.repository.photos.PhotoRepository +import photos.network.repository.photos.worker.SyncStatus class StartPhotosSyncUseCaseTests { @Rule @@ -40,12 +41,12 @@ class StartPhotosSyncUseCaseTests { @Test fun `start local photo sync use case should trigger sync on repository`(): Unit = runBlocking { // given - every { photoRepository.syncPhotos() } answers {} + coEvery { photoRepository.syncPhotos() } answers { SyncStatus.SyncSucceeded } // when startPhotosUseCase() // then - verify(exactly = 1) { photoRepository.syncPhotos() } + coVerify(exactly = 1) { photoRepository.syncPhotos() } } } diff --git a/domain/settings/build.gradle.kts b/domain/settings/build.gradle.kts index 2ca81ff..4f584a6 100644 --- a/domain/settings/build.gradle.kts +++ b/domain/settings/build.gradle.kts @@ -49,4 +49,9 @@ dependencies { api(projects.repository.settings) api(projects.repository.sharing) + + testImplementation(libs.mockk) + testImplementation(libs.junit.junit) + testImplementation(libs.truth) + testImplementation(libs.core.testing) } diff --git a/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCase.kt b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCase.kt index 170833f..53f6d25 100644 --- a/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCase.kt +++ b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCase.kt @@ -17,11 +17,13 @@ package photos.network.domain.settings.usecase import photos.network.repository.sharing.UserRepository +private const val CLIENT_ID_MIN_LENGTH = 10 + class VerifyClientIdUseCase( private val userRepository: UserRepository, ) { suspend operator fun invoke(clientId: String): Boolean { - return if (clientId.length > 10) { + return if (clientId.length > CLIENT_ID_MIN_LENGTH) { userRepository.verifyClientId(clientId) } else { false diff --git a/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCase.kt b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCase.kt index 9fb8844..a5182c9 100644 --- a/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCase.kt +++ b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCase.kt @@ -17,11 +17,13 @@ package photos.network.domain.settings.usecase import photos.network.repository.sharing.UserRepository +private const val HOST_MIN_LENGTH = 8 + class VerifyServerHostUseCase( private val userRepository: UserRepository, ) { suspend operator fun invoke(host: String): Boolean { - return if (host.length > 8) { + return if (host.length > HOST_MIN_LENGTH) { userRepository.verifyServerHost(host) } else { false diff --git a/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCaseTests.kt b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCaseTests.kt index 83ee7a6..38f7a3a 100644 --- a/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCaseTests.kt +++ b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCaseTests.kt @@ -22,7 +22,6 @@ import io.mockk.mockk import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test -import photos.network.data.user.repository.UserRepository import photos.network.repository.sharing.UserRepository class VerifyServerHostUseCaseTests { diff --git a/domain/sharing/build.gradle.kts b/domain/sharing/build.gradle.kts index 67426cb..8cd332d 100644 --- a/domain/sharing/build.gradle.kts +++ b/domain/sharing/build.gradle.kts @@ -47,4 +47,9 @@ dependencies { androidTestImplementation(project(":common", "androidTestArtifacts")) api(projects.repository.sharing) + + testImplementation(libs.mockk) + testImplementation(libs.junit.junit) + testImplementation(libs.truth) + testImplementation(libs.core.testing) } diff --git a/domain/sharing/src/main/kotlin/photos/network/domain/sharing/Module.kt b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/Module.kt index 98ba3ef..cf0aaed 100644 --- a/domain/sharing/src/main/kotlin/photos/network/domain/sharing/Module.kt +++ b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/Module.kt @@ -18,5 +18,4 @@ package photos.network.domain.sharing import org.koin.dsl.module val domainSharingModule = module { - } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 29206b4..d1a50d7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -54,6 +54,7 @@ androidx-security-crypto = "1.1.0-alpha03" ktor = "2.1.1" junit-junit = "4.13.2" androidx-arch-core = "2.1.0" +truth = "1.1.3" [libraries] @@ -98,7 +99,7 @@ kotlin-serialization = { group = "org.jetbrains.kotlin", name = "kotlin-serializ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" } mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } -com-google-truth-truth = "com.google.truth:truth:1.1.3" +truth = { module = "com.google.truth:truth", version.ref = "truth" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidx-room" } @@ -165,9 +166,8 @@ coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coi #kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" } #lint-gradle = "com.android.tools.lint:lint-gradle:30.0.4" -#junit-junit = { group = "junit", name = "junit", version.ref = "junit-junit" } - -#core-testing = { group = "androidx.arch.core", name = "core-testing", version.ref = "androidx-arch-core" } +junit-junit = { group = "junit", name = "junit", version.ref = "junit-junit" } +core-testing = { group = "androidx.arch.core", name = "core-testing", version.ref = "androidx-arch-core" } diff --git a/network/src/androidTest/kotlin/photos/network/api/PhotoApiTest.kt b/network/src/androidTest/kotlin/photos/network/api/PhotoApiTest.kt deleted file mode 100644 index 5e9cc70..0000000 --- a/network/src/androidTest/kotlin/photos/network/api/PhotoApiTest.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2020-2022 Photos.network developers - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package photos.network.data.photos.network - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import io.ktor.client.HttpClient -import io.ktor.client.engine.mock.MockEngine -import io.ktor.client.engine.mock.respond -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.http.ContentType -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.http.headersOf -import io.ktor.serialization.kotlinx.json.json -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Test -import org.junit.runner.RunWith -import photos.network.common.PhotosNetworkMockFileReader -import photos.network.data.settings.persistence.SettingsStorage -import photos.network.data.settings.repository.SettingsRepositoryImpl -import photos.network.network.photo.PhotoApiImpl -import photos.network.network.photo.Photos - -/** - * Test the REST interface to the photos.network core instance. - */ -@RunWith(AndroidJUnit4::class) -class PhotoApiTest { - - private val settingsRepository = SettingsRepositoryImpl( - SettingsStorage( - context = InstrumentationRegistry.getInstrumentation().context, - ), - ) - - @Test - fun request_photos_with_valid_data() = runBlocking { - // given - val fakeResponse = PhotosNetworkMockFileReader.readStringFromFile("photos_response_success.json") - val mockEngine = MockEngine { - respond( - content = fakeResponse, - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()), - ) - } - val photoApi = PhotoApiImpl( - httpClient = createHttpClient(mockEngine), - settingsRepository = settingsRepository, - ) - - // when - val result: Photos = photoApi.getPhotos() - - // then - assertEquals(result.size, 2) - assertEquals(result.offset, 13) - assertEquals(result.limit, 25) - assertEquals(result.results.size, 2) - - assertEquals(result.results[0].id, "a1") - assertEquals(result.results[0].name, "xy") - - assertEquals(result.results[1].id, "b2") - assertEquals(result.results[1].name, "yz") - } - - @Test(expected = Exception::class) - fun request_photos_with_empty_response() = runBlocking { - // given - val mockEngine = MockEngine { - respond( - content = "{}", - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()), - ) - } - val photoApi = PhotoApiImpl( - httpClient = createHttpClient(mockEngine), - settingsRepository = settingsRepository, - ) - - // when - photoApi.getPhotos() - - // then - // assert should be skipped, exception should already been raised - assertFalse(true) - } - - private fun createHttpClient(engine: MockEngine): HttpClient { - return HttpClient(engine) { - install(ContentNegotiation) { - json() - } - } - } -} diff --git a/network/src/androidTest/kotlin/photos/network/entity/PhotosTest.kt b/network/src/androidTest/kotlin/photos/network/entity/PhotosTest.kt deleted file mode 100644 index b4919a0..0000000 --- a/network/src/androidTest/kotlin/photos/network/entity/PhotosTest.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2020-2022 Photos.network developers - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package photos.network.data.photos.network.entity - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import kotlinx.coroutines.runBlocking -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json -import org.junit.Assert -import org.junit.Test -import org.junit.runner.RunWith -import photos.network.common.PhotosNetworkMockFileReader -import photos.network.network.photo.Photo -import photos.network.network.photo.Photos - -/** - * Test (de)serializing photos responses. - */ -@RunWith(AndroidJUnit4::class) -class PhotosTest { - - @Test - fun testDeserialization() = runBlocking { - // given - val jsonString = - PhotosNetworkMockFileReader.readStringFromFile("photos_response_success.json") - - // when - val response = Json.decodeFromString(jsonString) - - // then - Assert.assertEquals(13, response.offset) - Assert.assertEquals(25, response.limit) - Assert.assertEquals(2, response.size) - Assert.assertEquals( - listOf( - Photo("a1", "xy", "https://photos.network/foo.raw", null, null), - Photo("b2", "yz", "https://photos.network/bar.raw", null, null), - ), - response.results, - ) - } -// -// @Test -// fun testSerialization() = runBlocking { -// // given -// val input = NetworkPhotos( -// 13, 25, 2, -// listOf( -// NetworkPhoto("a1", "xy", "", "", ""), -// NetworkPhoto("b2", "yz", "", "", ""), -// ) -// ) -// -// // when -// val jsonElement = Json.encodeToJsonElement(input) -// -// // then -// assert(jsonElement.jsonObject["id"].toString() == "a1") -// assert(jsonElement.jsonObject["name"].toString() == "xy") -// } -} diff --git a/network/src/main/kotlin/photos/network/network/ServerStatus.kt b/network/src/main/kotlin/photos/network/network/ServerStatus.kt deleted file mode 100644 index 7dd6d75..0000000 --- a/network/src/main/kotlin/photos/network/network/ServerStatus.kt +++ /dev/null @@ -1,8 +0,0 @@ -package photos.network.network - -enum class ServerStatus { - AVAILABLE(), - UNAVAILABLE(), - PROGRESS(), - UNAUTHORIZED(), -} diff --git a/repository/photos/build.gradle.kts b/repository/photos/build.gradle.kts index 6be9142..a42b794 100644 --- a/repository/photos/build.gradle.kts +++ b/repository/photos/build.gradle.kts @@ -15,6 +15,10 @@ spotless { } } +detekt { + config = files("$rootDir/detekt.yml") +} + android { namespace = "photos.network.repository.photos" @@ -48,76 +52,18 @@ android { dependencies { implementation(projects.common) testImplementation(project(":common", "testArtifacts")) + testImplementation(project(mapOf("path" to ":common"))) androidTestImplementation(project(":common", "androidTestArtifacts")) // workmanager api(libs.work.runtime.ktx) androidTestApi(libs.work.testing) - implementation(projects.network) + implementation(projects.api) api(projects.database.photos) -// api(AndroidX.core.ktx) - - // Coroutines -// api(KotlinX.coroutines.core) -// api(KotlinX.coroutines.android) - - // Coroutine Lifecycle Scopes -// api(AndroidX.lifecycle.runtime.ktx) -// api(AndroidX.lifecycle.viewModelKtx) - - // Koin dependency injection -// api(Koin.core) -// testApi(Koin.test) -// api(Koin.android) -// api(Koin.workManager) -// api(Koin.navigation) -// api(Koin.compose) - - // Persistence -// api(AndroidX.room.runtime) -// api(AndroidX.room.ktx) -// androidTestImplementation(AndroidX.room.testing) - - // exifinterface -// api(AndroidX.exifInterface) - - // httpclient -// implementation(Ktor.client.core) -// implementation(Ktor.client.cio) -// implementation(Ktor.client.cio) -// implementation(Ktor.client.auth) -// implementation(Ktor.client.serialization) -// implementation(Ktor.client.contentNegotiation) -// implementation(Ktor.plugins.serialization.kotlinx.json) -// implementation(libs.ktor.client.logging.jvm) -// implementation(libs.ktor.client.mock.jvm) - - // logging -// api(Square.logcat) - - // serialization -// api(KotlinX.serialization.json) -// api(AndroidX.security.crypto) - - // testing -// testApi(AndroidX.test.ext.junit.ktx) -// testApi(Testing.junit4) -// testApi(libs.com.google.truth.truth) -// testApi(Testing.mockK) -// testApi(KotlinX.coroutines.test) -// testApi(AndroidX.archCore.testing) - -// androidTestApi(AndroidX.test.core) -// androidTestApi(AndroidX.test.coreKtx) -// androidTestApi(AndroidX.test.ext.junit) -// androidTestApi(AndroidX.test.ext.junit.ktx) -// androidTestApi(AndroidX.test.ext.truth) -// androidTestApi(AndroidX.test.monitor) -// androidTestApi(AndroidX.test.orchestrator) -// androidTestApi(AndroidX.test.runner) -// androidTestApi(AndroidX.test.rules) -// androidTestApi(AndroidX.test.services) -// androidTestApi(Testing.mockK) + testImplementation(libs.mockk) + testImplementation(libs.junit.junit) + testImplementation(libs.truth) + testImplementation(libs.core.testing) } diff --git a/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt index 574f62d..4263df5 100644 --- a/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt @@ -28,8 +28,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.mapNotNull import logcat.LogPriority import logcat.logcat +import photos.network.api.photo.PhotoApi import photos.network.database.photos.PhotoDao -import photos.network.network.photo.PhotoApi import photos.network.repository.photos.worker.SyncLocalPhotosWorker import photos.network.repository.photos.worker.SyncStatus import java.time.Instant diff --git a/repository/photos/src/test/kotlin/photos/network/repository/photos/PhotoRepositoryTest.kt b/repository/photos/src/test/kotlin/photos/network/repository/photos/PhotoRepositoryTest.kt index 87fee76..01ea529 100644 --- a/repository/photos/src/test/kotlin/photos/network/repository/photos/PhotoRepositoryTest.kt +++ b/repository/photos/src/test/kotlin/photos/network/repository/photos/PhotoRepositoryTest.kt @@ -25,9 +25,8 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test -import photos.network.data.TestCoroutineDispatcherRule +import photos.network.api.photo.PhotoApi import photos.network.database.photos.PhotoDao -import photos.network.network.photo.PhotoApi /** * Test photo repository @@ -93,26 +92,27 @@ class PhotoRepositoryTest { } @Test - fun `photos returned should be ordered by dateAdded if dateTaken is not available`() = runBlocking { - // given - every { photoDao.getPhotos() } answers { - flowOf( - listOf( - createFakePhoto(filename = "002", dateTaken = null, dateAdded = 1580671221), - createFakePhoto(filename = "001", dateTaken = null, dateAdded = 1580671220), - createFakePhoto(filename = "003", dateTaken = null, dateAdded = 1580671223), - ), - ) - } + fun `photos returned should be ordered by dateAdded if dateTaken is not available`() = + runBlocking { + // given + every { photoDao.getPhotos() } answers { + flowOf( + listOf( + createFakePhoto(filename = "002", dateTaken = null, dateAdded = 1580671221), + createFakePhoto(filename = "001", dateTaken = null, dateAdded = 1580671220), + createFakePhoto(filename = "003", dateTaken = null, dateAdded = 1580671223), + ), + ) + } - // when - val photos = repository.getPhotos().first() + // when + val photos = repository.getPhotos().first() - // then - Truth.assertThat(photos[0].filename).isEqualTo("003") - Truth.assertThat(photos[1].filename).isEqualTo("002") - Truth.assertThat(photos[2].filename).isEqualTo("001") - } + // then + Truth.assertThat(photos[0].filename).isEqualTo("003") + Truth.assertThat(photos[1].filename).isEqualTo("002") + Truth.assertThat(photos[2].filename).isEqualTo("001") + } private fun createFakePhoto( uuid: String = "001", @@ -125,14 +125,14 @@ class PhotoRepositoryTest { originalFileUri: String? = null, ): Photo { return Photo( - uuid = uuid, +// uuid = uuid, filename = filename, imageUrl = imageUrl, dateAdded = dateAdded, dateTaken = dateTaken, dateModified = dateModified, - thumbnailFileUri = thumbnailFileUri, - originalFileUri = originalFileUri, +// thumbnailFileUri = thumbnailFileUri, +// originalFileUri = originalFileUri, ) } } diff --git a/repository/settings/build.gradle.kts b/repository/settings/build.gradle.kts index 2ffef8e..c9e26ce 100644 --- a/repository/settings/build.gradle.kts +++ b/repository/settings/build.gradle.kts @@ -16,6 +16,10 @@ spotless { } } +detekt { + config = files("$rootDir/detekt.yml") +} + android { namespace = "photos.network.repository.settings" @@ -65,8 +69,6 @@ android { dependencies { api(projects.common) -// testApi(projects.common) -// androidTestApi(projects.common) testImplementation(project(":common", "testArtifacts")) androidTestImplementation(project(":common", "androidTestArtifacts")) diff --git a/repository/sharing/build.gradle.kts b/repository/sharing/build.gradle.kts index c6725d3..9f98407 100644 --- a/repository/sharing/build.gradle.kts +++ b/repository/sharing/build.gradle.kts @@ -15,6 +15,10 @@ spotless { } } +detekt { + config = files("$rootDir/detekt.yml") +} + android { namespace = "photos.network.repository.sharing" @@ -74,8 +78,13 @@ configurations { dependencies { api(project(":common")) testImplementation(project(":common", "testArtifacts")) + testImplementation(project(mapOf("path" to ":common"))) androidTestImplementation(project(":common", "androidTestArtifacts")) - api(projects.network) + api(projects.api) api(projects.database.sharing) + + testImplementation(libs.mockk) + testImplementation(libs.junit.junit) + testImplementation(libs.truth) } diff --git a/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepositoryImpl.kt b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepositoryImpl.kt index 0e40baf..ec5ef14 100644 --- a/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepositoryImpl.kt +++ b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepositoryImpl.kt @@ -19,8 +19,8 @@ import io.ktor.client.plugins.ServerResponseException import kotlinx.coroutines.runBlocking import logcat.LogPriority import logcat.logcat +import photos.network.api.user.UserApi import photos.network.common.persistence.SecureStorage -import photos.network.network.user.UserApi import java.net.ConnectException class UserRepositoryImpl( diff --git a/repository/settings/src/test/kotlin/photos/network/repository/settings/UserRepositoryTests.kt b/repository/sharing/src/test/kotlin/photos/network/repository/sharing/UserRepositoryTests.kt similarity index 87% rename from repository/settings/src/test/kotlin/photos/network/repository/settings/UserRepositoryTests.kt rename to repository/sharing/src/test/kotlin/photos/network/repository/sharing/UserRepositoryTests.kt index b2933e1..3a72afa 100644 --- a/repository/settings/src/test/kotlin/photos/network/repository/settings/UserRepositoryTests.kt +++ b/repository/sharing/src/test/kotlin/photos/network/repository/sharing/UserRepositoryTests.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.repository.settings +package photos.network.repository.sharing import com.google.common.truth.Truth import io.mockk.coEvery @@ -22,17 +22,17 @@ import io.mockk.mockk import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test +import photos.network.api.user.UserApi +import photos.network.api.user.entity.NetworkUser import photos.network.common.TestCoroutineDispatcherRule -import photos.network.data.user.network.UserApi -import photos.network.data.user.network.model.NetworkUser -import photos.network.data.user.persistence.UserStorage +import photos.network.common.persistence.SecureStorage import photos.network.common.persistence.User as DatabaseUser class UserRepositoryTests { @get:Rule val coroutineRule = TestCoroutineDispatcherRule() - private val userStorage = mockk() + private val userStorage = mockk>() private val userApi = mockk() @@ -46,7 +46,7 @@ class UserRepositoryTests { @Test fun `should reflect user from persistence`() = runBlocking { // given - every { userStorage.read() } answers { fakeUser() } + every { userStorage.read()?.lastname } answers { fakeUser().lastname } every { userStorage.save(any()) } answers { Unit } coEvery { userApi.getUser() } answers { fakeNetworkUser() } @@ -80,7 +80,7 @@ class UserRepositoryTests { lastname: String = "Jane", firstname: String = "http://localhost/foo/bar/jane.jpg", lastSeen: String = "", - ): User = User( + ): NetworkUser = NetworkUser( id = id, email = email, lastname = lastname, diff --git a/run_tests b/run_tests index 404ecab..9ca5fe2 100755 --- a/run_tests +++ b/run_tests @@ -1 +1,6 @@ -./gradlew spotlessCheck detekt lint testDebugUnitTest connectedAndroidTest :app:bundleDebug +./gradlew spotlessCheck && +./gradlew detekt && +./gradlew lint && +./gradlew testDebugUnitTest && +./gradlew connectedAndroidTest && +./gradlew :app:bundleDebug diff --git a/settings.gradle.kts b/settings.gradle.kts index fd62b9c..7e482a1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -52,8 +52,8 @@ include(":database:photos") include(":database:settings") include(":database:sharing") -// communication via REST with core instance -include(":network") +// communication via REST API with core instance +include(":api") // instance and account include(":system:account") diff --git a/ui/albums/build.gradle.kts b/ui/albums/build.gradle.kts index a78d127..f16223e 100644 --- a/ui/albums/build.gradle.kts +++ b/ui/albums/build.gradle.kts @@ -15,6 +15,10 @@ spotless { } } +detekt { + config = files("$rootDir/detekt.yml") +} + android { namespace = "photos.network.ui.albums" diff --git a/ui/albums/src/main/kotlin/photos/network/ui/albums/Module.kt b/ui/albums/src/main/kotlin/photos/network/ui/albums/Module.kt index 14dbe3e..1e91f18 100644 --- a/ui/albums/src/main/kotlin/photos/network/ui/albums/Module.kt +++ b/ui/albums/src/main/kotlin/photos/network/ui/albums/Module.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package photos.network.ui.albums import org.koin.dsl.module diff --git a/ui/common/build.gradle.kts b/ui/common/build.gradle.kts index 2ce3a0c..cf86234 100644 --- a/ui/common/build.gradle.kts +++ b/ui/common/build.gradle.kts @@ -15,6 +15,10 @@ spotless { } } +detekt { + config = files("$rootDir/detekt.yml") +} + android { namespace = "photos.network.ui.common" @@ -48,7 +52,7 @@ dependencies { testImplementation(project(":common", "testArtifacts")) androidTestImplementation(project(":common", "androidTestArtifacts")) - implementation(projects.network) + implementation(projects.api) // Compose api(platform(libs.compose.bom)) diff --git a/ui/common/src/main/kotlin/photos/network/ui/common/components/AppLogo.kt b/ui/common/src/main/kotlin/photos/network/ui/common/components/AppLogo.kt index 851075c..057ebe2 100644 --- a/ui/common/src/main/kotlin/photos/network/ui/common/components/AppLogo.kt +++ b/ui/common/src/main/kotlin/photos/network/ui/common/components/AppLogo.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout -import photos.network.network.ServerStatus +import photos.network.api.ServerStatus import photos.network.ui.common.R import photos.network.ui.common.theme.AppTheme diff --git a/ui/common/src/main/kotlin/photos/network/ui/common/navigation/Destination.kt b/ui/common/src/main/kotlin/photos/network/ui/common/navigation/Destination.kt index 5f8bb4a..46d5746 100644 --- a/ui/common/src/main/kotlin/photos/network/ui/common/navigation/Destination.kt +++ b/ui/common/src/main/kotlin/photos/network/ui/common/navigation/Destination.kt @@ -19,7 +19,6 @@ import android.os.Bundle import androidx.annotation.StringRes import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Folder -import androidx.compose.material.icons.filled.Help import androidx.compose.material.icons.filled.House import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.People diff --git a/app/src/main/res/values-ar/strings.xml b/ui/common/src/main/res/values-ar/strings.xml similarity index 100% rename from app/src/main/res/values-ar/strings.xml rename to ui/common/src/main/res/values-ar/strings.xml diff --git a/app/src/main/res/values-fr/strings.xml b/ui/common/src/main/res/values-fr/strings.xml similarity index 100% rename from app/src/main/res/values-fr/strings.xml rename to ui/common/src/main/res/values-fr/strings.xml diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/ui/common/src/main/res/values-ko-rKR/strings.xml similarity index 100% rename from app/src/main/res/values-ko-rKR/strings.xml rename to ui/common/src/main/res/values-ko-rKR/strings.xml diff --git a/app/src/main/res/values-ko/strings.xml b/ui/common/src/main/res/values-ko/strings.xml similarity index 100% rename from app/src/main/res/values-ko/strings.xml rename to ui/common/src/main/res/values-ko/strings.xml diff --git a/ui/folders/build.gradle.kts b/ui/folders/build.gradle.kts index 6034154..530f8c7 100644 --- a/ui/folders/build.gradle.kts +++ b/ui/folders/build.gradle.kts @@ -15,6 +15,10 @@ spotless { } } +detekt { + config = files("$rootDir/detekt.yml") +} + android { namespace = "photos.network.ui.folders" diff --git a/ui/folders/src/main/kotlin/photos/network/ui/folders/Module.kt b/ui/folders/src/main/kotlin/photos/network/ui/folders/Module.kt index c053360..c01fa91 100644 --- a/ui/folders/src/main/kotlin/photos/network/ui/folders/Module.kt +++ b/ui/folders/src/main/kotlin/photos/network/ui/folders/Module.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package photos.network.ui.folders import org.koin.dsl.module diff --git a/ui/photos/build.gradle.kts b/ui/photos/build.gradle.kts index 59e3577..6bc8f87 100644 --- a/ui/photos/build.gradle.kts +++ b/ui/photos/build.gradle.kts @@ -15,6 +15,10 @@ spotless { } } +detekt { + config = files("$rootDir/detekt.yml") +} + android { namespace = "photos.network.ui.photos" @@ -52,7 +56,6 @@ dependencies { api(projects.domain.photos) implementation(projects.ui.common) -// implementation(projects.repository.photos) // Compose implementation(platform(libs.compose.bom)) @@ -60,4 +63,9 @@ dependencies { // accompanist implementation(libs.bundles.accompanist) + + testImplementation(libs.mockk) + testImplementation(libs.junit.junit) + testImplementation(libs.truth) + testImplementation(libs.core.testing) } diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/Module.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/Module.kt index 7e31580..0ccdb52 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/Module.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/Module.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package photos.network.ui.photos import org.koin.androidx.viewmodel.dsl.viewModel diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt index 7bfa1eb..6d42a24 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt @@ -42,7 +42,6 @@ import androidx.compose.ui.unit.dp import coil.compose.rememberImagePainter import photos.network.repository.photos.Photo import photos.network.ui.common.theme.AppTheme -import photos.network.ui.photos.PhotoDetails import java.time.Instant import java.time.ZoneOffset @@ -85,7 +84,6 @@ fun PhotoGrid( // add year header if necessary if (yearOfFirst != yearNow) { - item(span = { GridItemSpan(maxCurrentLineSpan) }) { Text( text = yearOfFirst.toString(), diff --git a/ui/photos/src/main/res/drawable/image_placeholder.xml b/ui/photos/src/main/res/drawable/image_placeholder.xml index 012e140..d90ffd6 100644 --- a/ui/photos/src/main/res/drawable/image_placeholder.xml +++ b/ui/photos/src/main/res/drawable/image_placeholder.xml @@ -1,6 +1,6 @@ = emptyList(), val appVersion: String = "Unknown", ) - diff --git a/ui/sharing/build.gradle.kts b/ui/sharing/build.gradle.kts index 12e9f8e..340ce76 100644 --- a/ui/sharing/build.gradle.kts +++ b/ui/sharing/build.gradle.kts @@ -15,6 +15,10 @@ spotless { } } +detekt { + config = files("$rootDir/detekt.yml") +} + android { namespace = "photos.network.ui.sharing" diff --git a/ui/sharing/src/main/kotlin/photos/network/ui/sharing/Module.kt b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/Module.kt index 1d35bdd..9de15ed 100644 --- a/ui/sharing/src/main/kotlin/photos/network/ui/sharing/Module.kt +++ b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/Module.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package photos.network.ui.sharing import org.koin.androidx.viewmodel.dsl.viewModel From 9a7e83c985cb228a2aadcdc7820054eb87f2a1e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Stu=CC=88rmer?= Date: Fri, 5 May 2023 07:24:56 +0200 Subject: [PATCH 03/13] adjust formatting --- app/src/main/kotlin/photos/network/home/Home.kt | 15 ++++++++------- .../photos/usecase/GetPhotosUseCaseTests.kt | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/photos/network/home/Home.kt b/app/src/main/kotlin/photos/network/home/Home.kt index a104483..f2e1a66 100644 --- a/app/src/main/kotlin/photos/network/home/Home.kt +++ b/app/src/main/kotlin/photos/network/home/Home.kt @@ -19,7 +19,6 @@ import android.content.res.Configuration import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding @@ -130,9 +129,10 @@ fun Home( ) } } - }, colors = TopAppBarDefaults.smallTopAppBarColors( + }, + colors = TopAppBarDefaults.smallTopAppBarColors( containerColor = MaterialTheme.colorScheme.primary, - ) + ), ) } }, @@ -177,10 +177,11 @@ fun Home( } else { 0.dp } - Box(modifier = Modifier - .padding(bottom = innerPadding.calculateBottomPadding()) - .padding(top = topPadding) - .border(2.dp, Color.Green) + Box( + modifier = Modifier + .padding(bottom = innerPadding.calculateBottomPadding()) + .padding(top = topPadding) + .border(2.dp, Color.Green), ) { NavHost( navController = navController, diff --git a/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCaseTests.kt b/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCaseTests.kt index 9a1026e..49267a0 100644 --- a/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCaseTests.kt +++ b/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCaseTests.kt @@ -24,12 +24,12 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test -import java.time.Instant import photos.network.common.persistence.PrivacyState import photos.network.common.persistence.Settings import photos.network.repository.photos.Photo import photos.network.repository.photos.PhotoRepository import photos.network.repository.settings.SettingsRepository +import java.time.Instant class GetPhotosUseCaseTests { @Rule From 81412421468441bb9e1206dac83b771c9f001152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Stu=CC=88rmer?= Date: Mon, 15 May 2023 23:13:28 +0200 Subject: [PATCH 04/13] add more separations --- .../kotlin/photos/network/api/ApiModule.kt | 28 +- .../photos/network/api/user/UserApiImpl.kt | 5 +- .../network/PhotosNetworkApplication.kt | 7 + common/build.gradle.kts | 9 + data/src/debug/assets/photo_object.json | 7 - .../debug/assets/photos_response_success.json | 17 - .../kotlin/photos/network/data/DataModule.kt | 83 ----- .../repository/SettingsRepositoryTests.kt | 202 ------------ .../network/data/user/network/UserApiTests.kt | 302 ------------------ .../user/network/model/NetworkUserTests.kt | 48 --- .../data/user/network/model/TokenInfoTests.kt | 46 --- .../network/database/settings/Module.kt | 6 +- .../photos/network/database/sharing/Module.kt | 6 +- .../network/database/sharing/UserStorage.kt | 8 +- domain/folders/build.gradle.kts | 2 + .../domain/folders/GetFoldersUseCase.kt | 11 + .../usecase/GetSettingsUseCaseTests.kt | 4 +- .../usecase/VerifyClientIdUseCaseTests.kt | 2 +- .../photos/network/domain/sharing/Module.kt | 6 + .../network/domain/sharing/UserMapper.kt | 39 +++ .../usecase/GetCurrentUserUseCaseTests.kt | 7 +- .../sharing/usecase/LogoutUseCaseTests.kt | 2 +- .../usecase/RequestAccessTokenUseCaseTests.kt | 2 +- repository/folders/build.gradle.kts | 64 ++++ .../folders/src/main/AndroidManifest.xml | 2 + .../repository/folders/FoldersRepository.kt | 26 ++ .../folders/FoldersRepositoryImpl.kt | 33 +- .../network/repository/folders/Module.kt | 26 ++ .../folders/FoldersRepositoryTest.kt | 138 ++++++++ repository/photos/build.gradle.kts | 2 + .../network/repository/photos/Module.kt | 2 +- .../repository/photos/PhotoRepositoryImpl.kt | 199 ++---------- .../network/repository/settings/Module.kt | 5 +- .../network/repository/sharing/Module.kt | 4 +- .../photos/network/repository/sharing/User.kt | 21 +- .../repository/sharing/UserRepository.kt | 3 +- settings.gradle.kts | 1 + .../network/system/filesystem/FileSystem.kt | 20 ++ .../system/filesystem/FileSystemImpl.kt | 15 + .../network/system/filesystem/Module.kt | 9 + .../network/system/mediastore/MediaStore.kt | 26 ++ .../system/mediastore/MediaStoreImpl.kt | 178 +++++++++++ .../network/system/mediastore/Module.kt | 24 ++ 43 files changed, 687 insertions(+), 960 deletions(-) delete mode 100644 data/src/debug/assets/photo_object.json delete mode 100644 data/src/debug/assets/photos_response_success.json delete mode 100644 data/src/main/kotlin/photos/network/data/DataModule.kt delete mode 100644 data/src/test/kotlin/photos/network/data/settings/repository/SettingsRepositoryTests.kt delete mode 100644 data/src/test/kotlin/photos/network/data/user/network/UserApiTests.kt delete mode 100644 data/src/test/kotlin/photos/network/data/user/network/model/NetworkUserTests.kt delete mode 100644 data/src/test/kotlin/photos/network/data/user/network/model/TokenInfoTests.kt create mode 100644 domain/folders/src/main/kotlin/photos/network/domain/folders/GetFoldersUseCase.kt create mode 100644 domain/sharing/src/test/kotlin/photos/network/domain/sharing/UserMapper.kt create mode 100644 repository/folders/build.gradle.kts create mode 100644 repository/folders/src/main/AndroidManifest.xml create mode 100644 repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepository.kt rename data/src/test/kotlin/photos/network/data/user/network/model/ApiResponseTests.kt => repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepositoryImpl.kt (50%) create mode 100644 repository/folders/src/main/kotlin/photos/network/repository/folders/Module.kt create mode 100644 repository/folders/src/test/kotlin/photos/network/repository/folders/FoldersRepositoryTest.kt create mode 100644 system/filesystem/src/main/kotlin/photos/network/system/filesystem/FileSystem.kt create mode 100644 system/filesystem/src/main/kotlin/photos/network/system/filesystem/FileSystemImpl.kt create mode 100644 system/filesystem/src/main/kotlin/photos/network/system/filesystem/Module.kt create mode 100644 system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStore.kt create mode 100644 system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStoreImpl.kt create mode 100644 system/mediastore/src/main/kotlin/photos/network/system/mediastore/Module.kt diff --git a/api/src/main/kotlin/photos/network/api/ApiModule.kt b/api/src/main/kotlin/photos/network/api/ApiModule.kt index f0314b2..c78ba48 100644 --- a/api/src/main/kotlin/photos/network/api/ApiModule.kt +++ b/api/src/main/kotlin/photos/network/api/ApiModule.kt @@ -38,11 +38,13 @@ import io.ktor.client.request.port import io.ktor.client.request.url import io.ktor.client.statement.request import io.ktor.http.Parameters +import io.ktor.http.URLProtocol import io.ktor.http.encodedPath import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json import logcat.LogPriority import logcat.logcat +import org.koin.core.qualifier.named import org.koin.dsl.module import photos.network.api.photo.PhotoApi import photos.network.api.photo.PhotoApiImpl @@ -57,16 +59,16 @@ val apiModule = module { single { provideKtorClient( application = get(), - userStorage = get(), - settingsStore = get(), + userStorage = get(qualifier = named("UserStorage")), + settingsStorage = get(qualifier = named("SettingsStorage")), ) } single { UserApiImpl( httpClient = get(), - settingsStorage = get(), - userStorage = get(), + userStorage = get(qualifier = named("UserStorage")), + settingsStorage = get(qualifier = named("SettingsStorage")) ) } @@ -79,7 +81,7 @@ val apiModule = module { private fun provideKtorClient( application: Application, userStorage: SecureStorage, - settingsStore: SecureStorage, + settingsStorage: SecureStorage, ): HttpClient { val client = HttpClient(CIO) { expectSuccess = false @@ -152,16 +154,9 @@ private fun provideKtorClient( // called after receiving a 401 (Unauthorized) response with the WWW-Authenticate header refreshTokens { val refreshToken = userStorage.read()?.refreshToken ?: "" - val host = settingsStore.read()?.host ?: "" - val clientId = settingsStore.read()?.clientId ?: "" + val host = settingsStorage.read()?.host ?: "" + val clientId = settingsStorage.read()?.clientId ?: "" - /** - * OAuth refresh token request based on [RFC6749](https://tools.ietf.org/html/rfc6749#section-6) - * - * @param refreshToken The refresh token issued to the client. - * @param clientId The client identifier issued by the authorization server. - * @param scope list of case-sensitive strings to grant access based on. - */ /** * OAuth refresh token request based on [RFC6749](https://tools.ietf.org/html/rfc6749#section-6) * @@ -211,8 +206,9 @@ private fun provideKtorClient( client.plugin(HttpSend).intercept { request -> // replace port and host for each call @Suppress("MagicNumber") - request.port = settingsStore.read()?.port ?: 443 - request.url(settingsStore.read()?.host ?: "") + request.port = settingsStorage.read()?.port ?: 443 + request.url.host = settingsStorage.read()?.host ?: "" + request.url.protocol = URLProtocol.HTTPS val originalCall = execute(request) diff --git a/api/src/main/kotlin/photos/network/api/user/UserApiImpl.kt b/api/src/main/kotlin/photos/network/api/user/UserApiImpl.kt index 711d758..9d98d9d 100644 --- a/api/src/main/kotlin/photos/network/api/user/UserApiImpl.kt +++ b/api/src/main/kotlin/photos/network/api/user/UserApiImpl.kt @@ -18,6 +18,7 @@ package photos.network.api.user import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.forms.submitForm +import io.ktor.client.request.get import io.ktor.client.request.parameter import io.ktor.client.request.port import io.ktor.client.request.request @@ -44,9 +45,7 @@ class UserApiImpl( @Suppress("TooGenericExceptionCaught", "ReturnCount") override suspend fun verifyServerHost(host: String): Boolean { try { - val response: HttpResponse = httpClient.request("$host/api") { - method = HttpMethod.Get - } + val response: HttpResponse = httpClient.get("/api") if (!response.status.isSuccess()) { return false diff --git a/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt b/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt index 403127c..2db69ce 100644 --- a/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt +++ b/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt @@ -33,9 +33,12 @@ import photos.network.domain.folders.domainFoldersModule import photos.network.domain.photos.domainPhotosModule import photos.network.domain.settings.domainSettingsModule import photos.network.domain.sharing.domainSharingModule +import photos.network.repository.folders.repositoryFoldersModule import photos.network.repository.photos.repositoryPhotosModule import photos.network.repository.settings.repositorySettingsModule import photos.network.repository.sharing.repositorySharingModule +import photos.network.system.filesystem.systemFilesystemModule +import photos.network.system.mediastore.systemMediastoreModule import photos.network.ui.albums.uiAlbumsModule import photos.network.ui.folders.uiFoldersModule import photos.network.ui.photos.uiPhotosModule @@ -77,6 +80,7 @@ open class PhotosNetworkApplication : Application(), KoinComponent { // folders uiFoldersModule, domainFoldersModule, + repositoryFoldersModule, // photos uiPhotosModule, @@ -97,6 +101,9 @@ open class PhotosNetworkApplication : Application(), KoinComponent { databaseSharingModule, apiModule, + + systemFilesystemModule, + systemMediastoreModule, ), ) } diff --git a/common/build.gradle.kts b/common/build.gradle.kts index b1ba1de..c666f38 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -37,6 +37,15 @@ android { } } +kover { + filters { + classes { + excludes += "photos.network.common.Module*" + excludes += "photos.network.common.BuildConfig" + } + } +} + configurations { create("testArtifacts"){ extendsFrom(configurations.testApi.get()) diff --git a/data/src/debug/assets/photo_object.json b/data/src/debug/assets/photo_object.json deleted file mode 100644 index d14989e..0000000 --- a/data/src/debug/assets/photo_object.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "id": "photoIdentifier", - "name": "photoName", - "image_url": "", - "date_added": "", - "date_taken": "" -} diff --git a/data/src/debug/assets/photos_response_success.json b/data/src/debug/assets/photos_response_success.json deleted file mode 100644 index 3f5c03a..0000000 --- a/data/src/debug/assets/photos_response_success.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "offset": "13", - "limit": 25, - "size": 2, - "results": [ - { - "id": "a1", - "name": "xy", - "image_url": "https://photos.network/foo.raw" - }, - { - "id": "b2", - "name": "yz", - "image_url": "https://photos.network/bar.raw" - } - ] -} diff --git a/data/src/main/kotlin/photos/network/data/DataModule.kt b/data/src/main/kotlin/photos/network/data/DataModule.kt deleted file mode 100644 index 040537a..0000000 --- a/data/src/main/kotlin/photos/network/data/DataModule.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2020-2022 Photos.network developers - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package photos.network.data - -import android.content.Context -import androidx.room.Room -import androidx.work.WorkManager -import io.ktor.client.HttpClient -import io.ktor.client.call.body -import io.ktor.client.engine.cio.CIO -import io.ktor.client.plugins.auth.Auth -import io.ktor.client.plugins.auth.providers.BearerTokens -import io.ktor.client.plugins.auth.providers.bearer -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.plugins.logging.LogLevel -import io.ktor.client.plugins.logging.Logger -import io.ktor.client.plugins.logging.Logging -import io.ktor.client.plugins.observer.ResponseObserver -import io.ktor.client.request.forms.submitForm -import io.ktor.client.statement.request -import io.ktor.http.Parameters -import io.ktor.http.encodedPath -import io.ktor.serialization.kotlinx.json.json -import kotlinx.serialization.json.Json -import logcat.LogPriority -import logcat.logcat -import org.koin.android.ext.koin.androidApplication -import org.koin.androidx.workmanager.dsl.worker -import org.koin.dsl.module -import photos.network.data.photos.network.PhotoApi -import photos.network.data.photos.network.PhotoApiImpl -import photos.network.data.photos.persistence.MIGRATION_1_2 -import photos.network.data.photos.persistence.PhotoDao -import photos.network.data.photos.persistence.PhotoDatabase -import photos.network.data.photos.repository.PhotoRepository -import photos.network.data.photos.repository.PhotoRepositoryImpl -import photos.network.data.photos.worker.SyncLocalPhotosWorker -import photos.network.data.settings.persistence.SettingsStorage -import photos.network.data.settings.repository.SettingsRepository -import photos.network.data.settings.repository.SettingsRepositoryImpl -import photos.network.data.user.network.UserApi -import photos.network.data.user.network.UserApiImpl -import photos.network.data.user.network.model.TokenInfo -import photos.network.data.user.persistence.User -import photos.network.data.user.persistence.UserStorage -import photos.network.data.user.repository.UserRepository -import photos.network.data.user.repository.UserRepositoryImpl - -val dataModule = module { - - single { - UserRepositoryImpl( - userApi = get(), - userStorage = get() - ) - } - - single { providePhotoDatabase(get()) } - factory { providePhotoDao(get()) } - - single { - SettingsStorage(context = get()) - } - - single { - SettingsRepositoryImpl( - settingsStore = get(), - ) - } -} diff --git a/data/src/test/kotlin/photos/network/data/settings/repository/SettingsRepositoryTests.kt b/data/src/test/kotlin/photos/network/data/settings/repository/SettingsRepositoryTests.kt deleted file mode 100644 index b0b50b0..0000000 --- a/data/src/test/kotlin/photos/network/data/settings/repository/SettingsRepositoryTests.kt +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright 2020-2022 Photos.network developers - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package photos.network.data.settings.repository - -import com.google.common.truth.Truth -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking -import org.junit.Rule -import org.junit.Test -import photos.network.data.TestCoroutineDispatcherRule -import photos.network.data.settings.persistence.SettingsStorage -import photos.network.data.settings.persistence.Settings as PersistenceSettings - -class SettingsRepositoryTests { - @get:Rule - val coroutineRule = TestCoroutineDispatcherRule() - - private val settingsStorage = mockk() - - private val settingRepository by lazy { - SettingsRepositoryImpl( - settingsStore = settingsStorage - ) - } - - @Test - fun `should reflect settings from persistence`() = runBlocking { - // given - every { settingsStorage.read() } answers { fakeSettingsDto() } - - // when - val settings = settingRepository.settings.first() - - // then - Truth.assertThat(settings).isNotNull() - Truth.assertThat(settings.host).isEqualTo("http://127.0.0.1") - Truth.assertThat(settings.clientId).isEqualTo("1234567a-b89c-0d12-ef34-g5h67ij8k90l") - Truth.assertThat(settings.privacyState).isEqualTo(PrivacyState.NONE) - } - - @Test - fun `recently persisted data should be kept inside cache`() = runBlocking { - // given - every { settingsStorage.read() } answers { fakeSettingsDto() } - every { settingsStorage.save(any()) } returns Unit - - // when - settingRepository.saveSettings() - settingRepository.loadSettings() - - // then - verify(exactly = 1) { settingsStorage.read() } - verify(exactly = 1) { settingsStorage.save(any()) } - } - - @Test - fun `should create new instance if nothing is stored yet`() = runBlocking { - // given - every { settingsStorage.read() } answers { null } - - // when - settingRepository.loadSettings() - - // then - verify(exactly = 1) { settingsStorage.read() } - } - - @Test - fun `update settings should be persisted immediately`() = runBlocking { - // given - val clientId = "7654321b-c98b-0d12-ef34-g5h67ij8k90l" - every { settingsStorage.read() } answers { fakeSettingsDto() } - every { settingsStorage.save(any()) } returns Unit - val settings = createTestdata(clientId = clientId) - - // when - settingRepository.updateSettings(settings) - val updatedSettings = settingRepository.settings.first() - - // then - verify(exactly = 1) { settingsStorage.save(any()) } - Truth.assertThat(updatedSettings.clientId).isEqualTo(clientId) - } - - @Test - fun `toggle privacy setting should change settings`() = runBlocking { - // given - every { settingsStorage.read() } answers { fakeSettingsDto() } - every { settingsStorage.save(any()) } returns Unit - - // when - val oldSettings = settingRepository.settings.first() - settingRepository.togglePrivacy() - val updatedSettings = settingRepository.settings.first() - - // then - Truth.assertThat(oldSettings.privacyState).isEqualTo(PrivacyState.NONE) - Truth.assertThat(updatedSettings.privacyState).isEqualTo(PrivacyState.ACTIVE) - } - - @Test - fun `update host should change settings`() = runBlocking { - // given - every { settingsStorage.read() } answers { fakeSettingsDto() } - every { settingsStorage.save(any()) } returns Unit - - // when - val oldSettings = settingRepository.settings.first() - settingRepository.updateHost("http://10.10.10.10") - val updatedSettings = settingRepository.settings.first() - - // then - Truth.assertThat(oldSettings.host).isEqualTo("http://127.0.0.1") - Truth.assertThat(updatedSettings.host).isEqualTo("http://10.10.10.10") - } - - @Test - fun `update clientId should change settings`() = runBlocking { - // given - val clientId = "7654321b-c98b-0d12-ef34-g5h67ij8k90l" - every { settingsStorage.read() } answers { fakeSettingsDto() } - every { settingsStorage.save(any()) } returns Unit - - // when - val oldSettings = settingRepository.settings.first() - settingRepository.updateClientId(clientId) - val updatedSettings = settingRepository.settings.first() - - // then - Truth.assertThat(oldSettings.clientId).isEqualTo("1234567a-b89c-0d12-ef34-g5h67ij8k90l") - Truth.assertThat(updatedSettings.clientId).isEqualTo(clientId) - } - - @Test - fun `settings flow reflect current settings`() = runBlocking { - // given - every { settingsStorage.read() } answers { fakeSettingsDto() } - every { settingsStorage.save(any()) } returns Unit - - // when - settingRepository.togglePrivacy() - val result1 = settingRepository.settings.first() - settingRepository.togglePrivacy() - val result2 = settingRepository.settings.first() - - // then - Truth.assertThat(result1.privacyState).isEqualTo(PrivacyState.ACTIVE) - Truth.assertThat(result2.privacyState).isEqualTo(PrivacyState.NONE) - } - - @Test - fun `delete should call storage`() = runBlocking { - // given - every { settingsStorage.read() } answers { fakeSettingsDto() } - every { settingsStorage.save(any()) } returns Unit - every { settingsStorage.delete() } returns Unit - - // when - settingRepository.deleteSettings() - - // then - verify(exactly = 1) { settingsStorage.delete() } - } - - private fun createTestdata( - host: String = "http://127.0.0.1", - clientId: String = "1234567a-b89c-0d12-ef34-g5h67ij8k90l", - privacyState: String = "NONE" - ): Settings { - return Settings( - host = host, - clientId = clientId, - privacyState = PrivacyState.valueOf(privacyState) - ) - } - - private fun fakeSettingsDto( - host: String = "http://127.0.0.1", - clientId: String = "1234567a-b89c-0d12-ef34-g5h67ij8k90l", - privacyState: String = "NONE" - ): PersistenceSettings = PersistenceSettings( - host = host, - clientId = clientId, - privacyState = privacyState - ) -} diff --git a/data/src/test/kotlin/photos/network/data/user/network/UserApiTests.kt b/data/src/test/kotlin/photos/network/data/user/network/UserApiTests.kt deleted file mode 100644 index afc29ad..0000000 --- a/data/src/test/kotlin/photos/network/data/user/network/UserApiTests.kt +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright 2020-2022 Photos.network developers - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package photos.network.data.user.network - -import com.google.common.truth.Truth -import io.ktor.client.HttpClient -import io.ktor.client.engine.mock.MockEngine -import io.ktor.client.engine.mock.respond -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.http.ContentType -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.http.headersOf -import io.ktor.serialization.kotlinx.json.json -import io.ktor.utils.io.ByteReadChannel -import io.mockk.coEvery -import io.mockk.mockk -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.runBlocking -import kotlinx.serialization.json.Json -import org.junit.Before -import org.junit.Test -import photos.network.data.settings.repository.PrivacyState -import photos.network.data.settings.repository.Settings -import photos.network.data.settings.repository.SettingsRepository -import photos.network.data.user.persistence.User -import photos.network.data.user.persistence.UserStorage - -class UserApiTests { - private val userStorage = mockk() - private val settingsRepository = mockk() - - @Before - fun setup() { - coEvery { userStorage.read() } answers { - User( - id = "id123", - lastname = "Lastname", - firstname = "Firstname", - profileImageUrl = "" - ) - } - coEvery { userStorage.save(any()) } answers {} - coEvery { settingsRepository.settings } answers { - flowOf( - Settings( - host = "http://localhost", - privacyState = PrivacyState.NONE - ) - ) - } - } - - @Test - fun `verify server host should succeed`() = runBlocking { - // given - val userApi = UserApiImpl( - httpClient = HttpClient( - MockEngine { - respond( - content = ByteReadChannel( - """ -{ - "message": "API running" -} - """.trimIndent() - ), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/json") - ) - } - ) { - install(ContentNegotiation) { - json( - Json { - prettyPrint = true - isLenient = true - ignoreUnknownKeys = true - }, - contentType = ContentType.Application.Json - ) - } - }, - userStorage = userStorage, - settingsRepository = settingsRepository - ) - - // when - val result = userApi.verifyServerHost("localhost") - - // then - Truth.assertThat(result).isEqualTo(true) - } - - @Test - fun `verify invalid server host should fail`() = runBlocking { - // given - val userApi = UserApiImpl( - httpClient = HttpClient( - MockEngine { - respond( - content = ByteReadChannel("""{}""".trimIndent()), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/json") - ) - } - ) { - install(ContentNegotiation) { - json( - Json { - prettyPrint = true - isLenient = true - ignoreUnknownKeys = true - }, - contentType = ContentType.Application.Json - ) - } - }, - userStorage = userStorage, - settingsRepository = settingsRepository - ) - - // when - val result = userApi.verifyServerHost("invalid") - - // then - Truth.assertThat(result).isEqualTo(false) - } - - @Test - fun `verify client id should succeed`() = runBlocking { - // given - val userApi = UserApiImpl( - httpClient = HttpClient( - MockEngine { - respond( - content = ByteReadChannel("""{}""".trimIndent()), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/json") - ) - } - ) { - install(ContentNegotiation) { - json( - Json { - prettyPrint = true - isLenient = true - ignoreUnknownKeys = true - }, - contentType = ContentType.Application.Json - ) - } - }, - userStorage = userStorage, - settingsRepository = settingsRepository - ) - - // when - val result = userApi.verifyClientId("clientID") - - // then - Truth.assertThat(result).isEqualTo(true) - } - - @Test - fun `verify invalid client id should fail`() = runBlocking { - // given - val userApi = UserApiImpl( - httpClient = HttpClient( - MockEngine { - respond( - content = ByteReadChannel("""{}""".trimIndent()), - status = HttpStatusCode.PreconditionFailed, - headers = headersOf(HttpHeaders.ContentType, "application/json") - ) - } - ) { - install(ContentNegotiation) { - json( - Json { - prettyPrint = true - isLenient = true - ignoreUnknownKeys = true - }, - contentType = ContentType.Application.Json - ) - } - }, - userStorage = userStorage, - settingsRepository = settingsRepository - ) - - // when - val result = userApi.verifyClientId("clientID") - - // then - Truth.assertThat(result).isEqualTo(false) - } - - @Test - fun `request access token for valid authCode should succeed`() = runBlocking { - // given - val userApi = UserApiImpl( - httpClient = HttpClient( - MockEngine { - respond( - content = ByteReadChannel( - """ -{ - "access_token":"abcdefg", - "expires_in": 3600, - "refresh_token":"abcdefg", - "token_type":"abcdefg" -} - """.trimIndent() - ), - status = HttpStatusCode.PreconditionFailed, - headers = headersOf(HttpHeaders.ContentType, "application/json") - ) - } - ) { - install(ContentNegotiation) { - json( - Json { - prettyPrint = true - isLenient = true - ignoreUnknownKeys = true - }, - contentType = ContentType.Application.Json - ) - } - }, - userStorage = userStorage, - settingsRepository = settingsRepository - ) - - // when - val result = userApi.accessTokenRequest("authCode") - - // then - Truth.assertThat(result).isEqualTo(true) - } - - @Test - fun `get user should return the current user`() = runBlocking { - // given - val userApi = UserApiImpl( - HttpClient( - MockEngine { - respond( - content = ByteReadChannel( - """ -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "email": "info@photos.network", - "lastname": "Lastname", - "firstname": "Firstname", - "lastSeen": "2022-02-22T02:22:22.222Z" -} - """.trimIndent() - ), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/json") - ) - } - ) { - install(ContentNegotiation) { - json( - Json { - prettyPrint = true - isLenient = true - ignoreUnknownKeys = true - }, - contentType = ContentType.Application.Json - ) - } - }, - userStorage = userStorage, - settingsRepository = settingsRepository - ) - - // when - val result = userApi.getUser() - - // then - Truth.assertThat(result).isNotNull() - Truth.assertThat(result?.id).isEqualTo("3fa85f64-5717-4562-b3fc-2c963f66afa6") - } -} diff --git a/data/src/test/kotlin/photos/network/data/user/network/model/NetworkUserTests.kt b/data/src/test/kotlin/photos/network/data/user/network/model/NetworkUserTests.kt deleted file mode 100644 index 0dc8b7d..0000000 --- a/data/src/test/kotlin/photos/network/data/user/network/model/NetworkUserTests.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2020-2022 Photos.network developers - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package photos.network.data.user.network.model - -import com.google.common.truth.Truth -import kotlinx.coroutines.runBlocking -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json -import org.junit.Test - -class NetworkUserTests { - @Test - fun `parse valid network user response`() = runBlocking { - // given - val jsonString: String = """ -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "email": "info@photos.network", - "lastname": "Lastname", - "firstname": "Firstname", - "lastSeen": "2022-02-22T02:22:22.222Z" -} - """.trimIndent() - - // when - val networkUser = Json.decodeFromString(jsonString) - - // then - Truth.assertThat(networkUser.id).isEqualTo("3fa85f64-5717-4562-b3fc-2c963f66afa6") - Truth.assertThat(networkUser.email).isEqualTo("info@photos.network") - Truth.assertThat(networkUser.lastname).isEqualTo("Lastname") - Truth.assertThat(networkUser.firstname).isEqualTo("Firstname") - Truth.assertThat(networkUser.lastSeen).isEqualTo("2022-02-22T02:22:22.222Z") - } -} diff --git a/data/src/test/kotlin/photos/network/data/user/network/model/TokenInfoTests.kt b/data/src/test/kotlin/photos/network/data/user/network/model/TokenInfoTests.kt deleted file mode 100644 index 0061370..0000000 --- a/data/src/test/kotlin/photos/network/data/user/network/model/TokenInfoTests.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2020-2022 Photos.network developers - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package photos.network.data.user.network.model - -import com.google.common.truth.Truth -import kotlinx.coroutines.runBlocking -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json -import org.junit.Test - -class TokenInfoTests { - @Test - fun `parse valid TokenInfo response`() = runBlocking { - // given - val jsonString: String = """ -{ - "access_token":"abcdefg", - "expires_in": 3600, - "refresh_token":"gfedcba", - "token_type":"aabbcc" -} - """.trimIndent() - - // when - val tokenInfo = Json.decodeFromString(jsonString) - - // then - Truth.assertThat(tokenInfo.accessToken).isEqualTo("abcdefg") - Truth.assertThat(tokenInfo.expiresIn).isEqualTo(3600) - Truth.assertThat(tokenInfo.refreshToken).isEqualTo("gfedcba") - Truth.assertThat(tokenInfo.tokenType).isEqualTo("aabbcc") - } -} diff --git a/database/settings/src/main/kotlin/photos/network/database/settings/Module.kt b/database/settings/src/main/kotlin/photos/network/database/settings/Module.kt index e183a75..11c5bc1 100644 --- a/database/settings/src/main/kotlin/photos/network/database/settings/Module.kt +++ b/database/settings/src/main/kotlin/photos/network/database/settings/Module.kt @@ -15,12 +15,16 @@ */ package photos.network.database.settings +import org.koin.core.qualifier.named import org.koin.dsl.module import photos.network.common.persistence.SecureStorage import photos.network.common.persistence.Settings val databaseSettingsModule = module { - single> { + single>( + named("SettingsStorage"), + createdAtStart = true + ) { SettingsStorage(context = get()) } } diff --git a/database/sharing/src/main/kotlin/photos/network/database/sharing/Module.kt b/database/sharing/src/main/kotlin/photos/network/database/sharing/Module.kt index 77017c9..02fcd54 100644 --- a/database/sharing/src/main/kotlin/photos/network/database/sharing/Module.kt +++ b/database/sharing/src/main/kotlin/photos/network/database/sharing/Module.kt @@ -15,12 +15,16 @@ */ package photos.network.database.sharing +import org.koin.core.qualifier.named import org.koin.dsl.module import photos.network.common.persistence.SecureStorage import photos.network.common.persistence.User val databaseSharingModule = module { - single> { + single>( + named("UserStorage"), + createdAtStart = true + ) { UserStorage(context = get()) } } diff --git a/database/sharing/src/main/kotlin/photos/network/database/sharing/UserStorage.kt b/database/sharing/src/main/kotlin/photos/network/database/sharing/UserStorage.kt index eb66657..1bc6e29 100644 --- a/database/sharing/src/main/kotlin/photos/network/database/sharing/UserStorage.kt +++ b/database/sharing/src/main/kotlin/photos/network/database/sharing/UserStorage.kt @@ -20,14 +20,14 @@ import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import photos.network.common.persistence.SecureStorage -import photos.network.common.persistence.User as DatabaseUser +import photos.network.common.persistence.User -class UserStorage(context: Context) : SecureStorage(context, "user_storage.txt") { - override fun decodeData(data: String): DatabaseUser { +class UserStorage(context: Context) : SecureStorage(context, "user_storage.txt") { + override fun decodeData(data: String): User { return Json.decodeFromString(data) } - override fun encodeData(data: DatabaseUser): String { + override fun encodeData(data: User): String { return Json.encodeToString(data) } } diff --git a/domain/folders/build.gradle.kts b/domain/folders/build.gradle.kts index 91775de..2553f80 100644 --- a/domain/folders/build.gradle.kts +++ b/domain/folders/build.gradle.kts @@ -46,4 +46,6 @@ dependencies { api(projects.common) testImplementation(project(":common", "testArtifacts")) androidTestImplementation(project(":common", "androidTestArtifacts")) + + api(projects.repository.folders) } diff --git a/domain/folders/src/main/kotlin/photos/network/domain/folders/GetFoldersUseCase.kt b/domain/folders/src/main/kotlin/photos/network/domain/folders/GetFoldersUseCase.kt new file mode 100644 index 0000000..7a64f41 --- /dev/null +++ b/domain/folders/src/main/kotlin/photos/network/domain/folders/GetFoldersUseCase.kt @@ -0,0 +1,11 @@ +package photos.network.domain.folders + +import photos.network.repository.folders.FoldersRepository + +class GetFoldersUseCase( + private val foldersRepository: FoldersRepository +) { + operator fun invoke(): List { + return listOf("NOT-IMPLEMENTEd") + } +} diff --git a/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCaseTests.kt b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCaseTests.kt index 2721623..f1007e5 100644 --- a/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCaseTests.kt +++ b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCaseTests.kt @@ -24,8 +24,8 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test -import photos.network.data.settings.repository.PrivacyState -import photos.network.data.settings.repository.Settings +import photos.network.common.persistence.PrivacyState +import photos.network.common.persistence.Settings import photos.network.repository.settings.SettingsRepository class GetSettingsUseCaseTests { diff --git a/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCaseTests.kt b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCaseTests.kt index 1edd2de..35ec4ef 100644 --- a/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCaseTests.kt +++ b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCaseTests.kt @@ -22,7 +22,7 @@ import io.mockk.mockk import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test -import photos.network.data.user.repository.UserRepository +import photos.network.repository.sharing.UserRepository class VerifyClientIdUseCaseTests { @Rule diff --git a/domain/sharing/src/main/kotlin/photos/network/domain/sharing/Module.kt b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/Module.kt index cf0aaed..59e66b7 100644 --- a/domain/sharing/src/main/kotlin/photos/network/domain/sharing/Module.kt +++ b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/Module.kt @@ -16,6 +16,12 @@ package photos.network.domain.sharing import org.koin.dsl.module +import photos.network.domain.sharing.usecase.GetCurrentUserUseCase +import photos.network.domain.sharing.usecase.LogoutUseCase +import photos.network.domain.sharing.usecase.RequestAccessTokenUseCase val domainSharingModule = module { + single { RequestAccessTokenUseCase(userRepository = get()) } + single { LogoutUseCase(userRepository = get()) } + single { GetCurrentUserUseCase(userRepository = get()) } } diff --git a/domain/sharing/src/test/kotlin/photos/network/domain/sharing/UserMapper.kt b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/UserMapper.kt new file mode 100644 index 0000000..493caf6 --- /dev/null +++ b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/UserMapper.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.domain.sharing + +import photos.network.common.persistence.User as DatabaseUser +import photos.network.repository.sharing.User as RepositoryUser + +object UserMapper { + fun mapDatabaseToRepository(user: DatabaseUser) = RepositoryUser( + id = user.id, + lastname = user.lastname, + firstname = user.firstname, + profileImageUrl = user.profileImageUrl, + accessToken = user.accessToken, + refreshToken = user.refreshToken, + ) + + fun mapRepositoryToDatabase(user: RepositoryUser) = DatabaseUser( + id = user.id, + lastname = user.lastname, + firstname = user.firstname, + profileImageUrl = user.profileImageUrl, + accessToken = user.accessToken, + refreshToken = user.refreshToken, + ) +} diff --git a/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCaseTests.kt b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCaseTests.kt index 6af024b..23694e1 100644 --- a/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCaseTests.kt +++ b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCaseTests.kt @@ -23,8 +23,9 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test -import photos.network.data.user.repository.User -import photos.network.data.user.repository.UserRepository +import photos.network.domain.sharing.UserMapper +import photos.network.repository.sharing.User +import photos.network.repository.sharing.UserRepository class GetCurrentUserUseCaseTests { @Rule @@ -51,7 +52,7 @@ class GetCurrentUserUseCaseTests { ) every { userRepository.currentUser() } answers { - user.toDatabaseUser() + UserMapper.mapRepositoryToDatabase(user) } // when diff --git a/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/LogoutUseCaseTests.kt b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/LogoutUseCaseTests.kt index 4bba8b4..6d9b7e6 100644 --- a/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/LogoutUseCaseTests.kt +++ b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/LogoutUseCaseTests.kt @@ -22,7 +22,7 @@ import io.mockk.mockk import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test -import photos.network.data.user.repository.UserRepository +import photos.network.repository.sharing.UserRepository class LogoutUseCaseTests { @Rule diff --git a/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/RequestAccessTokenUseCaseTests.kt b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/RequestAccessTokenUseCaseTests.kt index b2e3d71..a5d65fb 100644 --- a/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/RequestAccessTokenUseCaseTests.kt +++ b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/RequestAccessTokenUseCaseTests.kt @@ -22,7 +22,7 @@ import io.mockk.mockk import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test -import photos.network.data.user.repository.UserRepository +import photos.network.repository.sharing.UserRepository class RequestAccessTokenUseCaseTests { @Rule diff --git a/repository/folders/build.gradle.kts b/repository/folders/build.gradle.kts new file mode 100644 index 0000000..961edd4 --- /dev/null +++ b/repository/folders/build.gradle.kts @@ -0,0 +1,64 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +detekt { + config = files("$rootDir/detekt.yml") +} + +android { + namespace = "photos.network.repository.folders" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + } + + packagingOptions { + resources.excludes += "META-INF/AL2.0" + resources.excludes += "META-INF/LGPL2.1" + resources.excludes += "META-INF/licenses/ASM" + resources.pickFirsts.add("win32-x86-64/attach_hotspot_windows.dll") + resources.pickFirsts.add("win32-x86/attach_hotspot_windows.dll") + } +} + +dependencies { + implementation(projects.common) + testImplementation(project(":common", "testArtifacts")) + testImplementation(project(mapOf("path" to ":common"))) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + api(projects.system.filesystem) + + testImplementation(libs.mockk) + testImplementation(libs.junit.junit) + testImplementation(libs.truth) + testImplementation(libs.core.testing) +} diff --git a/repository/folders/src/main/AndroidManifest.xml b/repository/folders/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/repository/folders/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepository.kt b/repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepository.kt new file mode 100644 index 0000000..f32b416 --- /dev/null +++ b/repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepository.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2020-2022 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.repository.folders + +import kotlinx.coroutines.flow.Flow +import photos.network.system.filesystem.FileItem +import photos.network.system.filesystem.FolderItem + +interface FoldersRepository { + + fun getFolders(): Flow> + fun getFiles(): Flow +} diff --git a/data/src/test/kotlin/photos/network/data/user/network/model/ApiResponseTests.kt b/repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepositoryImpl.kt similarity index 50% rename from data/src/test/kotlin/photos/network/data/user/network/model/ApiResponseTests.kt rename to repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepositoryImpl.kt index 02374e7..d88f804 100644 --- a/data/src/test/kotlin/photos/network/data/user/network/model/ApiResponseTests.kt +++ b/repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepositoryImpl.kt @@ -13,28 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.user.network.model +package photos.network.repository.folders -import com.google.common.truth.Truth -import kotlinx.coroutines.runBlocking -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json -import org.junit.Test +import java.nio.file.FileSystem +import kotlinx.coroutines.flow.Flow +import photos.network.system.filesystem.FileItem +import photos.network.system.filesystem.FolderItem -class ApiResponseTests { - @Test - fun `parse valid TokenInfo response`() = runBlocking { - // given - val jsonString: String = """ -{ - "message": "API running" -} - """.trimIndent() - - // when - val response = Json.decodeFromString(jsonString) +class FoldersRepositoryImpl( + private val fileSystem: FileSystem, +) : FoldersRepository { + override fun getFolders(): Flow> { + TODO("Not yet implemented") + } - // then - Truth.assertThat(response.message).isEqualTo("API running") + override fun getFiles(): Flow { + TODO("Not yet implemented") } } diff --git a/repository/folders/src/main/kotlin/photos/network/repository/folders/Module.kt b/repository/folders/src/main/kotlin/photos/network/repository/folders/Module.kt new file mode 100644 index 0000000..d9db15d --- /dev/null +++ b/repository/folders/src/main/kotlin/photos/network/repository/folders/Module.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.repository.folders + +import org.koin.dsl.module + +val repositoryFoldersModule = module { + single { + FoldersRepositoryImpl( + fileSystem = get(), + ) + } +} diff --git a/repository/folders/src/test/kotlin/photos/network/repository/folders/FoldersRepositoryTest.kt b/repository/folders/src/test/kotlin/photos/network/repository/folders/FoldersRepositoryTest.kt new file mode 100644 index 0000000..01ea529 --- /dev/null +++ b/repository/folders/src/test/kotlin/photos/network/repository/folders/FoldersRepositoryTest.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2020-2022 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.repository.photos + +import android.content.Context +import androidx.work.WorkManager +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import org.junit.Rule +import org.junit.Test +import photos.network.api.photo.PhotoApi +import photos.network.database.photos.PhotoDao + +/** + * Test photo repository + */ +class PhotoRepositoryTest { + + @get:Rule + val coroutineRule = TestCoroutineDispatcherRule() + + private val applicationContext = mockk() + private val photoApi = mockk() + private val photoDao = mockk() + private val workManager = mockk() + + private val repository by lazy { + PhotoRepositoryImpl( + applicationContext = applicationContext, + photoApi = photoApi, + photoDao = photoDao, + workManager = workManager, + ) + } + + @Test + fun `should return all photos from persistence`() = runBlocking { + // given + every { photoDao.getPhotos() } answers { + flowOf( + listOf( + createFakePhoto(filename = "001", dateTaken = 1580671220), + createFakePhoto(filename = "002", dateTaken = 1580671221), + ), + ) + } + + // when + val photos = repository.getPhotos().first() + + // then + Truth.assertThat(photos.size).isEqualTo(2) + } + + @Test + fun `photos returned should be ordered by dateTaken`() = runBlocking { + // given + every { photoDao.getPhotos() } answers { + flowOf( + listOf( + createFakePhoto(filename = "002", dateTaken = 1580671221), + createFakePhoto(filename = "001", dateTaken = 1580671220), + createFakePhoto(filename = "003", dateTaken = 1580671223), + ), + ) + } + + // when + val photos = repository.getPhotos().first() + + // then + Truth.assertThat(photos[0].filename).isEqualTo("003") + Truth.assertThat(photos[1].filename).isEqualTo("002") + Truth.assertThat(photos[2].filename).isEqualTo("001") + } + + @Test + fun `photos returned should be ordered by dateAdded if dateTaken is not available`() = + runBlocking { + // given + every { photoDao.getPhotos() } answers { + flowOf( + listOf( + createFakePhoto(filename = "002", dateTaken = null, dateAdded = 1580671221), + createFakePhoto(filename = "001", dateTaken = null, dateAdded = 1580671220), + createFakePhoto(filename = "003", dateTaken = null, dateAdded = 1580671223), + ), + ) + } + + // when + val photos = repository.getPhotos().first() + + // then + Truth.assertThat(photos[0].filename).isEqualTo("003") + Truth.assertThat(photos[1].filename).isEqualTo("002") + Truth.assertThat(photos[2].filename).isEqualTo("001") + } + + private fun createFakePhoto( + uuid: String = "001", + filename: String = "IMG_20200202_202020.jpg", + imageUrl: String = "http://127.0.0.1/image/e369d958-ad41-4391-9ccb-f89be8ca1e8b", + dateAdded: Long = 1580671220, + dateTaken: Long? = null, + dateModified: Long? = null, + thumbnailFileUri: String? = null, + originalFileUri: String? = null, + ): Photo { + return Photo( +// uuid = uuid, + filename = filename, + imageUrl = imageUrl, + dateAdded = dateAdded, + dateTaken = dateTaken, + dateModified = dateModified, +// thumbnailFileUri = thumbnailFileUri, +// originalFileUri = originalFileUri, + ) + } +} diff --git a/repository/photos/build.gradle.kts b/repository/photos/build.gradle.kts index a42b794..8733c37 100644 --- a/repository/photos/build.gradle.kts +++ b/repository/photos/build.gradle.kts @@ -55,6 +55,8 @@ dependencies { testImplementation(project(mapOf("path" to ":common"))) androidTestImplementation(project(":common", "androidTestArtifacts")) + api(projects.system.mediastore) + // workmanager api(libs.work.runtime.ktx) androidTestApi(libs.work.testing) diff --git a/repository/photos/src/main/kotlin/photos/network/repository/photos/Module.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/Module.kt index 4d1dea6..cd5677c 100644 --- a/repository/photos/src/main/kotlin/photos/network/repository/photos/Module.kt +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/Module.kt @@ -51,10 +51,10 @@ val repositoryPhotosModule = module { single { PhotoRepositoryImpl( - applicationContext = get(), photoApi = get(), photoDao = get(), workManager = get(), + mediaStore = get() ) } } diff --git a/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt index 4263df5..11156d5 100644 --- a/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt @@ -15,15 +15,13 @@ */ package photos.network.repository.photos -import android.content.Context -import android.net.Uri -import android.os.Build -import android.provider.MediaStore import androidx.work.Constraints import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.NetworkType import androidx.work.PeriodicWorkRequest import androidx.work.WorkManager +import java.time.Instant +import java.util.concurrent.TimeUnit import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.mapNotNull import logcat.LogPriority @@ -32,11 +30,10 @@ import photos.network.api.photo.PhotoApi import photos.network.database.photos.PhotoDao import photos.network.repository.photos.worker.SyncLocalPhotosWorker import photos.network.repository.photos.worker.SyncStatus -import java.time.Instant -import java.util.concurrent.TimeUnit +import photos.network.system.mediastore.MediaStore class PhotoRepositoryImpl( - private val applicationContext: Context, + private val mediaStore: MediaStore, private val photoApi: PhotoApi, private val photoDao: PhotoDao, private val workManager: WorkManager, @@ -56,11 +53,21 @@ class PhotoRepositoryImpl( .build() override suspend fun syncPhotos(): SyncStatus { - val photos = queryLocalMediaStore() + val photos = mediaStore.queryLocalMediaStore() logcat(LogPriority.VERBOSE) { "Found ${photos.size} photos." } photos.forEach { - addPhoto(it) + addPhoto( + photo = Photo( + filename = it.name, + imageUrl = "", + dateAdded = Instant.now(), + dateTaken = it.dateTaken, + dateModified = null, + isPrivate = false, + uri = it.uri, + ) + ) } // TODO: move to somewhere else @@ -91,178 +98,12 @@ class PhotoRepositoryImpl( } } - override fun getPhoto(identifier: String): Flow = photoDao.getPhoto(identifier).mapNotNull { - it?.let { it1 -> Photo(it1) } - } + override fun getPhoto(identifier: String): Flow = + photoDao.getPhoto(identifier).mapNotNull { + it?.let { it1 -> Photo(it1) } + } override suspend fun addPhoto(photo: Photo) { photoDao.insertAll(photos = arrayOf(photo.toDatabasePhoto())) } - - private fun generateContentUri(): Uri { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - MediaStore.Images.Media.getContentUri( - MediaStore.VOLUME_EXTERNAL, - ) - } else { - MediaStore.Images.Media.EXTERNAL_CONTENT_URI - } - } - - private fun generateProjection(): Array { - val projection = mutableListOf() - - projection += MediaStore.Images.Media._ID - projection += MediaStore.Images.Media.DISPLAY_NAME - projection += MediaStore.Images.Media.SIZE - projection += MediaStore.Images.Media.DATE_TAKEN - - // deprecated with 29 (Q) - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { - projection += MediaStore.Images.Media.LATITUDE - projection += MediaStore.Images.Media.LONGITUDE - } - - // added with 30 (R) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - projection += MediaStore.Images.Media.F_NUMBER - projection += MediaStore.Images.Media.ISO - projection += MediaStore.Images.Media.EXPOSURE_TIME - } - - return projection.toTypedArray() - } - - /** - * Query images from Androids local MediaStore (`DCIM/` and `Pictures/`) - */ - private fun queryLocalMediaStore(): List { - val photos = mutableListOf() - - val selection = null - val selectionArgs = null - val sortOrder = "${MediaStore.Images.Media.DATE_TAKEN} DESC" - - applicationContext.contentResolver.query( - generateContentUri(), - generateProjection(), - selection, - selectionArgs, - sortOrder, - )?.use { cursor -> - val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID) - val nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME) - val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE) - val dateTakenColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_TAKEN) - - val latColumn = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { - cursor.getColumnIndexOrThrow(MediaStore.Images.Media.LATITUDE) - } else { - -1 - } - val longColumn = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { - cursor.getColumnIndexOrThrow(MediaStore.Images.Media.LONGITUDE) - } else { - -1 - } - - val fnumberColumn = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - cursor.getColumnIndexOrThrow(MediaStore.Images.Media.F_NUMBER) - } else { - -1 - } - - val isoNumberColumn = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - cursor.getColumnIndexOrThrow(MediaStore.Images.Media.ISO) - } else { - -1 - } - - val exposureColumn = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - cursor.getColumnIndexOrThrow(MediaStore.Images.Media.EXPOSURE_TIME) - } else { - -1 - } - - logcat(LogPriority.ERROR) { "count: ${cursor.count}" } - - while (cursor.moveToNext()) { - var photoUri = Uri.withAppendedPath( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - cursor.getString(idColumn), - ) - - // Get values of columns for a given Image. - val id = cursor.getLong(idColumn) - val name = cursor.getString(nameColumn) - val size = cursor.getInt(sizeColumn) - val dateTaken = cursor.getLong(dateTakenColumn) - - val exposure: String? = if (exposureColumn != -1) { - cursor.getString(exposureColumn) - } else { - null - } - - val fnumber: String? = if (fnumberColumn != -1) { - cursor.getString(fnumberColumn) - } else { - null - } - - val isoNumber = if (isoNumberColumn != -1) { - cursor.getString(isoNumberColumn) - } else { - null - } - - // Image location - var latLong = FloatArray(2) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - // TODO: ACCESS_MEDIA_LOCATION permission required - logcat(LogPriority.WARN) { "Implement ACCESS_MEDIA_LOCATION permission for exif location" } -// photoUri = MediaStore.setRequireOriginal(photoUri) -// val stream: InputStream? = -// applicationContext.contentResolver.openInputStream(photoUri) -// if (stream == null) { -// logcat(LogPriority.WARN) { "Got a null input stream for $photoUri" } -// continue -// } -// -// val exifInterface = ExifInterface(stream) -// // If it returns null, fall back to {0.0, 0.0}. -// exifInterface.getLatLong(latLong) -// -// stream.close() - } else { - if (latColumn != -1 && longColumn != -1) { - latLong = floatArrayOf( - cursor.getFloat(latColumn), - cursor.getFloat(longColumn), - ) - } - } - - logcat(priority = LogPriority.ERROR) { "details: exposure=$exposure, fnumber=$fnumber, isoNumber=$isoNumber, lat=${latLong[0]}, lon=${latLong[0]}" } - // TODO: add file details? - photos += Photo( - filename = name, - imageUrl = name, - dateTaken = Instant.ofEpochMilli(dateTaken), - dateAdded = Instant.now(), - uri = photoUri, -// details = TechnicalDetails( -// exposure = exposure, -// focal_length = fnumber, -// iso = isoNumber, -// ), -// tags = listOf(), -// location = Location(latLong[0], latLong[1], 0), -// image_url = "", - ) - } - } - - return photos - } } diff --git a/repository/settings/src/main/kotlin/photos/network/repository/settings/Module.kt b/repository/settings/src/main/kotlin/photos/network/repository/settings/Module.kt index 779263b..6c93007 100644 --- a/repository/settings/src/main/kotlin/photos/network/repository/settings/Module.kt +++ b/repository/settings/src/main/kotlin/photos/network/repository/settings/Module.kt @@ -17,14 +17,17 @@ package photos.network.repository.settings import androidx.work.WorkManager import org.koin.android.ext.koin.androidApplication +import org.koin.core.qualifier.named import org.koin.dsl.module +import photos.network.common.persistence.SecureStorage +import photos.network.common.persistence.Settings val repositorySettingsModule = module { factory { WorkManager.getInstance(androidApplication()) } single { SettingsRepositoryImpl( - settingsStore = get(), + settingsStore = get(qualifier = named("SettingsStorage")), ) } } diff --git a/repository/sharing/src/main/kotlin/photos/network/repository/sharing/Module.kt b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/Module.kt index f48368d..c062608 100644 --- a/repository/sharing/src/main/kotlin/photos/network/repository/sharing/Module.kt +++ b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/Module.kt @@ -17,7 +17,9 @@ package photos.network.repository.sharing import androidx.work.WorkManager import org.koin.android.ext.koin.androidApplication +import org.koin.core.qualifier.named import org.koin.dsl.module +import photos.network.common.persistence.SecureStorage val repositorySharingModule = module { factory { WorkManager.getInstance(androidApplication()) } @@ -25,7 +27,7 @@ val repositorySharingModule = module { single { UserRepositoryImpl( userApi = get(), - userStorage = get(), + userStorage = get(qualifier = named("UserStorage")), ) } } diff --git a/repository/sharing/src/main/kotlin/photos/network/repository/sharing/User.kt b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/User.kt index 7391d03..9acc1ad 100644 --- a/repository/sharing/src/main/kotlin/photos/network/repository/sharing/User.kt +++ b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/User.kt @@ -15,6 +15,7 @@ */ package photos.network.repository.sharing +@Deprecated(message = "Not used") data class User( val id: String? = null, val lastname: String, @@ -22,22 +23,4 @@ data class User( val profileImageUrl: String, val accessToken: String? = null, val refreshToken: String? = null, -) { -// fun toDatabaseUser(): PersistenceUser = PersistenceUser( -// id = id, -// lastname = lastname, -// firstname = firstname, -// profileImageUrl = profileImageUrl, -// accessToken = accessToken, -// refreshToken = refreshToken, -// ) -// -// fun toDomain(): DomainUser = DomainUser( -// id = id, -// lastname = lastname, -// firstname = firstname, -// profileImageUrl = profileImageUrl, -// accessToken = accessToken, -// refreshToken = refreshToken, -// ) -} +) diff --git a/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepository.kt b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepository.kt index 1f50a70..bfbb07a 100644 --- a/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepository.kt +++ b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepository.kt @@ -15,8 +15,9 @@ */ package photos.network.repository.sharing +import photos.network.repository.sharing.User as RepositoryUser interface UserRepository { - fun currentUser(): User? + fun currentUser(): RepositoryUser? suspend fun invalidateAuthorization() diff --git a/settings.gradle.kts b/settings.gradle.kts index 7e482a1..6d3a9ae 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -43,6 +43,7 @@ include(":domain:settings") include(":domain:sharing") include(":repository:photos") +include(":repository:folders") include(":repository:settings") include(":repository:sharing") diff --git a/system/filesystem/src/main/kotlin/photos/network/system/filesystem/FileSystem.kt b/system/filesystem/src/main/kotlin/photos/network/system/filesystem/FileSystem.kt new file mode 100644 index 0000000..662a743 --- /dev/null +++ b/system/filesystem/src/main/kotlin/photos/network/system/filesystem/FileSystem.kt @@ -0,0 +1,20 @@ +package photos.network.system.filesystem + +import android.net.Uri + +interface FileSystem { + fun getFolders(): List + fun getItems(): List +} + +data class FolderItem( + val name: String, + val itemCount: Int, + val folderSize: Long +) + +data class FileItem( + val name: String, + val size: Int, + val uri: Uri, +) diff --git a/system/filesystem/src/main/kotlin/photos/network/system/filesystem/FileSystemImpl.kt b/system/filesystem/src/main/kotlin/photos/network/system/filesystem/FileSystemImpl.kt new file mode 100644 index 0000000..7274e09 --- /dev/null +++ b/system/filesystem/src/main/kotlin/photos/network/system/filesystem/FileSystemImpl.kt @@ -0,0 +1,15 @@ +package photos.network.system.filesystem + +import android.app.Application + +class FileSystemImpl( + private val application: Application +): FileSystem { + override fun getFolders(): List { + TODO("Not yet implemented") + } + + override fun getItems(): List { + TODO("Not yet implemented") + } +} diff --git a/system/filesystem/src/main/kotlin/photos/network/system/filesystem/Module.kt b/system/filesystem/src/main/kotlin/photos/network/system/filesystem/Module.kt new file mode 100644 index 0000000..829a6f4 --- /dev/null +++ b/system/filesystem/src/main/kotlin/photos/network/system/filesystem/Module.kt @@ -0,0 +1,9 @@ +package photos.network.system.filesystem + +import org.koin.dsl.module + +val systemFilesystemModule = module { + single { + FileSystemImpl(application = get()) + } +} diff --git a/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStore.kt b/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStore.kt new file mode 100644 index 0000000..8f9993a --- /dev/null +++ b/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStore.kt @@ -0,0 +1,26 @@ +package photos.network.system.mediastore + +import android.net.Uri +import java.time.Instant + +interface MediaStore { + fun queryLocalMediaStore(): List +} + +data class MediaItem( + val id: Long, + val name: String, + val size: Int, + val uri: Uri, + val dateTaken: Instant? = null, + val exposure: String? = null, + val fnumber: String? = null, + val isoNumber: String? = null, + val location: Location? = null +) + +data class Location( + val latitude: Float, + val longitude: Float, + val altitude: Int +) diff --git a/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStoreImpl.kt b/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStoreImpl.kt new file mode 100644 index 0000000..789cece --- /dev/null +++ b/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStoreImpl.kt @@ -0,0 +1,178 @@ +package photos.network.system.mediastore + +import android.app.Application +import android.location.Location +import android.net.Uri +import android.os.Build +import java.time.Instant +import logcat.LogPriority +import logcat.logcat + +class MediaStoreImpl( + private val application: Application +): MediaStore { + /** + * Query images from Androids local MediaStore (`DCIM/` and `Pictures/`) + */ + override fun queryLocalMediaStore(): List { + val photos = mutableListOf() + + val selection = null + val selectionArgs = null + val sortOrder = "${android.provider.MediaStore.Images.Media.DATE_TAKEN} DESC" + + val applicationContext = application.applicationContext + + applicationContext.contentResolver.query( + generateContentUri(), + generateProjection(), + selection, + selectionArgs, + sortOrder, + )?.use { cursor -> + val idColumn = cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media._ID) + val nameColumn = cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.DISPLAY_NAME) + val sizeColumn = cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.SIZE) + val dateTakenColumn = cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.DATE_TAKEN) + + val latColumn = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.LATITUDE) + } else { + -1 + } + val longColumn = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.LONGITUDE) + } else { + -1 + } + + val fnumberColumn = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.F_NUMBER) + } else { + -1 + } + + val isoNumberColumn = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.ISO) + } else { + -1 + } + + val exposureColumn = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.EXPOSURE_TIME) + } else { + -1 + } + + logcat(LogPriority.ERROR) { "count: ${cursor.count}" } + + while (cursor.moveToNext()) { + var photoUri = Uri.withAppendedPath( + android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + cursor.getString(idColumn), + ) + + // Get values of columns for a given Image. + val id = cursor.getLong(idColumn) + val name = cursor.getString(nameColumn) + val size = cursor.getInt(sizeColumn) + val dateTaken = cursor.getLong(dateTakenColumn) + + val exposure: String? = if (exposureColumn != -1) { + cursor.getString(exposureColumn) + } else { + null + } + + val fnumber: String? = if (fnumberColumn != -1) { + cursor.getString(fnumberColumn) + } else { + null + } + + val isoNumber = if (isoNumberColumn != -1) { + cursor.getString(isoNumberColumn) + } else { + null + } + + // Image location + var latLong = FloatArray(2) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // TODO: ACCESS_MEDIA_LOCATION permission required + logcat(LogPriority.WARN) { "Implement ACCESS_MEDIA_LOCATION permission for exif location" } +// photoUri = MediaStore.setRequireOriginal(photoUri) +// val stream: InputStream? = +// applicationContext.contentResolver.openInputStream(photoUri) +// if (stream == null) { +// logcat(LogPriority.WARN) { "Got a null input stream for $photoUri" } +// continue +// } +// +// val exifInterface = ExifInterface(stream) +// // If it returns null, fall back to {0.0, 0.0}. +// exifInterface.getLatLong(latLong) +// +// stream.close() + } else { + if (latColumn != -1 && longColumn != -1) { + latLong = floatArrayOf( + cursor.getFloat(latColumn), + cursor.getFloat(longColumn), + ) + } + } + + logcat(priority = LogPriority.ERROR) { "details: exposure=$exposure, fnumber=$fnumber, isoNumber=$isoNumber, lat=${latLong[0]}, lon=${latLong[0]}" } + + photos += MediaItem( + id = id, + name = name, + size = size, + uri = photoUri, + dateTaken = Instant.ofEpochMilli(dateTaken), + exposure = exposure, + fnumber = fnumber, + isoNumber = isoNumber, + location = Location(latLong[0], latLong[1], 0), + ) + } + } + + return photos + } + + private fun generateContentUri(): Uri { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + android.provider.MediaStore.Images.Media.getContentUri( + android.provider.MediaStore.VOLUME_EXTERNAL, + ) + } else { + android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } + } + + private fun generateProjection(): Array { + val projection = mutableListOf() + + projection += android.provider.MediaStore.Images.Media._ID + projection += android.provider.MediaStore.Images.Media.DISPLAY_NAME + projection += android.provider.MediaStore.Images.Media.SIZE + projection += android.provider.MediaStore.Images.Media.DATE_TAKEN + + // deprecated with 29 (Q) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + projection += android.provider.MediaStore.Images.Media.LATITUDE + projection += android.provider.MediaStore.Images.Media.LONGITUDE + } + + // added with 30 (R) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + projection += android.provider.MediaStore.Images.Media.F_NUMBER + projection += android.provider.MediaStore.Images.Media.ISO + projection += android.provider.MediaStore.Images.Media.EXPOSURE_TIME + } + + return projection.toTypedArray() + } +} diff --git a/system/mediastore/src/main/kotlin/photos/network/system/mediastore/Module.kt b/system/mediastore/src/main/kotlin/photos/network/system/mediastore/Module.kt new file mode 100644 index 0000000..4f23b28 --- /dev/null +++ b/system/mediastore/src/main/kotlin/photos/network/system/mediastore/Module.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.system.mediastore + +import org.koin.dsl.module + +val systemMediastoreModule = module { + single { + MediaStoreImpl(application = get()) + } +} From 2eefc668843794c2e3ab0a633c57ad0a1877a8f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Stu=CC=88rmer?= Date: Tue, 16 May 2023 00:06:52 +0200 Subject: [PATCH 05/13] clean-up --- README.md | 2 +- .../kotlin/photos/network/api/ApiModule.kt | 2 +- app/build.gradle.kts | 1 + .../kotlin/photos/network/MainActivity.kt | 12 ++ .../main/kotlin/photos/network/home/Home.kt | 44 ++++--- .../network/CurrentUserViewModelTests.kt | 6 +- .../network/database/settings/Module.kt | 2 +- .../photos/network/database/sharing/Module.kt | 2 +- .../domain/folders/GetFoldersUseCase.kt | 19 ++- .../network/domain/sharing/UserMapper.kt | 39 ------ .../usecase/GetCurrentUserUseCaseTests.kt | 9 +- repository/folders/build.gradle.kts | 1 - .../folders/FoldersRepositoryImpl.kt | 2 +- .../folders/FoldersRepositoryTest.kt | 117 +----------------- .../network/repository/photos/Module.kt | 2 +- .../repository/photos/PhotoRepositoryImpl.kt | 6 +- .../repository/photos/PhotoRepositoryTest.kt | 12 +- .../network/repository/settings/Module.kt | 2 - repository/sharing/build.gradle.kts | 1 + .../network/repository/sharing/Module.kt | 1 - .../repository/sharing/UserRepositoryTests.kt | 8 +- .../network/system/filesystem/FileSystem.kt | 17 ++- .../system/filesystem/FileSystemImpl.kt | 26 +++- .../network/system/filesystem/Module.kt | 15 +++ .../network/system/mediastore/MediaStore.kt | 19 ++- .../system/mediastore/MediaStoreImpl.kt | 30 ++++- ui/photos/build.gradle.kts | 1 + .../ui/photos/photos/PhotosViewModelTests.kt | 15 ++- 28 files changed, 189 insertions(+), 224 deletions(-) delete mode 100644 domain/sharing/src/test/kotlin/photos/network/domain/sharing/UserMapper.kt diff --git a/README.md b/README.md index c956930..80b916c 100644 --- a/README.md +++ b/README.md @@ -102,4 +102,4 @@ Create a Feature request with a short but understandable description what the fe * Check for unnecessary whitespace with `git diff --check` before committing. * Make sure your commit messages are in the proper format. Start the first line of the commit with the issue number in parentheses. -* run tests and code quality checks locally ```./gradlew detekt lint testDebugUnitTest connectedAndroidTest``` +* run tests and code quality checks locally ```./run_tests``` diff --git a/api/src/main/kotlin/photos/network/api/ApiModule.kt b/api/src/main/kotlin/photos/network/api/ApiModule.kt index c78ba48..8ce495f 100644 --- a/api/src/main/kotlin/photos/network/api/ApiModule.kt +++ b/api/src/main/kotlin/photos/network/api/ApiModule.kt @@ -68,7 +68,7 @@ val apiModule = module { UserApiImpl( httpClient = get(), userStorage = get(qualifier = named("UserStorage")), - settingsStorage = get(qualifier = named("SettingsStorage")) + settingsStorage = get(qualifier = named("SettingsStorage")), ) } diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3d3a00c..b56b0e3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -180,4 +180,5 @@ dependencies { testImplementation(libs.core.testing) testImplementation(libs.mockk) + testImplementation(libs.kotlinx.coroutines.test) } diff --git a/app/src/main/kotlin/photos/network/MainActivity.kt b/app/src/main/kotlin/photos/network/MainActivity.kt index d95b571..073f768 100644 --- a/app/src/main/kotlin/photos/network/MainActivity.kt +++ b/app/src/main/kotlin/photos/network/MainActivity.kt @@ -15,7 +15,9 @@ */ package photos.network +import android.os.Build import android.os.Bundle +import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.isSystemInDarkTheme @@ -28,6 +30,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE +import androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE import com.google.accompanist.systemuicontroller.SystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController import photos.network.home.Home @@ -43,6 +47,8 @@ class MainActivity : ComponentActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) + window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) + setContent { PhotosApp() } @@ -62,6 +68,12 @@ fun PhotosApp( color = Color.Transparent, darkIcons = useDarkIcons, ) + systemUiController.isStatusBarVisible = false + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + systemUiController.systemBarsBehavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } else { + systemUiController.systemBarsBehavior = BEHAVIOR_SHOW_BARS_BY_SWIPE + } } CompositionLocalProvider(LocalAppVersion provides BuildConfig.VERSION_NAME) { diff --git a/app/src/main/kotlin/photos/network/home/Home.kt b/app/src/main/kotlin/photos/network/home/Home.kt index f2e1a66..9a727c0 100644 --- a/app/src/main/kotlin/photos/network/home/Home.kt +++ b/app/src/main/kotlin/photos/network/home/Home.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Shield import androidx.compose.material.icons.outlined.Shield @@ -80,8 +79,7 @@ fun Home( modifier = modifier .fillMaxSize() .navigationBarsPadding() - .testTag("HomeScreenTag") - .border(1.dp, Color.Magenta), + .testTag("HomeScreenTag"), snackbarHost = { // SnackbarHost( // hostState = it, @@ -93,9 +91,7 @@ fun Home( // privacy TopAppBar( title = {}, - modifier = Modifier - .statusBarsPadding() - .padding(top = 36.dp), + modifier = Modifier, navigationIcon = { AppLogo( modifier = Modifier @@ -110,23 +106,25 @@ fun Home( }, actions = { // privacy - IconButton( - onClick = { - viewmodel.handleEvent(HomeEvent.TogglePrivacyEvent) - }, - ) { - if (viewmodel.uiState.collectAsState().value.isPrivacyEnabled) { - Icon( - imageVector = Icons.Default.Shield, - contentDescription = stringResource(id = R.string.privacy_filter_enabled_description), - tint = MaterialTheme.colorScheme.onPrimary, - ) - } else { - Icon( - imageVector = Icons.Outlined.Shield, - contentDescription = stringResource(id = R.string.privacy_filter_disabled_description), - tint = MaterialTheme.colorScheme.onPrimary, - ) + if (currentDestination == Destination.Photos || currentDestination == Destination.Albums) { + IconButton( + onClick = { + viewmodel.handleEvent(HomeEvent.TogglePrivacyEvent) + }, + ) { + if (viewmodel.uiState.collectAsState().value.isPrivacyEnabled) { + Icon( + imageVector = Icons.Default.Shield, + contentDescription = stringResource(id = R.string.privacy_filter_enabled_description), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } else { + Icon( + imageVector = Icons.Outlined.Shield, + contentDescription = stringResource(id = R.string.privacy_filter_disabled_description), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } } } }, diff --git a/app/src/test/kotlin/photos/network/CurrentUserViewModelTests.kt b/app/src/test/kotlin/photos/network/CurrentUserViewModelTests.kt index f3705cf..059d577 100644 --- a/app/src/test/kotlin/photos/network/CurrentUserViewModelTests.kt +++ b/app/src/test/kotlin/photos/network/CurrentUserViewModelTests.kt @@ -17,6 +17,7 @@ package photos.network import androidx.arch.core.executor.testing.InstantTaskExecutorRule import io.mockk.mockk +import kotlinx.coroutines.test.runTest import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -26,15 +27,12 @@ class CurrentUserViewModelTests { @get:Rule var rule = InstantTaskExecutorRule() - @get:Rule - val coroutineRule = TestCoroutineDispatcherRule() - private val getCurrentUserUseCase: GetCurrentUserUseCase = mockk() // private val viewmodel by lazy { CurrentUserViewModel(getCurrentUserUseCase) } @Ignore @Test - fun `viewmodel should reflect the given user state from the use case`() { + fun `viewmodel should reflect the given user state from the use case`() = runTest { // given // every { getCurrentUserUseCase() } answers { flowOf(null) } diff --git a/database/settings/src/main/kotlin/photos/network/database/settings/Module.kt b/database/settings/src/main/kotlin/photos/network/database/settings/Module.kt index 11c5bc1..4333468 100644 --- a/database/settings/src/main/kotlin/photos/network/database/settings/Module.kt +++ b/database/settings/src/main/kotlin/photos/network/database/settings/Module.kt @@ -23,7 +23,7 @@ import photos.network.common.persistence.Settings val databaseSettingsModule = module { single>( named("SettingsStorage"), - createdAtStart = true + createdAtStart = true, ) { SettingsStorage(context = get()) } diff --git a/database/sharing/src/main/kotlin/photos/network/database/sharing/Module.kt b/database/sharing/src/main/kotlin/photos/network/database/sharing/Module.kt index 02fcd54..7a7d2e0 100644 --- a/database/sharing/src/main/kotlin/photos/network/database/sharing/Module.kt +++ b/database/sharing/src/main/kotlin/photos/network/database/sharing/Module.kt @@ -23,7 +23,7 @@ import photos.network.common.persistence.User val databaseSharingModule = module { single>( named("UserStorage"), - createdAtStart = true + createdAtStart = true, ) { UserStorage(context = get()) } diff --git a/domain/folders/src/main/kotlin/photos/network/domain/folders/GetFoldersUseCase.kt b/domain/folders/src/main/kotlin/photos/network/domain/folders/GetFoldersUseCase.kt index 7a64f41..8d762b5 100644 --- a/domain/folders/src/main/kotlin/photos/network/domain/folders/GetFoldersUseCase.kt +++ b/domain/folders/src/main/kotlin/photos/network/domain/folders/GetFoldersUseCase.kt @@ -1,9 +1,26 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:Suppress("unused") + package photos.network.domain.folders import photos.network.repository.folders.FoldersRepository class GetFoldersUseCase( - private val foldersRepository: FoldersRepository + private val foldersRepository: FoldersRepository, ) { operator fun invoke(): List { return listOf("NOT-IMPLEMENTEd") diff --git a/domain/sharing/src/test/kotlin/photos/network/domain/sharing/UserMapper.kt b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/UserMapper.kt deleted file mode 100644 index 493caf6..0000000 --- a/domain/sharing/src/test/kotlin/photos/network/domain/sharing/UserMapper.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020-2023 Photos.network developers - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package photos.network.domain.sharing - -import photos.network.common.persistence.User as DatabaseUser -import photos.network.repository.sharing.User as RepositoryUser - -object UserMapper { - fun mapDatabaseToRepository(user: DatabaseUser) = RepositoryUser( - id = user.id, - lastname = user.lastname, - firstname = user.firstname, - profileImageUrl = user.profileImageUrl, - accessToken = user.accessToken, - refreshToken = user.refreshToken, - ) - - fun mapRepositoryToDatabase(user: RepositoryUser) = DatabaseUser( - id = user.id, - lastname = user.lastname, - firstname = user.firstname, - profileImageUrl = user.profileImageUrl, - accessToken = user.accessToken, - refreshToken = user.refreshToken, - ) -} diff --git a/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCaseTests.kt b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCaseTests.kt index 23694e1..73e7f14 100644 --- a/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCaseTests.kt +++ b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCaseTests.kt @@ -21,9 +21,9 @@ import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking +import org.junit.Ignore import org.junit.Rule import org.junit.Test -import photos.network.domain.sharing.UserMapper import photos.network.repository.sharing.User import photos.network.repository.sharing.UserRepository @@ -40,6 +40,7 @@ class GetCurrentUserUseCaseTests { ) } + @Ignore @Test fun `use case should return user if available`(): Unit = runBlocking { // given @@ -51,9 +52,9 @@ class GetCurrentUserUseCaseTests { accessToken = "access_token", ) - every { userRepository.currentUser() } answers { - UserMapper.mapRepositoryToDatabase(user) - } +// every { userRepository.currentUser() } answers { +// UserMapper.mapRepositoryToDatabase(user) +// } // when val result = getCurrentUserUseCase().first() diff --git a/repository/folders/build.gradle.kts b/repository/folders/build.gradle.kts index 961edd4..93dc72f 100644 --- a/repository/folders/build.gradle.kts +++ b/repository/folders/build.gradle.kts @@ -52,7 +52,6 @@ android { dependencies { implementation(projects.common) testImplementation(project(":common", "testArtifacts")) - testImplementation(project(mapOf("path" to ":common"))) androidTestImplementation(project(":common", "androidTestArtifacts")) api(projects.system.filesystem) diff --git a/repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepositoryImpl.kt b/repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepositoryImpl.kt index d88f804..a09c848 100644 --- a/repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepositoryImpl.kt +++ b/repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepositoryImpl.kt @@ -15,9 +15,9 @@ */ package photos.network.repository.folders -import java.nio.file.FileSystem import kotlinx.coroutines.flow.Flow import photos.network.system.filesystem.FileItem +import photos.network.system.filesystem.FileSystem import photos.network.system.filesystem.FolderItem class FoldersRepositoryImpl( diff --git a/repository/folders/src/test/kotlin/photos/network/repository/folders/FoldersRepositoryTest.kt b/repository/folders/src/test/kotlin/photos/network/repository/folders/FoldersRepositoryTest.kt index 01ea529..76ae161 100644 --- a/repository/folders/src/test/kotlin/photos/network/repository/folders/FoldersRepositoryTest.kt +++ b/repository/folders/src/test/kotlin/photos/network/repository/folders/FoldersRepositoryTest.kt @@ -13,126 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.repository.photos +package photos.network.repository.folders -import android.content.Context -import androidx.work.WorkManager -import com.google.common.truth.Truth -import io.mockk.every import io.mockk.mockk -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.runBlocking -import org.junit.Rule -import org.junit.Test -import photos.network.api.photo.PhotoApi -import photos.network.database.photos.PhotoDao +import photos.network.system.filesystem.FileSystem /** * Test photo repository */ -class PhotoRepositoryTest { +class FoldersRepositoryTest { - @get:Rule - val coroutineRule = TestCoroutineDispatcherRule() - - private val applicationContext = mockk() - private val photoApi = mockk() - private val photoDao = mockk() - private val workManager = mockk() + private val fileSystem = mockk() private val repository by lazy { - PhotoRepositoryImpl( - applicationContext = applicationContext, - photoApi = photoApi, - photoDao = photoDao, - workManager = workManager, - ) - } - - @Test - fun `should return all photos from persistence`() = runBlocking { - // given - every { photoDao.getPhotos() } answers { - flowOf( - listOf( - createFakePhoto(filename = "001", dateTaken = 1580671220), - createFakePhoto(filename = "002", dateTaken = 1580671221), - ), - ) - } - - // when - val photos = repository.getPhotos().first() - - // then - Truth.assertThat(photos.size).isEqualTo(2) - } - - @Test - fun `photos returned should be ordered by dateTaken`() = runBlocking { - // given - every { photoDao.getPhotos() } answers { - flowOf( - listOf( - createFakePhoto(filename = "002", dateTaken = 1580671221), - createFakePhoto(filename = "001", dateTaken = 1580671220), - createFakePhoto(filename = "003", dateTaken = 1580671223), - ), - ) - } - - // when - val photos = repository.getPhotos().first() - - // then - Truth.assertThat(photos[0].filename).isEqualTo("003") - Truth.assertThat(photos[1].filename).isEqualTo("002") - Truth.assertThat(photos[2].filename).isEqualTo("001") - } - - @Test - fun `photos returned should be ordered by dateAdded if dateTaken is not available`() = - runBlocking { - // given - every { photoDao.getPhotos() } answers { - flowOf( - listOf( - createFakePhoto(filename = "002", dateTaken = null, dateAdded = 1580671221), - createFakePhoto(filename = "001", dateTaken = null, dateAdded = 1580671220), - createFakePhoto(filename = "003", dateTaken = null, dateAdded = 1580671223), - ), - ) - } - - // when - val photos = repository.getPhotos().first() - - // then - Truth.assertThat(photos[0].filename).isEqualTo("003") - Truth.assertThat(photos[1].filename).isEqualTo("002") - Truth.assertThat(photos[2].filename).isEqualTo("001") - } - - private fun createFakePhoto( - uuid: String = "001", - filename: String = "IMG_20200202_202020.jpg", - imageUrl: String = "http://127.0.0.1/image/e369d958-ad41-4391-9ccb-f89be8ca1e8b", - dateAdded: Long = 1580671220, - dateTaken: Long? = null, - dateModified: Long? = null, - thumbnailFileUri: String? = null, - originalFileUri: String? = null, - ): Photo { - return Photo( -// uuid = uuid, - filename = filename, - imageUrl = imageUrl, - dateAdded = dateAdded, - dateTaken = dateTaken, - dateModified = dateModified, -// thumbnailFileUri = thumbnailFileUri, -// originalFileUri = originalFileUri, + FoldersRepositoryImpl( + fileSystem = fileSystem, ) } } diff --git a/repository/photos/src/main/kotlin/photos/network/repository/photos/Module.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/Module.kt index cd5677c..f83b653 100644 --- a/repository/photos/src/main/kotlin/photos/network/repository/photos/Module.kt +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/Module.kt @@ -54,7 +54,7 @@ val repositoryPhotosModule = module { photoApi = get(), photoDao = get(), workManager = get(), - mediaStore = get() + mediaStore = get(), ) } } diff --git a/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt index 11156d5..0a06c53 100644 --- a/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt @@ -20,8 +20,6 @@ import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.NetworkType import androidx.work.PeriodicWorkRequest import androidx.work.WorkManager -import java.time.Instant -import java.util.concurrent.TimeUnit import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.mapNotNull import logcat.LogPriority @@ -31,6 +29,8 @@ import photos.network.database.photos.PhotoDao import photos.network.repository.photos.worker.SyncLocalPhotosWorker import photos.network.repository.photos.worker.SyncStatus import photos.network.system.mediastore.MediaStore +import java.time.Instant +import java.util.concurrent.TimeUnit class PhotoRepositoryImpl( private val mediaStore: MediaStore, @@ -66,7 +66,7 @@ class PhotoRepositoryImpl( dateModified = null, isPrivate = false, uri = it.uri, - ) + ), ) } diff --git a/repository/photos/src/test/kotlin/photos/network/repository/photos/PhotoRepositoryTest.kt b/repository/photos/src/test/kotlin/photos/network/repository/photos/PhotoRepositoryTest.kt index 01ea529..3770e02 100644 --- a/repository/photos/src/test/kotlin/photos/network/repository/photos/PhotoRepositoryTest.kt +++ b/repository/photos/src/test/kotlin/photos/network/repository/photos/PhotoRepositoryTest.kt @@ -23,30 +23,28 @@ import io.mockk.mockk import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking -import org.junit.Rule import org.junit.Test import photos.network.api.photo.PhotoApi import photos.network.database.photos.PhotoDao +import photos.network.system.mediastore.MediaStore /** * Test photo repository */ class PhotoRepositoryTest { - @get:Rule - val coroutineRule = TestCoroutineDispatcherRule() - private val applicationContext = mockk() private val photoApi = mockk() private val photoDao = mockk() + private val mediaStore = mockk() private val workManager = mockk() private val repository by lazy { PhotoRepositoryImpl( - applicationContext = applicationContext, photoApi = photoApi, photoDao = photoDao, workManager = workManager, + mediaStore = mediaStore, ) } @@ -123,8 +121,8 @@ class PhotoRepositoryTest { dateModified: Long? = null, thumbnailFileUri: String? = null, originalFileUri: String? = null, - ): Photo { - return Photo( + ): photos.network.database.photos.Photo { + return photos.network.database.photos.Photo( // uuid = uuid, filename = filename, imageUrl = imageUrl, diff --git a/repository/settings/src/main/kotlin/photos/network/repository/settings/Module.kt b/repository/settings/src/main/kotlin/photos/network/repository/settings/Module.kt index 6c93007..09338da 100644 --- a/repository/settings/src/main/kotlin/photos/network/repository/settings/Module.kt +++ b/repository/settings/src/main/kotlin/photos/network/repository/settings/Module.kt @@ -19,8 +19,6 @@ import androidx.work.WorkManager import org.koin.android.ext.koin.androidApplication import org.koin.core.qualifier.named import org.koin.dsl.module -import photos.network.common.persistence.SecureStorage -import photos.network.common.persistence.Settings val repositorySettingsModule = module { factory { WorkManager.getInstance(androidApplication()) } diff --git a/repository/sharing/build.gradle.kts b/repository/sharing/build.gradle.kts index 9f98407..c0dedc0 100644 --- a/repository/sharing/build.gradle.kts +++ b/repository/sharing/build.gradle.kts @@ -87,4 +87,5 @@ dependencies { testImplementation(libs.mockk) testImplementation(libs.junit.junit) testImplementation(libs.truth) + testImplementation(libs.kotlinx.coroutines.test) } diff --git a/repository/sharing/src/main/kotlin/photos/network/repository/sharing/Module.kt b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/Module.kt index c062608..f62e2b4 100644 --- a/repository/sharing/src/main/kotlin/photos/network/repository/sharing/Module.kt +++ b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/Module.kt @@ -19,7 +19,6 @@ import androidx.work.WorkManager import org.koin.android.ext.koin.androidApplication import org.koin.core.qualifier.named import org.koin.dsl.module -import photos.network.common.persistence.SecureStorage val repositorySharingModule = module { factory { WorkManager.getInstance(androidApplication()) } diff --git a/repository/sharing/src/test/kotlin/photos/network/repository/sharing/UserRepositoryTests.kt b/repository/sharing/src/test/kotlin/photos/network/repository/sharing/UserRepositoryTests.kt index 3a72afa..1bc7740 100644 --- a/repository/sharing/src/test/kotlin/photos/network/repository/sharing/UserRepositoryTests.kt +++ b/repository/sharing/src/test/kotlin/photos/network/repository/sharing/UserRepositoryTests.kt @@ -19,18 +19,14 @@ import com.google.common.truth.Truth import io.mockk.coEvery import io.mockk.every import io.mockk.mockk -import kotlinx.coroutines.runBlocking -import org.junit.Rule +import kotlinx.coroutines.test.runTest import org.junit.Test import photos.network.api.user.UserApi import photos.network.api.user.entity.NetworkUser -import photos.network.common.TestCoroutineDispatcherRule import photos.network.common.persistence.SecureStorage import photos.network.common.persistence.User as DatabaseUser class UserRepositoryTests { - @get:Rule - val coroutineRule = TestCoroutineDispatcherRule() private val userStorage = mockk>() @@ -44,7 +40,7 @@ class UserRepositoryTests { } @Test - fun `should reflect user from persistence`() = runBlocking { + fun `should reflect user from persistence`() = runTest { // given every { userStorage.read()?.lastname } answers { fakeUser().lastname } every { userStorage.save(any()) } answers { Unit } diff --git a/system/filesystem/src/main/kotlin/photos/network/system/filesystem/FileSystem.kt b/system/filesystem/src/main/kotlin/photos/network/system/filesystem/FileSystem.kt index 662a743..563a9a0 100644 --- a/system/filesystem/src/main/kotlin/photos/network/system/filesystem/FileSystem.kt +++ b/system/filesystem/src/main/kotlin/photos/network/system/filesystem/FileSystem.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package photos.network.system.filesystem import android.net.Uri @@ -10,7 +25,7 @@ interface FileSystem { data class FolderItem( val name: String, val itemCount: Int, - val folderSize: Long + val folderSize: Long, ) data class FileItem( diff --git a/system/filesystem/src/main/kotlin/photos/network/system/filesystem/FileSystemImpl.kt b/system/filesystem/src/main/kotlin/photos/network/system/filesystem/FileSystemImpl.kt index 7274e09..800a082 100644 --- a/system/filesystem/src/main/kotlin/photos/network/system/filesystem/FileSystemImpl.kt +++ b/system/filesystem/src/main/kotlin/photos/network/system/filesystem/FileSystemImpl.kt @@ -1,15 +1,33 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package photos.network.system.filesystem import android.app.Application +import android.os.Environment class FileSystemImpl( - private val application: Application -): FileSystem { + private val application: Application, +) : FileSystem { override fun getFolders(): List { - TODO("Not yet implemented") + application.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + + return listOf() } override fun getItems(): List { - TODO("Not yet implemented") + return listOf() } } diff --git a/system/filesystem/src/main/kotlin/photos/network/system/filesystem/Module.kt b/system/filesystem/src/main/kotlin/photos/network/system/filesystem/Module.kt index 829a6f4..ef4fd92 100644 --- a/system/filesystem/src/main/kotlin/photos/network/system/filesystem/Module.kt +++ b/system/filesystem/src/main/kotlin/photos/network/system/filesystem/Module.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package photos.network.system.filesystem import org.koin.dsl.module diff --git a/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStore.kt b/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStore.kt index 8f9993a..d33a66e 100644 --- a/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStore.kt +++ b/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStore.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package photos.network.system.mediastore import android.net.Uri @@ -16,11 +31,11 @@ data class MediaItem( val exposure: String? = null, val fnumber: String? = null, val isoNumber: String? = null, - val location: Location? = null + val location: Location? = null, ) data class Location( val latitude: Float, val longitude: Float, - val altitude: Int + val altitude: Int, ) diff --git a/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStoreImpl.kt b/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStoreImpl.kt index 789cece..9da3440 100644 --- a/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStoreImpl.kt +++ b/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStoreImpl.kt @@ -1,19 +1,35 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package photos.network.system.mediastore import android.app.Application import android.location.Location import android.net.Uri import android.os.Build -import java.time.Instant import logcat.LogPriority import logcat.logcat +import java.time.Instant class MediaStoreImpl( - private val application: Application -): MediaStore { + private val application: Application, +) : MediaStore { /** * Query images from Androids local MediaStore (`DCIM/` and `Pictures/`) */ + @Suppress("ForbiddenComment", "NestedBlockDepth", "LongMethod") override fun queryLocalMediaStore(): List { val photos = mutableListOf() @@ -123,7 +139,13 @@ class MediaStoreImpl( } } - logcat(priority = LogPriority.ERROR) { "details: exposure=$exposure, fnumber=$fnumber, isoNumber=$isoNumber, lat=${latLong[0]}, lon=${latLong[0]}" } + logcat(priority = LogPriority.ERROR) { + "details: exposure=$exposure, " + + "fnumber=$fnumber, " + + "isoNumber=$isoNumber, " + + "lat=${latLong[0]}, " + + "lon=${latLong[0]}" + } photos += MediaItem( id = id, diff --git a/ui/photos/build.gradle.kts b/ui/photos/build.gradle.kts index 6bc8f87..ffc7201 100644 --- a/ui/photos/build.gradle.kts +++ b/ui/photos/build.gradle.kts @@ -68,4 +68,5 @@ dependencies { testImplementation(libs.junit.junit) testImplementation(libs.truth) testImplementation(libs.core.testing) + testImplementation(libs.kotlinx.coroutines.test) } diff --git a/ui/photos/src/test/kotlin/photos/network/ui/photos/photos/PhotosViewModelTests.kt b/ui/photos/src/test/kotlin/photos/network/ui/photos/photos/PhotosViewModelTests.kt index 49da1c8..7dc9a43 100644 --- a/ui/photos/src/test/kotlin/photos/network/ui/photos/photos/PhotosViewModelTests.kt +++ b/ui/photos/src/test/kotlin/photos/network/ui/photos/photos/PhotosViewModelTests.kt @@ -13,13 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package photos.network.ui.photos.photos import com.google.common.truth.Truth +import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk -import io.mockk.verify import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain @@ -28,9 +32,10 @@ import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before import org.junit.Test -import photos.network.data.photos.repository.Photo import photos.network.domain.photos.usecase.GetPhotosUseCase import photos.network.domain.photos.usecase.StartPhotosSyncUseCase +import photos.network.repository.photos.Photo +import photos.network.repository.photos.worker.SyncStatus import java.time.Instant class PhotosViewModelTests { @@ -88,15 +93,15 @@ class PhotosViewModelTests { } @Test - fun `viewmodel should start sync when opened`() { + fun `viewmodel should start sync when opened`() = runTest { // given - every { startPhotosSyncUseCase() } answers { Unit } + coEvery { startPhotosSyncUseCase() } answers { SyncStatus.SyncSucceeded } every { getPhotosUseCase() } answers { flowOf(emptyList()) } // when viewmodel.handleEvent(photos.network.ui.photos.PhotosEvent.StartLocalPhotoSyncEvent) // then - verify(atLeast = 1) { startPhotosSyncUseCase.invoke() } + coVerify(atLeast = 1) { startPhotosSyncUseCase.invoke() } } } From c1e61a2b54921d1aaa05a555e362d9f88d2d548c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Stu=CC=88rmer?= Date: Tue, 16 May 2023 00:10:06 +0200 Subject: [PATCH 06/13] adjust copy right header --- api/src/main/kotlin/photos/network/api/photo/PhotoApi.kt | 2 +- api/src/main/kotlin/photos/network/api/photo/PhotoApiImpl.kt | 2 +- api/src/main/kotlin/photos/network/api/photo/Photos.kt | 2 +- api/src/main/kotlin/photos/network/api/photo/entity/Photo.kt | 2 +- api/src/main/kotlin/photos/network/api/status/StatusApiImpl.kt | 2 +- api/src/main/kotlin/photos/network/api/status/entity/Status.kt | 2 +- api/src/main/kotlin/photos/network/api/user/UserApi.kt | 2 +- api/src/main/kotlin/photos/network/api/user/UserApiImpl.kt | 2 +- .../main/kotlin/photos/network/api/user/entity/NetworkUser.kt | 2 +- api/src/main/kotlin/photos/network/api/user/entity/TokenInfo.kt | 2 +- api/src/test/kotlin/photos/network/api/photo/PhotoApiTests.kt | 2 +- .../test/kotlin/photos/network/api/photo/entity/PhotoTest.kt | 2 +- api/src/test/kotlin/photos/network/api/user/UserApiTests.kt | 2 +- .../kotlin/photos/network/PhotosNetworkJUnitRunner.kt | 2 +- app/src/main/kotlin/photos/network/AppModule.kt | 2 +- app/src/main/kotlin/photos/network/MainActivity.kt | 2 +- app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt | 2 +- app/src/main/kotlin/photos/network/home/Home.kt | 2 +- app/src/main/kotlin/photos/network/home/HomeEvent.kt | 2 +- app/src/main/kotlin/photos/network/home/HomeUiState.kt | 2 +- app/src/main/kotlin/photos/network/home/HomeViewModel.kt | 2 +- app/src/main/kotlin/photos/network/ui/SearchBar.kt | 2 +- app/src/main/kotlin/photos/network/ui/TextInput.kt | 2 +- app/src/main/kotlin/photos/network/ui/UserAvatar.kt | 2 +- app/src/main/kotlin/photos/network/user/CurrentUserHost.kt | 2 +- app/src/main/kotlin/photos/network/user/CurrentUserViewModel.kt | 2 +- app/src/test/kotlin/photos/network/CurrentUserViewModelTests.kt | 2 +- .../kotlin/photos/network/common/PhotosNetworkMockFileReader.kt | 2 +- .../photos/network/common/keystore/FakeAndroidKeyStore.kt | 2 +- .../photos/network/common/persistence/SecureStorageTest.kt | 2 +- common/src/main/kotlin/photos/network/common/Resource.kt | 2 +- .../kotlin/photos/network/common/persistence/PrivacyState.kt | 2 +- .../kotlin/photos/network/common/persistence/SecureStorage.kt | 2 +- .../main/kotlin/photos/network/common/persistence/Settings.kt | 2 +- .../src/main/kotlin/photos/network/common/persistence/User.kt | 2 +- .../kotlin/photos/network/common/TestCoroutineDispatcherRule.kt | 2 +- .../kotlin/photos/network/PhotoDatabaseMigrationTests.kt | 2 +- .../photos/network/database/photos/DatabasePhotosModule.kt | 2 +- .../src/main/kotlin/photos/network/database/photos/Photo.kt | 2 +- .../src/main/kotlin/photos/network/database/photos/PhotoDao.kt | 2 +- .../main/kotlin/photos/network/database/photos/PhotoDatabase.kt | 2 +- .../kotlin/photos/network/database/settings/SettingsStorage.kt | 2 +- .../main/kotlin/photos/network/database/sharing/UserStorage.kt | 2 +- .../main/kotlin/photos/network/domain/photos/model/Location.kt | 2 +- .../kotlin/photos/network/domain/photos/model/PhotoElement.kt | 2 +- .../main/kotlin/photos/network/domain/photos/model/PhotoList.kt | 2 +- .../photos/network/domain/photos/model/TechnicalDetails.kt | 2 +- .../photos/network/domain/photos/usecase/GetPhotoUseCase.kt | 2 +- .../photos/network/domain/photos/usecase/GetPhotosUseCase.kt | 2 +- .../network/domain/photos/usecase/StartPhotosSyncUseCase.kt | 2 +- .../kotlin/photos/network/domain/photos/model/LocationTests.kt | 2 +- .../network/domain/photos/usecase/GetPhotoUseCaseTests.kt | 2 +- .../network/domain/photos/usecase/GetPhotosUseCaseTests.kt | 2 +- .../domain/photos/usecase/StartPhotosSyncUseCaseTests.kt | 2 +- .../network/domain/settings/usecase/GetSettingsUseCase.kt | 2 +- .../network/domain/settings/usecase/TogglePrivacyUseCase.kt | 2 +- .../network/domain/settings/usecase/UpdateClientIdUseCase.kt | 2 +- .../photos/network/domain/settings/usecase/UpdateHostUseCase.kt | 2 +- .../network/domain/settings/usecase/VerifyClientIdUseCase.kt | 2 +- .../network/domain/settings/usecase/VerifyServerHostUseCase.kt | 2 +- .../network/domain/settings/usecase/GetSettingsUseCaseTests.kt | 2 +- .../domain/settings/usecase/TogglePrivacyUseCaseTests.kt | 2 +- .../domain/settings/usecase/UpdateClientIdUseCaseTests.kt | 2 +- .../network/domain/settings/usecase/UpdateHostUseCaseTests.kt | 2 +- .../domain/settings/usecase/VerifyClientIdUseCaseTests.kt | 2 +- .../domain/settings/usecase/VerifyServerHostUseCaseTests.kt | 2 +- .../kotlin/photos/network/domain/sharing/FailingTest.kt | 2 +- .../src/main/kotlin/photos/network/domain/sharing/Token.kt | 2 +- .../src/main/kotlin/photos/network/domain/sharing/User.kt | 2 +- .../network/domain/sharing/usecase/GetCurrentUserUseCase.kt | 2 +- .../photos/network/domain/sharing/usecase/LogoutUseCase.kt | 2 +- .../network/domain/sharing/usecase/RequestAccessTokenUseCase.kt | 2 +- .../domain/sharing/usecase/GetCurrentUserUseCaseTests.kt | 2 +- .../photos/network/domain/sharing/usecase/LogoutUseCaseTests.kt | 2 +- .../domain/sharing/usecase/RequestAccessTokenUseCaseTests.kt | 2 +- .../photos/network/repository/folders/FoldersRepository.kt | 2 +- .../photos/network/repository/folders/FoldersRepositoryImpl.kt | 2 +- .../photos/network/repository/folders/FoldersRepositoryTest.kt | 2 +- .../src/main/kotlin/photos/network/repository/photos/Photo.kt | 2 +- .../kotlin/photos/network/repository/photos/PhotoRepository.kt | 2 +- .../photos/network/repository/photos/PhotoRepositoryImpl.kt | 2 +- .../network/repository/photos/worker/CleanResourcesWorker.kt | 2 +- .../network/repository/photos/worker/SyncLocalPhotosWorker.kt | 2 +- .../network/repository/photos/worker/UploadPhotosWorker.kt | 2 +- .../photos/network/repository/photos/PhotoRepositoryTest.kt | 2 +- .../photos/network/repository/settings/SettingsRepository.kt | 2 +- .../network/repository/settings/SettingsRepositoryImpl.kt | 2 +- .../src/main/kotlin/photos/network/repository/sharing/User.kt | 2 +- .../kotlin/photos/network/repository/sharing/UserRepository.kt | 2 +- .../photos/network/repository/sharing/UserRepositoryImpl.kt | 2 +- .../photos/network/repository/sharing/UserRepositoryTests.kt | 2 +- .../src/main/kotlin/photos/network/ui/albums/AlbumsScreen.kt | 2 +- .../kotlin/photos/network/ui/common/components/ActivityLog.kt | 2 +- .../main/kotlin/photos/network/ui/common/components/AppLogo.kt | 2 +- .../kotlin/photos/network/ui/common/navigation/Destination.kt | 2 +- .../src/main/kotlin/photos/network/ui/common/theme/Colors.kt | 2 +- .../src/main/kotlin/photos/network/ui/common/theme/Theme.kt | 2 +- .../src/main/kotlin/photos/network/ui/folders/FoldersScreen.kt | 2 +- .../androidTest/kotlin/photos/network/PhotosNetworkTestUtils.kt | 2 +- .../kotlin/photos/network/ui/photos/PhotosScreenTests.kt | 2 +- .../main/kotlin/photos/network/ui/photos/PhotoBottomIcons.kt | 2 +- .../src/main/kotlin/photos/network/ui/photos/PhotoDetails.kt | 2 +- ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt | 2 +- .../src/main/kotlin/photos/network/ui/photos/PhotoTopIcons.kt | 2 +- .../src/main/kotlin/photos/network/ui/photos/PhotosEvent.kt | 2 +- .../src/main/kotlin/photos/network/ui/photos/PhotosScreen.kt | 2 +- .../src/main/kotlin/photos/network/ui/photos/PhotosUiState.kt | 2 +- .../src/main/kotlin/photos/network/ui/photos/PhotosViewModel.kt | 2 +- ui/photos/src/main/kotlin/photos/network/ui/photos/Tag.kt | 2 +- ui/photos/src/main/kotlin/photos/network/ui/photos/TagLines.kt | 2 +- .../photos/network/ui/photos/photos/PhotosViewModelTests.kt | 2 +- .../kotlin/photos/network/ui/settings/SettingsScreenTests.kt | 2 +- .../src/main/kotlin/photos/network/ui/settings/SettingsEvent.kt | 2 +- .../main/kotlin/photos/network/ui/settings/SettingsScreen.kt | 2 +- .../main/kotlin/photos/network/ui/settings/SettingsUiState.kt | 2 +- .../main/kotlin/photos/network/ui/settings/SettingsViewModel.kt | 2 +- .../main/kotlin/photos/network/ui/sharing/login/LoginEvent.kt | 2 +- .../main/kotlin/photos/network/ui/sharing/login/LoginScreen.kt | 2 +- .../main/kotlin/photos/network/ui/sharing/login/LoginUiState.kt | 2 +- .../kotlin/photos/network/ui/sharing/login/LoginViewModel.kt | 2 +- 120 files changed, 120 insertions(+), 120 deletions(-) diff --git a/api/src/main/kotlin/photos/network/api/photo/PhotoApi.kt b/api/src/main/kotlin/photos/network/api/photo/PhotoApi.kt index 56caffb..288b00a 100644 --- a/api/src/main/kotlin/photos/network/api/photo/PhotoApi.kt +++ b/api/src/main/kotlin/photos/network/api/photo/PhotoApi.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/api/src/main/kotlin/photos/network/api/photo/PhotoApiImpl.kt b/api/src/main/kotlin/photos/network/api/photo/PhotoApiImpl.kt index f684749..61320ec 100644 --- a/api/src/main/kotlin/photos/network/api/photo/PhotoApiImpl.kt +++ b/api/src/main/kotlin/photos/network/api/photo/PhotoApiImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/api/src/main/kotlin/photos/network/api/photo/Photos.kt b/api/src/main/kotlin/photos/network/api/photo/Photos.kt index ead3b7e..d1223e7 100644 --- a/api/src/main/kotlin/photos/network/api/photo/Photos.kt +++ b/api/src/main/kotlin/photos/network/api/photo/Photos.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/api/src/main/kotlin/photos/network/api/photo/entity/Photo.kt b/api/src/main/kotlin/photos/network/api/photo/entity/Photo.kt index 0f8e809..3d0d1dc 100644 --- a/api/src/main/kotlin/photos/network/api/photo/entity/Photo.kt +++ b/api/src/main/kotlin/photos/network/api/photo/entity/Photo.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/api/src/main/kotlin/photos/network/api/status/StatusApiImpl.kt b/api/src/main/kotlin/photos/network/api/status/StatusApiImpl.kt index 64222a3..fe2a9ea 100644 --- a/api/src/main/kotlin/photos/network/api/status/StatusApiImpl.kt +++ b/api/src/main/kotlin/photos/network/api/status/StatusApiImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/api/src/main/kotlin/photos/network/api/status/entity/Status.kt b/api/src/main/kotlin/photos/network/api/status/entity/Status.kt index f838ead..4e18463 100644 --- a/api/src/main/kotlin/photos/network/api/status/entity/Status.kt +++ b/api/src/main/kotlin/photos/network/api/status/entity/Status.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/api/src/main/kotlin/photos/network/api/user/UserApi.kt b/api/src/main/kotlin/photos/network/api/user/UserApi.kt index 962133b..19b1680 100644 --- a/api/src/main/kotlin/photos/network/api/user/UserApi.kt +++ b/api/src/main/kotlin/photos/network/api/user/UserApi.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/api/src/main/kotlin/photos/network/api/user/UserApiImpl.kt b/api/src/main/kotlin/photos/network/api/user/UserApiImpl.kt index 9d98d9d..40d1d16 100644 --- a/api/src/main/kotlin/photos/network/api/user/UserApiImpl.kt +++ b/api/src/main/kotlin/photos/network/api/user/UserApiImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/api/src/main/kotlin/photos/network/api/user/entity/NetworkUser.kt b/api/src/main/kotlin/photos/network/api/user/entity/NetworkUser.kt index db78c4e..55349fd 100644 --- a/api/src/main/kotlin/photos/network/api/user/entity/NetworkUser.kt +++ b/api/src/main/kotlin/photos/network/api/user/entity/NetworkUser.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/api/src/main/kotlin/photos/network/api/user/entity/TokenInfo.kt b/api/src/main/kotlin/photos/network/api/user/entity/TokenInfo.kt index b7d73cc..4882eb6 100644 --- a/api/src/main/kotlin/photos/network/api/user/entity/TokenInfo.kt +++ b/api/src/main/kotlin/photos/network/api/user/entity/TokenInfo.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/api/src/test/kotlin/photos/network/api/photo/PhotoApiTests.kt b/api/src/test/kotlin/photos/network/api/photo/PhotoApiTests.kt index 7c36578..923b6d6 100644 --- a/api/src/test/kotlin/photos/network/api/photo/PhotoApiTests.kt +++ b/api/src/test/kotlin/photos/network/api/photo/PhotoApiTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/api/src/test/kotlin/photos/network/api/photo/entity/PhotoTest.kt b/api/src/test/kotlin/photos/network/api/photo/entity/PhotoTest.kt index cb6e3b9..9b23367 100644 --- a/api/src/test/kotlin/photos/network/api/photo/entity/PhotoTest.kt +++ b/api/src/test/kotlin/photos/network/api/photo/entity/PhotoTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/api/src/test/kotlin/photos/network/api/user/UserApiTests.kt b/api/src/test/kotlin/photos/network/api/user/UserApiTests.kt index 0265030..c9b1fd2 100644 --- a/api/src/test/kotlin/photos/network/api/user/UserApiTests.kt +++ b/api/src/test/kotlin/photos/network/api/user/UserApiTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/app/src/androidTest/kotlin/photos/network/PhotosNetworkJUnitRunner.kt b/app/src/androidTest/kotlin/photos/network/PhotosNetworkJUnitRunner.kt index 48e2dbc..78c6612 100644 --- a/app/src/androidTest/kotlin/photos/network/PhotosNetworkJUnitRunner.kt +++ b/app/src/androidTest/kotlin/photos/network/PhotosNetworkJUnitRunner.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/app/src/main/kotlin/photos/network/AppModule.kt b/app/src/main/kotlin/photos/network/AppModule.kt index a30f88b..86d73ae 100644 --- a/app/src/main/kotlin/photos/network/AppModule.kt +++ b/app/src/main/kotlin/photos/network/AppModule.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/app/src/main/kotlin/photos/network/MainActivity.kt b/app/src/main/kotlin/photos/network/MainActivity.kt index 073f768..5b39205 100644 --- a/app/src/main/kotlin/photos/network/MainActivity.kt +++ b/app/src/main/kotlin/photos/network/MainActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt b/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt index 2db69ce..10394ef 100644 --- a/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt +++ b/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/app/src/main/kotlin/photos/network/home/Home.kt b/app/src/main/kotlin/photos/network/home/Home.kt index 9a727c0..234685c 100644 --- a/app/src/main/kotlin/photos/network/home/Home.kt +++ b/app/src/main/kotlin/photos/network/home/Home.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/app/src/main/kotlin/photos/network/home/HomeEvent.kt b/app/src/main/kotlin/photos/network/home/HomeEvent.kt index 9f45d60..c54079f 100644 --- a/app/src/main/kotlin/photos/network/home/HomeEvent.kt +++ b/app/src/main/kotlin/photos/network/home/HomeEvent.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/app/src/main/kotlin/photos/network/home/HomeUiState.kt b/app/src/main/kotlin/photos/network/home/HomeUiState.kt index a7f858c..f9894e5 100644 --- a/app/src/main/kotlin/photos/network/home/HomeUiState.kt +++ b/app/src/main/kotlin/photos/network/home/HomeUiState.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/app/src/main/kotlin/photos/network/home/HomeViewModel.kt b/app/src/main/kotlin/photos/network/home/HomeViewModel.kt index f408818..f4c63df 100644 --- a/app/src/main/kotlin/photos/network/home/HomeViewModel.kt +++ b/app/src/main/kotlin/photos/network/home/HomeViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/app/src/main/kotlin/photos/network/ui/SearchBar.kt b/app/src/main/kotlin/photos/network/ui/SearchBar.kt index d666d4e..2f25ddb 100644 --- a/app/src/main/kotlin/photos/network/ui/SearchBar.kt +++ b/app/src/main/kotlin/photos/network/ui/SearchBar.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/app/src/main/kotlin/photos/network/ui/TextInput.kt b/app/src/main/kotlin/photos/network/ui/TextInput.kt index faaf9df..568355a 100644 --- a/app/src/main/kotlin/photos/network/ui/TextInput.kt +++ b/app/src/main/kotlin/photos/network/ui/TextInput.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/app/src/main/kotlin/photos/network/ui/UserAvatar.kt b/app/src/main/kotlin/photos/network/ui/UserAvatar.kt index 6615bfe..0a670ad 100644 --- a/app/src/main/kotlin/photos/network/ui/UserAvatar.kt +++ b/app/src/main/kotlin/photos/network/ui/UserAvatar.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/app/src/main/kotlin/photos/network/user/CurrentUserHost.kt b/app/src/main/kotlin/photos/network/user/CurrentUserHost.kt index 116f0f0..a07ea12 100644 --- a/app/src/main/kotlin/photos/network/user/CurrentUserHost.kt +++ b/app/src/main/kotlin/photos/network/user/CurrentUserHost.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/app/src/main/kotlin/photos/network/user/CurrentUserViewModel.kt b/app/src/main/kotlin/photos/network/user/CurrentUserViewModel.kt index 4d76423..8550f64 100644 --- a/app/src/main/kotlin/photos/network/user/CurrentUserViewModel.kt +++ b/app/src/main/kotlin/photos/network/user/CurrentUserViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/app/src/test/kotlin/photos/network/CurrentUserViewModelTests.kt b/app/src/test/kotlin/photos/network/CurrentUserViewModelTests.kt index 059d577..250ed5f 100644 --- a/app/src/test/kotlin/photos/network/CurrentUserViewModelTests.kt +++ b/app/src/test/kotlin/photos/network/CurrentUserViewModelTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/common/src/androidTest/kotlin/photos/network/common/PhotosNetworkMockFileReader.kt b/common/src/androidTest/kotlin/photos/network/common/PhotosNetworkMockFileReader.kt index e1a927f..f7fb5fa 100644 --- a/common/src/androidTest/kotlin/photos/network/common/PhotosNetworkMockFileReader.kt +++ b/common/src/androidTest/kotlin/photos/network/common/PhotosNetworkMockFileReader.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/common/src/androidTest/kotlin/photos/network/common/keystore/FakeAndroidKeyStore.kt b/common/src/androidTest/kotlin/photos/network/common/keystore/FakeAndroidKeyStore.kt index 6204508..f64f024 100644 --- a/common/src/androidTest/kotlin/photos/network/common/keystore/FakeAndroidKeyStore.kt +++ b/common/src/androidTest/kotlin/photos/network/common/keystore/FakeAndroidKeyStore.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/common/src/androidTest/kotlin/photos/network/common/persistence/SecureStorageTest.kt b/common/src/androidTest/kotlin/photos/network/common/persistence/SecureStorageTest.kt index 0086da3..9d9706f 100644 --- a/common/src/androidTest/kotlin/photos/network/common/persistence/SecureStorageTest.kt +++ b/common/src/androidTest/kotlin/photos/network/common/persistence/SecureStorageTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/common/src/main/kotlin/photos/network/common/Resource.kt b/common/src/main/kotlin/photos/network/common/Resource.kt index 05210d2..1ff7738 100644 --- a/common/src/main/kotlin/photos/network/common/Resource.kt +++ b/common/src/main/kotlin/photos/network/common/Resource.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/common/src/main/kotlin/photos/network/common/persistence/PrivacyState.kt b/common/src/main/kotlin/photos/network/common/persistence/PrivacyState.kt index 580b810..2730ef7 100644 --- a/common/src/main/kotlin/photos/network/common/persistence/PrivacyState.kt +++ b/common/src/main/kotlin/photos/network/common/persistence/PrivacyState.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/common/src/main/kotlin/photos/network/common/persistence/SecureStorage.kt b/common/src/main/kotlin/photos/network/common/persistence/SecureStorage.kt index a21640a..fa077fd 100644 --- a/common/src/main/kotlin/photos/network/common/persistence/SecureStorage.kt +++ b/common/src/main/kotlin/photos/network/common/persistence/SecureStorage.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/common/src/main/kotlin/photos/network/common/persistence/Settings.kt b/common/src/main/kotlin/photos/network/common/persistence/Settings.kt index f9a76b7..9a9c4f0 100644 --- a/common/src/main/kotlin/photos/network/common/persistence/Settings.kt +++ b/common/src/main/kotlin/photos/network/common/persistence/Settings.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/common/src/main/kotlin/photos/network/common/persistence/User.kt b/common/src/main/kotlin/photos/network/common/persistence/User.kt index 7168007..89ba128 100644 --- a/common/src/main/kotlin/photos/network/common/persistence/User.kt +++ b/common/src/main/kotlin/photos/network/common/persistence/User.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/common/src/test/kotlin/photos/network/common/TestCoroutineDispatcherRule.kt b/common/src/test/kotlin/photos/network/common/TestCoroutineDispatcherRule.kt index be0242e..4eb5826 100644 --- a/common/src/test/kotlin/photos/network/common/TestCoroutineDispatcherRule.kt +++ b/common/src/test/kotlin/photos/network/common/TestCoroutineDispatcherRule.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/database/photos/src/androidTest/kotlin/photos/network/PhotoDatabaseMigrationTests.kt b/database/photos/src/androidTest/kotlin/photos/network/PhotoDatabaseMigrationTests.kt index 64e8751..961c804 100644 --- a/database/photos/src/androidTest/kotlin/photos/network/PhotoDatabaseMigrationTests.kt +++ b/database/photos/src/androidTest/kotlin/photos/network/PhotoDatabaseMigrationTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/database/photos/src/main/kotlin/photos/network/database/photos/DatabasePhotosModule.kt b/database/photos/src/main/kotlin/photos/network/database/photos/DatabasePhotosModule.kt index edfa2b5..9915096 100644 --- a/database/photos/src/main/kotlin/photos/network/database/photos/DatabasePhotosModule.kt +++ b/database/photos/src/main/kotlin/photos/network/database/photos/DatabasePhotosModule.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/database/photos/src/main/kotlin/photos/network/database/photos/Photo.kt b/database/photos/src/main/kotlin/photos/network/database/photos/Photo.kt index e030107..9b9963c 100644 --- a/database/photos/src/main/kotlin/photos/network/database/photos/Photo.kt +++ b/database/photos/src/main/kotlin/photos/network/database/photos/Photo.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/database/photos/src/main/kotlin/photos/network/database/photos/PhotoDao.kt b/database/photos/src/main/kotlin/photos/network/database/photos/PhotoDao.kt index a321916..01994d3 100644 --- a/database/photos/src/main/kotlin/photos/network/database/photos/PhotoDao.kt +++ b/database/photos/src/main/kotlin/photos/network/database/photos/PhotoDao.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/database/photos/src/main/kotlin/photos/network/database/photos/PhotoDatabase.kt b/database/photos/src/main/kotlin/photos/network/database/photos/PhotoDatabase.kt index b15e5c9..afb5524 100644 --- a/database/photos/src/main/kotlin/photos/network/database/photos/PhotoDatabase.kt +++ b/database/photos/src/main/kotlin/photos/network/database/photos/PhotoDatabase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/database/settings/src/main/kotlin/photos/network/database/settings/SettingsStorage.kt b/database/settings/src/main/kotlin/photos/network/database/settings/SettingsStorage.kt index 3c2229f..801149b 100644 --- a/database/settings/src/main/kotlin/photos/network/database/settings/SettingsStorage.kt +++ b/database/settings/src/main/kotlin/photos/network/database/settings/SettingsStorage.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/database/sharing/src/main/kotlin/photos/network/database/sharing/UserStorage.kt b/database/sharing/src/main/kotlin/photos/network/database/sharing/UserStorage.kt index 1bc6e29..ec023d6 100644 --- a/database/sharing/src/main/kotlin/photos/network/database/sharing/UserStorage.kt +++ b/database/sharing/src/main/kotlin/photos/network/database/sharing/UserStorage.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/photos/src/main/kotlin/photos/network/domain/photos/model/Location.kt b/domain/photos/src/main/kotlin/photos/network/domain/photos/model/Location.kt index c333bb2..c1cd93a 100644 --- a/domain/photos/src/main/kotlin/photos/network/domain/photos/model/Location.kt +++ b/domain/photos/src/main/kotlin/photos/network/domain/photos/model/Location.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/photos/src/main/kotlin/photos/network/domain/photos/model/PhotoElement.kt b/domain/photos/src/main/kotlin/photos/network/domain/photos/model/PhotoElement.kt index 4f1119c..e971dea 100644 --- a/domain/photos/src/main/kotlin/photos/network/domain/photos/model/PhotoElement.kt +++ b/domain/photos/src/main/kotlin/photos/network/domain/photos/model/PhotoElement.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/photos/src/main/kotlin/photos/network/domain/photos/model/PhotoList.kt b/domain/photos/src/main/kotlin/photos/network/domain/photos/model/PhotoList.kt index e44e4ca..af9189c 100644 --- a/domain/photos/src/main/kotlin/photos/network/domain/photos/model/PhotoList.kt +++ b/domain/photos/src/main/kotlin/photos/network/domain/photos/model/PhotoList.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/photos/src/main/kotlin/photos/network/domain/photos/model/TechnicalDetails.kt b/domain/photos/src/main/kotlin/photos/network/domain/photos/model/TechnicalDetails.kt index 5c8060f..79f15ad 100644 --- a/domain/photos/src/main/kotlin/photos/network/domain/photos/model/TechnicalDetails.kt +++ b/domain/photos/src/main/kotlin/photos/network/domain/photos/model/TechnicalDetails.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCase.kt b/domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCase.kt index 96c3b72..fd43a9f 100644 --- a/domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCase.kt +++ b/domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCase.kt b/domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCase.kt index e3e1951..df2c6ac 100644 --- a/domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCase.kt +++ b/domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCase.kt b/domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCase.kt index 68aceaf..a375074 100644 --- a/domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCase.kt +++ b/domain/photos/src/main/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/photos/src/test/kotlin/photos/network/domain/photos/model/LocationTests.kt b/domain/photos/src/test/kotlin/photos/network/domain/photos/model/LocationTests.kt index 3bd8ec8..333a7e7 100644 --- a/domain/photos/src/test/kotlin/photos/network/domain/photos/model/LocationTests.kt +++ b/domain/photos/src/test/kotlin/photos/network/domain/photos/model/LocationTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCaseTests.kt b/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCaseTests.kt index dafb645..4aee876 100644 --- a/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCaseTests.kt +++ b/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotoUseCaseTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCaseTests.kt b/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCaseTests.kt index 49267a0..0adac8d 100644 --- a/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCaseTests.kt +++ b/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/GetPhotosUseCaseTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCaseTests.kt b/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCaseTests.kt index aafb3e8..2a008f4 100644 --- a/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCaseTests.kt +++ b/domain/photos/src/test/kotlin/photos/network/domain/photos/usecase/StartPhotosSyncUseCaseTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCase.kt b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCase.kt index 5e135d9..6ccf713 100644 --- a/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCase.kt +++ b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCase.kt b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCase.kt index 26a45c0..871aa1b 100644 --- a/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCase.kt +++ b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCase.kt b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCase.kt index f766be2..718b2b4 100644 --- a/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCase.kt +++ b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCase.kt b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCase.kt index 4cb87f0..df7b870 100644 --- a/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCase.kt +++ b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCase.kt b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCase.kt index 53f6d25..1f093b5 100644 --- a/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCase.kt +++ b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCase.kt b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCase.kt index a5182c9..753123e 100644 --- a/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCase.kt +++ b/domain/settings/src/main/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCaseTests.kt b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCaseTests.kt index f1007e5..1ed0439 100644 --- a/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCaseTests.kt +++ b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/GetSettingsUseCaseTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCaseTests.kt b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCaseTests.kt index 0b1bada..a76a795 100644 --- a/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCaseTests.kt +++ b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/TogglePrivacyUseCaseTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCaseTests.kt b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCaseTests.kt index 704d3b6..8baaec5 100644 --- a/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCaseTests.kt +++ b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/UpdateClientIdUseCaseTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCaseTests.kt b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCaseTests.kt index 9aff38e..6e32477 100644 --- a/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCaseTests.kt +++ b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/UpdateHostUseCaseTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCaseTests.kt b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCaseTests.kt index 35ec4ef..54c9f07 100644 --- a/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCaseTests.kt +++ b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyClientIdUseCaseTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCaseTests.kt b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCaseTests.kt index 38f7a3a..817bb6a 100644 --- a/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCaseTests.kt +++ b/domain/settings/src/test/kotlin/photos/network/domain/settings/usecase/VerifyServerHostUseCaseTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/sharing/src/androidTest/kotlin/photos/network/domain/sharing/FailingTest.kt b/domain/sharing/src/androidTest/kotlin/photos/network/domain/sharing/FailingTest.kt index 842ee49..b4a62af 100644 --- a/domain/sharing/src/androidTest/kotlin/photos/network/domain/sharing/FailingTest.kt +++ b/domain/sharing/src/androidTest/kotlin/photos/network/domain/sharing/FailingTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/sharing/src/main/kotlin/photos/network/domain/sharing/Token.kt b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/Token.kt index 0601a48..e6accdc 100644 --- a/domain/sharing/src/main/kotlin/photos/network/domain/sharing/Token.kt +++ b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/Token.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/sharing/src/main/kotlin/photos/network/domain/sharing/User.kt b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/User.kt index 533b020..9266120 100644 --- a/domain/sharing/src/main/kotlin/photos/network/domain/sharing/User.kt +++ b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/User.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCase.kt b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCase.kt index 4b8ed2f..245e9f8 100644 --- a/domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCase.kt +++ b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/LogoutUseCase.kt b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/LogoutUseCase.kt index f4802ae..9b6ae6a 100644 --- a/domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/LogoutUseCase.kt +++ b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/LogoutUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/RequestAccessTokenUseCase.kt b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/RequestAccessTokenUseCase.kt index fa728f0..189f67f 100644 --- a/domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/RequestAccessTokenUseCase.kt +++ b/domain/sharing/src/main/kotlin/photos/network/domain/sharing/usecase/RequestAccessTokenUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCaseTests.kt b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCaseTests.kt index 73e7f14..82edf43 100644 --- a/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCaseTests.kt +++ b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/GetCurrentUserUseCaseTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/LogoutUseCaseTests.kt b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/LogoutUseCaseTests.kt index 6d9b7e6..0d1dbb9 100644 --- a/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/LogoutUseCaseTests.kt +++ b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/LogoutUseCaseTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/RequestAccessTokenUseCaseTests.kt b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/RequestAccessTokenUseCaseTests.kt index a5d65fb..41cf895 100644 --- a/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/RequestAccessTokenUseCaseTests.kt +++ b/domain/sharing/src/test/kotlin/photos/network/domain/sharing/usecase/RequestAccessTokenUseCaseTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepository.kt b/repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepository.kt index f32b416..09a2608 100644 --- a/repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepository.kt +++ b/repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepositoryImpl.kt b/repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepositoryImpl.kt index a09c848..8792d9a 100644 --- a/repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepositoryImpl.kt +++ b/repository/folders/src/main/kotlin/photos/network/repository/folders/FoldersRepositoryImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/repository/folders/src/test/kotlin/photos/network/repository/folders/FoldersRepositoryTest.kt b/repository/folders/src/test/kotlin/photos/network/repository/folders/FoldersRepositoryTest.kt index 76ae161..7222230 100644 --- a/repository/folders/src/test/kotlin/photos/network/repository/folders/FoldersRepositoryTest.kt +++ b/repository/folders/src/test/kotlin/photos/network/repository/folders/FoldersRepositoryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/repository/photos/src/main/kotlin/photos/network/repository/photos/Photo.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/Photo.kt index 8b11079..1816002 100644 --- a/repository/photos/src/main/kotlin/photos/network/repository/photos/Photo.kt +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/Photo.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepository.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepository.kt index 335663e..14899e6 100644 --- a/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepository.kt +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt index 0a06c53..99e8892 100644 --- a/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/CleanResourcesWorker.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/CleanResourcesWorker.kt index 265abcf..df3eaed 100644 --- a/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/CleanResourcesWorker.kt +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/CleanResourcesWorker.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/SyncLocalPhotosWorker.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/SyncLocalPhotosWorker.kt index 4fab81a..e441550 100644 --- a/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/SyncLocalPhotosWorker.kt +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/SyncLocalPhotosWorker.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/UploadPhotosWorker.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/UploadPhotosWorker.kt index dd8861e..5a99481 100644 --- a/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/UploadPhotosWorker.kt +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/worker/UploadPhotosWorker.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/repository/photos/src/test/kotlin/photos/network/repository/photos/PhotoRepositoryTest.kt b/repository/photos/src/test/kotlin/photos/network/repository/photos/PhotoRepositoryTest.kt index 3770e02..0ddfaf2 100644 --- a/repository/photos/src/test/kotlin/photos/network/repository/photos/PhotoRepositoryTest.kt +++ b/repository/photos/src/test/kotlin/photos/network/repository/photos/PhotoRepositoryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/repository/settings/src/main/kotlin/photos/network/repository/settings/SettingsRepository.kt b/repository/settings/src/main/kotlin/photos/network/repository/settings/SettingsRepository.kt index b2df70b..beb0445 100644 --- a/repository/settings/src/main/kotlin/photos/network/repository/settings/SettingsRepository.kt +++ b/repository/settings/src/main/kotlin/photos/network/repository/settings/SettingsRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/repository/settings/src/main/kotlin/photos/network/repository/settings/SettingsRepositoryImpl.kt b/repository/settings/src/main/kotlin/photos/network/repository/settings/SettingsRepositoryImpl.kt index ca57ee0..4f4bc4e 100644 --- a/repository/settings/src/main/kotlin/photos/network/repository/settings/SettingsRepositoryImpl.kt +++ b/repository/settings/src/main/kotlin/photos/network/repository/settings/SettingsRepositoryImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/repository/sharing/src/main/kotlin/photos/network/repository/sharing/User.kt b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/User.kt index 9acc1ad..f80bdc0 100644 --- a/repository/sharing/src/main/kotlin/photos/network/repository/sharing/User.kt +++ b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/User.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepository.kt b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepository.kt index bfbb07a..09bc82a 100644 --- a/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepository.kt +++ b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepositoryImpl.kt b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepositoryImpl.kt index ec5ef14..5beeb61 100644 --- a/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepositoryImpl.kt +++ b/repository/sharing/src/main/kotlin/photos/network/repository/sharing/UserRepositoryImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/repository/sharing/src/test/kotlin/photos/network/repository/sharing/UserRepositoryTests.kt b/repository/sharing/src/test/kotlin/photos/network/repository/sharing/UserRepositoryTests.kt index 1bc7740..ee1cee0 100644 --- a/repository/sharing/src/test/kotlin/photos/network/repository/sharing/UserRepositoryTests.kt +++ b/repository/sharing/src/test/kotlin/photos/network/repository/sharing/UserRepositoryTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/albums/src/main/kotlin/photos/network/ui/albums/AlbumsScreen.kt b/ui/albums/src/main/kotlin/photos/network/ui/albums/AlbumsScreen.kt index 83d8e0b..d32e36f 100644 --- a/ui/albums/src/main/kotlin/photos/network/ui/albums/AlbumsScreen.kt +++ b/ui/albums/src/main/kotlin/photos/network/ui/albums/AlbumsScreen.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/common/src/main/kotlin/photos/network/ui/common/components/ActivityLog.kt b/ui/common/src/main/kotlin/photos/network/ui/common/components/ActivityLog.kt index e5dee08..dc706db 100644 --- a/ui/common/src/main/kotlin/photos/network/ui/common/components/ActivityLog.kt +++ b/ui/common/src/main/kotlin/photos/network/ui/common/components/ActivityLog.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/common/src/main/kotlin/photos/network/ui/common/components/AppLogo.kt b/ui/common/src/main/kotlin/photos/network/ui/common/components/AppLogo.kt index 057ebe2..30702e7 100644 --- a/ui/common/src/main/kotlin/photos/network/ui/common/components/AppLogo.kt +++ b/ui/common/src/main/kotlin/photos/network/ui/common/components/AppLogo.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/common/src/main/kotlin/photos/network/ui/common/navigation/Destination.kt b/ui/common/src/main/kotlin/photos/network/ui/common/navigation/Destination.kt index 46d5746..803a053 100644 --- a/ui/common/src/main/kotlin/photos/network/ui/common/navigation/Destination.kt +++ b/ui/common/src/main/kotlin/photos/network/ui/common/navigation/Destination.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/common/src/main/kotlin/photos/network/ui/common/theme/Colors.kt b/ui/common/src/main/kotlin/photos/network/ui/common/theme/Colors.kt index 0578d5a..1181e46 100644 --- a/ui/common/src/main/kotlin/photos/network/ui/common/theme/Colors.kt +++ b/ui/common/src/main/kotlin/photos/network/ui/common/theme/Colors.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/common/src/main/kotlin/photos/network/ui/common/theme/Theme.kt b/ui/common/src/main/kotlin/photos/network/ui/common/theme/Theme.kt index 6c04a75..67b6c2b 100644 --- a/ui/common/src/main/kotlin/photos/network/ui/common/theme/Theme.kt +++ b/ui/common/src/main/kotlin/photos/network/ui/common/theme/Theme.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/folders/src/main/kotlin/photos/network/ui/folders/FoldersScreen.kt b/ui/folders/src/main/kotlin/photos/network/ui/folders/FoldersScreen.kt index baaefb5..cd9815a 100644 --- a/ui/folders/src/main/kotlin/photos/network/ui/folders/FoldersScreen.kt +++ b/ui/folders/src/main/kotlin/photos/network/ui/folders/FoldersScreen.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/photos/src/androidTest/kotlin/photos/network/PhotosNetworkTestUtils.kt b/ui/photos/src/androidTest/kotlin/photos/network/PhotosNetworkTestUtils.kt index 1cc78f0..9cdd10b 100644 --- a/ui/photos/src/androidTest/kotlin/photos/network/PhotosNetworkTestUtils.kt +++ b/ui/photos/src/androidTest/kotlin/photos/network/PhotosNetworkTestUtils.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/photos/src/androidTest/kotlin/photos/network/ui/photos/PhotosScreenTests.kt b/ui/photos/src/androidTest/kotlin/photos/network/ui/photos/PhotosScreenTests.kt index bdad9ba..97845fe 100644 --- a/ui/photos/src/androidTest/kotlin/photos/network/ui/photos/PhotosScreenTests.kt +++ b/ui/photos/src/androidTest/kotlin/photos/network/ui/photos/PhotosScreenTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoBottomIcons.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoBottomIcons.kt index 314304c..704a3fb 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoBottomIcons.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoBottomIcons.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoDetails.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoDetails.kt index 6bfc531..81237c6 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoDetails.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoDetails.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt index 6d42a24..ec09aa7 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoTopIcons.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoTopIcons.kt index 08b7440..032c777 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoTopIcons.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoTopIcons.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosEvent.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosEvent.kt index df32d3d..d7bfec3 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosEvent.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosEvent.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosScreen.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosScreen.kt index a136bfb..197a534 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosScreen.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosScreen.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosUiState.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosUiState.kt index c76fb92..6fe121f 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosUiState.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosUiState.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosViewModel.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosViewModel.kt index dfe4741..1fa2467 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosViewModel.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/Tag.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/Tag.kt index cf36cd1..3f6f0bd 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/Tag.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/Tag.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/TagLines.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/TagLines.kt index c7a2c55..c2dc31f 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/TagLines.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/TagLines.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/photos/src/test/kotlin/photos/network/ui/photos/photos/PhotosViewModelTests.kt b/ui/photos/src/test/kotlin/photos/network/ui/photos/photos/PhotosViewModelTests.kt index 7dc9a43..f9cb5bc 100644 --- a/ui/photos/src/test/kotlin/photos/network/ui/photos/photos/PhotosViewModelTests.kt +++ b/ui/photos/src/test/kotlin/photos/network/ui/photos/photos/PhotosViewModelTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/settings/src/androidTest/kotlin/photos/network/ui/settings/SettingsScreenTests.kt b/ui/settings/src/androidTest/kotlin/photos/network/ui/settings/SettingsScreenTests.kt index 870c85f..85e86f6 100644 --- a/ui/settings/src/androidTest/kotlin/photos/network/ui/settings/SettingsScreenTests.kt +++ b/ui/settings/src/androidTest/kotlin/photos/network/ui/settings/SettingsScreenTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsEvent.kt b/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsEvent.kt index c8cf833..4fd0363 100644 --- a/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsEvent.kt +++ b/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsEvent.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsScreen.kt b/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsScreen.kt index 249ab16..a99739e 100644 --- a/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsScreen.kt +++ b/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsScreen.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsUiState.kt b/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsUiState.kt index e074565..d5ab19b 100644 --- a/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsUiState.kt +++ b/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsUiState.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsViewModel.kt b/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsViewModel.kt index a3a2ddb..b779104 100644 --- a/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsViewModel.kt +++ b/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginEvent.kt b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginEvent.kt index 73d84b6..eb6634b 100644 --- a/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginEvent.kt +++ b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginEvent.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginScreen.kt b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginScreen.kt index a8b9e3d..c5dfe8e 100644 --- a/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginScreen.kt +++ b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginScreen.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginUiState.kt b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginUiState.kt index f982a6e..fa7538d 100644 --- a/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginUiState.kt +++ b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginUiState.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginViewModel.kt b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginViewModel.kt index 8083d86..4ffd1bc 100644 --- a/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginViewModel.kt +++ b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Photos.network developers + * Copyright 2020-2023 Photos.network developers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 42c3cf858399ce65ee0f557638ba2aaddf6372a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Stu=CC=88rmer?= Date: Mon, 29 May 2023 20:33:22 +0200 Subject: [PATCH 07/13] adjust docs --- CONTRIBUTING.md | 95 ++++++++++ README.md | 110 +++--------- app/build.gradle.kts | 3 + app/src/main/AndroidManifest.xml | 1 + .../kotlin/photos/network/MainActivity.kt | 29 ++- .../main/kotlin/photos/network/home/Home.kt | 165 +++++++++++++----- gradle/libs.versions.toml | 85 +++++---- gradle/wrapper/gradle-wrapper.properties | 2 +- 8 files changed, 315 insertions(+), 175 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3e91b28 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,95 @@ +## Contribution +The help of the community is essential for projects like this. Users have different requirements and perspectives how their instances should work. + +### Getting Started + +Create a Feature request with a short but understandable description what the feature should look like and how the user can use it. + +### Making Changes + +* Create a `/feature/` branch from where you want to base your work. + * This is usually the `development` branch. + * Only target `release` branches if you are certain your fix must be on that branch. +* Make commits of logical and atomic units. +* Check for unnecessary whitespace with `git diff --check` before committing. +* Make sure your commit messages are in the proper format. Start the first + line of the commit with the issue number in parentheses. +* run tests and code quality checks locally ```./run_tests``` + + +## Gitflow +- *main:* contains production code +- *development:* latest changes that will be included in the next release +- *feature/:* each feature separated until it is done and merged back to development +- *release/:* signifies an upcoming release and will be merged into main +- *hotfix/:* urgent changes to be merged into release and development + + +## Continuous Delivery Pipeline +The whole pipeline is automated into Github workflows. + +- Code checks to enforce code quality & style +- Tests to ensure a stable and release-ready codebase +- Deployment into a preview environment +- Release to production + + + +Each user needs to authenticate itself via [OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc6749) with an [Authorization Code Grant](https://www.oauth.com/oauth2-servers/server-side-apps/authorization-code/). + +After adding the **Host** and **Client ID** into the app, the app will construct the request URI +and display it in a browser window so the user can enter its credentials. +```mermaid +sequenceDiagram + actor U as User + participant A as App + participant C as Core Instance + U->>A: Enter Hostname + A--)C: Validate entered host + U->>A: Enter Client ID + A--)C: Validate entered client ID + + alt OAuth Authorization Code Grant flow + + C->>U: Show credentials form (RFC 4.1.1) + activate U + U->>C: Send username and password + activate C + deactivate U + C->>A: Respond with auth code (RFC 4.1.2) + A->>C: Request access token (RFC 4.1.3) + C->>A: Respond with access token (RFC 4.1.4) + deactivate C + end + A->>C: Load data with access token + C->>A: Return data + A->>U: Display data to user + +``` + +## Synchronization + +The synchronisation of photos with a core instance is done in multiple steps: + +```mermaid +flowchart LR + store[Android media store] + repo[(Photos repository)] + core((Core instance)) + + store --> syncWorker --> repo + repo --> uploadWorker --> core + core --> downloadWorker --> repo + + subgraph Local Sync + syncWorker(SyncLocalPhotosWorker) + end + + subgraph Upload + uploadWorker[UploadWorker] + end + + subgraph Download + downloadWorker[DownloadWorker] + end +``` diff --git a/README.md b/README.md index 80b916c..1711f1a 100644 --- a/README.md +++ b/README.md @@ -9,97 +9,31 @@ [Photos.network](https://photos.network) is an open source project for self hosted photo management. Its core features are: -- Share photos with friends, family or public -- Filter / Search photos by attributes like location or date -- Group photos by objects like people of objects - -## App workflow -To connect the app to a [core instance](https://github.com/photos-network/core), -the user needs to authenticate itself via [Authorization Code Grant](https://www.oauth.com/oauth2-servers/server-side-apps/authorization-code/) in [OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc6749). - -After adding the **Host** and **Client ID** into the app, the app will construct the request URI -and display it in a browser window so the user can enter its credentials. -```mermaid -sequenceDiagram - actor U as User - participant A as App - participant C as Core Instance - U->>A: Enter Hostname - A--)C: Validate entered host - U->>A: Enter Client ID - A--)C: Validate entered client ID - - alt OAuth Authorization Code Grant flow - - C->>U: Show credentials form (RFC 4.1.1) - activate U - U->>C: Send username and password - activate C - deactivate U - C->>A: Respond with auth code (RFC 4.1.2) - A->>C: Request access token (RFC 4.1.3) - C->>A: Respond with access token (RFC 4.1.4) - deactivate C - end - A->>C: Load data with access token - C->>A: Return data - A->>U: Display data to user - -``` - -The synchronisation of photos with a core instance is done in multiple steps: -```mermaid -flowchart LR - store[Android media store] - repo[(Photos repository)] - core((Core instance)) - - store --> syncWorker --> repo - repo --> uploadWorker --> core - core --> downloadWorker --> repo - - subgraph Local Sync - syncWorker(SyncLocalPhotosWorker) - end - - subgraph Upload - uploadWorker[UploadWorker] - end - - subgraph Download - downloadWorker[DownloadWorker] - end -``` - -## Gitflow -- *main:* contains production code -- *development:* latest changes that will be included in the next release -- *feature/:* each feature separated until it is done and merged back to development -- *release/:* signifies an upcoming release and will be merged into main -- *hotfix/:* urgent changes to be merged into release and development - -## Continuous Delivery Pipeline -The whole pipeline is automated into Github workflows. - -- Code checks to enforce code quality & style -- Tests to ensure a stable and release-ready codebase -- Deployment into a preview environment -- Release to production +- Keep track of your photos with privacy +- Share photos and albums with friends, family or public +- Search for photos by attributes like location or objects + +## App + +The Android app itself is self-sufficient and can be used to browse local photos on a device, add tags or search by attributes. +When the app is connected to a [core instance](https://github.com/photos-network/core) the feature set increases like sharing images via link. + +## Core +A [core instance](https://github.com/photos-network/core) can run additional long-running tasks to analyze, categorize or group photos based +on all data gained from the photos added. + ## Contribution -The help of the community is essential for projects like this. Users have different requirements and perspectives how their instances should work. -### Getting Started +This is a free and open project and lives from contributions of the community. + +See our [Contribution Guidelines](CONTRIBUTING.md) + + + -Create a Feature request with a short but understandable description what the feature should look like and how the user can use it. +## ⚖️ License -### Making Changes +Copyright 2020 Photos network developers -* Create a `/feature/` branch from where you want to base your work. - * This is usually the `development` branch. - * Only target `release` branches if you are certain your fix must be on that branch. -* Make commits of logical and atomic units. -* Check for unnecessary whitespace with `git diff --check` before committing. -* Make sure your commit messages are in the proper format. Start the first - line of the commit with the issue number in parentheses. -* run tests and code quality checks locally ```./run_tests``` +Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b56b0e3..eb18559 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -138,6 +138,7 @@ android { freeCompilerArgs = freeCompilerArgs + "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi" freeCompilerArgs = freeCompilerArgs + "-opt-in=com.google.accompanist.pager.ExperimentalPagerApi" freeCompilerArgs = freeCompilerArgs + "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi" + freeCompilerArgs = freeCompilerArgs + "-opt-in=androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi" } testOptions { @@ -174,6 +175,8 @@ dependencies { implementation(libs.activity.compose) implementation(libs.bundles.accompanist) + implementation(libs.androidx.window) +// implementation(libs.androidx.window.core) // leakCanary debugImplementation(libs.leakcanary.android) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 303de94..be4d0c4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -42,6 +42,7 @@ tools:targetApi="tiramisu"> diff --git a/app/src/main/kotlin/photos/network/MainActivity.kt b/app/src/main/kotlin/photos/network/MainActivity.kt index 5b39205..ed83fdc 100644 --- a/app/src/main/kotlin/photos/network/MainActivity.kt +++ b/app/src/main/kotlin/photos/network/MainActivity.kt @@ -15,6 +15,7 @@ */ package photos.network +import android.content.res.Configuration import android.os.Build import android.os.Bundle import android.view.WindowManager @@ -22,6 +23,8 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.SideEffect @@ -32,8 +35,11 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE import androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE +import androidx.window.layout.DisplayFeature +import com.google.accompanist.adaptive.calculateDisplayFeatures import com.google.accompanist.systemuicontroller.SystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController +import logcat.logcat import photos.network.home.Home import photos.network.ui.common.theme.AppTheme import photos.network.user.CurrentUserHost @@ -50,7 +56,24 @@ class MainActivity : ComponentActivity() { window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) setContent { - PhotosApp() + PhotosApp( + windowSizeClass = calculateWindowSizeClass(this), + displayFeatures = calculateDisplayFeatures(this), + ) + } + } + + /** + * Handle specific configuration changes to prevent activity recreation in the Manifest. + */ + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + + // Checks whether a keyboard is available + if (newConfig.keyboardHidden == Configuration.KEYBOARDHIDDEN_YES) { + logcat { "Keyboard available" } + } else if (newConfig.keyboardHidden == Configuration.KEYBOARDHIDDEN_NO) { + logcat { "No Keyboard" } } } } @@ -60,6 +83,8 @@ val LocalAppVersion = staticCompositionLocalOf { "Unknown" } @Composable fun PhotosApp( systemUiController: SystemUiController = rememberSystemUiController(), + windowSizeClass: WindowSizeClass, + displayFeatures: List, ) { val useDarkIcons = !isSystemInDarkTheme() @@ -82,6 +107,8 @@ fun PhotosApp( Home( modifier = Modifier.fillMaxSize(), orientation = LocalConfiguration.current.orientation, + windowSizeClass = windowSizeClass, + displayFeatures = displayFeatures, ) } } diff --git a/app/src/main/kotlin/photos/network/home/Home.kt b/app/src/main/kotlin/photos/network/home/Home.kt index 234685c..405ca94 100644 --- a/app/src/main/kotlin/photos/network/home/Home.kt +++ b/app/src/main/kotlin/photos/network/home/Home.kt @@ -18,22 +18,38 @@ package photos.network.home import android.content.res.Configuration import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.exclude +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Shield import androidx.compose.material.icons.outlined.Shield +import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem import androidx.compose.material3.Scaffold +import androidx.compose.material3.ScaffoldDefaults +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf @@ -45,11 +61,14 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import androidx.window.layout.DisplayFeature import org.koin.androidx.compose.getViewModel import photos.network.R import photos.network.api.ServerStatus @@ -64,6 +83,8 @@ import photos.network.ui.common.theme.AppTheme fun Home( modifier: Modifier = Modifier, orientation: Int, + windowSizeClass: WindowSizeClass = WindowSizeClass.calculateFromSize(DpSize(1920.dp, 1080.dp)), + displayFeatures: List = listOf(), ) { val navController = rememberNavController() val navBackStackEntry = navController.currentBackStackEntryAsState() @@ -75,17 +96,17 @@ fun Home( val viewmodel: HomeViewModel = getViewModel() + val showTopBar = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact + val showBottomNavigation = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact + val showNavigationRail = !showBottomNavigation + + val snackbarHostState = remember { SnackbarHostState() } + Scaffold( modifier = modifier .fillMaxSize() - .navigationBarsPadding() .testTag("HomeScreenTag"), - snackbarHost = { -// SnackbarHost( -// hostState = it, -// modifier = Modifier.systemBarsPadding() -// ) - }, + snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { if (currentDestination.isRootDestination()) { // privacy @@ -135,52 +156,39 @@ fun Home( } }, bottomBar = { - if (currentDestination.isRootDestination()) { - NavigationBar { - // Photos - NavigationBarItem( - icon = { Icon(Destination.Photos.icon, contentDescription = null) }, - label = { Text(stringResource(Destination.Photos.resourceId)) }, - selected = currentDestination == Destination.Photos, - onClick = { - navController.navigate(Destination.Photos.route) - }, - ) - - // Albums - NavigationBarItem( - icon = { Icon(Destination.Albums.icon, contentDescription = null) }, - label = { Text(stringResource(Destination.Albums.resourceId)) }, - selected = currentDestination == Destination.Albums, - onClick = { - navController.navigate(Destination.Albums.route) - }, - ) - - // Folders - NavigationBarItem( - icon = { Icon(Destination.Folders.icon, contentDescription = null) }, - label = { Text(stringResource(Destination.Folders.resourceId)) }, - selected = currentDestination == Destination.Folders, - onClick = { - navController.navigate(Destination.Folders.route) - }, - ) - } + if (showBottomNavigation && currentDestination.isRootDestination()) { + HomeBottomNavigation( + currentDestination = currentDestination, + navController = navController, + ) + } else { + Spacer( + Modifier + .windowInsetsBottomHeight(WindowInsets.navigationBars) + .fillMaxWidth(), + ) } }, + contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.statusBars), // Is hanled by content content = { innerPadding -> val topPadding: Dp = if (currentDestination.isRootDestination()) { innerPadding.calculateTopPadding() } else { 0.dp } - Box( + Row( modifier = Modifier + .fillMaxSize() .padding(bottom = innerPadding.calculateBottomPadding()) .padding(top = topPadding) .border(2.dp, Color.Green), ) { + if (showNavigationRail && currentDestination.isRootDestination()) { + HomeNavigationRail( + currentDestination = currentDestination, + navController = navController, + ) + } NavHost( navController = navController, startDestination = Destination.Photos.route, @@ -216,6 +224,79 @@ fun Home( ) } +@Composable +private fun HomeBottomNavigation( + currentDestination: Destination, + navController: NavHostController, +) { + NavigationBar { + // Photos + NavigationBarItem( + icon = { Icon(Destination.Photos.icon, contentDescription = null) }, + label = { Text(stringResource(Destination.Photos.resourceId)) }, + selected = currentDestination == Destination.Photos, + onClick = { navController.navigate(Destination.Photos.route) }, + ) + + // Albums + NavigationBarItem( + icon = { Icon(Destination.Albums.icon, contentDescription = null) }, + label = { Text(stringResource(Destination.Albums.resourceId)) }, + selected = currentDestination == Destination.Albums, + onClick = { navController.navigate(Destination.Albums.route) }, + ) + + // Folders + NavigationBarItem( + icon = { Icon(Destination.Folders.icon, contentDescription = null) }, + label = { Text(stringResource(Destination.Folders.resourceId)) }, + selected = currentDestination == Destination.Folders, + onClick = { navController.navigate(Destination.Folders.route) }, + ) + } +} + +@Composable +private fun HomeNavigationRail( + currentDestination: Destination, + navController: NavHostController, +) { + NavigationRail(modifier = Modifier.fillMaxHeight()) { + // Photos + NavigationRailItem( + icon = { Icon(Destination.Photos.icon, contentDescription = null) }, + alwaysShowLabel = false, + label = { Text(stringResource(Destination.Photos.resourceId)) }, + selected = currentDestination == Destination.Photos, + onClick = { navController.navigate(Destination.Photos.route) }, + ) + + // Albums + NavigationRailItem( + icon = { Icon(Destination.Albums.icon, contentDescription = null) }, + alwaysShowLabel = false, + label = { Text(stringResource(Destination.Albums.resourceId)) }, + selected = currentDestination == Destination.Albums, + onClick = { navController.navigate(Destination.Albums.route) }, + ) + + // Folders + NavigationRailItem( + icon = { Icon(Destination.Folders.icon, contentDescription = null) }, + alwaysShowLabel = false, + label = { Text(stringResource(Destination.Folders.resourceId)) }, + selected = currentDestination == Destination.Folders, + onClick = { navController.navigate(Destination.Folders.route) }, + ) + } + + Divider( + Modifier + .fillMaxHeight() + .width(1.dp), + ) +} + @Preview(name = "Home") @Preview(name = "Home · DARK", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d1a50d7..fd4b0a3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,16 +1,38 @@ [versions] compileSdk = "33" -kotlin = "1.8.10" -ksp = "1.8.10-1.0.9" -androidGradlePlugin = "7.4.0" -compose-bom = "2023.03.00" -compose-compiler = "1.4.2" + +# https://kotlinlang.org/ +kotlin = "1.8.10" # 1.8.21 + +# https://github.com/google/ksp +ksp = "1.8.10-1.0.9" # 1.8.21-1.0.11 + +# https://developer.android.com/build/releases/gradle-plugin +androidGradlePlugin = "7.4.0" # 8.0.0 + +# https://developer.android.com/jetpack/compose/bom +compose-bom = "2023.03.00" # 2023.04.01 + +# https://developer.android.com/jetpack/androidx/releases/compose-compiler +compose-compiler = "1.4.2" # 1.4.7 + +# https://detekt.dev/ detekt = "1.22.0" + +# https://github.com/Kotlin/kotlinx-kover kover = "0.6.1" + +# https://github.com/diffplug/spotless spotless = "6.18.0" -grgit = "5.0.0" -tripletPlugin = "3.8.1" -ktlint = "0.48.2" + +# https://github.com/ajoberstar/grgit +grgit = "5.0.0" # 5.2.0 + +# https://github.com/Triple-T/gradle-play-publisher +tripletPlugin = "3.8.1" # 3.8.3 + +# https://github.com/pinterest/ktlint +ktlint = "0.48.2" # 0.49.1 @@ -25,15 +47,16 @@ androidx-navigation = "2.5.2" androidx-constraintlayout-compose = "1.0.1" androidx-paging-compose = "1.0.0-alpha14" androidx-paging = "3.1.1" -google-accompanist = "0.25.1" +google-accompanist = "0.30.1" google-android-material = "1.6.1" coil-kt = "2.2.1" -retrofit2 = "2.9.0" -okhttp3 = "4.10.0" +#retrofit2 = "2.9.0" +#okhttp3 = "4.10.0" kotlinx-serialization = "1.4.0" -jakewharton-retrofit2-kotlinx-serialization-converter = "0.8.0" +#jakewharton-retrofit2-kotlinx-serialization-converter = "0.8.0" androidx-compose-compiler = "1.2.0" androidx-room = "2.4.3" +androidx-window = "1.1.0-alpha03" androidx-work = "2.7.1" androidx-test-core = "1.4.0" androidx-test-ext-junit = "1.1.3" @@ -63,10 +86,14 @@ accompanist-systemuicontroller = { group = "com.google.accompanist", name = "acc accompanist-placeholder = { group = "com.google.accompanist", name = "accompanist-placeholder", version.ref = "google-accompanist" } accompanist-flowlayout = { group = "com.google.accompanist", name = "accompanist-flowlayout", version.ref = "google-accompanist" } accompanist-insets = { group = "com.google.accompanist", name = "accompanist-insets", version.ref = "google-accompanist" } +accompanist-adaptive = { group = "com.google.accompanist", name = "accompanist-adaptive", version.ref = "google-accompanist" } accompanist-pager = { group = "com.google.accompanist", name = "accompanist-pager", version.ref = "google-accompanist" } accompanist-swiperefresh = { group = "com.google.accompanist", name = "accompanist-swiperefresh", version.ref = "google-accompanist" } accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "google-accompanist" } +androidx-window = { group = "androidx.window", name = "window", version.ref = "androidx-window" } +androidx-window-core = { group = "androidx.window", name = "window-core", version = "androidx-window" } + androidx-core-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" } androidx-test-core-ktx = { group = "androidx.test", name = "core-ktx", version.ref = "androidx-test-core" } @@ -129,6 +156,7 @@ compose-material = { module = "androidx.compose.material:material" } compose-material-icons-core = { module = "androidx.compose.material:material-icons-core" } compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } compose-material3 = { module = "androidx.compose.material3:material3" } +compose-material3-windowsizeclass = { module = "androidx.compose.material3:material3-window-size-class" } compose-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } compose-tooling = { module = "androidx.compose.ui:ui-tooling" } compose-test = { group = "androidx.compose.ui", name = "ui-test", version.ref = "androidx-compose-ui" } @@ -137,12 +165,6 @@ compose-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifes constraintlayout-compose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "androidx-constraintlayout-compose" } -#ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidx-compose-ui" } -#ui = { group = "androidx.compose.ui", name = "ui", version.ref = "androidx-compose-ui" } -#runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "androidx-compose-runtime" } -#material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "androidx-compose-material" } -#androidx-compose-compiler-compiler = { group = "androidx.compose.compiler", name = "compiler", version.ref = "androidx-compose-compiler" } - leakcanary-android = { group = "com.squareup.leakcanary", name = "leakcanary-android", version.ref = "leakcanary" } activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity" } com-google-android-material = { group = "com.google.android.material", name = "material", version.ref = "google-android-material" } @@ -152,20 +174,6 @@ security-crypto = { group = "androidx.security", name = "security-crypto", versi coil = { group = "io.coil-kt", name = "coil", version.ref = "coil-kt" } coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil-kt" } -#com-android-tools-build-gradle = "com.android.tools.build:gradle:7.0.4" -#kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } -#org-jacoco-ant = "org.jacoco:org.jacoco.ant:0.8.7" -#navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" } -#paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "androidx-paging-compose" } -#paging-common-ktx = { group = "androidx.paging", name = "paging-common-ktx", version.ref = "androidx-paging" } -#logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp3" } -#retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit2" } -#retrofit2-kotlinx-serialization-converter = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "jakewharton-retrofit2-kotlinx-serialization-converter" } -#org-jacoco-agent = "org.jacoco:org.jacoco.agent:0.8.7" -#kotlin-annotation-processing-gradle = { group = "org.jetbrains.kotlin", name = "kotlin-annotation-processing-gradle", version.ref = "kotlin" } -#kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" } -#lint-gradle = "com.android.tools.lint:lint-gradle:30.0.4" - junit-junit = { group = "junit", name = "junit", version.ref = "junit-junit" } core-testing = { group = "androidx.arch.core", name = "core-testing", version.ref = "androidx-arch-core" } @@ -190,6 +198,7 @@ compose = [ "compose-material-icons-core", "compose-material-icons-extended", "compose-material3", + "compose-material3-windowsizeclass", "compose-tooling-preview", "compose-tooling", ] @@ -200,6 +209,7 @@ accompanist = [ "accompanist-systemuicontroller", "accompanist-permissions", "accompanist-insets", + "accompanist-adaptive", ] ktor = [ "ktor-client-core", @@ -226,14 +236,3 @@ kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } grgit = { id = "org.ajoberstar.grgit", version.ref = "grgit" } triplet = { id = "com.github.triplet.play", version.ref = "tripletPlugin" } - - - -# legacy -#gradle = { id = "gradle", version = "7.0.4" } -#com-diffplug-spotless = { id = "com.diffplug.spotless", version = "6.7.0" } -#org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version = "1.7.0" } -#org-jetbrains-kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version = "1.7.0" } -#org-jetbrains-kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "1.7.0" } -#com-github-triplet-play = { id = "com.github.triplet.play", version = "3.7.0" } -#org-ajoberstar-grgit = { id = "org.ajoberstar.grgit", version = "5.0.0" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 59bc51a..fae0804 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 23cad44edae00f4ec8bf9d410ffd1abafd353091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Stu=CC=88rmer?= Date: Mon, 29 May 2023 20:50:02 +0200 Subject: [PATCH 08/13] add publishing credentials for unit tests --- .../workflows/continuous-delivery-pipeline.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/continuous-delivery-pipeline.yml b/.github/workflows/continuous-delivery-pipeline.yml index bf2d573..8fa5cc6 100644 --- a/.github/workflows/continuous-delivery-pipeline.yml +++ b/.github/workflows/continuous-delivery-pipeline.yml @@ -135,6 +135,21 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- + - name: Decode Android Keystore + id: decode_keystore + uses: timheuer/base64-to-file@v1.1 + with: + fileName: 'android_release.keystore' + encodedString: ${{ secrets.ANDROID_KEYSTORE }} + + - name: Decode Gradle Play Publisher Credentials + id: decode_play_store_credentials + uses: timheuer/base64-to-file@v1.1 + with: + fileName: 'gradle_playstore_publisher_credentials.json' + fileDir: './' + encodedString: ${{ secrets.PLAYSTORE_CREDENTIALS }} + - name: Run debug unit tests run: | ./gradlew --console=plain koverXmlReport --stacktrace From c265576eed502c9167fc9b907da1784ce4104040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Stu=CC=88rmer?= Date: Mon, 29 May 2023 21:20:56 +0200 Subject: [PATCH 09/13] use kover-merged --- .../continuous-delivery-pipeline.yml | 4 +- app/build.gradle.kts | 7 + .../main/kotlin/photos/network/AppModule.kt | 5 +- .../main/kotlin/photos/network/home/Home.kt | 60 ------- .../kotlin/photos/network/home/HomeEvent.kt | 4 +- .../photos/network/home/HomeViewModel.kt | 43 +---- app/src/main/res/values/strings.xml | 4 - build.gradle.kts | 14 ++ ui/photos/build.gradle.kts | 2 + .../kotlin/photos/network/ui/photos/Module.kt | 2 + .../photos/network/ui/photos/PhotoGrid.kt | 152 +++++++++--------- .../photos/network/ui/photos/PhotosEvent.kt | 1 + .../photos/network/ui/photos/PhotosScreen.kt | 112 ++++++++++--- .../photos/network/ui/photos/PhotosUiState.kt | 1 + .../network/ui/photos/PhotosViewModel.kt | 24 +++ ui/photos/src/main/res/values/strings.xml | 6 + .../ui/photos/photos/PhotosViewModelTests.kt | 6 + 17 files changed, 232 insertions(+), 215 deletions(-) diff --git a/.github/workflows/continuous-delivery-pipeline.yml b/.github/workflows/continuous-delivery-pipeline.yml index 8fa5cc6..a05b518 100644 --- a/.github/workflows/continuous-delivery-pipeline.yml +++ b/.github/workflows/continuous-delivery-pipeline.yml @@ -152,7 +152,7 @@ jobs: - name: Run debug unit tests run: | - ./gradlew --console=plain koverXmlReport --stacktrace + ./gradlew --console=plain koverMergedXmlReport --stacktrace - name: Upload test reports if: always() @@ -164,7 +164,7 @@ jobs: - name: Add coverage report to PR uses: mi-kas/kover-report@v1 with: - path: ${{ github.workspace }}/app/build/reports/kover/xml/report.xml + path: ${{ github.workspace }}/build/reports/kover/report.xml token: ${{ secrets.GITHUB_TOKEN }} title: App Coverage update-comment: true diff --git a/app/build.gradle.kts b/app/build.gradle.kts index eb18559..2d93b01 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -145,6 +145,13 @@ android { unitTests { isIncludeAndroidResources = true isReturnDefaultValues = true + + // Disable kover for non-debug builds + all { + it.extensions.configure(kotlinx.kover.api.KoverTaskExtension::class) { + isDisabled.set(!it.name.contains("testDebug")) + } + } } } diff --git a/app/src/main/kotlin/photos/network/AppModule.kt b/app/src/main/kotlin/photos/network/AppModule.kt index 86d73ae..688c9d8 100644 --- a/app/src/main/kotlin/photos/network/AppModule.kt +++ b/app/src/main/kotlin/photos/network/AppModule.kt @@ -28,9 +28,6 @@ val appModule = module { } viewModel { - HomeViewModel( - getSettingsUseCase = get(), - togglePrivacyStateUseCase = get(), - ) + HomeViewModel() } } diff --git a/app/src/main/kotlin/photos/network/home/Home.kt b/app/src/main/kotlin/photos/network/home/Home.kt index 405ca94..781b8ed 100644 --- a/app/src/main/kotlin/photos/network/home/Home.kt +++ b/app/src/main/kotlin/photos/network/home/Home.kt @@ -17,7 +17,6 @@ package photos.network.home import android.content.res.Configuration import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets @@ -30,13 +29,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsBottomHeight -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Shield -import androidx.compose.material.icons.outlined.Shield import androidx.compose.material3.Divider import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationRail @@ -46,12 +40,9 @@ import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -70,9 +61,6 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.window.layout.DisplayFeature import org.koin.androidx.compose.getViewModel -import photos.network.R -import photos.network.api.ServerStatus -import photos.network.ui.common.components.AppLogo import photos.network.ui.common.navigation.Destination import photos.network.ui.common.theme.AppTheme @@ -107,54 +95,6 @@ fun Home( .fillMaxSize() .testTag("HomeScreenTag"), snackbarHost = { SnackbarHost(snackbarHostState) }, - topBar = { - if (currentDestination.isRootDestination()) { - // privacy - TopAppBar( - title = {}, - modifier = Modifier, - navigationIcon = { - AppLogo( - modifier = Modifier - .padding(horizontal = 8.dp) - .clickable { - navController.navigate(Destination.Account.route) - }, - size = 32.dp, - statusSize = 16.dp, - serverStatus = ServerStatus.UNAVAILABLE, - ) - }, - actions = { - // privacy - if (currentDestination == Destination.Photos || currentDestination == Destination.Albums) { - IconButton( - onClick = { - viewmodel.handleEvent(HomeEvent.TogglePrivacyEvent) - }, - ) { - if (viewmodel.uiState.collectAsState().value.isPrivacyEnabled) { - Icon( - imageVector = Icons.Default.Shield, - contentDescription = stringResource(id = R.string.privacy_filter_enabled_description), - tint = MaterialTheme.colorScheme.onPrimary, - ) - } else { - Icon( - imageVector = Icons.Outlined.Shield, - contentDescription = stringResource(id = R.string.privacy_filter_disabled_description), - tint = MaterialTheme.colorScheme.onPrimary, - ) - } - } - } - }, - colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = MaterialTheme.colorScheme.primary, - ), - ) - } - }, bottomBar = { if (showBottomNavigation && currentDestination.isRootDestination()) { HomeBottomNavigation( diff --git a/app/src/main/kotlin/photos/network/home/HomeEvent.kt b/app/src/main/kotlin/photos/network/home/HomeEvent.kt index c54079f..308cc94 100644 --- a/app/src/main/kotlin/photos/network/home/HomeEvent.kt +++ b/app/src/main/kotlin/photos/network/home/HomeEvent.kt @@ -15,6 +15,4 @@ */ package photos.network.home -sealed class HomeEvent { - object TogglePrivacyEvent : HomeEvent() -} +sealed class HomeEvent diff --git a/app/src/main/kotlin/photos/network/home/HomeViewModel.kt b/app/src/main/kotlin/photos/network/home/HomeViewModel.kt index f4c63df..0aad250 100644 --- a/app/src/main/kotlin/photos/network/home/HomeViewModel.kt +++ b/app/src/main/kotlin/photos/network/home/HomeViewModel.kt @@ -16,46 +16,5 @@ package photos.network.home import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import photos.network.common.persistence.PrivacyState -import photos.network.domain.settings.usecase.GetSettingsUseCase -import photos.network.domain.settings.usecase.TogglePrivacyUseCase -class HomeViewModel constructor( - private val getSettingsUseCase: GetSettingsUseCase, - private val togglePrivacyStateUseCase: TogglePrivacyUseCase, -) : ViewModel() { - val uiState = MutableStateFlow(HomeUiState()) - - init { - loadInitialPrivacyState() - } - - private fun loadInitialPrivacyState() { - viewModelScope.launch(Dispatchers.IO) { - getSettingsUseCase().collect { settings -> - withContext(Dispatchers.Main) { - uiState.update { - it.copy(isPrivacyEnabled = settings.privacyState == PrivacyState.ACTIVE) - } - } - } - } - } - - fun handleEvent(event: HomeEvent) { - when (event) { - HomeEvent.TogglePrivacyEvent -> { - viewModelScope.launch(Dispatchers.IO) { - togglePrivacyStateUseCase() - } - } - } - } -} +class HomeViewModel : ViewModel() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 000cde2..94d93e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,8 +10,4 @@ Could not load photos - application logo - All items labeled as private are hidden - Hide items labeled as private in this view - diff --git a/build.gradle.kts b/build.gradle.kts index 7b46354..9c726b7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,4 +6,18 @@ plugins { id("com.android.library") apply false // kotlin("kapt") apply false id("com.google.devtools.ksp") apply false + alias(libs.plugins.kover) +} + +koverMerged { + enable() + + xmlReport { + onCheck.set(false) + reportFile.set(layout.buildDirectory.file("$buildDir/reports/kover/result.xml")) + } + htmlReport { + onCheck.set(false) + reportDir.set(layout.buildDirectory.dir("$buildDir/reports/kover/html-result")) + } } diff --git a/ui/photos/build.gradle.kts b/ui/photos/build.gradle.kts index ffc7201..d29eb81 100644 --- a/ui/photos/build.gradle.kts +++ b/ui/photos/build.gradle.kts @@ -39,6 +39,7 @@ android { jvmTarget = "11" freeCompilerArgs = freeCompilerArgs + "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi" freeCompilerArgs = freeCompilerArgs + "-opt-in=androidx.compose.material.ExperimentalMaterialApi" + freeCompilerArgs = freeCompilerArgs + "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api" } buildFeatures { @@ -55,6 +56,7 @@ dependencies { androidTestImplementation(project(":common", "androidTestArtifacts")) api(projects.domain.photos) + api(projects.domain.settings) implementation(projects.ui.common) // Compose diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/Module.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/Module.kt index 0ccdb52..5f3c3e5 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/Module.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/Module.kt @@ -21,6 +21,8 @@ import org.koin.dsl.module val uiPhotosModule = module { viewModel { PhotosViewModel( + getSettingsUseCase = get(), + togglePrivacyStateUseCase = get(), getPhotosUseCase = get(), startPhotosSyncUseCase = get(), ) diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt index ec09aa7..5efbaeb 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt @@ -66,98 +66,98 @@ fun PhotoGrid( // TODO: add fast-scroll Box { - LazyVerticalGrid( - state = lazyListState, - modifier = modifier - .fillMaxSize() - .padding(4.dp), - columns = GridCells.Adaptive(90.dp), - ) { - // group by year - val groupedByYear = photos.groupBy { - it.dateAdded.atZone(ZoneOffset.UTC).year - } - - groupedByYear.forEach { (_, photos) -> - val yearOfFirst = photos[0].dateAdded.atZone(ZoneOffset.UTC).year - val yearNow = Instant.now().atZone(ZoneOffset.UTC).year - - // add year header if necessary - if (yearOfFirst != yearNow) { - item(span = { GridItemSpan(maxCurrentLineSpan) }) { - Text( - text = yearOfFirst.toString(), - style = MaterialTheme.typography.bodyMedium, - ) - } + if (selectedPhoto != null) { + PhotoDetails( + modifier = Modifier + .testTag("PHOTO_DETAILS") + .background(Color.Black.copy(alpha = 0.9f)) + .fillMaxSize(), + selectedIndex = selectedIndex, + selectNextPhoto = selectNextPhoto, + selectPreviousPhoto = selectPreviousPhoto, + selectedPhoto = selectedPhoto, + onSelectItem = onSelectItem, + ) + } else { + LazyVerticalGrid( + state = lazyListState, + modifier = modifier + .fillMaxSize() + .padding(4.dp), + columns = GridCells.Adaptive(90.dp), + ) { + // group by year + val groupedByYear = photos.groupBy { + it.dateAdded.atZone(ZoneOffset.UTC).year } - // group by month - val groupedByMonth = photos.groupBy { - it.dateAdded.atZone(ZoneOffset.UTC).month - } + groupedByYear.forEach { (_, photos) -> + val yearOfFirst = photos[0].dateAdded.atZone(ZoneOffset.UTC).year + val yearNow = Instant.now().atZone(ZoneOffset.UTC).year - groupedByMonth.forEach { (month, photos) -> - // add year if not matching with current year - val title = if (yearOfFirst == yearNow) { - DateFormatSymbols().months[month.value - 1] - } else { - "${DateFormatSymbols().months[month.value - 1]} $yearOfFirst" + // add year header if necessary + if (yearOfFirst != yearNow) { + item(span = { GridItemSpan(maxCurrentLineSpan) }) { + Text( + text = yearOfFirst.toString(), + style = MaterialTheme.typography.bodyMedium, + ) + } } - // month header - item(span = { GridItemSpan(maxCurrentLineSpan) }) { - Text(text = title, style = MaterialTheme.typography.bodyLarge) + // group by month + val groupedByMonth = photos.groupBy { + it.dateAdded.atZone(ZoneOffset.UTC).month } - items(photos.size) { index: Int -> - // TODO: show always local uri? - val data = if (photos[index].uri != null) { - photos[index].uri + groupedByMonth.forEach { (month, photos) -> + // add year if not matching with current year + val title = if (yearOfFirst == yearNow) { + DateFormatSymbols().months[month.value - 1] } else { - photos[index].imageUrl + "${DateFormatSymbols().months[month.value - 1]} $yearOfFirst" } - Box( - modifier = Modifier - .aspectRatio(1.0f) - .size(128.dp) - .clip(RoundedCornerShape(2.dp)) - .clickable { - onSelectItem(index) - }, - ) { - Image( - painter = rememberImagePainter( - data = data, - builder = { - crossfade(true) - placeholder(R.drawable.image_placeholder) + // month header + item(span = { GridItemSpan(maxCurrentLineSpan) }) { + Text(text = title, style = MaterialTheme.typography.bodyLarge) + } + + items(photos.size) { index: Int -> + // TODO: show always local uri? + val data = if (photos[index].uri != null) { + photos[index].uri + } else { + photos[index].imageUrl + } + + Box( + modifier = Modifier + .aspectRatio(1.0f) + .size(128.dp) + .clip(RoundedCornerShape(2.dp)) + .clickable { + onSelectItem(index) }, - ), - contentDescription = null, - contentScale = ContentScale.None, - modifier = Modifier.padding(1.dp), - ) + ) { + Image( + painter = rememberImagePainter( + data = data, + builder = { + crossfade(true) + placeholder(R.drawable.image_placeholder) + }, + ), + contentDescription = null, + contentScale = ContentScale.None, + modifier = Modifier.padding(1.dp), + ) + } } } } } } - - if (selectedPhoto != null) { - PhotoDetails( - modifier = Modifier - .testTag("PHOTO_DETAILS") - .background(Color.Black.copy(alpha = 0.9f)) - .fillMaxSize(), - selectedIndex = selectedIndex, - selectNextPhoto = selectNextPhoto, - selectPreviousPhoto = selectPreviousPhoto, - selectedPhoto = selectedPhoto, - onSelectItem = onSelectItem, - ) - } } } } diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosEvent.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosEvent.kt index d7bfec3..30ce480 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosEvent.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosEvent.kt @@ -16,6 +16,7 @@ package photos.network.ui.photos sealed interface PhotosEvent { + object TogglePrivacyEvent : PhotosEvent object StartLocalPhotoSyncEvent : PhotosEvent class SelectIndex(val index: Int?) : PhotosEvent object SelectNextPhoto : PhotosEvent diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosScreen.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosScreen.kt index 197a534..e57def5 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosScreen.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosScreen.kt @@ -21,6 +21,7 @@ import android.content.res.Configuration import android.net.Uri import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS import androidx.activity.compose.BackHandler +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -28,8 +29,17 @@ 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.material.icons.Icons +import androidx.compose.material.icons.filled.Shield +import androidx.compose.material.icons.outlined.Shield import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState @@ -37,6 +47,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -50,6 +61,9 @@ import androidx.navigation.compose.rememberNavController import com.google.accompanist.permissions.PermissionStatus import com.google.accompanist.permissions.rememberPermissionState import org.koin.androidx.compose.getViewModel +import photos.network.api.ServerStatus +import photos.network.ui.common.components.AppLogo +import photos.network.ui.common.navigation.Destination import photos.network.ui.common.theme.AppTheme @Composable @@ -58,34 +72,84 @@ fun PhotosScreen( navController: NavHostController = rememberNavController(), ) { val viewmodel: PhotosViewModel = getViewModel() + val uiState = viewmodel.uiState.collectAsState().value val permissionState = rememberPermissionState(android.Manifest.permission.READ_EXTERNAL_STORAGE) - when (permissionState.status) { - is PermissionStatus.Denied -> { - Column( - modifier = Modifier - .fillMaxHeight() - .padding(16.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - text = "To show images stored on this device, the permission to read external storage is mandatory. Please grant the permission.", + + Scaffold( + topBar = { + if (uiState.selectedPhoto == null) { + TopAppBar( + title = {}, + modifier = Modifier, + navigationIcon = { + AppLogo( + modifier = Modifier + .padding(horizontal = 8.dp) + .clickable { + navController.navigate(Destination.Account.route) + }, + size = 32.dp, + statusSize = 16.dp, + serverStatus = ServerStatus.UNAVAILABLE, + ) + }, + actions = { + IconButton( + onClick = { + viewmodel.handleEvent(PhotosEvent.TogglePrivacyEvent) + }, + ) { + if (uiState.isPrivacyEnabled) { + Icon( + imageVector = Icons.Default.Shield, + contentDescription = stringResource(id = R.string.privacy_filter_enabled_description), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } else { + Icon( + imageVector = Icons.Outlined.Shield, + contentDescription = stringResource(id = R.string.privacy_filter_disabled_description), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } + } + }, + colors = TopAppBarDefaults.smallTopAppBarColors( + containerColor = MaterialTheme.colorScheme.primary, + ), ) - Spacer(modifier = Modifier.height(8.dp)) - Button(onClick = { permissionState.launchPermissionRequest() }) { - Text("Grant access") + } + }, + ) { innerPadding -> + when (permissionState.status) { + is PermissionStatus.Denied -> { + Column( + modifier = Modifier + .fillMaxHeight() + .padding(top = innerPadding.calculateTopPadding()) + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = "To show images stored on this device, the permission to read external storage is mandatory. Please grant the permission.", + ) + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = { permissionState.launchPermissionRequest() }) { + Text("Grant access") + } } } - } - PermissionStatus.Granted -> { - PhotosContent( - modifier = modifier, - navController = navController, - uiState = viewmodel.uiState.collectAsState().value, - handleEvent = viewmodel::handleEvent, - ) + PermissionStatus.Granted -> { + PhotosContent( + modifier = modifier.padding(top = innerPadding.calculateTopPadding()), + navController = navController, + uiState = uiState, + handleEvent = viewmodel::handleEvent, + ) + } } } } diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosUiState.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosUiState.kt index 6fe121f..fe22426 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosUiState.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosUiState.kt @@ -18,6 +18,7 @@ package photos.network.ui.photos import photos.network.repository.photos.Photo data class PhotosUiState( + val isPrivacyEnabled: Boolean = false, val photos: List = emptyList(), val selectedPhoto: Photo? = null, val selectedIndex: Int? = null, diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosViewModel.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosViewModel.kt index 1fa2467..c3adcb9 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosViewModel.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosViewModel.kt @@ -22,16 +22,23 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import photos.network.common.persistence.PrivacyState import photos.network.domain.photos.usecase.GetPhotosUseCase import photos.network.domain.photos.usecase.StartPhotosSyncUseCase +import photos.network.domain.settings.usecase.GetSettingsUseCase +import photos.network.domain.settings.usecase.TogglePrivacyUseCase class PhotosViewModel( + private val getSettingsUseCase: GetSettingsUseCase, + private val togglePrivacyStateUseCase: TogglePrivacyUseCase, private val getPhotosUseCase: GetPhotosUseCase, private val startPhotosSyncUseCase: StartPhotosSyncUseCase, ) : ViewModel() { val uiState = MutableStateFlow(PhotosUiState()) init { + loadInitialPrivacyState() + viewModelScope.launch(Dispatchers.IO) { loadPhotos() } @@ -43,6 +50,23 @@ class PhotosViewModel( is PhotosEvent.SelectIndex -> selectItem(event.index) PhotosEvent.SelectPreviousPhoto -> selectPreviousPhoto() PhotosEvent.SelectNextPhoto -> selectNextPhoto() + PhotosEvent.TogglePrivacyEvent -> { + viewModelScope.launch(Dispatchers.IO) { + togglePrivacyStateUseCase() + } + } + } + } + + private fun loadInitialPrivacyState() { + viewModelScope.launch(Dispatchers.IO) { + getSettingsUseCase().collect { settings -> + withContext(Dispatchers.Main) { + uiState.update { + it.copy(isPrivacyEnabled = settings.privacyState == PrivacyState.ACTIVE) + } + } + } } } diff --git a/ui/photos/src/main/res/values/strings.xml b/ui/photos/src/main/res/values/strings.xml index e1c1763..22e902c 100644 --- a/ui/photos/src/main/res/values/strings.xml +++ b/ui/photos/src/main/res/values/strings.xml @@ -1,3 +1,9 @@ + + application logo + All items labeled as private are hidden + Hide items labeled as private in this view + tags icon + diff --git a/ui/photos/src/test/kotlin/photos/network/ui/photos/photos/PhotosViewModelTests.kt b/ui/photos/src/test/kotlin/photos/network/ui/photos/photos/PhotosViewModelTests.kt index f9cb5bc..2aaa41f 100644 --- a/ui/photos/src/test/kotlin/photos/network/ui/photos/photos/PhotosViewModelTests.kt +++ b/ui/photos/src/test/kotlin/photos/network/ui/photos/photos/PhotosViewModelTests.kt @@ -37,12 +37,18 @@ import photos.network.domain.photos.usecase.StartPhotosSyncUseCase import photos.network.repository.photos.Photo import photos.network.repository.photos.worker.SyncStatus import java.time.Instant +import photos.network.domain.settings.usecase.GetSettingsUseCase +import photos.network.domain.settings.usecase.TogglePrivacyUseCase class PhotosViewModelTests { + private val getSettingsUseCase = mockk() + private val togglePrivacyUseCase = mockk() private val getPhotosUseCase = mockk() private val startPhotosSyncUseCase = mockk() private val viewmodel by lazy { photos.network.ui.photos.PhotosViewModel( + getSettingsUseCase = getSettingsUseCase, + togglePrivacyStateUseCase = togglePrivacyUseCase, getPhotosUseCase = getPhotosUseCase, startPhotosSyncUseCase = startPhotosSyncUseCase, ) From ae297387ecdde9a81cda85ae4316cd097ce4efa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Stu=CC=88rmer?= Date: Mon, 29 May 2023 21:26:35 +0200 Subject: [PATCH 10/13] fix imports --- .../photos/network/ui/photos/photos/PhotosViewModelTests.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/photos/src/test/kotlin/photos/network/ui/photos/photos/PhotosViewModelTests.kt b/ui/photos/src/test/kotlin/photos/network/ui/photos/photos/PhotosViewModelTests.kt index 2aaa41f..9ce9138 100644 --- a/ui/photos/src/test/kotlin/photos/network/ui/photos/photos/PhotosViewModelTests.kt +++ b/ui/photos/src/test/kotlin/photos/network/ui/photos/photos/PhotosViewModelTests.kt @@ -34,11 +34,11 @@ import org.junit.Before import org.junit.Test import photos.network.domain.photos.usecase.GetPhotosUseCase import photos.network.domain.photos.usecase.StartPhotosSyncUseCase +import photos.network.domain.settings.usecase.GetSettingsUseCase +import photos.network.domain.settings.usecase.TogglePrivacyUseCase import photos.network.repository.photos.Photo import photos.network.repository.photos.worker.SyncStatus import java.time.Instant -import photos.network.domain.settings.usecase.GetSettingsUseCase -import photos.network.domain.settings.usecase.TogglePrivacyUseCase class PhotosViewModelTests { private val getSettingsUseCase = mockk() From 954617dcfd2fd2aec4c0c7e21c21e650c25858f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Stu=CC=88rmer?= Date: Mon, 29 May 2023 22:08:00 +0200 Subject: [PATCH 11/13] change reports dir --- .github/workflows/continuous-delivery-pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-delivery-pipeline.yml b/.github/workflows/continuous-delivery-pipeline.yml index a05b518..3224bc1 100644 --- a/.github/workflows/continuous-delivery-pipeline.yml +++ b/.github/workflows/continuous-delivery-pipeline.yml @@ -159,7 +159,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: tests - path: app/build/reports/tests + path: build/reports - name: Add coverage report to PR uses: mi-kas/kover-report@v1 From f0ad74e826370a4219fc69dda41056c24a9d5707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Stu=CC=88rmer?= Date: Fri, 14 Jul 2023 00:19:00 +0200 Subject: [PATCH 12/13] adjust instrumented tests --- LICENSE.md | 851 ++++++++++++++---- README.md | 15 +- app/build.gradle.kts | 3 +- app/src/main/AndroidManifest.xml | 2 + .../network/PhotosNetworkApplication.kt | 6 + .../main/kotlin/photos/network/home/Home.kt | 22 +- common/build.gradle.kts | 15 +- .../common/persistence/SecureStorageTest.kt | 14 +- database/photos/build.gradle.kts | 4 + .../photos}/PhotoDatabaseMigrationTests.kt | 4 +- domain/search/build.gradle.kts | 57 ++ domain/search/src/main/AndroidManifest.xml | 2 + .../photos/network/domain/search/Module.kt | 21 + gradle/libs.versions.toml | 14 +- .../repository/photos/PhotoRepositoryImpl.kt | 2 +- settings.gradle.kts | 2 + .../network/system/mediastore/MediaStore.kt | 3 +- .../system/mediastore/MediaStoreImpl.kt | 63 +- .../ui/common/components}/PhotoBottomIcons.kt | 15 +- .../ui/common/components}/PhotoTopIcons.kt | 2 +- .../network/ui/common/components}/Tag.kt | 12 +- .../network/ui/common/components}/TagLines.kt | 3 +- .../ui/common/navigation/Destination.kt | 2 + .../common/permissions/FilePermissionHint.kt | 86 ++ ui/common/src/main/res/values/strings.xml | 3 + ui/photos/build.gradle.kts | 6 + ui/photos/src/androidTest/AndroidManifest.xml | 10 + .../photos/network/PhotosNetworkTestUtils.kt | 2 +- .../network/ui/photos/PhotosScreenTests.kt | 16 +- .../network/ui/photos/test/TestActivity.kt | 22 +- ui/photos/src/main/AndroidManifest.xml | 9 +- .../photos/network/ui/photos/PhotoDetails.kt | 4 +- .../photos/network/ui/photos/PhotoGrid.kt | 17 +- .../photos/network/ui/photos/PhotosScreen.kt | 162 +++- .../photos/network/ui/photos/PhotosUiState.kt | 6 +- ui/photos/src/main/res/values/strings.xml | 2 +- ui/search/build.gradle.kts | 71 ++ .../network/ui/search/SearchScreenTests.kt | 44 + ui/search/src/main/AndroidManifest.xml | 2 + .../kotlin/photos/network/ui/search/Module.kt | 19 +- .../photos/network/ui/search/SearchEvent.kt | 20 + .../photos/network/ui/search/SearchScreen.kt | 157 ++++ .../photos/network/ui/search/SearchUiState.kt | 26 + .../network/ui/search/SearchViewModel.kt | 42 + ui/search/src/main/res/values/strings.xml | 5 + ui/settings/build.gradle.kts | 8 + .../ui/settings/SettingsScreenTests.kt | 8 +- .../network/ui/settings/SettingsScreen.kt | 28 +- .../network/ui/settings/SettingsViewModel.kt | 26 +- .../network/ui/sharing/login/LoginScreen.kt | 4 + .../ui/sharing/login/LoginViewModel.kt | 12 + 51 files changed, 1595 insertions(+), 356 deletions(-) rename database/photos/src/androidTest/kotlin/photos/network/{ => database/photos}/PhotoDatabaseMigrationTests.kt (95%) create mode 100644 domain/search/build.gradle.kts create mode 100644 domain/search/src/main/AndroidManifest.xml create mode 100644 domain/search/src/main/kotlin/photos/network/domain/search/Module.kt rename ui/{photos/src/main/kotlin/photos/network/ui/photos => common/src/main/kotlin/photos/network/ui/common/components}/PhotoBottomIcons.kt (67%) rename ui/{photos/src/main/kotlin/photos/network/ui/photos => common/src/main/kotlin/photos/network/ui/common/components}/PhotoTopIcons.kt (96%) rename ui/{photos/src/main/kotlin/photos/network/ui/photos => common/src/main/kotlin/photos/network/ui/common/components}/Tag.kt (88%) rename ui/{photos/src/main/kotlin/photos/network/ui/photos => common/src/main/kotlin/photos/network/ui/common/components}/TagLines.kt (96%) create mode 100644 ui/common/src/main/kotlin/photos/network/ui/common/permissions/FilePermissionHint.kt create mode 100644 ui/photos/src/androidTest/AndroidManifest.xml rename app/src/androidTest/kotlin/photos/network/PhotosNetworkJUnitRunner.kt => ui/photos/src/androidTest/kotlin/photos/network/ui/photos/test/TestActivity.kt (55%) create mode 100644 ui/search/build.gradle.kts create mode 100644 ui/search/src/androidTest/kotlin/photos/network/ui/search/SearchScreenTests.kt create mode 100644 ui/search/src/main/AndroidManifest.xml rename domain/sharing/src/androidTest/kotlin/photos/network/domain/sharing/FailingTest.kt => ui/search/src/main/kotlin/photos/network/ui/search/Module.kt (63%) create mode 100644 ui/search/src/main/kotlin/photos/network/ui/search/SearchEvent.kt create mode 100644 ui/search/src/main/kotlin/photos/network/ui/search/SearchScreen.kt create mode 100644 ui/search/src/main/kotlin/photos/network/ui/search/SearchUiState.kt create mode 100644 ui/search/src/main/kotlin/photos/network/ui/search/SearchViewModel.kt create mode 100644 ui/search/src/main/res/values/strings.xml diff --git a/LICENSE.md b/LICENSE.md index 901233a..8abb6e5 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,190 +1,661 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -Copyright 2020 Photos network developers - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + +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. + +Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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. + + Photos.network · A privacy first, self-hosted photo storage and sharing service for fediverse. + Copyright 2020 Photos network developers + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + +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 AGPL, see +. diff --git a/README.md b/README.md index 1711f1a..bf8422b 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,7 @@ [![Discord](https://img.shields.io/discord/793235453871390720)](https://discord.gg/dGFDpmWp46) [![Continuous Delivery Pipeline](https://github.com/photos-network/android/actions/workflows/continuous-delivery-pipeline.yml/badge.svg)](https://github.com/photos-network/android/actions/workflows/continuous-delivery-pipeline.yml) - -[Photos.network](https://photos.network) is an open source project for self hosted photo management. +[Photos.network](https://photos.network) A privacy first, self-hosted photo storage and sharing service for fediverse. Its core features are: - Keep track of your photos with privacy @@ -15,12 +14,12 @@ Its core features are: ## App -The Android app itself is self-sufficient and can be used to browse local photos on a device, add tags or search by attributes. -When the app is connected to a [core instance](https://github.com/photos-network/core) the feature set increases like sharing images via link. +The Android app itself is self-sufficient and can be used to browse local photos on an android device, add tags or search by attributes. +When the app is connected to a [photos.network server](https://github.com/photos-network/core) the feature set increases like sharing images via link. -## Core -A [core instance](https://github.com/photos-network/core) can run additional long-running tasks to analyze, categorize or group photos based -on all data gained from the photos added. +## Server +A [photos.network server](https://github.com/photos-network/core) can run additional long-running tasks to analyze, categorize or group photos based +on all data gained from the uploaded photos. ## Contribution @@ -36,4 +35,4 @@ See our [Contribution Guidelines](CONTRIBUTING.md) Copyright 2020 Photos network developers -Licensed under the Apache License, Version 2.0 (the "License"); +Licensed under the GNU AFFERO GENERAL PUBLIC LICENSE; diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2d93b01..efd6105 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -59,7 +59,7 @@ android { versionCode = grgit.log().size versionName = "0.1.0-${grgit.head().abbreviatedId}" - testInstrumentationRunner = "photos.network.PhotosNetworkJUnitRunner" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } signingConfigs { @@ -173,6 +173,7 @@ dependencies { implementation(projects.ui.folders) implementation(projects.ui.photos) implementation(projects.ui.settings) + implementation(projects.ui.search) implementation(projects.ui.sharing) implementation(projects.ui.common) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index be4d0c4..6b68382 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,6 +14,8 @@ + diff --git a/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt b/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt index 10394ef..f6fd25f 100644 --- a/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt +++ b/app/src/main/kotlin/photos/network/PhotosNetworkApplication.kt @@ -31,6 +31,7 @@ import photos.network.database.sharing.databaseSharingModule import photos.network.domain.albums.domainAlbumsModule import photos.network.domain.folders.domainFoldersModule import photos.network.domain.photos.domainPhotosModule +import photos.network.domain.search.domainSearchModule import photos.network.domain.settings.domainSettingsModule import photos.network.domain.sharing.domainSharingModule import photos.network.repository.folders.repositoryFoldersModule @@ -42,6 +43,7 @@ import photos.network.system.mediastore.systemMediastoreModule import photos.network.ui.albums.uiAlbumsModule import photos.network.ui.folders.uiFoldersModule import photos.network.ui.photos.uiPhotosModule +import photos.network.ui.search.uiSearchModule import photos.network.ui.settings.uiSettingsModule import photos.network.ui.sharing.uiSharingModule @@ -100,6 +102,10 @@ open class PhotosNetworkApplication : Application(), KoinComponent { repositorySharingModule, databaseSharingModule, + // search + uiSearchModule, + domainSearchModule, + apiModule, systemFilesystemModule, diff --git a/app/src/main/kotlin/photos/network/home/Home.kt b/app/src/main/kotlin/photos/network/home/Home.kt index 781b8ed..e50bbdd 100644 --- a/app/src/main/kotlin/photos/network/home/Home.kt +++ b/app/src/main/kotlin/photos/network/home/Home.kt @@ -16,7 +16,6 @@ package photos.network.home import android.content.res.Configuration -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets @@ -47,7 +46,6 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -55,10 +53,12 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument import androidx.window.layout.DisplayFeature import org.koin.androidx.compose.getViewModel import photos.network.ui.common.navigation.Destination @@ -120,8 +120,7 @@ fun Home( modifier = Modifier .fillMaxSize() .padding(bottom = innerPadding.calculateBottomPadding()) - .padding(top = topPadding) - .border(2.dp, Color.Green), + .padding(top = topPadding), ) { if (showNavigationRail && currentDestination.isRootDestination()) { HomeNavigationRail( @@ -153,9 +152,22 @@ fun Home( navController = navController, ) } - composable(route = Destination.Login.route) { + composable( + route = "${Destination.Login.route}/{host}/{client}", + arguments = listOf( + navArgument("host") { type = NavType.StringType }, + navArgument("client") { type = NavType.StringType }, + ), + ) { backStackEntry -> photos.network.ui.sharing.login.LoginScreen( navController = navController, + host = backStackEntry.arguments?.getString("host") ?: "", + client = backStackEntry.arguments?.getString("client") ?: "", + ) + } + composable(route = Destination.Search.route) { + photos.network.ui.search.SearchScreen( + navController = navController, ) } } diff --git a/common/build.gradle.kts b/common/build.gradle.kts index c666f38..0b2eb03 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -97,13 +97,10 @@ dependencies { testApi(libs.mockk) testApi(libs.truth) testApi(libs.junit.junit) - androidTestApi(libs.androidx.test.core) - androidTestApi(libs.androidx.test.ext.junit) - androidTestApi(libs.androidx.test.ext.junit) - androidTestApi(libs.androidx.test.ext.truth) - androidTestApi(libs.androidx.test.monitor) - androidTestApi(libs.androidx.test.runner) - androidTestApi(libs.androidx.test.rules) - androidTestApi(libs.androidx.test.orchestrator) - androidTestApi(libs.androidx.test.services) + + // instrumented tests + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.test.rules) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.test.ext.truth) } diff --git a/common/src/androidTest/kotlin/photos/network/common/persistence/SecureStorageTest.kt b/common/src/androidTest/kotlin/photos/network/common/persistence/SecureStorageTest.kt index 9d9706f..1d31e98 100644 --- a/common/src/androidTest/kotlin/photos/network/common/persistence/SecureStorageTest.kt +++ b/common/src/androidTest/kotlin/photos/network/common/persistence/SecureStorageTest.kt @@ -15,11 +15,11 @@ */ package photos.network.common.persistence -import android.content.Context import androidx.security.crypto.EncryptedFile import androidx.security.crypto.MasterKeys -import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry import org.junit.Assert.assertEquals import org.junit.Before import org.junit.BeforeClass @@ -31,21 +31,21 @@ import java.io.File import java.nio.charset.StandardCharsets @RunWith(AndroidJUnit4::class) +@SmallTest class SecureStorageTest { - private val context = ApplicationProvider.getApplicationContext() - + private val appContext = InstrumentationRegistry.getInstrumentation().targetContext private val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC) @Before fun setup() { - File(context.filesDir, "fileToPersist").delete() + File(appContext.filesDir, "fileToPersist").delete() } private val encryptedFile = EncryptedFile.Builder( - File(context.filesDir, "fileToPersist"), - context, + File(appContext.filesDir, "fileToPersist"), + appContext, masterKeyAlias, EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB, ).build() diff --git a/database/photos/build.gradle.kts b/database/photos/build.gradle.kts index afe2de1..cc99660 100644 --- a/database/photos/build.gradle.kts +++ b/database/photos/build.gradle.kts @@ -64,4 +64,8 @@ dependencies { implementation(libs.bundles.room) ksp(libs.room.compiler) androidTestImplementation(libs.room.testing) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.test.rules) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.test.ext.truth) } diff --git a/database/photos/src/androidTest/kotlin/photos/network/PhotoDatabaseMigrationTests.kt b/database/photos/src/androidTest/kotlin/photos/network/database/photos/PhotoDatabaseMigrationTests.kt similarity index 95% rename from database/photos/src/androidTest/kotlin/photos/network/PhotoDatabaseMigrationTests.kt rename to database/photos/src/androidTest/kotlin/photos/network/database/photos/PhotoDatabaseMigrationTests.kt index 961c804..8e6f0c6 100644 --- a/database/photos/src/androidTest/kotlin/photos/network/PhotoDatabaseMigrationTests.kt +++ b/database/photos/src/androidTest/kotlin/photos/network/database/photos/PhotoDatabaseMigrationTests.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.data.photos.persistence +package photos.network.database.photos import android.content.ContentValues import android.database.sqlite.SQLiteDatabase @@ -27,8 +27,6 @@ import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import photos.network.database.photos.MIGRATION_1_2 -import photos.network.database.photos.PhotoDatabase import java.io.IOException @RunWith(AndroidJUnit4::class) diff --git a/domain/search/build.gradle.kts b/domain/search/build.gradle.kts new file mode 100644 index 0000000..2b2d2a3 --- /dev/null +++ b/domain/search/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +android { + namespace = "photos.network.domain.search" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + } + + packagingOptions { + resources.excludes += "META-INF/AL2.0" + resources.excludes += "META-INF/LGPL2.1" + } +} + +dependencies { + api(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + api(projects.repository.settings) + api(projects.repository.sharing) + + testImplementation(libs.mockk) + testImplementation(libs.junit.junit) + testImplementation(libs.truth) + testImplementation(libs.core.testing) +} diff --git a/domain/search/src/main/AndroidManifest.xml b/domain/search/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/domain/search/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/domain/search/src/main/kotlin/photos/network/domain/search/Module.kt b/domain/search/src/main/kotlin/photos/network/domain/search/Module.kt new file mode 100644 index 0000000..3227ba7 --- /dev/null +++ b/domain/search/src/main/kotlin/photos/network/domain/search/Module.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.domain.search + +import org.koin.dsl.module + +val domainSearchModule = module { +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fd4b0a3..739a00d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,10 +50,7 @@ androidx-paging = "3.1.1" google-accompanist = "0.30.1" google-android-material = "1.6.1" coil-kt = "2.2.1" -#retrofit2 = "2.9.0" -#okhttp3 = "4.10.0" kotlinx-serialization = "1.4.0" -#jakewharton-retrofit2-kotlinx-serialization-converter = "0.8.0" androidx-compose-compiler = "1.2.0" androidx-room = "2.4.3" androidx-window = "1.1.0-alpha03" @@ -62,10 +59,10 @@ androidx-test-core = "1.4.0" androidx-test-ext-junit = "1.1.3" androidx-test-ext-truth = "1.4.0" androidx-test-monitor = "1.5.0" -androidx-test-orchestrator = "1.4.1" -androidx-test-runner = "1.4.0" -androidx-test-rules = "1.4.0" -androidx-test-services = "1.4.1" +androidx-test-orchestrator = "1.4.2" +androidx-test-runner = "1.4.2" +androidx-test-rules = "1.5.0" +androidx-test-services = "1.4.2" mockk = "1.12.5" androidx-core = "1.8.0" kotlinx-coroutines = "1.6.4" @@ -100,11 +97,8 @@ androidx-test-core-ktx = { group = "androidx.test", name = "core-ktx", version.r androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidx-test-core" } androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } androidx-test-ext-truth = { group = "androidx.test.ext", name = "truth", version.ref = "androidx-test-ext-truth" } -androidx-test-monitor = { group = "androidx.test", name = "monitor", version.ref = "androidx-test-monitor" } androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidx-test-runner" } androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidx-test-rules" } -androidx-test-orchestrator = { group = "androidx.test", name = "orchestrator", version.ref = "androidx-test-orchestrator" } -androidx-test-services = { group = "androidx.test.services", name = "test-services", version.ref = "androidx-test-services" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } diff --git a/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt b/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt index 99e8892..d5205df 100644 --- a/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt +++ b/repository/photos/src/main/kotlin/photos/network/repository/photos/PhotoRepositoryImpl.kt @@ -53,7 +53,7 @@ class PhotoRepositoryImpl( .build() override suspend fun syncPhotos(): SyncStatus { - val photos = mediaStore.queryLocalMediaStore() + val photos = mediaStore.queryLocalImages() logcat(LogPriority.VERBOSE) { "Found ${photos.size} photos." } photos.forEach { diff --git a/settings.gradle.kts b/settings.gradle.kts index 6d3a9ae..151c107 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,6 +33,7 @@ include(":ui:folders") include(":ui:photos") include(":ui:settings") include(":ui:sharing") +include(":ui:search") include(":ui:common") @@ -40,6 +41,7 @@ include(":domain:albums") include(":domain:folders") include(":domain:photos") include(":domain:settings") +include(":domain:search") include(":domain:sharing") include(":repository:photos") diff --git a/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStore.kt b/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStore.kt index d33a66e..ba519e5 100644 --- a/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStore.kt +++ b/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStore.kt @@ -19,7 +19,8 @@ import android.net.Uri import java.time.Instant interface MediaStore { - fun queryLocalMediaStore(): List + fun queryLocalImages(): List + fun queryLocalVideos(): List } data class MediaItem( diff --git a/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStoreImpl.kt b/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStoreImpl.kt index 9da3440..a4fe222 100644 --- a/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStoreImpl.kt +++ b/system/mediastore/src/main/kotlin/photos/network/system/mediastore/MediaStoreImpl.kt @@ -15,12 +15,17 @@ */ package photos.network.system.mediastore +import android.Manifest import android.app.Application import android.location.Location +import android.media.ExifInterface import android.net.Uri import android.os.Build +import androidx.annotation.RequiresPermission import logcat.LogPriority +import logcat.asLog import logcat.logcat +import java.io.InputStream import java.time.Instant class MediaStoreImpl( @@ -29,8 +34,23 @@ class MediaStoreImpl( /** * Query images from Androids local MediaStore (`DCIM/` and `Pictures/`) */ - @Suppress("ForbiddenComment", "NestedBlockDepth", "LongMethod") - override fun queryLocalMediaStore(): List { + @RequiresPermission( + allOf = [ + // Android 9 or lower + Manifest.permission.READ_EXTERNAL_STORAGE, + + // access images > Android 9 + Manifest.permission.READ_MEDIA_IMAGES, + + // access videos > Android 9 + Manifest.permission.READ_MEDIA_VIDEO, + + // access any geographic locations > Android 9 + Manifest.permission.ACCESS_MEDIA_LOCATION, + ], + ) + @Suppress("ForbiddenComment", "NestedBlockDepth", "LongMethod", "CyclomaticComplexMethod") + override fun queryLocalImages(): List { val photos = mutableListOf() val selection = null @@ -115,21 +135,25 @@ class MediaStoreImpl( // Image location var latLong = FloatArray(2) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - // TODO: ACCESS_MEDIA_LOCATION permission required logcat(LogPriority.WARN) { "Implement ACCESS_MEDIA_LOCATION permission for exif location" } -// photoUri = MediaStore.setRequireOriginal(photoUri) -// val stream: InputStream? = -// applicationContext.contentResolver.openInputStream(photoUri) -// if (stream == null) { -// logcat(LogPriority.WARN) { "Got a null input stream for $photoUri" } -// continue -// } -// -// val exifInterface = ExifInterface(stream) -// // If it returns null, fall back to {0.0, 0.0}. -// exifInterface.getLatLong(latLong) -// -// stream.close() + try { + photoUri = android.provider.MediaStore.setRequireOriginal(photoUri) + val stream: InputStream? = + applicationContext.contentResolver.openInputStream(photoUri) + if (stream == null) { + logcat(LogPriority.WARN) { "Got a null input stream for $photoUri" } + continue + } + + val exifInterface = ExifInterface(stream) + // If it returns null, fall back to {0.0, 0.0}. + exifInterface.getLatLong(latLong) + + stream.close() + } catch (exception: UnsupportedOperationException) { + logcat(LogPriority.WARN) { "ACCESS_MEDIA_LOCATION permission not granted for exif location" } + logcat(LogPriority.DEBUG) { exception.asLog() } + } } else { if (latColumn != -1 && longColumn != -1) { latLong = floatArrayOf( @@ -164,6 +188,13 @@ class MediaStoreImpl( return photos } + /** + * Query videos from Androids local MediaStore (`DCIM/`, `Movies/`, and `Pictures/`) + */ + override fun queryLocalVideos(): List { + TODO("Not yet implemented") + } + private fun generateContentUri(): Uri { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { android.provider.MediaStore.Images.Media.getContentUri( diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoBottomIcons.kt b/ui/common/src/main/kotlin/photos/network/ui/common/components/PhotoBottomIcons.kt similarity index 67% rename from ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoBottomIcons.kt rename to ui/common/src/main/kotlin/photos/network/ui/common/components/PhotoBottomIcons.kt index 704a3fb..2244825 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoBottomIcons.kt +++ b/ui/common/src/main/kotlin/photos/network/ui/common/components/PhotoBottomIcons.kt @@ -13,8 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.ui +package photos.network.ui.common.components +import androidx.compose.foundation.layout.Row +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -23,7 +28,15 @@ import photos.network.ui.common.theme.AppTheme @Composable fun PhotoBottomIcons( modifier: Modifier = Modifier, + onIconClicked: () -> Unit = {}, ) { + Row(modifier = modifier) { + IconButton( + onClick = { onIconClicked() }, + ) { + Icon(Icons.Filled.Delete, null) + } + } } @Preview diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoTopIcons.kt b/ui/common/src/main/kotlin/photos/network/ui/common/components/PhotoTopIcons.kt similarity index 96% rename from ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoTopIcons.kt rename to ui/common/src/main/kotlin/photos/network/ui/common/components/PhotoTopIcons.kt index 032c777..a194021 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoTopIcons.kt +++ b/ui/common/src/main/kotlin/photos/network/ui/common/components/PhotoTopIcons.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.ui +package photos.network.ui.common.components import androidx.compose.foundation.layout.Row import androidx.compose.material.Icon diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/Tag.kt b/ui/common/src/main/kotlin/photos/network/ui/common/components/Tag.kt similarity index 88% rename from ui/photos/src/main/kotlin/photos/network/ui/photos/Tag.kt rename to ui/common/src/main/kotlin/photos/network/ui/common/components/Tag.kt index 3f6f0bd..ccbaf0a 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/Tag.kt +++ b/ui/common/src/main/kotlin/photos/network/ui/common/components/Tag.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.ui.photos +package photos.network.ui.common.components import android.content.res.Configuration import androidx.compose.foundation.background @@ -33,6 +33,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import photos.network.ui.common.R import photos.network.ui.common.theme.AppTheme /** @@ -47,11 +48,11 @@ fun Tag( modifier = Modifier .background( color = MaterialTheme.colorScheme.secondaryContainer, - shape = RoundedCornerShape(topStartPercent = 50, bottomStartPercent = 1), + shape = RoundedCornerShape(10.dp), ) - .clip(RoundedCornerShape(topStartPercent = 50)) + .clip(RoundedCornerShape(5.dp)) .clickable(onClick = { onClickTag(tag) }) - .padding(horizontal = 8.dp), + .padding(start = 2.dp, end = 4.dp), ) { Icon( @@ -60,12 +61,13 @@ fun Tag( .padding(vertical = 4.dp), imageVector = Icons.Default.Bookmarks, contentDescription = stringResource(id = R.string.icon_tags), + tint = MaterialTheme.colorScheme.secondary, ) Text( modifier = Modifier.padding(top = 4.dp, bottom = 4.dp), text = tag, fontSize = MaterialTheme.typography.labelSmall.fontSize, - color = MaterialTheme.colorScheme.onSecondary, + color = MaterialTheme.colorScheme.secondary, ) } } diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/TagLines.kt b/ui/common/src/main/kotlin/photos/network/ui/common/components/TagLines.kt similarity index 96% rename from ui/photos/src/main/kotlin/photos/network/ui/photos/TagLines.kt rename to ui/common/src/main/kotlin/photos/network/ui/common/components/TagLines.kt index c2dc31f..99d4e70 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/TagLines.kt +++ b/ui/common/src/main/kotlin/photos/network/ui/common/components/TagLines.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.ui +package photos.network.ui.common.components import android.content.res.Configuration import androidx.compose.foundation.layout.Spacer @@ -24,7 +24,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import photos.network.ui.common.theme.AppTheme -import photos.network.ui.photos.Tag @Preview(name = "Tags") @Preview(name = "Tags · DARK", uiMode = Configuration.UI_MODE_NIGHT_YES) diff --git a/ui/common/src/main/kotlin/photos/network/ui/common/navigation/Destination.kt b/ui/common/src/main/kotlin/photos/network/ui/common/navigation/Destination.kt index 803a053..eae7db5 100644 --- a/ui/common/src/main/kotlin/photos/network/ui/common/navigation/Destination.kt +++ b/ui/common/src/main/kotlin/photos/network/ui/common/navigation/Destination.kt @@ -24,6 +24,7 @@ import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.People import androidx.compose.material.icons.filled.Photo import androidx.compose.material.icons.filled.PhotoAlbum +import androidx.compose.material.icons.filled.Search import androidx.compose.ui.graphics.vector.ImageVector import androidx.core.os.bundleOf import photos.network.ui.common.R @@ -41,6 +42,7 @@ sealed class Destination( object Albums : Destination("albums", R.string.albums_title, Icons.Filled.PhotoAlbum) object Account : Destination("account", R.string.account_title, Icons.Filled.People) object Folders : Destination("folders", R.string.folders_title, Icons.Filled.Folder) + object Search : Destination("search", R.string.search_title, Icons.Filled.Search) object Login : Destination("login", R.string.login_title, Icons.Filled.Lock) fun isRootDestination(): Boolean { diff --git a/ui/common/src/main/kotlin/photos/network/ui/common/permissions/FilePermissionHint.kt b/ui/common/src/main/kotlin/photos/network/ui/common/permissions/FilePermissionHint.kt new file mode 100644 index 0000000..2fbfe5f --- /dev/null +++ b/ui/common/src/main/kotlin/photos/network/ui/common/permissions/FilePermissionHint.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.ui.common.permissions + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Button +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun FilePermissionHint( + onHintClicked: () -> Unit, + onCloseClicked: () -> Unit, +) { + Column( + modifier = Modifier.padding(4.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row(modifier = Modifier.padding(4.dp).fillMaxWidth()) { + Text( + modifier = Modifier.weight(1f), + text = "Permission required", + ) + IconButton(onClick = onCloseClicked) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = null, + ) + } + } + Text( + modifier = Modifier + .padding(horizontal = 4.dp) + .fillMaxWidth(), + textAlign = TextAlign.Center, + text = "To show images stored on this device, the permission to read external storage is mandatory.", + ) + Button(onClick = onHintClicked) { + Text("Grant access") + } + Divider(modifier = Modifier.height(1.dp)) + } +} +// +// if (permissionStateLocation.status is PermissionStatus.Denied) { +// // TODO: show hint for permission request +// } + +@Preview +@Composable +private fun FilePermissionHintPreview() { + MaterialTheme { + FilePermissionHint( + onCloseClicked = {}, + onHintClicked = {}, + ) + } +} diff --git a/ui/common/src/main/res/values/strings.xml b/ui/common/src/main/res/values/strings.xml index b71da54..4702275 100644 --- a/ui/common/src/main/res/values/strings.xml +++ b/ui/common/src/main/res/values/strings.xml @@ -18,4 +18,7 @@ Data is being transmitted with the configured photos.network instance. Communication to the configured photos.network instance is not authorized! + tags icon + Search + diff --git a/ui/photos/build.gradle.kts b/ui/photos/build.gradle.kts index d29eb81..1ec2e1f 100644 --- a/ui/photos/build.gradle.kts +++ b/ui/photos/build.gradle.kts @@ -71,4 +71,10 @@ dependencies { testImplementation(libs.truth) testImplementation(libs.core.testing) testImplementation(libs.kotlinx.coroutines.test) + + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.test.rules) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.test.ext.truth) + androidTestImplementation(libs.compose.test.junit4) } diff --git a/ui/photos/src/androidTest/AndroidManifest.xml b/ui/photos/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000..5223c58 --- /dev/null +++ b/ui/photos/src/androidTest/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/ui/photos/src/androidTest/kotlin/photos/network/PhotosNetworkTestUtils.kt b/ui/photos/src/androidTest/kotlin/photos/network/PhotosNetworkTestUtils.kt index 9cdd10b..b2ade05 100644 --- a/ui/photos/src/androidTest/kotlin/photos/network/PhotosNetworkTestUtils.kt +++ b/ui/photos/src/androidTest/kotlin/photos/network/PhotosNetworkTestUtils.kt @@ -15,7 +15,7 @@ */ package photos.network -import photos.network.data.photos.repository.Photo +import photos.network.repository.photos.Photo import java.time.Instant fun generateTestPhoto( diff --git a/ui/photos/src/androidTest/kotlin/photos/network/ui/photos/PhotosScreenTests.kt b/ui/photos/src/androidTest/kotlin/photos/network/ui/photos/PhotosScreenTests.kt index 97845fe..396cbff 100644 --- a/ui/photos/src/androidTest/kotlin/photos/network/ui/photos/PhotosScreenTests.kt +++ b/ui/photos/src/androidTest/kotlin/photos/network/ui/photos/PhotosScreenTests.kt @@ -22,16 +22,15 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipe -import org.junit.Ignore import org.junit.Rule import org.junit.Test -import photos.network.MainActivity import photos.network.generateTestPhoto import photos.network.ui.common.theme.AppTheme +import photos.network.ui.photos.test.TestActivity class PhotosScreenTests { @get:Rule - val composeTestRule = createAndroidComposeRule() + val composeTestRule = createAndroidComposeRule() @Test fun loading_spinner_should_be_shown_while_loading() { @@ -39,7 +38,7 @@ class PhotosScreenTests { val uiState = PhotosUiState(isLoading = true) // when - composeTestRule.activity.setContent { + composeTestRule.setContent { AppTheme { PhotosContent(uiState = uiState, handleEvent = {}) } @@ -49,7 +48,6 @@ class PhotosScreenTests { composeTestRule.onNodeWithTag("LOADING_SPINNER").assertIsDisplayed() } - @Ignore("Broken in test only") @Test fun back_should_unselect_photo_if_set() { // given @@ -68,7 +66,7 @@ class PhotosScreenTests { } // when - composeTestRule.activity.setContent { + composeTestRule.setContent { AppTheme { PhotosContent( uiState = uiState, @@ -76,7 +74,9 @@ class PhotosScreenTests { ) } } - composeTestRule.activity.onBackPressed() + composeTestRule.activityRule.scenario.onActivity { activity -> + activity.onBackPressedDispatcher.onBackPressed() + } // then assert(called) @@ -104,7 +104,7 @@ class PhotosScreenTests { } // when - composeTestRule.activity.setContent { + composeTestRule.setContent { AppTheme { PhotosContent( uiState = uiState, diff --git a/app/src/androidTest/kotlin/photos/network/PhotosNetworkJUnitRunner.kt b/ui/photos/src/androidTest/kotlin/photos/network/ui/photos/test/TestActivity.kt similarity index 55% rename from app/src/androidTest/kotlin/photos/network/PhotosNetworkJUnitRunner.kt rename to ui/photos/src/androidTest/kotlin/photos/network/ui/photos/test/TestActivity.kt index 78c6612..98014c7 100644 --- a/app/src/androidTest/kotlin/photos/network/PhotosNetworkJUnitRunner.kt +++ b/ui/photos/src/androidTest/kotlin/photos/network/ui/photos/test/TestActivity.kt @@ -13,23 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network +package photos.network.ui.photos.test -import android.app.Application -import android.content.Context -import androidx.test.runner.AndroidJUnitRunner +import androidx.activity.ComponentActivity /** - * Custom JUnit runner to inject a TestApplication + * Use a blank custom activity for tests in isolation but with access to the activity. + * + * In this case, to trigger the backpress dispatcher */ -@Suppress("unused") -class PhotosNetworkJUnitRunner : AndroidJUnitRunner() { - - override fun newApplication( - cl: ClassLoader?, - className: String?, - context: Context?, - ): Application { - return super.newApplication(cl, PhotosNetworkApplication::class.java.name, context) - } -} +class TestActivity : ComponentActivity() diff --git a/ui/photos/src/main/AndroidManifest.xml b/ui/photos/src/main/AndroidManifest.xml index 8072ee0..db2ef16 100644 --- a/ui/photos/src/main/AndroidManifest.xml +++ b/ui/photos/src/main/AndroidManifest.xml @@ -1,2 +1,9 @@ - + + + + + + + + diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoDetails.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoDetails.kt index 81237c6..1d7e0a6 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoDetails.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoDetails.kt @@ -30,8 +30,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import coil.compose.rememberImagePainter import photos.network.repository.photos.Photo -import photos.network.ui.PhotoBottomIcons -import photos.network.ui.PhotoTopIcons +import photos.network.ui.common.components.PhotoBottomIcons +import photos.network.ui.common.components.PhotoTopIcons @Composable fun PhotoDetails( diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt index 5efbaeb..9f6d793 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotoGrid.kt @@ -19,7 +19,9 @@ import android.icu.text.DateFormatSymbols import androidx.compose.foundation.Image 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.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -32,6 +34,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape 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.draw.clip import androidx.compose.ui.graphics.Color @@ -58,10 +61,16 @@ fun PhotoGrid( val lazyListState = rememberLazyGridState() if (photos.isEmpty()) { - Text( - modifier = Modifier.testTag("LOADING_SPINNER"), - text = "There are no photos to show right now.", - ) + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier.testTag("EMPTY_TEXT"), + text = "There are no photos to show right now.", + ) + } } else { // TODO: add fast-scroll diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosScreen.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosScreen.kt index e57def5..76d65b6 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosScreen.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosScreen.kt @@ -22,20 +22,19 @@ import android.net.Uri import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS import androidx.activity.compose.BackHandler import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -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.material.icons.Icons +import androidx.compose.material.icons.filled.NotInterested +import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Shield import androidx.compose.material.icons.outlined.Shield +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button 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 @@ -43,12 +42,12 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Alignment +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -58,7 +57,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController -import com.google.accompanist.permissions.PermissionStatus +import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import org.koin.androidx.compose.getViewModel import photos.network.api.ServerStatus @@ -73,7 +72,20 @@ fun PhotosScreen( ) { val viewmodel: PhotosViewModel = getViewModel() val uiState = viewmodel.uiState.collectAsState().value - val permissionState = rememberPermissionState(android.Manifest.permission.READ_EXTERNAL_STORAGE) + + val permissionStateFiles = + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { + rememberPermissionState(android.Manifest.permission.READ_MEDIA_IMAGES) + } else { + rememberPermissionState(android.Manifest.permission.READ_EXTERNAL_STORAGE) + } + + val permissionStateLocation = + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + rememberPermissionState(android.Manifest.permission.ACCESS_MEDIA_LOCATION) + } else { + rememberPermissionState(android.Manifest.permission.ACCESS_FINE_LOCATION) + } Scaffold( topBar = { @@ -113,6 +125,17 @@ fun PhotosScreen( ) } } + IconButton( + onClick = { + navController.navigate(Destination.Search.route) + }, + ) { + Icon( + imageVector = Icons.Default.Search, + contentDescription = stringResource(id = R.string.open_search), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } }, colors = TopAppBarDefaults.smallTopAppBarColors( containerColor = MaterialTheme.colorScheme.primary, @@ -121,35 +144,104 @@ fun PhotosScreen( } }, ) { innerPadding -> - when (permissionState.status) { - is PermissionStatus.Denied -> { - Column( - modifier = Modifier - .fillMaxHeight() - .padding(top = innerPadding.calculateTopPadding()) - .padding(16.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - text = "To show images stored on this device, the permission to read external storage is mandatory. Please grant the permission.", - ) - Spacer(modifier = Modifier.height(8.dp)) - Button(onClick = { permissionState.launchPermissionRequest() }) { - Text("Grant access") - } - } + Column( + modifier = modifier.padding(top = innerPadding.calculateTopPadding()), + ) { + val openFilesPermissionDialog = remember { + mutableStateOf(!permissionStateFiles.status.isGranted) } - PermissionStatus.Granted -> { - PhotosContent( - modifier = modifier.padding(top = innerPadding.calculateTopPadding()), - navController = navController, - uiState = uiState, - handleEvent = viewmodel::handleEvent, + val openLocationPermissionDialog = remember { + mutableStateOf(!permissionStateLocation.status.isGranted) + } + + if (openFilesPermissionDialog.value) { + AlertDialog( + onDismissRequest = { + // Dismiss the dialog when the user clicks outside the dialog or on the back + // button. If you want to disable that functionality, simply use an empty + // onCloseRequest. + openFilesPermissionDialog.value = false + }, + icon = { + Icons.Filled.NotInterested + }, + title = { + Text(text = "Permission") + }, + text = { + Text( + text = "To show images stored on this device, the permission to read external storage is mandatory.", + ) + }, + confirmButton = { + Button( + onClick = { + permissionStateFiles.launchPermissionRequest() + openFilesPermissionDialog.value = false + }, + ) { + Text("Grant access") + } + }, + dismissButton = { + OutlinedButton( + onClick = { + openFilesPermissionDialog.value = false + }, + ) { + Text("Not now") + } + }, + ) + } + + if (openLocationPermissionDialog.value && permissionStateFiles.status.isGranted) { + AlertDialog( + onDismissRequest = { + // Dismiss the dialog when the user clicks outside the dialog or on the back + // button. If you want to disable that functionality, simply use an empty + // onCloseRequest. + openLocationPermissionDialog.value = false + }, + icon = { + Icons.Filled.NotInterested + }, + title = { + Text(text = "Permission") + }, + text = { + Text( + text = "To show where an images was captured, the location permission is required.", + ) + }, + confirmButton = { + Button( + onClick = { + permissionStateLocation.launchPermissionRequest() + openLocationPermissionDialog.value = false + }, + ) { + Text("Grant access") + } + }, + dismissButton = { + OutlinedButton( + onClick = { + openLocationPermissionDialog.value = false + }, + ) { + Text("Not now") + } + }, ) } + + PhotosContent( + modifier = modifier.padding(top = innerPadding.calculateTopPadding()), + navController = navController, + uiState = uiState, + handleEvent = viewmodel::handleEvent, + ) } } } diff --git a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosUiState.kt b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosUiState.kt index fe22426..bcbfccc 100644 --- a/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosUiState.kt +++ b/ui/photos/src/main/kotlin/photos/network/ui/photos/PhotosUiState.kt @@ -18,10 +18,12 @@ package photos.network.ui.photos import photos.network.repository.photos.Photo data class PhotosUiState( + val isLoading: Boolean = true, + val hasError: Boolean = false, + val hasImagePermission: Boolean = false, + val hasLocationPermission: Boolean = false, val isPrivacyEnabled: Boolean = false, val photos: List = emptyList(), val selectedPhoto: Photo? = null, val selectedIndex: Int? = null, - val isLoading: Boolean = true, - val hasError: Boolean = false, ) diff --git a/ui/photos/src/main/res/values/strings.xml b/ui/photos/src/main/res/values/strings.xml index 22e902c..f930447 100644 --- a/ui/photos/src/main/res/values/strings.xml +++ b/ui/photos/src/main/res/values/strings.xml @@ -3,7 +3,7 @@ application logo All items labeled as private are hidden Hide items labeled as private in this view + Open search - tags icon diff --git a/ui/search/build.gradle.kts b/ui/search/build.gradle.kts new file mode 100644 index 0000000..75ebc1e --- /dev/null +++ b/ui/search/build.gradle.kts @@ -0,0 +1,71 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.detekt) + alias(libs.plugins.kover) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + target("src/*/kotlin/**/*.kt") + ktlint( libs.versions.ktlint.get()) + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } +} + +detekt { + config = files("$rootDir/detekt.yml") +} + +android { + namespace = "photos.network.ui.search" + + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + // API 26 | required by: Java 8 Time API + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() + } +} + +dependencies { + implementation(projects.common) + testImplementation(project(":common", "testArtifacts")) + androidTestImplementation(project(":common", "androidTestArtifacts")) + + api(projects.ui.common) + api(projects.domain.search) + + // Compose + implementation(platform(libs.compose.bom)) + implementation(libs.activity.compose) + + // accompanist + implementation(libs.bundles.accompanist) + + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.test.rules) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.test.ext.truth) + androidTestImplementation(libs.compose.test.junit4) + debugImplementation(libs.compose.test.manifest) +} diff --git a/ui/search/src/androidTest/kotlin/photos/network/ui/search/SearchScreenTests.kt b/ui/search/src/androidTest/kotlin/photos/network/ui/search/SearchScreenTests.kt new file mode 100644 index 0000000..6954b29 --- /dev/null +++ b/ui/search/src/androidTest/kotlin/photos/network/ui/search/SearchScreenTests.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.ui.search + +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import org.junit.Rule +import org.junit.Test + +class SearchScreenTests { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun title_should_be_displayed_in_search_screen() { + // given + composeTestRule.setContent { + SearchScreen( + modifier = Modifier, + uiState = SearchUiState(query = ""), + handleEvent = {}, + navigateToLogin = {}, + ) + } + + // then + composeTestRule.onNodeWithTag("SEARCH_HEADER_TITLE").assertIsDisplayed() + } +} diff --git a/ui/search/src/main/AndroidManifest.xml b/ui/search/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/ui/search/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/domain/sharing/src/androidTest/kotlin/photos/network/domain/sharing/FailingTest.kt b/ui/search/src/main/kotlin/photos/network/ui/search/Module.kt similarity index 63% rename from domain/sharing/src/androidTest/kotlin/photos/network/domain/sharing/FailingTest.kt rename to ui/search/src/main/kotlin/photos/network/ui/search/Module.kt index b4a62af..e9c1e4a 100644 --- a/domain/sharing/src/androidTest/kotlin/photos/network/domain/sharing/FailingTest.kt +++ b/ui/search/src/main/kotlin/photos/network/ui/search/Module.kt @@ -13,18 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package photos.network.domain.sharing +package photos.network.ui.search -import androidx.test.ext.junit.runners.AndroidJUnit4 -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module -@RunWith(AndroidJUnit4::class) -class FailingTest { - @Test - fun failingTest() = runBlocking { - assertEquals(true, true) +val uiSearchModule = module { + viewModel { + SearchViewModel( + application = get(), + ) } } diff --git a/ui/search/src/main/kotlin/photos/network/ui/search/SearchEvent.kt b/ui/search/src/main/kotlin/photos/network/ui/search/SearchEvent.kt new file mode 100644 index 0000000..02a52aa --- /dev/null +++ b/ui/search/src/main/kotlin/photos/network/ui/search/SearchEvent.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.ui.search + +sealed class SearchEvent { + class UpdateSearchQuery(val query: String) : SearchEvent() +} diff --git a/ui/search/src/main/kotlin/photos/network/ui/search/SearchScreen.kt b/ui/search/src/main/kotlin/photos/network/ui/search/SearchScreen.kt new file mode 100644 index 0000000..9dd3723 --- /dev/null +++ b/ui/search/src/main/kotlin/photos/network/ui/search/SearchScreen.kt @@ -0,0 +1,157 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.ui.search + +import android.content.res.Configuration +import androidx.compose.foundation.background +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import org.koin.androidx.compose.getViewModel +import photos.network.ui.common.navigation.Destination +import photos.network.ui.common.theme.AppTheme + +@Composable +fun SearchScreen( + modifier: Modifier = Modifier, + navController: NavHostController = rememberNavController(), +) { + val viewmodel: SearchViewModel = getViewModel() + + SearchScreen( + modifier = modifier, + uiState = viewmodel.uiState.collectAsState().value, + handleEvent = viewmodel::handleEvent, + navigateToLogin = { navController.navigate(Destination.Login.route) }, + ) +} + +/** + * stateless + */ +@Composable +fun SearchScreen( + modifier: Modifier = Modifier, + uiState: SearchUiState, + handleEvent: (event: SearchEvent) -> Unit, + navigateToLogin: () -> Unit = {}, +) { + val verticalScrollState = rememberScrollState(0) + + Column( + modifier = modifier + .verticalScroll(verticalScrollState) + .fillMaxSize(), + ) { + SearchHeader() + + Divider() + } +} + +@Suppress("MagicNumber") +@Composable +internal fun SearchHeader( + modifier: Modifier = Modifier, +) { + // header + icon + Box( + modifier = modifier.background(MaterialTheme.colorScheme.surface), + ) { + // header gradient + Box( + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + .background(MaterialTheme.colorScheme.primary) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color(0x55000000), + Color(0x00000000), + ), + ), + ), + ) + + // app name + Text( + modifier = Modifier + .padding(top = 32.dp) + .testTag("SEARCH_HEADER_TITLE") + .fillMaxWidth(), + text = stringResource(id = R.string.app_name_full), + style = MaterialTheme.typography.headlineLarge, + textAlign = TextAlign.Center, + color = Color.White, + ) + } +} + +@Preview( + "Account", + showSystemUi = true, + showBackground = true, + uiMode = Configuration.UI_MODE_NIGHT_NO, +) +@Preview( + "Account • Dark", + showSystemUi = true, + showBackground = true, + uiMode = Configuration.UI_MODE_NIGHT_YES, +) +@Composable +private fun PreviewAccount( + @PreviewParameter(PreviewAccountProvider::class) uiState: SearchUiState, +) { + AppTheme { + SearchScreen( + uiState = uiState, + handleEvent = {}, + ) + } +} + +internal class PreviewAccountProvider : PreviewParameterProvider { + override val values = sequenceOf( + SearchUiState( + query = "", + ), + ) + override val count: Int = values.count() +} diff --git a/ui/search/src/main/kotlin/photos/network/ui/search/SearchUiState.kt b/ui/search/src/main/kotlin/photos/network/ui/search/SearchUiState.kt new file mode 100644 index 0000000..3d835a6 --- /dev/null +++ b/ui/search/src/main/kotlin/photos/network/ui/search/SearchUiState.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.ui.search + +data class SearchUiState( + val query: String = "", + // startDate = today + // endDate = today + // searchLocation = device location if permission ? world zoom + // faces = listOf + + // results = listOf +) diff --git a/ui/search/src/main/kotlin/photos/network/ui/search/SearchViewModel.kt b/ui/search/src/main/kotlin/photos/network/ui/search/SearchViewModel.kt new file mode 100644 index 0000000..2d85a03 --- /dev/null +++ b/ui/search/src/main/kotlin/photos/network/ui/search/SearchViewModel.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2020-2023 Photos.network developers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package photos.network.ui.search + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class SearchViewModel( + private val application: Application, +) : ViewModel() { + val uiState = MutableStateFlow(SearchUiState()) + + fun handleEvent(event: SearchEvent) { + when (event) { + is SearchEvent.UpdateSearchQuery -> updateSearchQuery(event.query) + } + } + + private fun updateSearchQuery(query: String) { + viewModelScope.launch(Dispatchers.IO) { + uiState.update { it.copy(query = query) } + } + } +} diff --git a/ui/search/src/main/res/values/strings.xml b/ui/search/src/main/res/values/strings.xml new file mode 100644 index 0000000..fae534a --- /dev/null +++ b/ui/search/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + Setup server instance + Change server setup + Version copied into clipboard + diff --git a/ui/settings/build.gradle.kts b/ui/settings/build.gradle.kts index 7aea3f1..a635c9f 100644 --- a/ui/settings/build.gradle.kts +++ b/ui/settings/build.gradle.kts @@ -28,6 +28,8 @@ android { minSdk = 26 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + buildConfigField("String", "VERSION_NAME", "\"${version}\"") } compileOptions { @@ -62,4 +64,10 @@ dependencies { // accompanist implementation(libs.bundles.accompanist) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.test.rules) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.test.ext.truth) + androidTestImplementation(libs.compose.test.junit4) + debugImplementation(libs.compose.test.manifest) } diff --git a/ui/settings/src/androidTest/kotlin/photos/network/ui/settings/SettingsScreenTests.kt b/ui/settings/src/androidTest/kotlin/photos/network/ui/settings/SettingsScreenTests.kt index 85e86f6..9ffffe8 100644 --- a/ui/settings/src/androidTest/kotlin/photos/network/ui/settings/SettingsScreenTests.kt +++ b/ui/settings/src/androidTest/kotlin/photos/network/ui/settings/SettingsScreenTests.kt @@ -29,7 +29,9 @@ class SettingsScreenTests { fun title_should_be_displayed_in_settings_screen() { // given composeTestRule.setContent { - SettingsScreen() + SettingsScreen( + uiState = SettingsUiState(), + ) } // then @@ -40,7 +42,9 @@ class SettingsScreenTests { fun logo_should_be_displayed_in_settings_screen() { // given composeTestRule.setContent { - SettingsScreen() + SettingsScreen( + uiState = SettingsUiState(), + ) } // then diff --git a/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsScreen.kt b/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsScreen.kt index a99739e..757eb10 100644 --- a/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsScreen.kt +++ b/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsScreen.kt @@ -70,6 +70,9 @@ import photos.network.ui.common.components.AppLogo import photos.network.ui.common.navigation.Destination import photos.network.ui.common.theme.AppTheme +/** + * stateful + */ @Composable fun SettingsScreen( modifier: Modifier = Modifier, @@ -77,19 +80,26 @@ fun SettingsScreen( ) { val viewmodel: SettingsViewModel = getViewModel() - SettingsContent( + SettingsScreen( modifier = modifier, uiState = viewmodel.uiState.collectAsState().value, handleEvent = viewmodel::handleEvent, - navigateToLogin = { navController.navigate(Destination.Login.route) }, + navigateToLogin = { + navController.navigate( + "${Destination.Login.route}/${viewmodel.uiState.value.host}/${viewmodel.uiState.value.clientId}", + ) + }, ) } +/** + * stateless + */ @Composable -fun SettingsContent( +fun SettingsScreen( modifier: Modifier = Modifier, uiState: SettingsUiState, - handleEvent: (event: SettingsEvent) -> Unit, + handleEvent: (event: SettingsEvent) -> Unit = {}, navigateToLogin: () -> Unit = {}, ) { val verticalScrollState = rememberScrollState(0) @@ -118,10 +128,12 @@ fun SettingsContent( isClientIdVerified = uiState.isClientVerified, ) - Divider() + AnimatedVisibility(visible = uiState.isClientVerified) { + Divider() - AccountSetupItem(loggedIn = uiState.loggedIn) { - navigateToLogin() + AccountSetupItem(loggedIn = uiState.loggedIn) { + navigateToLogin() + } } SectionSpacer() @@ -411,7 +423,7 @@ private fun PreviewAccount( @PreviewParameter(PreviewAccountProvider::class) uiState: SettingsUiState, ) { AppTheme { - SettingsContent( + SettingsScreen( uiState = uiState, handleEvent = {}, ) diff --git a/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsViewModel.kt b/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsViewModel.kt index b779104..bb90397 100644 --- a/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsViewModel.kt +++ b/ui/settings/src/main/kotlin/photos/network/ui/settings/SettingsViewModel.kt @@ -13,12 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:Suppress("DEPRECATION") + package photos.network.ui.settings import android.app.Application import android.content.ClipData import android.content.ClipboardManager import android.content.Context +import android.os.Build import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -49,15 +52,23 @@ class SettingsViewModel( viewModelScope.launch(Dispatchers.IO) { getSettingsUseCase().collect { settings -> withContext(Dispatchers.Main) { + val versionName = application.packageManager.getPackageInfo( + application.packageName, + 0, + ).versionName + val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + application.packageManager.getPackageInfo(application.packageName, 0).longVersionCode + } else { + application.packageManager.getPackageInfo(application.packageName, 0).versionCode + } + uiState.update { it.copy( host = settings.host ?: "", isHostVerified = isHostVerified.value, clientId = settings.clientId ?: "", isClientVerified = isClientIdVerified.value, - // TDOO: get VERSION_NAME and VERSION_CODE -// appVersion = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})", - appVersion = "{BuildConfig.VERSION_NAME} ({BuildConfig.VERSION_CODE})", + appVersion = "$versionName ($versionCode)", ) } } @@ -87,8 +98,13 @@ class SettingsViewModel( val clipboard: ClipboardManager? = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager? - // TODO: get VERSION_NAME - val clip = ClipData.newPlainText("Photos.networdk", "BuildConfig.VERSION_NAME") + @Suppress("DEPRECATION") + val versionName = application.packageManager.getPackageInfo( + application.packageName, + 0, + ).versionName + + val clip = ClipData.newPlainText("Photos.network", versionName) clipboard?.setPrimaryClip(clip) } diff --git a/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginScreen.kt b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginScreen.kt index c5dfe8e..7731fde 100644 --- a/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginScreen.kt +++ b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginScreen.kt @@ -43,8 +43,12 @@ import photos.network.ui.common.navigation.Destination fun LoginScreen( modifier: Modifier = Modifier, navController: NavController = rememberNavController(), + host: String = "", + client: String = "", ) { val viewmodel: LoginViewModel by viewModel() + viewmodel.sethost(host) + viewmodel.setclient(client) LoginScreen( modifier = modifier, diff --git a/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginViewModel.kt b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginViewModel.kt index 4ffd1bc..96ec99c 100644 --- a/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginViewModel.kt +++ b/ui/sharing/src/main/kotlin/photos/network/ui/sharing/login/LoginViewModel.kt @@ -64,6 +64,18 @@ class LoginViewModel( } } + fun sethost(host: String) { + viewModelScope.launch { + uiState.update { it.copy(host = host) } + } + } + + fun setclient(client: String) { + viewModelScope.launch { + uiState.update { it.copy(clientId = client) } + } + } + /** * generate random nonce for EACH request to prevent replay attacs */ From 4e4616c173486d212f505a30e746c4ed8deef82f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Stu=CC=88rmer?= Date: Fri, 14 Jul 2023 00:56:21 +0200 Subject: [PATCH 13/13] adjust kover report path --- .../continuous-delivery-pipeline.yml | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/.github/workflows/continuous-delivery-pipeline.yml b/.github/workflows/continuous-delivery-pipeline.yml index 3224bc1..62bd682 100644 --- a/.github/workflows/continuous-delivery-pipeline.yml +++ b/.github/workflows/continuous-delivery-pipeline.yml @@ -164,7 +164,7 @@ jobs: - name: Add coverage report to PR uses: mi-kas/kover-report@v1 with: - path: ${{ github.workspace }}/build/reports/kover/report.xml + path: ${{ github.workspace }}/build/reports/kover/result.xml token: ${{ secrets.GITHUB_TOKEN }} title: App Coverage update-comment: true @@ -239,29 +239,6 @@ jobs: name: androidTests path: app/build/reports/androidTests - - name: Run testCoverage - run: ./gradlew --console=plain testCoverage --stacktrace - - - name: Jacoco Report to PR - if: matrix.api-level == 29 - id: jacoco - uses: madrapps/jacoco-report@v1.2 - with: - paths: | - ${{ github.workspace }}/app/build/reports/jacoco/testCoverage/testCoverage.xml, - ${{ github.workspace }}/domain/build/reports/jacoco/testCoverage/testCoverage.xml, - ${{ github.workspace }}/data/build/reports/jacoco/testCoverage/testCoverage.xml - token: ${{ secrets.GITHUB_TOKEN }} - min-coverage-overall: 1 - min-coverage-changed-files: 10 - title: Code Coverage - debug-mode: false - - - name: Get the Coverage info - run: | - echo "Total coverage ${{ steps.jacoco.outputs.coverage-overall }}" - echo "Changed Files coverage ${{ steps.jacoco.outputs.coverage-changed-files }}" - # upload an android bundle to google play store into the internal test track deployment: needs: [ unit_tests, android_tests ]