From 6d0221d705e09f012328f5a140fe27ca40c77721 Mon Sep 17 00:00:00 2001 From: S1210 Date: Tue, 23 May 2023 03:07:22 +0200 Subject: [PATCH] Encrypt databases --- app/build.gradle | 91 +- .../1.json | 0 .../10.json | 992 ++++++++++++++++++ .../2.json | 0 .../3.json | 0 .../4.json | 0 .../6.json | 0 .../7.json | 0 .../8.json | 0 .../9.json | 0 .../1.json | 0 app/src/main/AndroidManifest.xml | 4 +- .../application/TreeTrackerApplication.kt | 6 +- .../TreeTracker/camera/ImageReviewScreen.kt | 13 +- .../TreeTracker/camera/SelfieScreen.kt | 5 +- .../TreeTracker/capture/TreeCaptureScreen.kt | 1 + .../capture/TreeCaptureViewModel.kt | 2 +- .../capture/TreeImageReviewScreen.kt | 4 +- .../TreeTracker/dashboard/DashboardScreen.kt | 6 +- .../dashboard/DashboardViewModel.kt | 11 +- .../dashboard/TreesToSyncHelper.kt | 2 +- .../data/repository/CommonRepositoryImpl.kt | 36 + .../data/repository/MessageRepositoryImpl.kt | 322 ++++++ .../data/repository/QuestionRepositoryImpl.kt | 16 + .../TreeTracker/data/repository/Repository.kt | 15 + .../data/repository/SurveyRepositoryImpl.kt | 16 + .../TreeTracker/database/AppDatabase.kt | 88 -- .../TreeTracker/database/app/AppDatabase.kt | 67 ++ .../database/{ => app}/Converters.kt | 4 +- .../database/{ => app}/Migrations.kt | 4 +- .../database/{ => app}/TreeTrackerDAO.kt | 27 +- .../{ => app}/entity/DeviceConfigEntity.kt | 18 +- .../{ => app}/entity/LocationEntity.kt | 16 +- .../{ => app}/entity/OrganizationEntity.kt | 17 +- .../{ => app}/entity/SessionEntity.kt | 30 +- .../database/{ => app}/entity/TreeEntity.kt | 28 +- .../database/{ => app}/entity/UserEntity.kt | 40 +- .../legacy/entity/LocationDataEntity.kt | 18 +- .../legacy/entity/PlanterCheckInEntity.kt | 18 +- .../legacy/entity/PlanterInfoEntity.kt | 30 +- .../legacy/entity/TreeAttributeEntity.kt | 12 +- .../legacy/entity/TreeCaptureEntity.kt | 28 +- .../legacy/views/TreeMapMarkerDbView.kt | 2 +- .../legacy/views/TreeUploadDbView.kt | 2 +- .../TreeTracker/database/common/CommonDao.kt | 49 + .../TreeTracker/database/common/Encrypt.kt | 38 + .../TreeTracker/database/common/RoomModel.kt | 3 + .../messages}/DatabaseConverters.kt | 80 +- .../database/messages/MessageDatabase.kt | 53 + .../database/messages/dao/MessageDao.kt | 67 ++ .../database/messages/dao/QuestionDao.kt | 17 + .../database/messages/dao/SurveyDao.kt | 17 + .../messages/entity}/MessageEntity.kt | 38 +- .../messages/entity}/QuestionEntity.kt | 6 +- .../messages/entity}/SurveyEntity.kt | 5 +- .../entity/embeded/MessagesForWallet.kt | 61 ++ .../entity/tuple/MessageAsUploadedTuple.kt | 10 + .../entity/tuple/MessageBundleIdTuple.kt | 10 + .../messages/entity/tuple/ReadMessageTuple.kt | 10 + .../tuple/SurveyMessageCompleteTuple.kt | 10 + .../devoptions/DevOptionsScreen.kt | 2 +- .../android/TreeTracker/di/AppModule.kt | 63 +- .../di/{RoomModule.kt => DaoModule.kt} | 15 +- .../TreeTracker/di/RepositoryModule.kt | 30 + .../domain/repository/CommonRepository.kt | 29 + .../domain/repository/MessageRepository.kt | 46 + .../domain/repository/QuestionRepository.kt | 9 + .../domain/repository/SurveyRepository.kt | 9 + .../TreeTracker/extension/BuildersUtils.kt | 14 + .../TreeTracker/extension/FloatUtils.kt | 3 + .../languagepicker/LanguageSelectScreen.kt | 4 +- .../announcementmessage/AnnouncementScreen.kt | 1 + .../AnnouncementViewModel.kt | 27 +- .../messages/directmessages/ChatScreen.kt | 4 +- .../messages/directmessages/ChatViewModel.kt | 12 +- .../IndividualMessageListScreen.kt | 39 +- .../IndividualMessageListViewModel.kt | 8 +- .../messages/survey/SurveyScreen.kt | 13 +- .../messages/survey/SurveyViewModel.kt | 28 +- .../TreeTracker/models/DeviceConfigUpdater.kt | 4 +- .../models/DeviceConfigUploader.kt | 2 +- .../TreeTracker/models/PlanterUploader.kt | 15 +- .../TreeTracker/models/SessionTracker.kt | 7 +- .../TreeTracker/models/SessionUploader.kt | 2 +- .../models/TreeTrackerViewModelFactory.kt | 2 +- .../TreeTracker/models/TreeUploader.kt | 16 +- .../models/location/LocationDataCapturer.kt | 4 +- .../models/messages/MessageUploader.kt | 67 -- .../models/messages/MessagesRepo.kt | 279 ----- .../messages/database/MessageDatabase.kt | 61 -- .../models/messages/database/MessagesDAO.kt | 110 -- .../models/organization/OrgRepo.kt | 4 +- .../TreeTracker/models/user/UserRepo.kt | 12 +- .../TreeTracker/orgpicker/AddOrgScreen.kt | 11 +- .../TreeTracker/orgpicker/OrgPickerScreen.kt | 11 +- .../TreeTracker/permissions/Permissions.kt | 18 +- .../sessionnote/SessionNoteScreen.kt | 12 +- .../TreeTracker/signup/CredentialEntryView.kt | 53 +- .../TreeTracker/signup/NameEntryView.kt | 2 + .../splash/SplashScreenViewModel.kt | 8 +- .../treeheight/TreeHeightColourSelection.kt | 4 +- .../usecases/CreateFakeTreesUseCase.kt | 6 +- .../usecases/CreateLegacyTreeUseCase.kt | 10 +- .../usecases/CreateTreeRequestUseCase.kt | 9 +- .../TreeTracker/usecases/CreateTreeUseCase.kt | 4 +- .../TreeTracker/usecases/SyncDataUseCase.kt | 8 +- .../usecases/UploadLocationDataUseCase.kt | 2 +- .../TreeTracker/userselect/UserSelect.kt | 7 +- .../android/TreeTracker/utils/Utils.kt | 7 +- .../walletselect/WalletSelectScreen.kt | 1 + .../walletselect/addwallet/AddWalletScreen.kt | 1 + .../dashboard/DashboardViewModelTest.kt | 18 +- .../database/TreeTrackerDaoTest.kt | 2 + .../repository/MessageRepositoryTest.kt} | 73 +- .../AnnouncementViewModelTest.kt | 12 +- .../directmessages/ChatViewModelTest.kt | 11 +- .../IndividualMessageListViewModelTest.kt | 8 +- .../messages/survey/SurveyViewModelTest.kt | 12 +- .../location/LocationDataCapturerTest.kt | 2 +- .../splash/SplashScreenViewModelTest.kt | 10 +- .../TreeTracker/utils/FakeFileGenerator.kt | 16 +- .../TreeTracker/utils/LiveDataUtilTest.kt | 2 +- build.gradle | 56 +- gradle.properties | 1 + gradle/wrapper/gradle-wrapper.properties | 2 +- 125 files changed, 2688 insertions(+), 1117 deletions(-) rename app/schemas/{org.greenstand.android.TreeTracker.database.AppDatabase => org.greenstand.android.TreeTracker.database.app.AppDatabase}/1.json (100%) create mode 100644 app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/10.json rename app/schemas/{org.greenstand.android.TreeTracker.database.AppDatabase => org.greenstand.android.TreeTracker.database.app.AppDatabase}/2.json (100%) rename app/schemas/{org.greenstand.android.TreeTracker.database.AppDatabase => org.greenstand.android.TreeTracker.database.app.AppDatabase}/3.json (100%) rename app/schemas/{org.greenstand.android.TreeTracker.database.AppDatabase => org.greenstand.android.TreeTracker.database.app.AppDatabase}/4.json (100%) rename app/schemas/{org.greenstand.android.TreeTracker.database.AppDatabase => org.greenstand.android.TreeTracker.database.app.AppDatabase}/6.json (100%) rename app/schemas/{org.greenstand.android.TreeTracker.database.AppDatabase => org.greenstand.android.TreeTracker.database.app.AppDatabase}/7.json (100%) rename app/schemas/{org.greenstand.android.TreeTracker.database.AppDatabase => org.greenstand.android.TreeTracker.database.app.AppDatabase}/8.json (100%) rename app/schemas/{org.greenstand.android.TreeTracker.database.AppDatabase => org.greenstand.android.TreeTracker.database.app.AppDatabase}/9.json (100%) rename app/schemas/{org.greenstand.android.TreeTracker.models.messages.database.MessageDatabase => org.greenstand.android.TreeTracker.database.messages.MessageDatabase}/1.json (100%) create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/data/repository/CommonRepositoryImpl.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/data/repository/MessageRepositoryImpl.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/data/repository/QuestionRepositoryImpl.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/data/repository/Repository.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/data/repository/SurveyRepositoryImpl.kt delete mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/database/AppDatabase.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/database/app/AppDatabase.kt rename app/src/main/java/org/greenstand/android/TreeTracker/database/{ => app}/Converters.kt (94%) rename app/src/main/java/org/greenstand/android/TreeTracker/database/{ => app}/Migrations.kt (98%) rename app/src/main/java/org/greenstand/android/TreeTracker/database/{ => app}/TreeTrackerDAO.kt (91%) rename app/src/main/java/org/greenstand/android/TreeTracker/database/{ => app}/entity/DeviceConfigEntity.kt (78%) rename app/src/main/java/org/greenstand/android/TreeTracker/database/{ => app}/entity/LocationEntity.kt (79%) rename app/src/main/java/org/greenstand/android/TreeTracker/database/{ => app}/entity/OrganizationEntity.kt (74%) rename app/src/main/java/org/greenstand/android/TreeTracker/database/{ => app}/entity/SessionEntity.kt (71%) rename app/src/main/java/org/greenstand/android/TreeTracker/database/{ => app}/entity/TreeEntity.kt (74%) rename app/src/main/java/org/greenstand/android/TreeTracker/database/{ => app}/entity/UserEntity.kt (67%) rename app/src/main/java/org/greenstand/android/TreeTracker/database/{ => app}/legacy/entity/LocationDataEntity.kt (77%) rename app/src/main/java/org/greenstand/android/TreeTracker/database/{ => app}/legacy/entity/PlanterCheckInEntity.kt (83%) rename app/src/main/java/org/greenstand/android/TreeTracker/database/{ => app}/legacy/entity/PlanterInfoEntity.kt (77%) rename app/src/main/java/org/greenstand/android/TreeTracker/database/{ => app}/legacy/entity/TreeAttributeEntity.kt (86%) rename app/src/main/java/org/greenstand/android/TreeTracker/database/{ => app}/legacy/entity/TreeCaptureEntity.kt (80%) rename app/src/main/java/org/greenstand/android/TreeTracker/database/{ => app}/legacy/views/TreeMapMarkerDbView.kt (91%) rename app/src/main/java/org/greenstand/android/TreeTracker/database/{ => app}/legacy/views/TreeUploadDbView.kt (97%) create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/database/common/CommonDao.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/database/common/Encrypt.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/database/common/RoomModel.kt rename app/src/main/java/org/greenstand/android/TreeTracker/{models/messages/database => database/messages}/DatabaseConverters.kt (60%) create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/database/messages/MessageDatabase.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/database/messages/dao/MessageDao.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/database/messages/dao/QuestionDao.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/database/messages/dao/SurveyDao.kt rename app/src/main/java/org/greenstand/android/TreeTracker/{models/messages/database/entities => database/messages/entity}/MessageEntity.kt (65%) rename app/src/main/java/org/greenstand/android/TreeTracker/{models/messages/database/entities => database/messages/entity}/QuestionEntity.kt (89%) rename app/src/main/java/org/greenstand/android/TreeTracker/{models/messages/database/entities => database/messages/entity}/SurveyEntity.kt (85%) create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/embeded/MessagesForWallet.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/tuple/MessageAsUploadedTuple.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/tuple/MessageBundleIdTuple.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/tuple/ReadMessageTuple.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/tuple/SurveyMessageCompleteTuple.kt rename app/src/main/java/org/greenstand/android/TreeTracker/di/{RoomModule.kt => DaoModule.kt} (63%) create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/di/RepositoryModule.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/domain/repository/CommonRepository.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/domain/repository/MessageRepository.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/domain/repository/QuestionRepository.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/domain/repository/SurveyRepository.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/extension/BuildersUtils.kt create mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/extension/FloatUtils.kt delete mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/models/messages/MessageUploader.kt delete mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/models/messages/MessagesRepo.kt delete mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/MessageDatabase.kt delete mode 100644 app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/MessagesDAO.kt rename app/src/test/java/org/greenstand/android/TreeTracker/{models/messages/MessagesRepoTest.kt => domain/repository/MessageRepositoryTest.kt} (76%) diff --git a/app/build.gradle b/app/build.gradle index 8a71328c1..77a8a87e6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,13 +18,13 @@ def loadExtraProperties(String fileName) { loadExtraProperties("treetracker.keys.properties") android { - compileSdkVersion 31 - buildToolsVersion '30.0.3' + compileSdkVersion 33 + buildToolsVersion '33.0.0' defaultConfig { applicationId "org.greenstand.android.TreeTracker" minSdkVersion 21 - targetSdkVersion 31 + targetSdkVersion 33 versionCode 196 versionName "2.1.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -44,11 +44,10 @@ android { buildConfigField 'Boolean', 'BLUR_DETECTION_ENABLED', "false" buildConfigField 'Boolean', 'USE_AWS_S3', "false" buildConfigField 'Boolean', 'ORG_LINK', "false" - - - javaCompileOptions { - annotationProcessorOptions { - arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] + buildConfigField "String", "CRYPTO_KEY", "\"THE_BEST_APP_IN_THE_WORLD\"" + kapt { + arguments { + arg("room.schemaLocation", "$projectDir/schemas") } } } @@ -59,20 +58,22 @@ android { } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" } composeOptions { - kotlinCompilerExtensionVersion compose_version + kotlinCompilerExtensionVersion compose_compiler_version } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 coreLibraryDesugaringEnabled = true } + namespace 'org.greenstand.android.TreeTracker' + lintOptions { checkReleaseBuilds false // Or, if you prefer, you can continue to check for errors in release builds, @@ -174,7 +175,7 @@ android { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" } } @@ -190,20 +191,20 @@ dependencies { // 2.0: https://docs.aws.amazon.com/sdk-for-java/v2/developer-guide/setup-project-gradle.html // android s3 sdk does not support transfer acceleration - implementation 'com.amazonaws:aws-android-sdk-core:2.16.8' - implementation 'com.amazonaws:aws-android-sdk-s3:2.16.8' + implementation 'com.amazonaws:aws-android-sdk-core:2.69.0' + implementation 'com.amazonaws:aws-android-sdk-s3:2.69.0' //koin dependencies implementation "io.insert-koin:koin-android:$koin_version" implementation "io.insert-koin:koin-androidx-compose:$koin_version" testImplementation "io.insert-koin:koin-test:$koin_version" - implementation 'androidx.appcompat:appcompat:1.3.1' + implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.multidex:multidex:2.0.1' - implementation 'androidx.exifinterface:exifinterface:1.3.2' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' - implementation "androidx.work:work-runtime-ktx:2.7.1" + implementation 'androidx.exifinterface:exifinterface:1.3.6' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' + implementation "androidx.work:work-runtime-ktx:2.8.1" // Compose implementation "androidx.compose.runtime:runtime:$compose_version" @@ -214,25 +215,27 @@ dependencies { implementation "androidx.compose.ui:ui-tooling:$compose_version" implementation "androidx.compose.material:material:$compose_version" implementation "androidx.compose.animation:animation:$compose_version" - implementation "androidx.activity:activity-compose:1.3.0" - implementation "androidx.navigation:navigation-compose:2.4.1" + implementation "androidx.activity:activity-compose:1.7.2" + implementation "androidx.navigation:navigation-compose:2.5.3" //Permissions management library for Jetpack Compose - implementation "com.google.accompanist:accompanist-permissions:0.21.1-beta" + implementation 'com.google.accompanist:accompanist-permissions:0.30.1' // Kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0' - implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2' - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1' + implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.4.0' + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") //Database - implementation 'androidx.room:room-runtime:2.4.0-beta01' - implementation 'androidx.room:room-ktx:2.4.0-beta01' - kapt "androidx.room:room-compiler:2.4.0-beta01" + implementation 'androidx.room:room-runtime:2.5.1' + implementation 'androidx.room:room-ktx:2.5.1' + kapt "androidx.room:room-compiler:2.5.1" - devImplementation 'com.amitshekhar.android:debug-db:1.0.6' + // Sqlcipher + implementation "net.zetetic:android-database-sqlcipher:4.5.4" + implementation "androidx.sqlite:sqlite-ktx:2.3.1" api "com.squareup.retrofit2:converter-gson:${retrofit2Version}" implementation "com.squareup.retrofit2:retrofit:${retrofit2Version}" @@ -254,25 +257,25 @@ dependencies { // If you to use Camera Extensions implementation "androidx.camera:camera-extensions:$camerax_ext_version" - api 'com.jakewharton.timber:timber:4.7.1' + api 'com.jakewharton.timber:timber:5.0.1' implementation "androidx.legacy:legacy-support-v4:${androidSupportVersion}" - implementation platform('com.google.firebase:firebase-bom:25.10.0') - implementation 'com.google.firebase:firebase-analytics:18.0.2' - implementation 'com.google.firebase:firebase-crashlytics-ktx:17.3.1' - implementation 'com.google.firebase:firebase-iid:21.0.1' + implementation platform('com.google.firebase:firebase-bom:32.0.0') + implementation 'com.google.firebase:firebase-analytics:21.2.2' + implementation 'com.google.firebase:firebase-crashlytics-ktx:18.3.7' + implementation 'com.google.firebase:firebase-iid:21.1.0' testImplementation 'androidx.test:core-ktx:1.5.0' - testImplementation "io.mockk:mockk:1.10.0" - testImplementation "junit:junit:4.13.1" - testImplementation "androidx.room:room-testing:2.2.6" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.9" - testImplementation "androidx.arch.core:core-testing:2.1.0" + testImplementation 'io.mockk:mockk:1.13.5' + testImplementation "junit:junit:4.13.2" + testImplementation "androidx.room:room-testing:2.5.1" + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1' + testImplementation "androidx.arch.core:core-testing:2.2.0" androidTestImplementation "com.android.support.test:runner:1.0.2" androidTestImplementation "com.android.support.test.espresso:espresso-core:3.0.2" - testImplementation "org.robolectric:robolectric:4.8.1" - androidTestImplementation "app.cash.turbine:turbine:0.12.1" - testImplementation "app.cash.turbine:turbine:0.12.1" + testImplementation 'org.robolectric:robolectric:4.10.3' + androidTestImplementation 'app.cash.turbine:turbine:0.13.0' + testImplementation 'app.cash.turbine:turbine:0.13.0' } diff --git a/app/schemas/org.greenstand.android.TreeTracker.database.AppDatabase/1.json b/app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/1.json similarity index 100% rename from app/schemas/org.greenstand.android.TreeTracker.database.AppDatabase/1.json rename to app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/1.json diff --git a/app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/10.json b/app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/10.json new file mode 100644 index 000000000..20b11b82e --- /dev/null +++ b/app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/10.json @@ -0,0 +1,992 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "68e6765fa2a009d116201719b3d3cdb4", + "entities": [ + { + "tableName": "planter_check_in", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`planter_info_id` INTEGER NOT NULL, `local_photo_path` TEXT, `photo_url` TEXT, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `created_at` INTEGER NOT NULL, `_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`planter_info_id`) REFERENCES `planter_info`(`_id`) ON UPDATE CASCADE ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "planterInfoId", + "columnName": "planter_info_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localPhotoPath", + "columnName": "local_photo_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "photoUrl", + "columnName": "photo_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_planter_check_in_planter_info_id", + "unique": false, + "columnNames": [ + "planter_info_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_planter_check_in_planter_info_id` ON `${TABLE_NAME}` (`planter_info_id`)" + }, + { + "name": "index_planter_check_in_local_photo_path", + "unique": false, + "columnNames": [ + "local_photo_path" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_planter_check_in_local_photo_path` ON `${TABLE_NAME}` (`local_photo_path`)" + } + ], + "foreignKeys": [ + { + "table": "planter_info", + "onDelete": "NO ACTION", + "onUpdate": "CASCADE", + "columns": [ + "planter_info_id" + ], + "referencedColumns": [ + "_id" + ] + } + ] + }, + { + "tableName": "planter_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`planter_identifier` TEXT NOT NULL, `first_name` TEXT NOT NULL, `last_name` TEXT NOT NULL, `organization` TEXT, `phone` TEXT, `email` TEXT, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `uploaded` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `bundle_id` TEXT, `record_uuid` TEXT NOT NULL DEFAULT '', `_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "identifier", + "columnName": "planter_identifier", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastName", + "columnName": "last_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "organization", + "columnName": "organization", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "phone", + "columnName": "phone", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bundleId", + "columnName": "bundle_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recordUuid", + "columnName": "record_uuid", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_planter_info_planter_identifier", + "unique": false, + "columnNames": [ + "planter_identifier" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_planter_info_planter_identifier` ON `${TABLE_NAME}` (`planter_identifier`)" + }, + { + "name": "index_planter_info_uploaded", + "unique": false, + "columnNames": [ + "uploaded" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_planter_info_uploaded` ON `${TABLE_NAME}` (`uploaded`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "tree_attribute", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, `tree_capture_id` INTEGER NOT NULL, `_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`tree_capture_id`) REFERENCES `tree_capture`(`_id`) ON UPDATE CASCADE ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "treeCaptureId", + "columnName": "tree_capture_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_tree_attribute_tree_capture_id", + "unique": false, + "columnNames": [ + "tree_capture_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tree_attribute_tree_capture_id` ON `${TABLE_NAME}` (`tree_capture_id`)" + } + ], + "foreignKeys": [ + { + "table": "tree_capture", + "onDelete": "NO ACTION", + "onUpdate": "CASCADE", + "columns": [ + "tree_capture_id" + ], + "referencedColumns": [ + "_id" + ] + } + ] + }, + { + "tableName": "tree_capture", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `planter_checkin_id` INTEGER NOT NULL, `local_photo_path` TEXT, `photo_url` TEXT, `note_content` TEXT NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `accuracy` REAL NOT NULL, `uploaded` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `bundle_id` TEXT, `_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`planter_checkin_id`) REFERENCES `planter_check_in`(`_id`) ON UPDATE CASCADE ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "planterCheckInId", + "columnName": "planter_checkin_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localPhotoPath", + "columnName": "local_photo_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "photoUrl", + "columnName": "photo_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "noteContent", + "columnName": "note_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "accuracy", + "columnName": "accuracy", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bundleId", + "columnName": "bundle_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_tree_capture_planter_checkin_id", + "unique": false, + "columnNames": [ + "planter_checkin_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tree_capture_planter_checkin_id` ON `${TABLE_NAME}` (`planter_checkin_id`)" + }, + { + "name": "index_tree_capture_uploaded", + "unique": false, + "columnNames": [ + "uploaded" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tree_capture_uploaded` ON `${TABLE_NAME}` (`uploaded`)" + } + ], + "foreignKeys": [ + { + "table": "planter_check_in", + "onDelete": "NO ACTION", + "onUpdate": "CASCADE", + "columns": [ + "planter_checkin_id" + ], + "referencedColumns": [ + "_id" + ] + } + ] + }, + { + "tableName": "location_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`json_value` TEXT NOT NULL, `_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uploaded` INTEGER NOT NULL, `created_at` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "locationDataJson", + "columnName": "json_value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_location_data_uploaded", + "unique": false, + "columnNames": [ + "uploaded" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_location_data_uploaded` ON `${TABLE_NAME}` (`uploaded`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "session", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `origin_user_id` TEXT NOT NULL, `origin_wallet` TEXT NOT NULL, `destination_wallet` TEXT NOT NULL, `start_time` TEXT NOT NULL, `end_time` TEXT, `organization` TEXT, `uploaded` INTEGER NOT NULL, `bundle_id` TEXT, `device_config_id` INTEGER, `note` TEXT, `_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`device_config_id`) REFERENCES `device_config`(`_id`) ON UPDATE CASCADE ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originUserId", + "columnName": "origin_user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originWallet", + "columnName": "origin_wallet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "destinationWallet", + "columnName": "destination_wallet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startTime", + "columnName": "start_time", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "endTime", + "columnName": "end_time", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "organization", + "columnName": "organization", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isUploaded", + "columnName": "uploaded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bundleId", + "columnName": "bundle_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deviceConfigId", + "columnName": "device_config_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_session_origin_wallet", + "unique": false, + "columnNames": [ + "origin_wallet" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_session_origin_wallet` ON `${TABLE_NAME}` (`origin_wallet`)" + }, + { + "name": "index_session_device_config_id", + "unique": false, + "columnNames": [ + "device_config_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_session_device_config_id` ON `${TABLE_NAME}` (`device_config_id`)" + } + ], + "foreignKeys": [ + { + "table": "device_config", + "onDelete": "NO ACTION", + "onUpdate": "CASCADE", + "columns": [ + "device_config_id" + ], + "referencedColumns": [ + "_id" + ] + } + ] + }, + { + "tableName": "user", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `wallet` TEXT NOT NULL, `first_name` TEXT NOT NULL, `last_name` TEXT NOT NULL, `phone` TEXT, `email` TEXT, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `uploaded` INTEGER NOT NULL, `created_at` TEXT NOT NULL, `bundle_id` TEXT, `photo_path` TEXT NOT NULL, `photo_url` TEXT DEFAULT NULL, `power_user` INTEGER NOT NULL DEFAULT 0, `_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wallet", + "columnName": "wallet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastName", + "columnName": "last_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phone", + "columnName": "phone", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bundleId", + "columnName": "bundle_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "photoPath", + "columnName": "photo_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "photoUrl", + "columnName": "photo_url", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "powerUser", + "columnName": "power_user", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_user_wallet", + "unique": false, + "columnNames": [ + "wallet" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_wallet` ON `${TABLE_NAME}` (`wallet`)" + }, + { + "name": "index_user_uploaded", + "unique": false, + "columnNames": [ + "uploaded" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_uploaded` ON `${TABLE_NAME}` (`uploaded`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "location", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`json_value` TEXT NOT NULL, `session_id` INTEGER NOT NULL, `_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uploaded` INTEGER NOT NULL, `create_at` INTEGER NOT NULL, FOREIGN KEY(`session_id`) REFERENCES `session`(`_id`) ON UPDATE CASCADE ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "locationDataJson", + "columnName": "json_value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "create_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_location_session_id", + "unique": false, + "columnNames": [ + "session_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_location_session_id` ON `${TABLE_NAME}` (`session_id`)" + }, + { + "name": "index_location_uploaded", + "unique": false, + "columnNames": [ + "uploaded" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_location_uploaded` ON `${TABLE_NAME}` (`uploaded`)" + } + ], + "foreignKeys": [ + { + "table": "session", + "onDelete": "NO ACTION", + "onUpdate": "CASCADE", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "_id" + ] + } + ] + }, + { + "tableName": "tree", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `session_id` INTEGER NOT NULL, `photo_path` TEXT, `photo_url` TEXT, `note` TEXT NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `uploaded` INTEGER NOT NULL, `created_at` TEXT NOT NULL, `bundle_id` TEXT DEFAULT NULL, `extra_attributes` TEXT DEFAULT NULL, `_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`session_id`) REFERENCES `session`(`_id`) ON UPDATE CASCADE ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "photoPath", + "columnName": "photo_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "photoUrl", + "columnName": "photo_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bundleId", + "columnName": "bundle_id", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "extraAttributes", + "columnName": "extra_attributes", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_tree_session_id", + "unique": false, + "columnNames": [ + "session_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tree_session_id` ON `${TABLE_NAME}` (`session_id`)" + }, + { + "name": "index_tree_uploaded", + "unique": false, + "columnNames": [ + "uploaded" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tree_uploaded` ON `${TABLE_NAME}` (`uploaded`)" + } + ], + "foreignKeys": [ + { + "table": "session", + "onDelete": "NO ACTION", + "onUpdate": "CASCADE", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "_id" + ] + } + ] + }, + { + "tableName": "device_config", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `app_version` TEXT NOT NULL, `app_build` INTEGER NOT NULL, `os_version` TEXT NOT NULL, `sdk_version` INTEGER NOT NULL, `logged_at` TEXT NOT NULL, `uploaded` INTEGER NOT NULL, `bundle_id` TEXT, `_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "appVersion", + "columnName": "app_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "appBuild", + "columnName": "app_build", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "osVersion", + "columnName": "os_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sdkVersion", + "columnName": "sdk_version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loggedAt", + "columnName": "logged_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isUploaded", + "columnName": "uploaded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bundleId", + "columnName": "bundle_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "organization", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `version` INTEGER NOT NULL, `name` TEXT NOT NULL, `wallet_id` TEXT NOT NULL, `capture_setup_flow_json` TEXT NOT NULL, `capture_flow_json` TEXT NOT NULL, PRIMARY KEY(`_id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "wallet_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "captureSetupFlowJson", + "columnName": "capture_setup_flow_json", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "captureFlowJson", + "columnName": "capture_flow_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '68e6765fa2a009d116201719b3d3cdb4')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/org.greenstand.android.TreeTracker.database.AppDatabase/2.json b/app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/2.json similarity index 100% rename from app/schemas/org.greenstand.android.TreeTracker.database.AppDatabase/2.json rename to app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/2.json diff --git a/app/schemas/org.greenstand.android.TreeTracker.database.AppDatabase/3.json b/app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/3.json similarity index 100% rename from app/schemas/org.greenstand.android.TreeTracker.database.AppDatabase/3.json rename to app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/3.json diff --git a/app/schemas/org.greenstand.android.TreeTracker.database.AppDatabase/4.json b/app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/4.json similarity index 100% rename from app/schemas/org.greenstand.android.TreeTracker.database.AppDatabase/4.json rename to app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/4.json diff --git a/app/schemas/org.greenstand.android.TreeTracker.database.AppDatabase/6.json b/app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/6.json similarity index 100% rename from app/schemas/org.greenstand.android.TreeTracker.database.AppDatabase/6.json rename to app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/6.json diff --git a/app/schemas/org.greenstand.android.TreeTracker.database.AppDatabase/7.json b/app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/7.json similarity index 100% rename from app/schemas/org.greenstand.android.TreeTracker.database.AppDatabase/7.json rename to app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/7.json diff --git a/app/schemas/org.greenstand.android.TreeTracker.database.AppDatabase/8.json b/app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/8.json similarity index 100% rename from app/schemas/org.greenstand.android.TreeTracker.database.AppDatabase/8.json rename to app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/8.json diff --git a/app/schemas/org.greenstand.android.TreeTracker.database.AppDatabase/9.json b/app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/9.json similarity index 100% rename from app/schemas/org.greenstand.android.TreeTracker.database.AppDatabase/9.json rename to app/schemas/org.greenstand.android.TreeTracker.database.app.AppDatabase/9.json diff --git a/app/schemas/org.greenstand.android.TreeTracker.models.messages.database.MessageDatabase/1.json b/app/schemas/org.greenstand.android.TreeTracker.database.messages.MessageDatabase/1.json similarity index 100% rename from app/schemas/org.greenstand.android.TreeTracker.models.messages.database.MessageDatabase/1.json rename to app/schemas/org.greenstand.android.TreeTracker.database.messages.MessageDatabase/1.json diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index da53d080d..56f0c9ebf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,11 +1,13 @@ + diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/application/TreeTrackerApplication.kt b/app/src/main/java/org/greenstand/android/TreeTracker/application/TreeTrackerApplication.kt index 557496994..d32eee4ad 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/application/TreeTrackerApplication.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/application/TreeTrackerApplication.kt @@ -22,8 +22,9 @@ import org.greenstand.android.TreeTracker.BuildConfig import org.greenstand.android.TreeTracker.analytics.ExceptionLogger import org.greenstand.android.TreeTracker.api.ObjectStorageClient import org.greenstand.android.TreeTracker.di.appModule +import org.greenstand.android.TreeTracker.di.daoModule import org.greenstand.android.TreeTracker.di.networkModule -import org.greenstand.android.TreeTracker.di.roomModule +import org.greenstand.android.TreeTracker.di.repositoryModule import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.core.context.startKoin @@ -43,8 +44,9 @@ class TreeTrackerApplication : Application() { androidContext(applicationContext) modules( appModule, - roomModule, + daoModule, networkModule, + repositoryModule ) } diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/camera/ImageReviewScreen.kt b/app/src/main/java/org/greenstand/android/TreeTracker/camera/ImageReviewScreen.kt index 75eed25a1..0d8b1e721 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/camera/ImageReviewScreen.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/camera/ImageReviewScreen.kt @@ -18,7 +18,11 @@ package org.greenstand.android.TreeTracker.camera import android.app.Activity import android.content.Intent import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -28,7 +32,8 @@ import androidx.compose.ui.unit.dp import org.greenstand.android.TreeTracker.activities.CaptureImageContract import org.greenstand.android.TreeTracker.models.NavRoute import org.greenstand.android.TreeTracker.root.LocalNavHostController -import org.greenstand.android.TreeTracker.view.* +import org.greenstand.android.TreeTracker.view.ApprovalButton +import org.greenstand.android.TreeTracker.view.LocalImage @Composable fun ImageReviewScreen(photoPath: String) { @@ -68,7 +73,9 @@ fun ImageReviewScreen(photoPath: String) { } ) { LocalImage( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .padding(it), imagePath = photoPath, contentDescription = null, contentScale = ContentScale.Fit diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/camera/SelfieScreen.kt b/app/src/main/java/org/greenstand/android/TreeTracker/camera/SelfieScreen.kt index 8298adb31..0f04136f5 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/camera/SelfieScreen.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/camera/SelfieScreen.kt @@ -72,9 +72,10 @@ fun SelfieScreen() { cameraControl = cameraControl, modifier = Modifier .fillMaxWidth() + .padding(it) .aspectRatio(1.0f), - onImageCaptured = { - navController.navigate(NavRoute.ImageReview.create(it.path)) + onImageCaptured = { file -> + navController.navigate(NavRoute.ImageReview.create(file.path)) } ) Box( diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/capture/TreeCaptureScreen.kt b/app/src/main/java/org/greenstand/android/TreeTracker/capture/TreeCaptureScreen.kt index 35303cf71..20ca53c6e 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/capture/TreeCaptureScreen.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/capture/TreeCaptureScreen.kt @@ -152,6 +152,7 @@ fun TreeCaptureScreen( } Camera( + modifier = Modifier.padding(it), isSelfieMode = false, cameraControl = cameraControl, onImageCaptured = { diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/capture/TreeCaptureViewModel.kt b/app/src/main/java/org/greenstand/android/TreeTracker/capture/TreeCaptureViewModel.kt index 2a77ffc70..f9f71a559 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/capture/TreeCaptureViewModel.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/capture/TreeCaptureViewModel.kt @@ -103,7 +103,7 @@ class TreeCaptureViewModel( class TreeCaptureViewModelFactory(private val profilePicUrl: String) : ViewModelProvider.Factory, KoinComponent { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { + override fun create(modelClass: Class): T { CaptureFlowScopeManager.open() return TreeCaptureViewModel(profilePicUrl, get(), get(), get(), get(), get(), get()) as T } diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/capture/TreeImageReviewScreen.kt b/app/src/main/java/org/greenstand/android/TreeTracker/capture/TreeImageReviewScreen.kt index 28c7ce203..c364a4cc8 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/capture/TreeImageReviewScreen.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/capture/TreeImageReviewScreen.kt @@ -115,7 +115,9 @@ fun TreeImageReviewScreen( ) } LocalImage( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .padding(it), imagePath = state.treeImagePath ?: "", contentDescription = null, contentScale = ContentScale.Fit diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/dashboard/DashboardScreen.kt b/app/src/main/java/org/greenstand/android/TreeTracker/dashboard/DashboardScreen.kt index 845a1459f..9ab771280 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/dashboard/DashboardScreen.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/dashboard/DashboardScreen.kt @@ -54,6 +54,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import org.greenstand.android.TreeTracker.R +import org.greenstand.android.TreeTracker.extension.ifNaN import org.greenstand.android.TreeTracker.models.NavRoute import org.greenstand.android.TreeTracker.root.LocalNavHostController import org.greenstand.android.TreeTracker.root.LocalViewModelFactory @@ -130,6 +131,7 @@ fun Dashboard( Column( modifier = Modifier .fillMaxSize() + .padding(it) .padding(bottom = 20.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, @@ -171,8 +173,8 @@ fun Dashboard( verticalArrangement = Arrangement.SpaceBetween, ) { DashboardUploadProgressBar( - progress = (state.treesRemainingToSync) - .toFloat() / (state.totalTreesToSync), + progress = ((state.treesRemainingToSync) + .toFloat() / (state.totalTreesToSync)).ifNaN(), modifier = Modifier.weight(1f), ) Text( diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/dashboard/DashboardViewModel.kt b/app/src/main/java/org/greenstand/android/TreeTracker/dashboard/DashboardViewModel.kt index 3267d7476..bd801a229 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/dashboard/DashboardViewModel.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/dashboard/DashboardViewModel.kt @@ -35,10 +35,10 @@ import kotlinx.coroutines.withContext import org.greenstand.android.TreeTracker.R import org.greenstand.android.TreeTracker.analytics.Analytics import org.greenstand.android.TreeTracker.background.TreeSyncWorker -import org.greenstand.android.TreeTracker.database.TreeTrackerDAO +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO +import org.greenstand.android.TreeTracker.domain.repository.MessageRepository import org.greenstand.android.TreeTracker.models.FeatureFlags import org.greenstand.android.TreeTracker.models.location.LocationDataCapturer -import org.greenstand.android.TreeTracker.models.messages.MessagesRepo import org.greenstand.android.TreeTracker.models.organization.OrgRepo import org.greenstand.android.TreeTracker.usecases.CheckForInternetUseCase import org.greenstand.android.TreeTracker.view.ConsumableSnackBar @@ -60,7 +60,7 @@ class DashboardViewModel( private val analytics: Analytics, private val treesToSyncHelper: TreesToSyncHelper, private val orgRepo: OrgRepo, - private val messagesRepo: MessagesRepo, + private val messageRepository: MessageRepository, private val checkForInternetUseCase: CheckForInternetUseCase, locationDataCapturer: LocationDataCapturer, ) : ViewModel() { @@ -121,6 +121,7 @@ class DashboardViewModel( triggerSnackBar(R.string.sync_preparing) _isSyncing = true } + else -> Unit } } @@ -134,7 +135,7 @@ class DashboardViewModel( fun syncMessages() { viewModelScope.launch { if (checkForInternetUseCase.execute(Unit)) { - messagesRepo.syncMessages() + messageRepository.syncMessages() } } } @@ -178,7 +179,7 @@ class DashboardViewModel( treesRemainingToSync = notSyncedTreeCount, treesSynced = syncedTreeCount, isOrgButtonEnabled = orgRepo.getOrgs().size > 1, - showUnreadMessageNotification = messagesRepo.checkForUnreadMessages(), + showUnreadMessageNotification = messageRepository.haveUnreadMessages(), showTreeSyncReminderDialog = totalTreesToSync >= 2000 ) } diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/dashboard/TreesToSyncHelper.kt b/app/src/main/java/org/greenstand/android/TreeTracker/dashboard/TreesToSyncHelper.kt index 7f4a387d3..f449e8946 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/dashboard/TreesToSyncHelper.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/dashboard/TreesToSyncHelper.kt @@ -15,7 +15,7 @@ */ package org.greenstand.android.TreeTracker.dashboard -import org.greenstand.android.TreeTracker.database.TreeTrackerDAO +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO import org.greenstand.android.TreeTracker.preferences.PrefKey import org.greenstand.android.TreeTracker.preferences.PrefKeys import org.greenstand.android.TreeTracker.preferences.Preferences diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/data/repository/CommonRepositoryImpl.kt b/app/src/main/java/org/greenstand/android/TreeTracker/data/repository/CommonRepositoryImpl.kt new file mode 100644 index 000000000..22a6b85c5 --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/data/repository/CommonRepositoryImpl.kt @@ -0,0 +1,36 @@ +package org.greenstand.android.TreeTracker.data.repository + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import org.greenstand.android.TreeTracker.database.common.CommonDao +import org.greenstand.android.TreeTracker.database.common.RoomModel +import org.greenstand.android.TreeTracker.domain.repository.CommonRepository + +abstract class CommonRepositoryImpl>( + private val dao: D, + ioDispatcher: CoroutineDispatcher = Dispatchers.IO +) : Repository(ioDispatcher), CommonRepository { + + override suspend fun insertItem(item: T) = withIO { dao.insertItem(item) } + + override suspend fun insertItems(items: List) = withIO { dao.insertItems(items) } + + override suspend fun replaceItem(item: T) = withIO { dao.replaceItem(item) } + + override suspend fun replaceItems(items: List) = withIO { dao.replaceItems(items) } + + override suspend fun updateItem(item: T) = withIO { dao.updateItem(item) } + + override suspend fun updateItems(items: List) = withIO { dao.updateItems(items) } + + override suspend fun upsertItem(item: T): Long = withIO { dao.upsertItem(item) } + + override suspend fun upsertItems(items: List) = withIO { dao.upsertItems(items) } + + override suspend fun deleteItem(item: T) = withIO { dao.deleteItem(item) } + + override suspend fun deleteItems(items: List) = withIO { dao.deleteItems(items) } + + override suspend fun deleteAll() = withIO { dao.deleteAll() } + +} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/data/repository/MessageRepositoryImpl.kt b/app/src/main/java/org/greenstand/android/TreeTracker/data/repository/MessageRepositoryImpl.kt new file mode 100644 index 000000000..f41bc5b5d --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/data/repository/MessageRepositoryImpl.kt @@ -0,0 +1,322 @@ +package org.greenstand.android.TreeTracker.data.repository + +import com.google.gson.Gson +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.datetime.Instant +import org.greenstand.android.TreeTracker.api.ObjectStorageClient +import org.greenstand.android.TreeTracker.api.models.requests.UploadBundle +import org.greenstand.android.TreeTracker.database.messages.DatabaseConverters +import org.greenstand.android.TreeTracker.database.messages.dao.MessageDao +import org.greenstand.android.TreeTracker.database.messages.entity.MessageEntity +import org.greenstand.android.TreeTracker.database.messages.entity.QuestionEntity +import org.greenstand.android.TreeTracker.database.messages.entity.SurveyEntity +import org.greenstand.android.TreeTracker.database.messages.entity.tuple.MessageAsUploadedTuple +import org.greenstand.android.TreeTracker.database.messages.entity.tuple.MessageBundleIdTuple +import org.greenstand.android.TreeTracker.database.messages.entity.tuple.ReadMessageTuple +import org.greenstand.android.TreeTracker.database.messages.entity.tuple.SurveyMessageCompleteTuple +import org.greenstand.android.TreeTracker.domain.repository.MessageRepository +import org.greenstand.android.TreeTracker.domain.repository.QuestionRepository +import org.greenstand.android.TreeTracker.domain.repository.SurveyRepository +import org.greenstand.android.TreeTracker.models.UserRepo +import org.greenstand.android.TreeTracker.models.messages.AnnouncementMessage +import org.greenstand.android.TreeTracker.models.messages.DirectMessage +import org.greenstand.android.TreeTracker.models.messages.Message +import org.greenstand.android.TreeTracker.models.messages.SurveyMessage +import org.greenstand.android.TreeTracker.models.messages.network.MessagesApiService +import org.greenstand.android.TreeTracker.models.messages.network.responses.MessageResponse +import org.greenstand.android.TreeTracker.models.messages.network.responses.MessageType +import org.greenstand.android.TreeTracker.models.messages.network.responses.MessagesResponse +import org.greenstand.android.TreeTracker.models.messages.network.responses.QueryResponse +import org.greenstand.android.TreeTracker.utilities.Constants +import org.greenstand.android.TreeTracker.utilities.TimeProvider +import org.greenstand.android.TreeTracker.utilities.md5 +import org.greenstand.android.TreeTracker.utils.runInParallel +import timber.log.Timber + +/** + * For test data, use 'handle2@test', 'handle3@test', or 'handle4@test' as a wallet (email) + */ + +class MessageRepositoryImpl( + private val messageDao: MessageDao, + private val surveyRepository: SurveyRepository, + private val questionRepository: QuestionRepository, + private val timeProvider: TimeProvider, + private val objectStorageClient: ObjectStorageClient, + private val apiService: MessagesApiService, + private val userRepo: UserRepo, + private val gson: Gson, + ioDispatcher: CoroutineDispatcher = Dispatchers.IO +) : CommonRepositoryImpl(messageDao, ioDispatcher), MessageRepository { + + companion object { + private const val LIMIT = 50 + } + + override suspend fun markMessageAsRead(messageId: String) = withIO { + markMessagesAsRead(listOf(messageId)) + } + + override suspend fun markMessagesAsRead(messageIds: List) = withIO { + messageDao.markMessagesAsRead(messageIds.map { ReadMessageTuple(id = it, isRead = true) }) + } + + override suspend fun saveMessage(wallet: String, to: String, body: String): Long = withIO { + messageDao.insertItem( + MessageEntity( + wallet = wallet, + type = MessageType.MESSAGE, + from = wallet, + to = to, + body = body, + composedAt = timeProvider.currentTime().toString(), + isRead = true + ) + ) + } + + override suspend fun saveSurveyAnswers( + messageId: String, + surveyResponse: List + ): Unit = withIO { + messageDao.getMessage(messageId)?.let { + // make messages point to surveys to have survey and response point to same survey + messageDao.insertItem( + MessageEntity( + wallet = it.to, + type = MessageType.SURVEY_RESPONSE, + from = it.to, + to = it.from, + composedAt = timeProvider.currentTime().toString(), + surveyResponse = surveyResponse, + isRead = true, + surveyId = it.surveyId, + isSurveyComplete = true + ) + ) + messageDao.markSurveyMessageComplete( + SurveyMessageCompleteTuple(id = it.id, isSurveyComplete = true) + ) + } + } + + override fun getMessageFlow(wallet: String): Flow> { + return messageDao.getMessagesForWalletFlow(wallet) + .map { messages -> + messages.map { DatabaseConverters.createMessageFromEntities(it) } + }.flowOn(Dispatchers.IO) + } + + override fun getDirectMessages( + wallet: String, + otherChatIdentifier: String + ): Flow> { + return messageDao.getDirectMessagesForWallet(wallet) + .map { messages -> + messages + .map { DatabaseConverters.createMessageFromEntities(it) } + .filterIsInstance() + .filter { (it.from == wallet || it.from == otherChatIdentifier) && (it.to == otherChatIdentifier || it.to == wallet) } + .sortedByDescending { it.composedAt } + }.flowOn(Dispatchers.IO) + } + + override suspend fun getAnnouncementMessages(id: String): AnnouncementMessage? = withIO { + messageDao.getMessageForWallet(id)?.let { + with(DatabaseConverters.createMessageFromEntities(it)) { + if (this is AnnouncementMessage) this else null + } + } + } + + override suspend fun getSurveyMessage(id: String): SurveyMessage? = withIO { + messageDao.getMessageForWallet(id)?.let { + with(DatabaseConverters.createMessageFromEntities(it)) { + if (this is SurveyMessage) this else null + } + } + } + + override suspend fun haveUnreadMessages(): Boolean = withIO { + messageDao.haveUnreadMessages() + } + + override suspend fun haveUnreadMessageCountForWallet(wallet: String): Boolean = withIO { + messageDao.haveUnreadMessageCountForWallet(wallet) + } + + override suspend fun getLatestSyncTimeForWallet(wallet: String): String = withIO { + messageDao.getLatestSyncTimeForWallet(wallet) + ?: Instant.fromEpochMilliseconds(0).toString() + } + + override suspend fun getMessagesByIds(ids: List): List = withIO { + messageDao.getMessagesByIds(ids) + } + + override suspend fun getMessageIdsToUpload(): List = withIO { + messageDao.getMessageIdsToUpload() + } + + override suspend fun updateMessageBundleIds(ids: List, bundleId: String) = withIO { + messageDao.updateMessageBundleIds(ids.map { + MessageBundleIdTuple( + id = it, + bundleId = bundleId + ) + }) + } + + override suspend fun markMessagesAsUploaded(ids: List) = withIO { + messageDao.markMessagesAsUploaded(ids.map { + MessageAsUploadedTuple( + id = it, + shouldUpload = false + ) + }) + } + + /** + * When uploading trees, messages will be synced locally by this method + */ + override suspend fun syncMessages() = withIO { + for (wallet in userRepo.getUserList().map { it.wallet }) { + try { + ensureActive() + fetchMessagesForWallet(wallet) + } catch (e: CancellationException) { + // rethrow cancellation exception + throw e + } catch (e: Exception) { + if (e.localizedMessage == Constants.LOCAL_MSG_ERROR_HTTP404) { + // 404 indicates the user has never had messages before + continue + } else { + Timber.e(e) + } + } + } + uploadMessages() + } + + private suspend fun fetchMessagesForWallet(wallet: String) = withIO { + val lastSyncTime = getLatestSyncTimeForWallet(wallet) + val query = QueryResponse( + handle = wallet, + limit = 100, + offset = 0, + total = -1, + ) + val result = + fetchMessagesFromServerAndSaveInDb(query.offset, query.limit, wallet, lastSyncTime) + if (result.query.total == 0) return@withIO + var offset = result.query.offset + val limit = result.query.limit + val total = result.query.total + val asyncExecutions = mutableListOf>() + while (total >= limit + offset) { + ensureActive() + offset += limit + // store this offset value in-case it gets changed in the next iteration. + val _offset = offset + asyncExecutions += async { + fetchMessagesFromServerAndSaveInDb( + _offset, + limit, + wallet, + lastSyncTime + ) // pass _offset instead of offset. + } + } + asyncExecutions.awaitAll() + } + + private suspend fun fetchMessagesFromServerAndSaveInDb( + offset: Int, + limit: Int, + wallet: String, + lastSyncTime: String + ): MessagesResponse = withIO { + val result = apiService.getMessages( + wallet = wallet, + lastSyncTime = lastSyncTime, + offset = offset, + limit = limit, + ) + if (result.query.total == 0) return@withIO result + result.messages.runInParallel { + saveMessageResponse(wallet, it.copy()) + } + return@withIO result + } + + private suspend fun saveMessageResponse(wallet: String, message: MessageResponse): Unit = + withIO { + if (message.type == MessageType.SURVEY_RESPONSE) return@withIO + val messageEntity = with(message) { + MessageEntity( + id = id, + wallet = wallet, + type = type, + from = from, + to = to, + subject = subject, + body = body, + composedAt = composedAt, + parentMessageId = parentMessageId, + videoLink = videoLink, + shouldUpload = false, + surveyId = survey?.id, + isSurveyComplete = survey?.let { false }, + ) + } + insertItem(messageEntity) + // If there is no survey, don't continue on + message.survey?.let { response -> + // If survey exists, we'll reuse response + if (surveyRepository.getSurvey(response.id) != null) { + return@withIO + } + val surveyEntity = SurveyEntity(id = response.id, title = response.title) + surveyRepository.insertItem(surveyEntity) + questionRepository.insertItems(response.questions.map { question -> + QuestionEntity( + surveyId = message.survey.id, + prompt = question.prompt, + choices = question.choices, + ) + }) + } + } + + override suspend fun uploadMessages(): Unit = withIO { + getMessageIdsToUpload() + .windowed(LIMIT, LIMIT, true) + .map { async { uploadMessageBundle(it) } } + .awaitAll() + } + + private suspend fun uploadMessageBundle(messageIdsToUpload: List) { + val messageEntitiesToUpload = getMessagesByIds(messageIdsToUpload) + val messageRequests = messageEntitiesToUpload.map { + DatabaseConverters.createMessageRequestFromEntities(it) + } + val jsonBundle = gson.toJson(UploadBundle.createV2(messages = messageRequests)) + val bundleId = jsonBundle.md5() + "_messages" + val messageIds = messageEntitiesToUpload.map { it.id } + // Update the trees in DB with the bundleId + updateMessageBundleIds(messageIds, bundleId) + objectStorageClient.uploadBundle(jsonBundle, bundleId) + markMessagesAsUploaded(messageIds) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/data/repository/QuestionRepositoryImpl.kt b/app/src/main/java/org/greenstand/android/TreeTracker/data/repository/QuestionRepositoryImpl.kt new file mode 100644 index 000000000..84ce46166 --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/data/repository/QuestionRepositoryImpl.kt @@ -0,0 +1,16 @@ +package org.greenstand.android.TreeTracker.data.repository + +import org.greenstand.android.TreeTracker.database.messages.dao.QuestionDao +import org.greenstand.android.TreeTracker.database.messages.entity.QuestionEntity +import org.greenstand.android.TreeTracker.domain.repository.QuestionRepository +import org.greenstand.android.TreeTracker.extension.withIO + +class QuestionRepositoryImpl( + private val questionDao: QuestionDao +) : CommonRepositoryImpl(questionDao), QuestionRepository { + + override suspend fun getQuestionsForSurvey(surveyId: String): List = withIO { + questionDao.getQuestionsForSurvey(surveyId) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/data/repository/Repository.kt b/app/src/main/java/org/greenstand/android/TreeTracker/data/repository/Repository.kt new file mode 100644 index 000000000..4ff83f65e --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/data/repository/Repository.kt @@ -0,0 +1,15 @@ +package org.greenstand.android.TreeTracker.data.repository + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +abstract class Repository( + private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO +) { + + protected suspend fun withIO(block: suspend CoroutineScope.() -> T) = + withContext(ioDispatcher, block) + +} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/data/repository/SurveyRepositoryImpl.kt b/app/src/main/java/org/greenstand/android/TreeTracker/data/repository/SurveyRepositoryImpl.kt new file mode 100644 index 000000000..302b99e18 --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/data/repository/SurveyRepositoryImpl.kt @@ -0,0 +1,16 @@ +package org.greenstand.android.TreeTracker.data.repository + +import org.greenstand.android.TreeTracker.database.messages.dao.SurveyDao +import org.greenstand.android.TreeTracker.database.messages.entity.SurveyEntity +import org.greenstand.android.TreeTracker.domain.repository.SurveyRepository +import org.greenstand.android.TreeTracker.extension.withIO + +class SurveyRepositoryImpl( + private val surveyDao: SurveyDao +) : CommonRepositoryImpl(surveyDao), SurveyRepository { + + override suspend fun getSurvey(id: String?): SurveyEntity? = withIO { + surveyDao.getSurvey(id) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/AppDatabase.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/AppDatabase.kt deleted file mode 100644 index 9019366f4..000000000 --- a/app/src/main/java/org/greenstand/android/TreeTracker/database/AppDatabase.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2023 Treetracker - * - * 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. - */ -package org.greenstand.android.TreeTracker.database - -import android.content.Context -import androidx.room.AutoMigration -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import org.greenstand.android.TreeTracker.database.entity.DeviceConfigEntity -import org.greenstand.android.TreeTracker.database.entity.LocationEntity -import org.greenstand.android.TreeTracker.database.entity.OrganizationEntity -import org.greenstand.android.TreeTracker.database.entity.SessionEntity -import org.greenstand.android.TreeTracker.database.entity.TreeEntity -import org.greenstand.android.TreeTracker.database.entity.UserEntity -import org.greenstand.android.TreeTracker.database.legacy.entity.LocationDataEntity -import org.greenstand.android.TreeTracker.database.legacy.entity.PlanterCheckInEntity -import org.greenstand.android.TreeTracker.database.legacy.entity.PlanterInfoEntity -import org.greenstand.android.TreeTracker.database.legacy.entity.TreeAttributeEntity -import org.greenstand.android.TreeTracker.database.legacy.entity.TreeCaptureEntity - -@Database( - version = 9, - exportSchema = true, - entities = [ - PlanterCheckInEntity::class, - PlanterInfoEntity::class, - TreeAttributeEntity::class, - TreeCaptureEntity::class, - LocationDataEntity::class, - SessionEntity::class, - UserEntity::class, - LocationEntity::class, - TreeEntity::class, - DeviceConfigEntity::class, - OrganizationEntity::class, - ], - autoMigrations = [ - // 8 -> 9 for v2.2 - AutoMigration(from = 8, to = 9) - ], -) -@TypeConverters(Converters::class) -abstract class AppDatabase : RoomDatabase() { - - abstract fun treeTrackerDao(): TreeTrackerDAO - - companion object { - - private var INSTANCE: AppDatabase? = null - - fun getInstance(context: Context): AppDatabase { - if (INSTANCE == null) { - synchronized(AppDatabase::class) { - INSTANCE = Room.databaseBuilder( - context.applicationContext, - AppDatabase::class.java, - DB_NAME - ) - .addMigrations( - MIGRATION_3_4, - MIGRATION_4_5, - MIGRATION_5_6, - MIGRATION_6_7, - ) - .build() - } - } - return INSTANCE!! - } - - private const val DB_NAME = "treetracker.v2.db" - } -} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/app/AppDatabase.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/AppDatabase.kt new file mode 100644 index 000000000..1741b51b4 --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/AppDatabase.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2023 Treetracker + * + * 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. + */ +package org.greenstand.android.TreeTracker.database.app + +import androidx.room.AutoMigration +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import org.greenstand.android.TreeTracker.database.app.entity.DeviceConfigEntity +import org.greenstand.android.TreeTracker.database.app.entity.LocationEntity +import org.greenstand.android.TreeTracker.database.app.entity.OrganizationEntity +import org.greenstand.android.TreeTracker.database.app.entity.SessionEntity +import org.greenstand.android.TreeTracker.database.app.entity.TreeEntity +import org.greenstand.android.TreeTracker.database.app.entity.UserEntity +import org.greenstand.android.TreeTracker.database.app.legacy.entity.LocationDataEntity +import org.greenstand.android.TreeTracker.database.app.legacy.entity.PlanterCheckInEntity +import org.greenstand.android.TreeTracker.database.app.legacy.entity.PlanterInfoEntity +import org.greenstand.android.TreeTracker.database.app.legacy.entity.TreeAttributeEntity +import org.greenstand.android.TreeTracker.database.app.legacy.entity.TreeCaptureEntity + +@Database( + version = 10, + exportSchema = true, + entities = [ + PlanterCheckInEntity::class, + PlanterInfoEntity::class, + TreeAttributeEntity::class, + TreeCaptureEntity::class, + LocationDataEntity::class, + SessionEntity::class, + UserEntity::class, + LocationEntity::class, + TreeEntity::class, + DeviceConfigEntity::class, + OrganizationEntity::class, + ], + autoMigrations = [ + // 8 -> 9 for v2.2 + AutoMigration(from = 8, to = 9), + AutoMigration(from = 9, to = 10) + ], +) +@TypeConverters(Converters::class) +abstract class AppDatabase : RoomDatabase() { + + abstract fun treeTrackerDao(): TreeTrackerDAO + + companion object { + + const val DB_NAME = "treetracker.v2.db" + const val DB_NAME_ENCRYPT = "treetracker.v2-encrypt.db" + + } +} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/Converters.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/Converters.kt similarity index 94% rename from app/src/main/java/org/greenstand/android/TreeTracker/database/Converters.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/app/Converters.kt index 54312422e..131ffd95c 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/database/Converters.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/Converters.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.database +package org.greenstand.android.TreeTracker.database.app import androidx.room.TypeConverter import com.google.gson.Gson @@ -38,7 +38,7 @@ object Converters { @TypeConverter fun instantToString(instant: Instant?): String? { - return instant?.let { it.toString() } + return instant?.toString() } @TypeConverter diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/Migrations.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/Migrations.kt similarity index 98% rename from app/src/main/java/org/greenstand/android/TreeTracker/database/Migrations.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/app/Migrations.kt index 317e351df..9e00a381c 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/database/Migrations.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/Migrations.kt @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.database +package org.greenstand.android.TreeTracker.database.app import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -import org.greenstand.android.TreeTracker.database.legacy.entity.PlanterInfoEntity +import org.greenstand.android.TreeTracker.database.app.legacy.entity.PlanterInfoEntity val MIGRATION_6_7 = object : Migration(6, 7) { diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/TreeTrackerDAO.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/TreeTrackerDAO.kt similarity index 91% rename from app/src/main/java/org/greenstand/android/TreeTracker/database/TreeTrackerDAO.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/app/TreeTrackerDAO.kt index b69865f2f..70219c98a 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/database/TreeTrackerDAO.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/TreeTrackerDAO.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.database +package org.greenstand.android.TreeTracker.database.app import androidx.room.Dao import androidx.room.Delete @@ -23,17 +23,17 @@ import androidx.room.Query import androidx.room.Transaction import androidx.room.Update import kotlinx.coroutines.flow.Flow -import org.greenstand.android.TreeTracker.database.entity.DeviceConfigEntity -import org.greenstand.android.TreeTracker.database.entity.LocationEntity -import org.greenstand.android.TreeTracker.database.entity.OrganizationEntity -import org.greenstand.android.TreeTracker.database.entity.SessionEntity -import org.greenstand.android.TreeTracker.database.entity.TreeEntity -import org.greenstand.android.TreeTracker.database.entity.UserEntity -import org.greenstand.android.TreeTracker.database.legacy.entity.PlanterCheckInEntity -import org.greenstand.android.TreeTracker.database.legacy.entity.PlanterInfoEntity -import org.greenstand.android.TreeTracker.database.legacy.entity.TreeAttributeEntity -import org.greenstand.android.TreeTracker.database.legacy.entity.TreeCaptureEntity -import org.greenstand.android.TreeTracker.database.legacy.views.TreeMapMarkerDbView +import org.greenstand.android.TreeTracker.database.app.entity.DeviceConfigEntity +import org.greenstand.android.TreeTracker.database.app.entity.LocationEntity +import org.greenstand.android.TreeTracker.database.app.entity.OrganizationEntity +import org.greenstand.android.TreeTracker.database.app.entity.SessionEntity +import org.greenstand.android.TreeTracker.database.app.entity.TreeEntity +import org.greenstand.android.TreeTracker.database.app.entity.UserEntity +import org.greenstand.android.TreeTracker.database.app.legacy.entity.PlanterCheckInEntity +import org.greenstand.android.TreeTracker.database.app.legacy.entity.PlanterInfoEntity +import org.greenstand.android.TreeTracker.database.app.legacy.entity.TreeAttributeEntity +import org.greenstand.android.TreeTracker.database.app.legacy.entity.TreeCaptureEntity +import org.greenstand.android.TreeTracker.database.app.legacy.views.TreeMapMarkerDbView @Dao interface TreeTrackerDAO { @@ -219,8 +219,7 @@ interface TreeTrackerDAO { ): Long { val treeId = insertTreeCapture(tree) attributes?.forEach { - it.treeCaptureId = treeId - insertTreeAttribute(it) + insertTreeAttribute(it.copy(treeCaptureId = treeId)) } return treeId } diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/entity/DeviceConfigEntity.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/entity/DeviceConfigEntity.kt similarity index 78% rename from app/src/main/java/org/greenstand/android/TreeTracker/database/entity/DeviceConfigEntity.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/app/entity/DeviceConfigEntity.kt index ab2d9c7e3..885032820 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/database/entity/DeviceConfigEntity.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/entity/DeviceConfigEntity.kt @@ -13,33 +13,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.database.entity +package org.greenstand.android.TreeTracker.database.app.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import kotlinx.datetime.Instant +import org.greenstand.android.TreeTracker.database.common.RoomModel @Entity(tableName = "device_config") data class DeviceConfigEntity( @ColumnInfo(name = "uuid") - val uuid: String, + val uuid: String = "", @ColumnInfo(name = "app_version") - val appVersion: String, + val appVersion: String = "", @ColumnInfo(name = "app_build") - val appBuild: Int, + val appBuild: Int = 0, @ColumnInfo(name = "os_version") - val osVersion: String, + val osVersion: String = "", @ColumnInfo(name = "sdk_version") - val sdkVersion: Int, + val sdkVersion: Int = 0, @ColumnInfo(name = "logged_at") - val loggedAt: Instant, + val loggedAt: Instant = Instant.DISTANT_FUTURE, @ColumnInfo(name = "uploaded") val isUploaded: Boolean = false, @ColumnInfo(name = "bundle_id") val bundleId: String? = null, -) { @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "_id") var id: Long = 0 -} \ No newline at end of file +) : RoomModel \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/entity/LocationEntity.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/entity/LocationEntity.kt similarity index 79% rename from app/src/main/java/org/greenstand/android/TreeTracker/database/entity/LocationEntity.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/app/entity/LocationEntity.kt index 86e39955b..c4172ec2c 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/database/entity/LocationEntity.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/entity/LocationEntity.kt @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.database.entity +package org.greenstand.android.TreeTracker.database.app.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey +import org.greenstand.android.TreeTracker.database.common.RoomModel @Entity( tableName = "location", @@ -33,15 +34,14 @@ import androidx.room.PrimaryKey ) data class LocationEntity( @ColumnInfo(name = "json_value") - var locationDataJson: String, + val locationDataJson: String, @ColumnInfo(name = "session_id", index = true) - var sessionId: Long, -) { + val sessionId: Long, @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "_id") - var id: Long = 0 + var id: Long = 0, @ColumnInfo(name = "uploaded", index = true) - var uploaded: Boolean = false + val uploaded: Boolean = false, @ColumnInfo(name = "create_at") - var createdAt: Long = System.currentTimeMillis() -} \ No newline at end of file + val createdAt: Long = System.currentTimeMillis() +) : RoomModel \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/entity/OrganizationEntity.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/entity/OrganizationEntity.kt similarity index 74% rename from app/src/main/java/org/greenstand/android/TreeTracker/database/entity/OrganizationEntity.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/app/entity/OrganizationEntity.kt index 3218116b4..440e0cca1 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/database/entity/OrganizationEntity.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/entity/OrganizationEntity.kt @@ -13,25 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.database.entity +package org.greenstand.android.TreeTracker.database.app.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import org.greenstand.android.TreeTracker.database.common.RoomModel @Entity(tableName = "organization") class OrganizationEntity( @PrimaryKey @ColumnInfo(name = "_id") - var id: String, + val id: String = "", @ColumnInfo(name = "version") - var version: Int, + val version: Int = 0, @ColumnInfo(name = "name") - var name: String, + val name: String = "", @ColumnInfo(name = "wallet_id") - var walletId: String, + val walletId: String = "", @ColumnInfo(name = "capture_setup_flow_json") - val captureSetupFlowJson: String, + val captureSetupFlowJson: String = "", @ColumnInfo(name = "capture_flow_json") - val captureFlowJson: String, -) \ No newline at end of file + val captureFlowJson: String = "", +) : RoomModel \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/entity/SessionEntity.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/entity/SessionEntity.kt similarity index 71% rename from app/src/main/java/org/greenstand/android/TreeTracker/database/entity/SessionEntity.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/app/entity/SessionEntity.kt index 0d3f3479d..2b8d7bf12 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/database/entity/SessionEntity.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/entity/SessionEntity.kt @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.database.entity +package org.greenstand.android.TreeTracker.database.app.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey import kotlinx.datetime.Instant +import org.greenstand.android.TreeTracker.database.common.RoomModel @Entity( tableName = "session", @@ -34,29 +35,28 @@ import kotlinx.datetime.Instant ) data class SessionEntity( @ColumnInfo(name = "uuid") - var uuid: String, + val uuid: String = "", @ColumnInfo(name = "origin_user_id") - var originUserId: String, + val originUserId: String = "", @ColumnInfo(name = "origin_wallet", index = true) - var originWallet: String, + val originWallet: String = "", @ColumnInfo(name = "destination_wallet") - var destinationWallet: String, + val destinationWallet: String = "", @ColumnInfo(name = "start_time") - var startTime: Instant, + val startTime: Instant = Instant.DISTANT_FUTURE, @ColumnInfo(name = "end_time") - var endTime: Instant? = null, + val endTime: Instant? = null, @ColumnInfo(name = "organization") - var organization: String?, + val organization: String? = "", @ColumnInfo(name = "uploaded") - var isUploaded: Boolean, + val isUploaded: Boolean = false, @ColumnInfo(name = "bundle_id") - var bundleId: String? = null, - @ColumnInfo(name = "device_config_id") - var deviceConfigId: Long? = null, + val bundleId: String? = null, + @ColumnInfo(name = "device_config_id", index = true) + val deviceConfigId: Long? = null, @ColumnInfo(name = "note") - var note: String? = null, -) { + val note: String? = null, @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "_id") var id: Long = 0 -} \ No newline at end of file +) : RoomModel \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/entity/TreeEntity.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/entity/TreeEntity.kt similarity index 74% rename from app/src/main/java/org/greenstand/android/TreeTracker/database/entity/TreeEntity.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/app/entity/TreeEntity.kt index 73f062417..5245507e5 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/database/entity/TreeEntity.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/entity/TreeEntity.kt @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.database.entity +package org.greenstand.android.TreeTracker.database.app.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey import kotlinx.datetime.Instant +import org.greenstand.android.TreeTracker.database.common.RoomModel @Entity( tableName = "tree", @@ -34,29 +35,28 @@ import kotlinx.datetime.Instant ) data class TreeEntity( @ColumnInfo(name = "uuid") - var uuid: String, + val uuid: String = "", @ColumnInfo(name = "session_id", index = true) - var sessionId: Long, + val sessionId: Long = 0, @ColumnInfo(name = "photo_path") - var photoPath: String?, + val photoPath: String? = null, @ColumnInfo(name = "photo_url") - var photoUrl: String?, + val photoUrl: String? = null, @ColumnInfo(name = "note") - var note: String, + val note: String = "", @ColumnInfo(name = "latitude") - var latitude: Double, + val latitude: Double = 0.0, @ColumnInfo(name = "longitude") - var longitude: Double, + val longitude: Double = 0.0, @ColumnInfo(name = "uploaded", index = true) - var uploaded: Boolean = false, + val uploaded: Boolean = false, @ColumnInfo(name = "created_at") - var createdAt: Instant, + val createdAt: Instant = Instant.DISTANT_FUTURE, @ColumnInfo(name = "bundle_id", defaultValue = "NULL") - var bundleId: String? = null, + val bundleId: String? = null, @ColumnInfo(name = "extra_attributes", defaultValue = "NULL") - var extraAttributes: Map? = null, -) { + val extraAttributes: Map? = null, @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "_id") var id: Long = 0 -} \ No newline at end of file +) : RoomModel \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/entity/UserEntity.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/entity/UserEntity.kt similarity index 67% rename from app/src/main/java/org/greenstand/android/TreeTracker/database/entity/UserEntity.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/app/entity/UserEntity.kt index 6b41af2f5..5481d487d 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/database/entity/UserEntity.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/entity/UserEntity.kt @@ -13,49 +13,45 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.database.entity +package org.greenstand.android.TreeTracker.database.app.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import kotlinx.datetime.Instant +import org.greenstand.android.TreeTracker.database.common.RoomModel @Entity(tableName = "user") data class UserEntity( @ColumnInfo(name = "uuid") - var uuid: String, + val uuid: String = "", @ColumnInfo(name = "wallet", index = true) - var wallet: String, + val wallet: String = "", @ColumnInfo(name = "first_name") - var firstName: String, + val firstName: String = "", @ColumnInfo(name = "last_name") - var lastName: String, + val lastName: String = "", @ColumnInfo(name = "phone") - var phone: String?, + val phone: String? = null, @ColumnInfo(name = "email") - var email: String?, + val email: String? = null, @ColumnInfo(name = "latitude") - var latitude: Double, + val latitude: Double = 0.0, @ColumnInfo(name = "longitude") - var longitude: Double, + val longitude: Double = 0.0, @ColumnInfo(name = "uploaded", index = true) - var uploaded: Boolean = false, + val uploaded: Boolean = false, @ColumnInfo(name = "created_at") - var createdAt: Instant, + val createdAt: Instant = Instant.DISTANT_FUTURE, @ColumnInfo(name = "bundle_id") - var bundleId: String? = null, + val bundleId: String? = null, @ColumnInfo(name = "photo_path") - var photoPath: String, + val photoPath: String = "", @ColumnInfo(name = "photo_url", defaultValue = "NULL") - var photoUrl: String?, - @ColumnInfo( - name = - "power_user", - defaultValue = "0" - ) - var powerUser: Boolean, -) { + val photoUrl: String? = null, + @ColumnInfo(name = "power_user", defaultValue = "0") + val powerUser: Boolean = false, @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "_id") var id: Long = 0 -} \ No newline at end of file +) : RoomModel \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/entity/LocationDataEntity.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/entity/LocationDataEntity.kt similarity index 77% rename from app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/entity/LocationDataEntity.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/entity/LocationDataEntity.kt index a08dca8ae..7bc5a33c8 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/entity/LocationDataEntity.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/entity/LocationDataEntity.kt @@ -13,31 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.database.legacy.entity +package org.greenstand.android.TreeTracker.database.app.legacy.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import org.greenstand.android.TreeTracker.database.common.RoomModel -@Entity( - tableName = LocationDataEntity.TABLE -) +@Entity(tableName = LocationDataEntity.TABLE) data class LocationDataEntity( @ColumnInfo(name = JSON_VALUE) - var locationDataJson: String -) { + val locationDataJson: String = "", @PrimaryKey(autoGenerate = true) @ColumnInfo(name = ID) - var id: Long = 0 + var id: Long = 0, @ColumnInfo(name = UPLOADED, index = true) - var uploaded: Boolean = false + val uploaded: Boolean = false, @ColumnInfo(name = CREATED_AT) - var createdAt: Long = System.currentTimeMillis() + val createdAt: Long = System.currentTimeMillis() +) : RoomModel { companion object { const val TABLE = "location_data" const val ID = "_id" + // Json string of LocationData defined in LocationUpdateManager.kt const val JSON_VALUE = "json_value" const val UPLOADED = "uploaded" diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/entity/PlanterCheckInEntity.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/entity/PlanterCheckInEntity.kt similarity index 83% rename from app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/entity/PlanterCheckInEntity.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/entity/PlanterCheckInEntity.kt index 892aeff27..4c58b1919 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/entity/PlanterCheckInEntity.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/entity/PlanterCheckInEntity.kt @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.database.legacy.entity +package org.greenstand.android.TreeTracker.database.app.legacy.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey +import org.greenstand.android.TreeTracker.database.common.RoomModel @Entity( tableName = PlanterCheckInEntity.TABLE, @@ -33,22 +34,21 @@ import androidx.room.PrimaryKey ) data class PlanterCheckInEntity( @ColumnInfo(name = PLANTER_INFO_ID, index = true) - var planterInfoId: Long, + val planterInfoId: Long = 0, @ColumnInfo(name = LOCAL_PHOTO_PATH, index = true) - var localPhotoPath: String?, + val localPhotoPath: String? = null, @ColumnInfo(name = PHOTO_URL) - var photoUrl: String?, + val photoUrl: String? = null, @ColumnInfo(name = LATITUDE) - var latitude: Double, + val latitude: Double = 0.0, @ColumnInfo(name = LONGITUDE) - var longitude: Double, + val longitude: Double = 0.0, @ColumnInfo(name = CREATED_AT) - var createdAt: Long -) { - + val createdAt: Long = 0, @PrimaryKey(autoGenerate = true) @ColumnInfo(name = ID) var id: Long = 0 +) : RoomModel { companion object { const val TABLE = "planter_check_in" diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/entity/PlanterInfoEntity.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/entity/PlanterInfoEntity.kt similarity index 77% rename from app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/entity/PlanterInfoEntity.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/entity/PlanterInfoEntity.kt index 2e94d85df..efb5bae80 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/entity/PlanterInfoEntity.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/entity/PlanterInfoEntity.kt @@ -13,43 +13,43 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.database.legacy.entity +package org.greenstand.android.TreeTracker.database.app.legacy.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import org.greenstand.android.TreeTracker.database.common.RoomModel @Entity(tableName = PlanterInfoEntity.TABLE) data class PlanterInfoEntity( @ColumnInfo(name = IDENTIFIER, index = true) - var identifier: String, + val identifier: String = "", @ColumnInfo(name = FIRST_NAME) - var firstName: String, + val firstName: String = "", @ColumnInfo(name = LAST_NAME) - var lastName: String, + val lastName: String = "", @ColumnInfo(name = ORGANIZATION) - var organization: String?, + val organization: String? = null, @ColumnInfo(name = PHONE) - var phone: String?, + val phone: String? = null, @ColumnInfo(name = EMAIL) - var email: String?, + val email: String? = null, @ColumnInfo(name = LATITUDE) - var latitude: Double, + val latitude: Double = 0.0, @ColumnInfo(name = LONGITUDE) - var longitude: Double, + val longitude: Double = 0.0, @ColumnInfo(name = UPLOADED, index = true) - var uploaded: Boolean = false, + val uploaded: Boolean = false, @ColumnInfo(name = CREATED_AT) - var createdAt: Long, + val createdAt: Long = 0, @ColumnInfo(name = BUNDLE_ID) - var bundleId: String? = null, + val bundleId: String? = null, @ColumnInfo(name = RECORD_UUID, defaultValue = "") - var recordUuid: String, -) { - + val recordUuid: String = "", @PrimaryKey(autoGenerate = true) @ColumnInfo(name = ID) var id: Long = 0 +) : RoomModel { companion object { const val TABLE = "planter_info" diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/entity/TreeAttributeEntity.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/entity/TreeAttributeEntity.kt similarity index 86% rename from app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/entity/TreeAttributeEntity.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/entity/TreeAttributeEntity.kt index 7f1b81ac1..fc357415e 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/entity/TreeAttributeEntity.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/entity/TreeAttributeEntity.kt @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.database.legacy.entity +package org.greenstand.android.TreeTracker.database.app.legacy.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey +import org.greenstand.android.TreeTracker.database.common.RoomModel @Entity( tableName = TreeAttributeEntity.TABLE, @@ -33,16 +34,15 @@ import androidx.room.PrimaryKey ) data class TreeAttributeEntity( @ColumnInfo(name = KEY) - var key: String, + val key: String = "", @ColumnInfo(name = VALUE) - var value: String, + val value: String = "", @ColumnInfo(name = TREE_CAPTURE_ID, index = true) - var treeCaptureId: Long -) { - + val treeCaptureId: Long = 0, @PrimaryKey(autoGenerate = true) @ColumnInfo(name = ID) var id: Long = 0 +) : RoomModel { companion object { const val TABLE = "tree_attribute" diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/entity/TreeCaptureEntity.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/entity/TreeCaptureEntity.kt similarity index 80% rename from app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/entity/TreeCaptureEntity.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/entity/TreeCaptureEntity.kt index 6f68a108d..4ea0a0ea5 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/entity/TreeCaptureEntity.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/entity/TreeCaptureEntity.kt @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.database.legacy.entity +package org.greenstand.android.TreeTracker.database.app.legacy.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey +import org.greenstand.android.TreeTracker.database.common.RoomModel @Entity( tableName = TreeCaptureEntity.TABLE, @@ -33,32 +34,31 @@ import androidx.room.PrimaryKey ) data class TreeCaptureEntity( @ColumnInfo(name = UUID) - var uuid: String, + val uuid: String = "", @ColumnInfo(name = PLANTER_CHECK_IN_ID, index = true) - var planterCheckInId: Long, + val planterCheckInId: Long = 0, @ColumnInfo(name = LOCAL_PHOTO_PATH) - var localPhotoPath: String?, + val localPhotoPath: String? = null, @ColumnInfo(name = PHOTO_URL) - var photoUrl: String?, + val photoUrl: String? = null, @ColumnInfo(name = NOTE_CONTENT) - var noteContent: String, + val noteContent: String = "", @ColumnInfo(name = LATITUDE) - var latitude: Double, + val latitude: Double = 0.0, @ColumnInfo(name = LONGITUDE) - var longitude: Double, + val longitude: Double = 0.0, @ColumnInfo(name = ACCURACY) - var accuracy: Double, + val accuracy: Double = 0.0, @ColumnInfo(name = UPLOADED, index = true) - var uploaded: Boolean = false, + val uploaded: Boolean = false, @ColumnInfo(name = CREATED_AT) - var createAt: Long, + val createAt: Long = 0, @ColumnInfo(name = BUNDLE_ID) - var bundleId: String? = null -) { - + val bundleId: String? = null, @PrimaryKey(autoGenerate = true) @ColumnInfo(name = ID) var id: Long = 0 +) : RoomModel { companion object { const val TABLE = "tree_capture" diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/views/TreeMapMarkerDbView.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/views/TreeMapMarkerDbView.kt similarity index 91% rename from app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/views/TreeMapMarkerDbView.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/views/TreeMapMarkerDbView.kt index 151afa7cf..c07b1d225 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/views/TreeMapMarkerDbView.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/views/TreeMapMarkerDbView.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.database.legacy.views +package org.greenstand.android.TreeTracker.database.app.legacy.views class TreeMapMarkerDbView( val latitude: Double, diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/views/TreeUploadDbView.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/views/TreeUploadDbView.kt similarity index 97% rename from app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/views/TreeUploadDbView.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/views/TreeUploadDbView.kt index 881d5435f..69a216d75 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/database/legacy/views/TreeUploadDbView.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/app/legacy/views/TreeUploadDbView.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.database.legacy.views +package org.greenstand.android.TreeTracker.database.app.legacy.views // @DatabaseView(""" // SELECT diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/common/CommonDao.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/common/CommonDao.kt new file mode 100644 index 000000000..974e6bbd8 --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/common/CommonDao.kt @@ -0,0 +1,49 @@ +package org.greenstand.android.TreeTracker.database.common + +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Transaction +import androidx.room.Update +import androidx.room.Upsert + +interface CommonDao { + + @Insert + suspend fun insertItem(item: T): Long + + @Transaction + @Insert + suspend fun insertItems(item: List) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun replaceItem(item: T): Long + + @Transaction + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun replaceItems(item: List) + + @Update + suspend fun updateItem(item: T) + + @Transaction + @Update + suspend fun updateItems(item: List) + + @Upsert + suspend fun upsertItem(item: T): Long + + @Transaction + @Upsert + suspend fun upsertItems(item: List) + + @Delete + suspend fun deleteItem(item: T) + + @Transaction + @Delete + suspend fun deleteItems(list: List) + + suspend fun deleteAll() + +} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/common/Encrypt.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/common/Encrypt.kt new file mode 100644 index 000000000..a409cad92 --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/common/Encrypt.kt @@ -0,0 +1,38 @@ +package org.greenstand.android.TreeTracker.database.common + +import android.content.Context +import net.sqlcipher.database.SQLiteDatabase +import org.greenstand.android.TreeTracker.BuildConfig +import org.greenstand.android.TreeTracker.database.app.AppDatabase + +object Encrypt { + + fun encrypt(context: Context, oldName: String, newName: String) { + SQLiteDatabase.loadLibs(context) + val dbFile = context.getDatabasePath(newName) + val legacyFile = context.getDatabasePath(oldName) + if (!dbFile.exists() && legacyFile.exists()) { + var db = SQLiteDatabase.openOrCreateDatabase(legacyFile, "", null) + db.rawExecSQL( + String.format( + "ATTACH DATABASE '%s' AS encrypted KEY '%s';", + dbFile.absolutePath, + if (BuildConfig.DEBUG) "" else BuildConfig.CRYPTO_KEY + ) + ) + db.rawExecSQL("SELECT sqlcipher_export('encrypted')") + db.rawExecSQL("DETACH DATABASE encrypted;") + val version = db.version + db.close() + db = SQLiteDatabase.openOrCreateDatabase( + dbFile, + if (BuildConfig.DEBUG) "" else BuildConfig.CRYPTO_KEY, + null + ) + db.version = version + db.close() + legacyFile.delete() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/common/RoomModel.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/common/RoomModel.kt new file mode 100644 index 000000000..d63457cca --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/common/RoomModel.kt @@ -0,0 +1,3 @@ +package org.greenstand.android.TreeTracker.database.common + +interface RoomModel \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/DatabaseConverters.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/DatabaseConverters.kt similarity index 60% rename from app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/DatabaseConverters.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/messages/DatabaseConverters.kt index fcc2877ff..ba943ea1e 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/DatabaseConverters.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/DatabaseConverters.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.models.messages.database +package org.greenstand.android.TreeTracker.database.messages import org.greenstand.android.TreeTracker.models.messages.AnnouncementMessage import org.greenstand.android.TreeTracker.models.messages.DirectMessage @@ -21,9 +21,8 @@ import org.greenstand.android.TreeTracker.models.messages.Message import org.greenstand.android.TreeTracker.models.messages.Question import org.greenstand.android.TreeTracker.models.messages.SurveyMessage import org.greenstand.android.TreeTracker.models.messages.SurveyResponseMessage -import org.greenstand.android.TreeTracker.models.messages.database.entities.MessageEntity -import org.greenstand.android.TreeTracker.models.messages.database.entities.QuestionEntity -import org.greenstand.android.TreeTracker.models.messages.database.entities.SurveyEntity +import org.greenstand.android.TreeTracker.database.messages.entity.MessageEntity +import org.greenstand.android.TreeTracker.database.messages.entity.embeded.MessagesForWallet import org.greenstand.android.TreeTracker.models.messages.network.requests.MessageRequest import org.greenstand.android.TreeTracker.models.messages.network.responses.MessageType @@ -63,45 +62,40 @@ object DatabaseConverters { } } - fun createMessageFromEntities( - messageEntity: MessageEntity, - surveyEntity: SurveyEntity? = null, - questionEntities: List? = null - ): Message { - - return when (messageEntity.type) { + fun createMessageFromEntities(messagesForWallet: MessagesForWallet): Message { + return when (messagesForWallet.type) { MessageType.MESSAGE -> DirectMessage( - id = messageEntity.id, - from = messageEntity.from, - to = messageEntity.to, - composedAt = messageEntity.composedAt, - parentMessageId = messageEntity.parentMessageId, - body = messageEntity.body ?: "", - isRead = messageEntity.isRead, + id = messagesForWallet.id, + from = messagesForWallet.from, + to = messagesForWallet.to, + composedAt = messagesForWallet.composedAt, + parentMessageId = messagesForWallet.parentMessageId, + body = messagesForWallet.body ?: "", + isRead = messagesForWallet.isRead, ) MessageType.ANNOUNCE -> AnnouncementMessage( - id = messageEntity.id, - from = messageEntity.from, - to = messageEntity.to, - composedAt = messageEntity.composedAt, - subject = messageEntity.subject ?: "", - body = messageEntity.body ?: "", - isRead = messageEntity.isRead, - videoLink = messageEntity.videoLink, + id = messagesForWallet.id, + from = messagesForWallet.from, + to = messagesForWallet.to, + composedAt = messagesForWallet.composedAt, + subject = messagesForWallet.subject ?: "", + body = messagesForWallet.body ?: "", + isRead = messagesForWallet.isRead, + videoLink = messagesForWallet.videoLink, ) MessageType.SURVEY -> SurveyMessage( - id = messageEntity.id, - from = messageEntity.from, - to = messageEntity.to, - composedAt = messageEntity.composedAt, - title = surveyEntity!!.title, - isRead = messageEntity.isRead, - surveyId = surveyEntity.id, - isComplete = messageEntity.isSurveyComplete ?: false, - questions = questionEntities?.map { + id = messagesForWallet.id, + from = messagesForWallet.from, + to = messagesForWallet.to, + composedAt = messagesForWallet.composedAt, + title = messagesForWallet.surveyTitle, + isRead = messagesForWallet.isRead, + surveyId = messagesForWallet.surveyId, + isComplete = messagesForWallet.isSurveyComplete ?: false, + questions = messagesForWallet.questionEntities?.map { Question( prompt = it.prompt, choices = it.choices @@ -110,14 +104,14 @@ object DatabaseConverters { ) MessageType.SURVEY_RESPONSE -> SurveyResponseMessage( - id = messageEntity.id, - from = messageEntity.from, - to = messageEntity.to, - composedAt = messageEntity.composedAt, - responses = messageEntity.surveyResponse ?: emptyList(), - isRead = messageEntity.isRead, - surveyId = surveyEntity!!.id, - questions = questionEntities?.map { + id = messagesForWallet.id, + from = messagesForWallet.from, + to = messagesForWallet.to, + composedAt = messagesForWallet.composedAt, + responses = messagesForWallet.surveyResponse ?: emptyList(), + isRead = messagesForWallet.isRead, + surveyId = messagesForWallet.surveyId, + questions = messagesForWallet.questionEntities?.map { Question( prompt = it.prompt, choices = it.choices diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/MessageDatabase.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/MessageDatabase.kt new file mode 100644 index 000000000..e95853938 --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/MessageDatabase.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2023 Treetracker + * + * 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. + */ +package org.greenstand.android.TreeTracker.database.messages + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import org.greenstand.android.TreeTracker.database.app.Converters +import org.greenstand.android.TreeTracker.database.messages.dao.MessageDao +import org.greenstand.android.TreeTracker.database.messages.dao.QuestionDao +import org.greenstand.android.TreeTracker.database.messages.dao.SurveyDao +import org.greenstand.android.TreeTracker.database.messages.entity.MessageEntity +import org.greenstand.android.TreeTracker.database.messages.entity.QuestionEntity +import org.greenstand.android.TreeTracker.database.messages.entity.SurveyEntity + +@Database( + version = 1, + exportSchema = true, + entities = [ + MessageEntity::class, + SurveyEntity::class, + QuestionEntity::class, + ], +) +@TypeConverters(Converters::class) +abstract class MessageDatabase : RoomDatabase() { + + abstract fun messageDao(): MessageDao + + abstract fun surveyDao(): SurveyDao + + abstract fun questionDao(): QuestionDao + + companion object { + + const val DB_NAME = "treetracker.messages.db" + const val DB_NAME_ENCRYPT = "treetracker.messages-encrypt.db" + + } +} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/dao/MessageDao.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/dao/MessageDao.kt new file mode 100644 index 000000000..c0e15996c --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/dao/MessageDao.kt @@ -0,0 +1,67 @@ +package org.greenstand.android.TreeTracker.database.messages.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import kotlinx.coroutines.flow.Flow +import org.greenstand.android.TreeTracker.database.common.CommonDao +import org.greenstand.android.TreeTracker.database.messages.entity.MessageEntity +import org.greenstand.android.TreeTracker.database.messages.entity.embeded.MessagesForWallet +import org.greenstand.android.TreeTracker.database.messages.entity.tuple.MessageAsUploadedTuple +import org.greenstand.android.TreeTracker.database.messages.entity.tuple.MessageBundleIdTuple +import org.greenstand.android.TreeTracker.database.messages.entity.tuple.ReadMessageTuple +import org.greenstand.android.TreeTracker.database.messages.entity.tuple.SurveyMessageCompleteTuple + +@Dao +interface MessageDao : CommonDao { + + @Query("SELECT * FROM messages WHERE id = :id") + suspend fun getMessage(id: String): MessageEntity? + + @Query("SELECT MAX(composed_at) FROM messages WHERE wallet = :wallet") + suspend fun getLatestSyncTimeForWallet(wallet: String): String? + + @Transaction + @Query("SELECT * FROM messages WHERE id = :id") + fun getMessageForWallet(id: String): MessagesForWallet? + + @Transaction + @Query("SELECT * FROM messages WHERE wallet = :wallet") + fun getMessagesForWalletFlow(wallet: String): Flow> + + @Transaction + @Query("SELECT * FROM messages WHERE wallet = :wallet AND type = 'MESSAGE'") + fun getDirectMessagesForWallet(wallet: String): Flow> + + @Transaction + @Update(entity = MessageEntity::class) + suspend fun updateMessageBundleIds(messageBundleIdTuples: List) + + @Query("SELECT id FROM messages WHERE should_upload = 1") + suspend fun getMessageIdsToUpload(): List + + @Query("SELECT * FROM messages WHERE id IN (:ids)") + suspend fun getMessagesByIds(ids: List): List + + @Transaction + @Update(entity = MessageEntity::class) + suspend fun markMessagesAsUploaded(messageAsUploadedTuples: List) + + @Transaction + @Update(entity = MessageEntity::class) + suspend fun markMessagesAsRead(readMessageTuples: List) + + @Update(entity = MessageEntity::class) + suspend fun markSurveyMessageComplete(surveyMessageCompleteTuple: SurveyMessageCompleteTuple) + + @Query("SELECT EXISTS(SELECT id FROM messages WHERE is_read = 0)") + suspend fun haveUnreadMessages(): Boolean + + @Query("SELECT EXISTS(SELECT id FROM messages WHERE wallet = (:wallet) AND is_read = 0)") + suspend fun haveUnreadMessageCountForWallet(wallet: String): Boolean + + @Query("DELETE FROM messages") + override suspend fun deleteAll() + +} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/dao/QuestionDao.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/dao/QuestionDao.kt new file mode 100644 index 000000000..aeedfba87 --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/dao/QuestionDao.kt @@ -0,0 +1,17 @@ +package org.greenstand.android.TreeTracker.database.messages.dao + +import androidx.room.Dao +import androidx.room.Query +import org.greenstand.android.TreeTracker.database.common.CommonDao +import org.greenstand.android.TreeTracker.database.messages.entity.QuestionEntity + +@Dao +interface QuestionDao : CommonDao { + + @Query("SELECT * FROM questions WHERE survey_id = :surveyId") + suspend fun getQuestionsForSurvey(surveyId: String): List + + @Query("DELETE FROM questions") + override suspend fun deleteAll() + +} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/dao/SurveyDao.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/dao/SurveyDao.kt new file mode 100644 index 000000000..fd39eb7f6 --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/dao/SurveyDao.kt @@ -0,0 +1,17 @@ +package org.greenstand.android.TreeTracker.database.messages.dao + +import androidx.room.Dao +import androidx.room.Query +import org.greenstand.android.TreeTracker.database.common.CommonDao +import org.greenstand.android.TreeTracker.database.messages.entity.SurveyEntity + +@Dao +interface SurveyDao : CommonDao { + + @Query("SELECT * FROM surveys WHERE id = :id") + suspend fun getSurvey(id: String?): SurveyEntity? + + @Query("DELETE FROM surveys") + override suspend fun deleteAll() + +} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/entities/MessageEntity.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/MessageEntity.kt similarity index 65% rename from app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/entities/MessageEntity.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/MessageEntity.kt index eda22b940..bf806e84d 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/entities/MessageEntity.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/MessageEntity.kt @@ -13,46 +13,48 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.models.messages.database.entities +package org.greenstand.android.TreeTracker.database.messages.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import org.greenstand.android.TreeTracker.database.common.RoomModel import org.greenstand.android.TreeTracker.models.messages.network.responses.MessageType +import java.util.UUID @Entity(tableName = "messages") data class MessageEntity( @PrimaryKey @ColumnInfo(name = "id") - val id: String, + val id: String = UUID.randomUUID().toString(), @ColumnInfo(name = "wallet") - val wallet: String, + val wallet: String = "", @ColumnInfo(name = "type") - val type: MessageType, + val type: MessageType = MessageType.MESSAGE, @ColumnInfo(name = "from") - val from: String, + val from: String = "", @ColumnInfo(name = "to") - val to: String, + val to: String = "", @ColumnInfo(name = "subject") - val subject: String?, + val subject: String? = null, @ColumnInfo(name = "body") - val body: String?, + val body: String? = null, @ColumnInfo(name = "composed_at") - val composedAt: String, + val composedAt: String = "", @ColumnInfo(name = "parent_message_id") - val parentMessageId: String?, + val parentMessageId: String? = null, @ColumnInfo(name = "video_link") - val videoLink: String?, + val videoLink: String? = null, @ColumnInfo(name = "survey_response") - val surveyResponse: List?, + val surveyResponse: List? = null, @ColumnInfo(name = "should_upload") - val shouldUpload: Boolean, + val shouldUpload: Boolean = true, @ColumnInfo(name = "bundle_id") - val bundleId: String?, + val bundleId: String? = null, @ColumnInfo(name = "is_read") - val isRead: Boolean, + val isRead: Boolean = false, @ColumnInfo(name = "survey_id", index = true) - val surveyId: String?, + val surveyId: String? = null, @ColumnInfo(name = "is_survey_complete") - val isSurveyComplete: Boolean?, -) \ No newline at end of file + val isSurveyComplete: Boolean? = null +) : RoomModel \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/entities/QuestionEntity.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/QuestionEntity.kt similarity index 89% rename from app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/entities/QuestionEntity.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/QuestionEntity.kt index cdf6430da..942a6d62a 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/entities/QuestionEntity.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/QuestionEntity.kt @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.models.messages.database.entities +package org.greenstand.android.TreeTracker.database.messages.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey +import org.greenstand.android.TreeTracker.database.common.RoomModel @Entity( tableName = "questions", @@ -38,8 +39,7 @@ class QuestionEntity( val prompt: String, @ColumnInfo(name = "choices") val choices: List, -) { @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "_id") var id: Long = 0 -} \ No newline at end of file +) : RoomModel \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/entities/SurveyEntity.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/SurveyEntity.kt similarity index 85% rename from app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/entities/SurveyEntity.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/SurveyEntity.kt index af0da9ebf..8039d6a8e 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/entities/SurveyEntity.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/SurveyEntity.kt @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.models.messages.database.entities +package org.greenstand.android.TreeTracker.database.messages.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import org.greenstand.android.TreeTracker.database.common.RoomModel @Entity(tableName = "surveys") class SurveyEntity( @@ -26,4 +27,4 @@ class SurveyEntity( val id: String, @ColumnInfo(name = "title") val title: String, -) \ No newline at end of file +) : RoomModel \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/embeded/MessagesForWallet.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/embeded/MessagesForWallet.kt new file mode 100644 index 000000000..29c35e1f6 --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/embeded/MessagesForWallet.kt @@ -0,0 +1,61 @@ +package org.greenstand.android.TreeTracker.database.messages.entity.embeded + +import androidx.room.Embedded +import androidx.room.Relation +import org.greenstand.android.TreeTracker.database.messages.entity.MessageEntity +import org.greenstand.android.TreeTracker.database.messages.entity.QuestionEntity +import org.greenstand.android.TreeTracker.database.messages.entity.SurveyEntity +import org.greenstand.android.TreeTracker.models.messages.network.responses.MessageType + +data class MessagesForWallet( + @Embedded + val messageEntity: MessageEntity, + @Relation(parentColumn = "survey_id", entityColumn = "id") + val surveyEntity: SurveyEntity?, + @Relation(parentColumn = "survey_id", entityColumn = "survey_id") + val questionEntities: List? +) { + + val id: String + get() = messageEntity.id + + val type: MessageType + get() = messageEntity.type + + val from: String + get() = messageEntity.from + + val to: String + get() = messageEntity.to + + val composedAt: String + get() = messageEntity.composedAt + + val parentMessageId: String? + get() = messageEntity.parentMessageId + + val body: String? + get() = messageEntity.body + + val isRead: Boolean + get() = messageEntity.isRead + + val subject: String? + get() = messageEntity.subject + + val videoLink: String? + get() = messageEntity.videoLink + + val surveyTitle: String + get() = surveyEntity?.title ?: "" + + val surveyId: String + get() = surveyEntity?.id ?: "" + + val isSurveyComplete: Boolean? + get() = messageEntity.isSurveyComplete + + val surveyResponse: List? + get() = messageEntity.surveyResponse + +} diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/tuple/MessageAsUploadedTuple.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/tuple/MessageAsUploadedTuple.kt new file mode 100644 index 000000000..cf0ee949e --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/tuple/MessageAsUploadedTuple.kt @@ -0,0 +1,10 @@ +package org.greenstand.android.TreeTracker.database.messages.entity.tuple + +import androidx.room.ColumnInfo + +data class MessageAsUploadedTuple( + @ColumnInfo(name = "id") + val id: String, + @ColumnInfo(name = "should_upload") + val shouldUpload: Boolean +) diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/tuple/MessageBundleIdTuple.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/tuple/MessageBundleIdTuple.kt new file mode 100644 index 000000000..17e5dcf6d --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/tuple/MessageBundleIdTuple.kt @@ -0,0 +1,10 @@ +package org.greenstand.android.TreeTracker.database.messages.entity.tuple + +import androidx.room.ColumnInfo + +data class MessageBundleIdTuple( + @ColumnInfo(name = "id") + val id: String, + @ColumnInfo(name = "bundle_id") + val bundleId: String? +) diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/tuple/ReadMessageTuple.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/tuple/ReadMessageTuple.kt new file mode 100644 index 000000000..a0a3ff6d6 --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/tuple/ReadMessageTuple.kt @@ -0,0 +1,10 @@ +package org.greenstand.android.TreeTracker.database.messages.entity.tuple + +import androidx.room.ColumnInfo + +data class ReadMessageTuple( + @ColumnInfo(name = "id") + val id: String, + @ColumnInfo(name = "is_read") + val isRead: Boolean +) diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/tuple/SurveyMessageCompleteTuple.kt b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/tuple/SurveyMessageCompleteTuple.kt new file mode 100644 index 000000000..024821db5 --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/database/messages/entity/tuple/SurveyMessageCompleteTuple.kt @@ -0,0 +1,10 @@ +package org.greenstand.android.TreeTracker.database.messages.entity.tuple + +import androidx.room.ColumnInfo + +data class SurveyMessageCompleteTuple( + @ColumnInfo(name = "id") + val id: String, + @ColumnInfo(name = "is_survey_complete") + val isSurveyComplete: Boolean? +) diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/devoptions/DevOptionsScreen.kt b/app/src/main/java/org/greenstand/android/TreeTracker/devoptions/DevOptionsScreen.kt index fae2ff337..ce61e5869 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/devoptions/DevOptionsScreen.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/devoptions/DevOptionsScreen.kt @@ -77,7 +77,7 @@ fun DevOptionsScreen( ) }, ) { - LazyColumn { + LazyColumn(modifier = Modifier.padding(it)) { items(state.params) { param -> ParamListItem(param) { newValue -> onParamUpdated(param, newValue) diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/di/AppModule.kt b/app/src/main/java/org/greenstand/android/TreeTracker/di/AppModule.kt index 2bbe93167..1f1b4a74c 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/di/AppModule.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/di/AppModule.kt @@ -21,11 +21,14 @@ import android.location.LocationManager import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.localbroadcastmanager.content.LocalBroadcastManager +import androidx.room.Room import androidx.work.WorkManager import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.ktx.Firebase import com.google.gson.GsonBuilder +import net.sqlcipher.database.SupportFactory +import org.greenstand.android.TreeTracker.BuildConfig import org.greenstand.android.TreeTracker.analytics.Analytics import org.greenstand.android.TreeTracker.analytics.ExceptionDataCollector import org.greenstand.android.TreeTracker.api.ObjectStorageClient @@ -33,6 +36,13 @@ import org.greenstand.android.TreeTracker.background.SyncNotificationManager import org.greenstand.android.TreeTracker.capture.TreeImageReviewViewModel import org.greenstand.android.TreeTracker.dashboard.DashboardViewModel import org.greenstand.android.TreeTracker.dashboard.TreesToSyncHelper +import org.greenstand.android.TreeTracker.database.app.AppDatabase +import org.greenstand.android.TreeTracker.database.app.MIGRATION_3_4 +import org.greenstand.android.TreeTracker.database.app.MIGRATION_4_5 +import org.greenstand.android.TreeTracker.database.app.MIGRATION_5_6 +import org.greenstand.android.TreeTracker.database.app.MIGRATION_6_7 +import org.greenstand.android.TreeTracker.database.common.Encrypt.encrypt +import org.greenstand.android.TreeTracker.database.messages.MessageDatabase import org.greenstand.android.TreeTracker.devoptions.Configurator import org.greenstand.android.TreeTracker.devoptions.DevOptionsViewModel import org.greenstand.android.TreeTracker.languagepicker.LanguagePickerViewModel @@ -57,8 +67,6 @@ import org.greenstand.android.TreeTracker.models.captureflowdata.CaptureFlowScop import org.greenstand.android.TreeTracker.models.captureflowdata.CaptureFlowScopeManager import org.greenstand.android.TreeTracker.models.location.LocationDataCapturer import org.greenstand.android.TreeTracker.models.location.LocationUpdateManager -import org.greenstand.android.TreeTracker.models.messages.MessageUploader -import org.greenstand.android.TreeTracker.models.messages.MessagesRepo import org.greenstand.android.TreeTracker.models.messages.network.MessageTypeDeserializer import org.greenstand.android.TreeTracker.models.messages.network.responses.MessageType import org.greenstand.android.TreeTracker.models.organization.OrgRepo @@ -125,7 +133,16 @@ val appModule = module { viewModel { PermissionViewModel(get()) } - single { UserRepo(get(), get(), get(), get(), get(), get()) } + single { + UserRepo( + get(), + get(), + get(), + get(), + get(), + get() + ) + } factory { CaptureFlowScopeManager.getData().get() } @@ -155,10 +172,6 @@ val appModule = module { single { androidContext().resources } - single { MessagesRepo(get(), get(), get(), get(), get()) } - - factory { MessageUploader(get(), get(), get()) } - single { LocationUpdateManager(get(), get(), get()) } single { ObjectStorageClient.instance() } @@ -243,4 +256,40 @@ val appModule = module { scoped { CaptureFlowNavigationController(get(), get(), get(), get(), get()) } scoped { TreeCapturer(get(), get(), get(), get(), get()) } } + + single { + encrypt( + context = get(), + oldName = AppDatabase.DB_NAME, + newName = AppDatabase.DB_NAME_ENCRYPT + ) + Room.databaseBuilder( + get(), + AppDatabase::class.java, + AppDatabase.DB_NAME_ENCRYPT + ).addMigrations( + MIGRATION_3_4, + MIGRATION_4_5, + MIGRATION_5_6, + MIGRATION_6_7, + ).openHelperFactory( + if (BuildConfig.DEBUG) null else SupportFactory(BuildConfig.CRYPTO_KEY.toByteArray()) + ).build() + } + + single { + encrypt( + context = get(), + oldName = MessageDatabase.DB_NAME, + newName = MessageDatabase.DB_NAME_ENCRYPT + ) + Room.databaseBuilder( + get(), + MessageDatabase::class.java, + MessageDatabase.DB_NAME_ENCRYPT + ).openHelperFactory( + if (BuildConfig.DEBUG) null else SupportFactory(BuildConfig.CRYPTO_KEY.toByteArray()) + ).build() + } + } \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/di/RoomModule.kt b/app/src/main/java/org/greenstand/android/TreeTracker/di/DaoModule.kt similarity index 63% rename from app/src/main/java/org/greenstand/android/TreeTracker/di/RoomModule.kt rename to app/src/main/java/org/greenstand/android/TreeTracker/di/DaoModule.kt index 26ce3a28c..d2e18286b 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/di/RoomModule.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/di/DaoModule.kt @@ -15,17 +15,18 @@ */ package org.greenstand.android.TreeTracker.di -import org.greenstand.android.TreeTracker.database.AppDatabase -import org.greenstand.android.TreeTracker.models.messages.database.MessageDatabase +import org.greenstand.android.TreeTracker.database.app.AppDatabase +import org.greenstand.android.TreeTracker.database.messages.MessageDatabase import org.koin.dsl.module -val roomModule = module { +val daoModule = module { - single { AppDatabase.getInstance(get()) } + single { get().treeTrackerDao() } - single { AppDatabase.getInstance(get()).treeTrackerDao() } + single { get().messageDao() } - single { MessageDatabase.getInstance(get()) } + single { get().surveyDao() } + + single { get().questionDao() } - single { MessageDatabase.getInstance(get()).messagesDao() } } \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/di/RepositoryModule.kt b/app/src/main/java/org/greenstand/android/TreeTracker/di/RepositoryModule.kt new file mode 100644 index 000000000..325b3509f --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/di/RepositoryModule.kt @@ -0,0 +1,30 @@ +package org.greenstand.android.TreeTracker.di + +import org.greenstand.android.TreeTracker.data.repository.MessageRepositoryImpl +import org.greenstand.android.TreeTracker.data.repository.QuestionRepositoryImpl +import org.greenstand.android.TreeTracker.data.repository.SurveyRepositoryImpl +import org.greenstand.android.TreeTracker.domain.repository.MessageRepository +import org.greenstand.android.TreeTracker.domain.repository.QuestionRepository +import org.greenstand.android.TreeTracker.domain.repository.SurveyRepository +import org.koin.dsl.module + +val repositoryModule = module { + + single { + MessageRepositoryImpl( + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() + ) + } + + single { SurveyRepositoryImpl(get()) } + + single { QuestionRepositoryImpl(get()) } + +} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/domain/repository/CommonRepository.kt b/app/src/main/java/org/greenstand/android/TreeTracker/domain/repository/CommonRepository.kt new file mode 100644 index 000000000..bf6b41ccf --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/domain/repository/CommonRepository.kt @@ -0,0 +1,29 @@ +package org.greenstand.android.TreeTracker.domain.repository + +import org.greenstand.android.TreeTracker.database.common.RoomModel + +interface CommonRepository { + + suspend fun insertItem(item: T): Long + + suspend fun insertItems(items: List) + + suspend fun replaceItem(item: T): Long + + suspend fun replaceItems(items: List) + + suspend fun updateItem(item: T) + + suspend fun updateItems(items: List) + + suspend fun upsertItem(item: T): Long + + suspend fun upsertItems(items: List) + + suspend fun deleteItem(item: T) + + suspend fun deleteItems(items: List) + + suspend fun deleteAll() + +} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/domain/repository/MessageRepository.kt b/app/src/main/java/org/greenstand/android/TreeTracker/domain/repository/MessageRepository.kt new file mode 100644 index 000000000..cd990379f --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/domain/repository/MessageRepository.kt @@ -0,0 +1,46 @@ +package org.greenstand.android.TreeTracker.domain.repository + +import kotlinx.coroutines.flow.Flow +import org.greenstand.android.TreeTracker.database.messages.entity.MessageEntity +import org.greenstand.android.TreeTracker.models.messages.AnnouncementMessage +import org.greenstand.android.TreeTracker.models.messages.DirectMessage +import org.greenstand.android.TreeTracker.models.messages.Message +import org.greenstand.android.TreeTracker.models.messages.SurveyMessage + +interface MessageRepository : CommonRepository { + + suspend fun markMessageAsRead(messageId: String) + + suspend fun markMessagesAsRead(messageIds: List) + + suspend fun saveMessage(wallet: String, to: String, body: String): Long + + suspend fun saveSurveyAnswers(messageId: String, surveyResponse: List) + + fun getMessageFlow(wallet: String): Flow> + + fun getDirectMessages(wallet: String, otherChatIdentifier: String): Flow> + + suspend fun getAnnouncementMessages(id: String): AnnouncementMessage? + + suspend fun getSurveyMessage(id: String): SurveyMessage? + + suspend fun haveUnreadMessages(): Boolean + + suspend fun haveUnreadMessageCountForWallet(wallet: String): Boolean + + suspend fun getLatestSyncTimeForWallet(wallet: String): String + + suspend fun getMessagesByIds(ids: List): List + + suspend fun getMessageIdsToUpload(): List + + suspend fun updateMessageBundleIds(ids: List, bundleId: String) + + suspend fun markMessagesAsUploaded(ids: List) + + suspend fun syncMessages() + + suspend fun uploadMessages() + +} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/domain/repository/QuestionRepository.kt b/app/src/main/java/org/greenstand/android/TreeTracker/domain/repository/QuestionRepository.kt new file mode 100644 index 000000000..7c1da1da7 --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/domain/repository/QuestionRepository.kt @@ -0,0 +1,9 @@ +package org.greenstand.android.TreeTracker.domain.repository + +import org.greenstand.android.TreeTracker.database.messages.entity.QuestionEntity + +interface QuestionRepository : CommonRepository { + + suspend fun getQuestionsForSurvey(surveyId: String): List + +} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/domain/repository/SurveyRepository.kt b/app/src/main/java/org/greenstand/android/TreeTracker/domain/repository/SurveyRepository.kt new file mode 100644 index 000000000..8048ca5d4 --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/domain/repository/SurveyRepository.kt @@ -0,0 +1,9 @@ +package org.greenstand.android.TreeTracker.domain.repository + +import org.greenstand.android.TreeTracker.database.messages.entity.SurveyEntity + +interface SurveyRepository : CommonRepository { + + suspend fun getSurvey(id: String?): SurveyEntity? + +} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/extension/BuildersUtils.kt b/app/src/main/java/org/greenstand/android/TreeTracker/extension/BuildersUtils.kt new file mode 100644 index 000000000..fbbfd84cc --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/extension/BuildersUtils.kt @@ -0,0 +1,14 @@ +package org.greenstand.android.TreeTracker.extension + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +suspend fun withMain(block: suspend CoroutineScope.() -> T) = + withContext(Dispatchers.Main, block) + +suspend fun withIO(block: suspend CoroutineScope.() -> T) = + withContext(Dispatchers.IO, block) + +suspend fun withDefault(block: suspend CoroutineScope.() -> T) = + withContext(Dispatchers.Default, block) \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/extension/FloatUtils.kt b/app/src/main/java/org/greenstand/android/TreeTracker/extension/FloatUtils.kt new file mode 100644 index 000000000..7d691b596 --- /dev/null +++ b/app/src/main/java/org/greenstand/android/TreeTracker/extension/FloatUtils.kt @@ -0,0 +1,3 @@ +package org.greenstand.android.TreeTracker.extension + +fun Float.ifNaN(block: () -> Float = { 0f }) = if (isNaN()) block() else this \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/languagepicker/LanguageSelectScreen.kt b/app/src/main/java/org/greenstand/android/TreeTracker/languagepicker/LanguageSelectScreen.kt index 7c5a74a50..1a3c32fe1 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/languagepicker/LanguageSelectScreen.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/languagepicker/LanguageSelectScreen.kt @@ -80,7 +80,9 @@ fun LanguageSelectScreen( LazyColumn( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() + .padding(it) ) { items(Language.values()) { language -> LanguageButton( diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/messages/announcementmessage/AnnouncementScreen.kt b/app/src/main/java/org/greenstand/android/TreeTracker/messages/announcementmessage/AnnouncementScreen.kt index f92bc8473..8d799f4ef 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/messages/announcementmessage/AnnouncementScreen.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/messages/announcementmessage/AnnouncementScreen.kt @@ -87,6 +87,7 @@ fun AnnouncementScreen( ) { Column( Modifier + .padding(it) .padding(top = 4.dp, start = 4.dp, end = 4.dp, bottom = 80.dp) .fillMaxSize() ) { diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/messages/announcementmessage/AnnouncementViewModel.kt b/app/src/main/java/org/greenstand/android/TreeTracker/messages/announcementmessage/AnnouncementViewModel.kt index a67cc0a4b..b29b4d1f2 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/messages/announcementmessage/AnnouncementViewModel.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/messages/announcementmessage/AnnouncementViewModel.kt @@ -21,9 +21,10 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.greenstand.android.TreeTracker.domain.repository.MessageRepository import org.greenstand.android.TreeTracker.models.messages.AnnouncementMessage -import org.greenstand.android.TreeTracker.models.messages.MessagesRepo import org.koin.core.component.KoinComponent import org.koin.core.component.get @@ -36,7 +37,7 @@ data class AnnouncementState( class AnnouncementViewModel( private val messageId: String, - private val messagesRepo: MessagesRepo, + private val messageRepository: MessageRepository, ) : ViewModel() { private var _state: MutableStateFlow = MutableStateFlow(AnnouncementState()) val state: StateFlow = _state.asStateFlow() @@ -45,14 +46,18 @@ class AnnouncementViewModel( init { viewModelScope.launch { - announcement = messagesRepo.getAnnouncementMessages(messageId) - _state.value = _state.value.copy( - from = announcement.from, - currentTitle = announcement.subject, - currentBody = announcement.body, - currentUrl = announcement.videoLink - ) - messagesRepo.markMessageAsRead(messageId) + messageRepository.getAnnouncementMessages(messageId)?.let { + announcement = it + _state.update { state -> + state.copy( + from = it.from, + currentTitle = it.subject, + currentBody = it.body, + currentUrl = it.videoLink + ) + } + } + messageRepository.markMessageAsRead(messageId) } } } @@ -62,7 +67,7 @@ class AnnouncementViewModelFactory( ) : ViewModelProvider.Factory, KoinComponent { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { + override fun create(modelClass: Class): T { return AnnouncementViewModel(messageId, get()) as T } } \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/messages/directmessages/ChatScreen.kt b/app/src/main/java/org/greenstand/android/TreeTracker/messages/directmessages/ChatScreen.kt index c23393be0..7b9bc7575 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/messages/directmessages/ChatScreen.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/messages/directmessages/ChatScreen.kt @@ -119,7 +119,9 @@ fun ChatScreen( } ) { Column( - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() + .padding(it) ) { Messages( state = state, diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/messages/directmessages/ChatViewModel.kt b/app/src/main/java/org/greenstand/android/TreeTracker/messages/directmessages/ChatViewModel.kt index 1b54f3b2e..1dbc5d2ac 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/messages/directmessages/ChatViewModel.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/messages/directmessages/ChatViewModel.kt @@ -22,9 +22,9 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch +import org.greenstand.android.TreeTracker.domain.repository.MessageRepository import org.greenstand.android.TreeTracker.models.UserRepo import org.greenstand.android.TreeTracker.models.messages.DirectMessage -import org.greenstand.android.TreeTracker.models.messages.MessagesRepo import org.greenstand.android.TreeTracker.models.user.User import org.koin.core.component.KoinComponent import org.koin.core.component.get @@ -41,7 +41,7 @@ class ChatViewModel( private val userId: Long, private val otherChatIdentifier: String, private val userRepo: UserRepo, - private val messagesRepo: MessagesRepo, + private val messageRepository: MessageRepository, ) : ViewModel() { private val _state = MutableLiveData() @@ -50,14 +50,14 @@ class ChatViewModel( init { viewModelScope.launch { val currentUser = userRepo.getUser(userId) - messagesRepo.getDirectMessages(currentUser!!.wallet, otherChatIdentifier).collect { messages -> + messageRepository.getDirectMessages(currentUser!!.wallet, otherChatIdentifier).collect { messages -> _state.value = ChatState( from = otherChatIdentifier, currentUser = currentUser, messages = messages, ) val unreadMessages = messages.filterNot { it.isRead }.map { it.id } - messagesRepo.markMessagesAsRead(unreadMessages) + messageRepository.markMessagesAsRead(unreadMessages) } } } @@ -84,7 +84,7 @@ class ChatViewModel( fun sendMessage() { viewModelScope.launch { - messagesRepo.saveMessage( + messageRepository.saveMessage( _state.value!!.currentUser!!.wallet, otherChatIdentifier, _state.value!!.draftText @@ -99,7 +99,7 @@ class ChatViewModel( class ChatViewModelFactory(private val userId: Long, private val otherChatIdentifier: String) : ViewModelProvider.Factory, KoinComponent { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { + override fun create(modelClass: Class): T { return ChatViewModel(userId, otherChatIdentifier, get(), get()) as T } } \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/messages/individualmeassagelist/IndividualMessageListScreen.kt b/app/src/main/java/org/greenstand/android/TreeTracker/messages/individualmeassagelist/IndividualMessageListScreen.kt index cc61c72b1..862daa421 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/messages/individualmeassagelist/IndividualMessageListScreen.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/messages/individualmeassagelist/IndividualMessageListScreen.kt @@ -18,9 +18,9 @@ package org.greenstand.android.TreeTracker.messages.individualmeassagelist import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.GridCells -import androidx.compose.foundation.lazy.LazyVerticalGrid -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.material.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -46,7 +46,11 @@ import org.greenstand.android.TreeTracker.view.UserImageButton @Composable fun IndividualMessageListScreen( userId: Long, - viewModel: IndividualMessageListViewModel = viewModel(factory = IndividualMessageListViewModelFactory(userId)) + viewModel: IndividualMessageListViewModel = viewModel( + factory = IndividualMessageListViewModelFactory( + userId + ) + ) ) { val navController = LocalNavHostController.current val state by viewModel.state.observeAsState(IndividualMessageListState()) @@ -75,9 +79,24 @@ fun IndividualMessageListScreen( colors = AppButtonColors.MessagePurple, onClick = { when (val msg = state.selectedMessage) { - is DirectMessage -> navController.navigate(NavRoute.Chat.create(userId, msg.from)) - is SurveyMessage -> navController.navigate(NavRoute.Survey.create(msg.id)) - is AnnouncementMessage -> navController.navigate(NavRoute.Announcement.create(msg.id)) + is DirectMessage -> navController.navigate( + NavRoute.Chat.create( + userId, + msg.from + ) + ) + + is SurveyMessage -> navController.navigate( + NavRoute.Survey.create( + msg.id + ) + ) + + is AnnouncementMessage -> navController.navigate( + NavRoute.Announcement.create( + msg.id + ) + ) } } ) @@ -96,12 +115,11 @@ fun IndividualMessageListScreen( ) { if (state.messages.isNotEmpty()) { LazyVerticalGrid( - cells = GridCells.Fixed(2), + columns = GridCells.Fixed(2), modifier = Modifier.padding(it), // Padding for bottom bar. contentPadding = PaddingValues(start = 8.dp, end = 8.dp, top = 10.dp) ) { items(state.messages) { message -> - val isSelected = state.selectedMessage == message key(message.id) { when (message) { @@ -115,6 +133,7 @@ fun IndividualMessageListScreen( ) { viewModel.selectMessage(message) } + is SurveyMessage -> IndividualMessageItem( isSelected = isSelected, @@ -125,6 +144,7 @@ fun IndividualMessageListScreen( ) { viewModel.selectMessage(message) } + is AnnouncementMessage -> IndividualMessageItem( isSelected = isSelected, @@ -136,6 +156,7 @@ fun IndividualMessageListScreen( ) { viewModel.selectMessage(message) } + else -> throw IllegalStateException("Unsupported type: $message") } } diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/messages/individualmeassagelist/IndividualMessageListViewModel.kt b/app/src/main/java/org/greenstand/android/TreeTracker/messages/individualmeassagelist/IndividualMessageListViewModel.kt index d3cc61081..053e6980c 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/messages/individualmeassagelist/IndividualMessageListViewModel.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/messages/individualmeassagelist/IndividualMessageListViewModel.kt @@ -22,11 +22,11 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch +import org.greenstand.android.TreeTracker.domain.repository.MessageRepository import org.greenstand.android.TreeTracker.models.UserRepo import org.greenstand.android.TreeTracker.models.messages.AnnouncementMessage import org.greenstand.android.TreeTracker.models.messages.DirectMessage import org.greenstand.android.TreeTracker.models.messages.Message -import org.greenstand.android.TreeTracker.models.messages.MessagesRepo import org.greenstand.android.TreeTracker.models.messages.SurveyMessage import org.greenstand.android.TreeTracker.models.user.User import org.koin.core.component.KoinComponent @@ -42,7 +42,7 @@ data class IndividualMessageListState( class IndividualMessageListViewModel( private val userId: Long, private val userRepo: UserRepo, - private val messagesRepo: MessagesRepo, + private val messageRepository: MessageRepository, ) : ViewModel() { private val _state = MutableLiveData() @@ -51,7 +51,7 @@ class IndividualMessageListViewModel( init { viewModelScope.launch { val currentUser = userRepo.getUser(userId) - messagesRepo.getMessageFlow(currentUser!!.wallet) + messageRepository.getMessageFlow(currentUser!!.wallet) .collect { updateMessages(it, currentUser) } } } @@ -92,7 +92,7 @@ class IndividualMessageListViewModel( class IndividualMessageListViewModelFactory(private val userId: Long) : ViewModelProvider.Factory, KoinComponent { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { + override fun create(modelClass: Class): T { return IndividualMessageListViewModel(userId, get(), get()) as T } } \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/messages/survey/SurveyScreen.kt b/app/src/main/java/org/greenstand/android/TreeTracker/messages/survey/SurveyScreen.kt index 17a8c4753..df9a0b8f3 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/messages/survey/SurveyScreen.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/messages/survey/SurveyScreen.kt @@ -27,7 +27,13 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.Scaffold import androidx.compose.material.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -96,7 +102,9 @@ fun SurveyScreen( } ) { Column( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .padding(it), horizontalAlignment = Alignment.CenterHorizontally, ) { QuestionPrompt(promptText = state.currentQuestion?.prompt ?: "") @@ -172,6 +180,7 @@ fun AnswerItem( } } } + @Composable fun ShowToastMessage(stringResId: Int) { val context = LocalContext.current diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/messages/survey/SurveyViewModel.kt b/app/src/main/java/org/greenstand/android/TreeTracker/messages/survey/SurveyViewModel.kt index e2b7b6c9f..2b285029e 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/messages/survey/SurveyViewModel.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/messages/survey/SurveyViewModel.kt @@ -21,9 +21,10 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.greenstand.android.TreeTracker.domain.repository.MessageRepository import org.greenstand.android.TreeTracker.models.UserRepo -import org.greenstand.android.TreeTracker.models.messages.MessagesRepo import org.greenstand.android.TreeTracker.models.messages.Question import org.greenstand.android.TreeTracker.models.messages.SurveyMessage import org.koin.core.component.KoinComponent @@ -37,7 +38,7 @@ data class SurveyScreenState( class SurveyViewModel( private val messageId: String, - private val messagesRepo: MessagesRepo, + private val messageRepository: MessageRepository, private val userRepo: UserRepo, ) : ViewModel() { @@ -50,13 +51,18 @@ class SurveyViewModel( init { viewModelScope.launch { - survey = messagesRepo.getSurveyMessage(messageId) - val user = userRepo.getUserWithWallet(survey.to) - _state.value = _state.value.copy( - userImagePath = user!!.photoPath, - currentQuestion = survey.questions[currentQuestionIndex] - ) - messagesRepo.markMessageAsRead(messageId) + messageRepository.getSurveyMessage(messageId)?.let { + survey = it + userRepo.getUserWithWallet(it.to)?.let { user -> + _state.update { state -> + state.copy( + userImagePath = user.photoPath, + currentQuestion = survey.questions[currentQuestionIndex] + ) + } + } + } + messageRepository.markMessageAsRead(messageId) } } @@ -76,7 +82,7 @@ class SurveyViewModel( val answerStrings = survey.questions.mapIndexed { index, question -> answers[index]?.let { question.choices[it] } }.requireNoNulls() - messagesRepo.saveSurveyAnswers(messageId, answerStrings) + messageRepository.saveSurveyAnswers(messageId, answerStrings) return false } currentQuestionIndex++ @@ -107,7 +113,7 @@ class SurveyViewModel( class SurveyViewModelFactory(private val messageId: String) : ViewModelProvider.Factory, KoinComponent { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { + override fun create(modelClass: Class): T { return SurveyViewModel(messageId, get(), get()) as T } } \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/DeviceConfigUpdater.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/DeviceConfigUpdater.kt index 29f5aa39b..677a19502 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/DeviceConfigUpdater.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/models/DeviceConfigUpdater.kt @@ -17,8 +17,8 @@ package org.greenstand.android.TreeTracker.models import android.os.Build import org.greenstand.android.TreeTracker.BuildConfig -import org.greenstand.android.TreeTracker.database.TreeTrackerDAO -import org.greenstand.android.TreeTracker.database.entity.DeviceConfigEntity +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO +import org.greenstand.android.TreeTracker.database.app.entity.DeviceConfigEntity import org.greenstand.android.TreeTracker.utilities.TimeProvider import java.util.* diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/DeviceConfigUploader.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/DeviceConfigUploader.kt index c7d03d128..fab63c6dd 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/DeviceConfigUploader.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/models/DeviceConfigUploader.kt @@ -19,7 +19,7 @@ import com.google.gson.Gson import org.greenstand.android.TreeTracker.api.ObjectStorageClient import org.greenstand.android.TreeTracker.api.models.requests.DeviceConfigRequest import org.greenstand.android.TreeTracker.api.models.requests.UploadBundle -import org.greenstand.android.TreeTracker.database.TreeTrackerDAO +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO import org.greenstand.android.TreeTracker.utilities.md5 class DeviceConfigUploader( diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/PlanterUploader.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/PlanterUploader.kt index a9a97b43b..5a398cae0 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/PlanterUploader.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/models/PlanterUploader.kt @@ -18,13 +18,14 @@ package org.greenstand.android.TreeTracker.models import com.google.gson.Gson import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext import org.greenstand.android.TreeTracker.api.ObjectStorageClient import org.greenstand.android.TreeTracker.api.models.requests.RegistrationRequest import org.greenstand.android.TreeTracker.api.models.requests.UploadBundle import org.greenstand.android.TreeTracker.api.models.requests.WalletRegistrationRequest -import org.greenstand.android.TreeTracker.database.TreeTrackerDAO +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO import org.greenstand.android.TreeTracker.usecases.UploadImageParams import org.greenstand.android.TreeTracker.usecases.UploadImageUseCase import org.greenstand.android.TreeTracker.utilities.md5 @@ -66,12 +67,10 @@ class PlanterUploader( ) ) imageUrl?.let { - planterCheckIn.photoUrl = imageUrl - dao.updatePlanterCheckIn(planterCheckIn) + dao.updatePlanterCheckIn(planterCheckIn.copy(photoUrl = imageUrl)) } } - } - .forEach { it.await() } + }.awaitAll() } } @@ -89,12 +88,10 @@ class PlanterUploader( ) ) imageUrl?.let { - user.photoUrl = imageUrl - dao.updateUser(user) + dao.updateUser(user.copy(photoUrl = imageUrl)) } } - } - .forEach { it.await() } + }.awaitAll() } } diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/SessionTracker.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/SessionTracker.kt index 1c552d7ad..3e8678540 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/SessionTracker.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/models/SessionTracker.kt @@ -19,8 +19,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.greenstand.android.TreeTracker.analytics.ExceptionDataCollector import org.greenstand.android.TreeTracker.dashboard.TreesToSyncHelper -import org.greenstand.android.TreeTracker.database.TreeTrackerDAO -import org.greenstand.android.TreeTracker.database.entity.SessionEntity +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO +import org.greenstand.android.TreeTracker.database.app.entity.SessionEntity import org.greenstand.android.TreeTracker.models.organization.OrgRepo import org.greenstand.android.TreeTracker.models.setupflow.CaptureSetupScopeManager import org.greenstand.android.TreeTracker.preferences.PrefKey @@ -77,8 +77,7 @@ class SessionTracker( suspend fun endSession() { _currentSessionId?.let { id -> val session = dao.getSessionById(id) - session.endTime = timeProvider.currentTime() - dao.updateSession(session) + dao.updateSession(session.copy(endTime = timeProvider.currentTime())) _currentSessionId = null diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/SessionUploader.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/SessionUploader.kt index 049b6e8da..25733bbe1 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/SessionUploader.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/models/SessionUploader.kt @@ -19,7 +19,7 @@ import com.google.gson.Gson import org.greenstand.android.TreeTracker.api.ObjectStorageClient import org.greenstand.android.TreeTracker.api.models.requests.SessionRequest import org.greenstand.android.TreeTracker.api.models.requests.UploadBundle -import org.greenstand.android.TreeTracker.database.TreeTrackerDAO +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO import org.greenstand.android.TreeTracker.utilities.md5 class SessionUploader( diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/TreeTrackerViewModelFactory.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/TreeTrackerViewModelFactory.kt index 32e85509d..489370b05 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/TreeTrackerViewModelFactory.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/models/TreeTrackerViewModelFactory.kt @@ -38,7 +38,7 @@ import org.koin.core.component.get @Suppress("UNCHECKED_CAST") class TreeTrackerViewModelFactory : ViewModelProvider.NewInstanceFactory(), KoinComponent { - override fun create(modelClass: Class): T { + override fun create(modelClass: Class): T { return when { modelClass.isAssignableFrom(UserSelectViewModel::class.java) -> get() as T modelClass.isAssignableFrom(DashboardViewModel::class.java) -> get() as T diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/TreeUploader.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/TreeUploader.kt index 3637d62a6..176441141 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/TreeUploader.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/models/TreeUploader.kt @@ -17,15 +17,16 @@ package org.greenstand.android.TreeTracker.models import com.google.gson.Gson import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.cancel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.isActive import org.greenstand.android.TreeTracker.api.ObjectStorageClient import org.greenstand.android.TreeTracker.api.models.requests.TreeCaptureRequest import org.greenstand.android.TreeTracker.api.models.requests.UploadBundle -import org.greenstand.android.TreeTracker.database.TreeTrackerDAO -import org.greenstand.android.TreeTracker.database.entity.TreeEntity -import org.greenstand.android.TreeTracker.database.legacy.entity.TreeCaptureEntity +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO +import org.greenstand.android.TreeTracker.database.app.entity.TreeEntity +import org.greenstand.android.TreeTracker.database.app.legacy.entity.TreeCaptureEntity import org.greenstand.android.TreeTracker.usecases.CreateTreeRequestParams import org.greenstand.android.TreeTracker.usecases.CreateTreeRequestUseCase import org.greenstand.android.TreeTracker.usecases.UploadImageParams @@ -103,11 +104,9 @@ class TreeUploader( ) ?: throw IllegalStateException("No imageUrl") // Update local tree data with image Url - tree.photoUrl = imageUrl - dao.updateTreeCapture(tree) + dao.updateTreeCapture(tree.copy(photoUrl = imageUrl)) } - } - .forEach { it.await() } + }.awaitAll() } log("Tree Image Upload Completed") } @@ -128,8 +127,7 @@ class TreeUploader( ) ?: throw IllegalStateException("No imageUrl") // Update local tree data with image Url - tree.photoUrl = imageUrl - dao.updateTree(tree) + dao.updateTree(tree.copy(photoUrl = imageUrl)) } } .forEach { it.await() } diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/location/LocationDataCapturer.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/location/LocationDataCapturer.kt index 4c0799849..92fca827c 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/location/LocationDataCapturer.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/models/location/LocationDataCapturer.kt @@ -25,8 +25,8 @@ import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout -import org.greenstand.android.TreeTracker.database.TreeTrackerDAO -import org.greenstand.android.TreeTracker.database.entity.LocationEntity +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO +import org.greenstand.android.TreeTracker.database.app.entity.LocationEntity import org.greenstand.android.TreeTracker.models.ConvergenceConfiguration import org.greenstand.android.TreeTracker.models.ConvergenceStatus import org.greenstand.android.TreeTracker.models.LocationData diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/MessageUploader.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/MessageUploader.kt deleted file mode 100644 index b0b4c5beb..000000000 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/MessageUploader.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2023 Treetracker - * - * 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. - */ -package org.greenstand.android.TreeTracker.models.messages - -import com.google.gson.Gson -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import org.greenstand.android.TreeTracker.api.ObjectStorageClient -import org.greenstand.android.TreeTracker.api.models.requests.UploadBundle -import org.greenstand.android.TreeTracker.models.messages.database.DatabaseConverters -import org.greenstand.android.TreeTracker.models.messages.database.MessagesDAO -import org.greenstand.android.TreeTracker.utilities.md5 -import kotlin.time.ExperimentalTime - -class MessageUploader( - private val objectStorageClient: ObjectStorageClient, - private val messagesDAO: MessagesDAO, - private val gson: Gson, -) { - - @OptIn(ExperimentalTime::class) - suspend fun uploadMessages() { - coroutineScope { - messagesDAO.getMessageIdsToUpload() - .windowed(LIMIT, LIMIT, true) - .map { async { uploadMessageBundle(it) } } - .onEach { it.await() } - } - } - - private suspend fun uploadMessageBundle(messageIdsToUpload: List) { - val messageEntitiesToUpload = messagesDAO.getMessagesByIds(messageIdsToUpload) - val messageRequests = messageEntitiesToUpload.map { messageEntity -> - DatabaseConverters.createMessageRequestFromEntities(messageEntity) - } - - val jsonBundle = gson.toJson( - UploadBundle.createV2( - messages = messageRequests, - ) - ) - val bundleId = jsonBundle.md5() + "_messages" - val messageIds = messageEntitiesToUpload.map { it.id } - - // Update the trees in DB with the bundleId - messagesDAO.updateMessageBundleIds(messageIds, bundleId) - objectStorageClient.uploadBundle(jsonBundle, bundleId) - messagesDAO.markMessagesAsUploaded(messageIds) - } - - companion object { - private const val LIMIT = 50 - } -} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/MessagesRepo.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/MessagesRepo.kt deleted file mode 100644 index 2a06f25e6..000000000 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/MessagesRepo.kt +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright 2023 Treetracker - * - * 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. - */ -package org.greenstand.android.TreeTracker.models.messages - -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import kotlinx.datetime.Instant -import org.greenstand.android.TreeTracker.models.UserRepo -import org.greenstand.android.TreeTracker.models.messages.database.DatabaseConverters -import org.greenstand.android.TreeTracker.models.messages.database.MessagesDAO -import org.greenstand.android.TreeTracker.models.messages.database.entities.MessageEntity -import org.greenstand.android.TreeTracker.models.messages.database.entities.QuestionEntity -import org.greenstand.android.TreeTracker.models.messages.database.entities.SurveyEntity -import org.greenstand.android.TreeTracker.models.messages.network.MessagesApiService -import org.greenstand.android.TreeTracker.models.messages.network.responses.MessageResponse -import org.greenstand.android.TreeTracker.models.messages.network.responses.MessageType -import org.greenstand.android.TreeTracker.models.messages.network.responses.MessagesResponse -import org.greenstand.android.TreeTracker.models.messages.network.responses.QueryResponse -import org.greenstand.android.TreeTracker.utilities.Constants -import org.greenstand.android.TreeTracker.utilities.TimeProvider -import org.greenstand.android.TreeTracker.utils.runInParallel -import timber.log.Timber -import java.util.* - -/** - * For test data, use 'handle2@test', 'handle3@test', or 'handle4@test' as a wallet (email) - */ -class MessagesRepo( - private val apiService: MessagesApiService, - private val userRepo: UserRepo, - private val timeProvider: TimeProvider, - private val messagesDao: MessagesDAO, - private val messageUploader: MessageUploader, -) { - - suspend fun markMessageAsRead(messageId: String) { - messagesDao.markMessageAsRead(listOf(messageId)) - } - - suspend fun markMessagesAsRead(messageIds: List) { - messagesDao.markMessageAsRead(messageIds) - } - - suspend fun saveMessage(wallet: String, to: String, body: String) { - messagesDao.insertMessage( - MessageEntity( - id = UUID.randomUUID().toString(), - wallet = wallet, - type = MessageType.MESSAGE, - from = wallet, - to = to, - subject = null, - body = body, - composedAt = timeProvider.currentTime().toString(), - parentMessageId = null, - videoLink = null, - surveyResponse = null, - shouldUpload = true, - bundleId = null, - isRead = true, - surveyId = null, - isSurveyComplete = null, - ) - ) - } - - suspend fun saveSurveyAnswers(messageId: String, surveyResponse: List) { - val surveyMessage = messagesDao.getMessage(messageId)!! - // make messages point to surveys to have survey and response point to same survey - messagesDao.insertMessage( - MessageEntity( - id = UUID.randomUUID().toString(), - wallet = surveyMessage.to, - type = MessageType.SURVEY_RESPONSE, - from = surveyMessage.to, - to = surveyMessage.from, - subject = null, - body = null, - composedAt = timeProvider.currentTime().toString(), - parentMessageId = null, - videoLink = null, - surveyResponse = surveyResponse, - shouldUpload = true, - bundleId = null, - isRead = true, - surveyId = surveyMessage.surveyId, - isSurveyComplete = true, - ) - ) - messagesDao.markSurveyMessageComplete(surveyMessage.id) - } - - fun getMessageFlow(wallet: String): Flow> { - return messagesDao.getMessagesForWalletFlow(wallet) - .map { messages -> messages.map { convertMessageEntityToMessage(it) } } - } - - fun getDirectMessages( - wallet: String, - otherChatIdentifier: String - ): Flow> { - return messagesDao.getDirectMessagesForWallet(wallet) - .map { messages -> - messages - .map { convertMessageEntityToMessage(it) } - .filterIsInstance() - .filter { (it.from == wallet || it.from == otherChatIdentifier) && (it.to == otherChatIdentifier || it.to == wallet) } - .sortedByDescending { it.composedAt } - } - } - - suspend fun getAnnouncementMessages(id: String): AnnouncementMessage { - return convertMessageEntityToMessage(messagesDao.getMessage(id)!!) as AnnouncementMessage - } - - suspend fun getSurveyMessage(id: String): SurveyMessage { - return convertMessageEntityToMessage(messagesDao.getMessage(id)!!) as SurveyMessage - } - - /** - * When uploading trees, messages will be synced locally by this method - */ - suspend fun syncMessages() = withContext(Dispatchers.IO) { - for (wallet in userRepo.getUserList().map { it.wallet }) { - try { - ensureActive() - fetchMessagesForWallet(wallet) - } catch (e: CancellationException) { - // rethrow cancellation exception - throw e - } catch (e: Exception) { - if (e.localizedMessage == Constants.LOCAL_MSG_ERROR_HTTP404) { - // 404 indicates the user has never had messages before - continue - } else { - Timber.e(e) - } - } - } - - messageUploader.uploadMessages() - } - - private suspend fun fetchMessagesForWallet(wallet: String) = withContext(Dispatchers.IO) { - val lastSyncTime = getLastSyncTime(wallet) - val query = QueryResponse( - handle = wallet, - limit = 100, - offset = 0, - total = -1, - ) - - val result = fetchMessagesFromServerAndSaveInDb(query.offset, query.limit, wallet, lastSyncTime) - if (result.query.total == 0) return@withContext - - var offset = result.query.offset - val limit = result.query.limit - val total = result.query.total - - val asyncExecutions = mutableListOf>() - - while (total >= limit + offset) { - ensureActive() - offset += limit - - // store this offset value in-case it gets changed in the next iteration. - val _offset = offset - - asyncExecutions += async { - fetchMessagesFromServerAndSaveInDb(_offset, limit, wallet, lastSyncTime) // pass _offset instead of offset. - } - } - - asyncExecutions.awaitAll() - } - - private suspend fun fetchMessagesFromServerAndSaveInDb( - offset: Int, - limit: Int, - wallet: String, - lastSyncTime: String - ): MessagesResponse = withContext(Dispatchers.IO) { - val result = apiService.getMessages( - wallet = wallet, - lastSyncTime = lastSyncTime, - offset = offset, - limit = limit, - ) - - if (result.query.total == 0) return@withContext result - - result.messages.runInParallel { - saveMessageResponse(wallet, it.copy()) - } - - return@withContext result - } - - private suspend fun saveMessageResponse(wallet: String, message: MessageResponse): Unit = withContext(Dispatchers.IO) { - if (message.type == MessageType.SURVEY_RESPONSE) return@withContext - - val messageEntity = with(message) { - MessageEntity( - id = id, - wallet = wallet, - type = type, - from = from, - to = to, - subject = subject, - body = body, - composedAt = composedAt, - parentMessageId = parentMessageId, - videoLink = videoLink, - surveyResponse = null, - shouldUpload = false, - bundleId = null, - isRead = false, - surveyId = survey?.id, - isSurveyComplete = survey?.let { false }, - ) - } - messagesDao.insertMessage(messageEntity) - - // If there is no survey, don't continue on - message.survey ?: return@withContext - - // If survey exists, we'll reuse it - if (messagesDao.getSurvey(message.survey.id) != null) { - return@withContext - } - - val surveyEntity = with(message.survey) { - SurveyEntity( - id = id, - title = title, - ) - } - messagesDao.insertSurvey(surveyEntity) - - message.survey.questions.map { question -> - val questionEntity = QuestionEntity( - surveyId = message.survey.id, - prompt = question.prompt, - choices = question.choices, - ) - messagesDao.insertQuestion(questionEntity) - } - } - - private suspend fun convertMessageEntityToMessage(messageEntity: MessageEntity): Message { - return messagesDao.getSurvey(messageEntity.surveyId)?.let { surveyEntity -> - val questionEntities = messagesDao.getQuestionsForSurvey(surveyEntity.id) - DatabaseConverters.createMessageFromEntities(messageEntity, surveyEntity, questionEntities) - } ?: DatabaseConverters.createMessageFromEntities(messageEntity, null, null) - } - - private suspend fun getLastSyncTime(wallet: String): String { - return messagesDao.getLatestSyncTimeForWallet(wallet) - ?: Instant.fromEpochMilliseconds(0).toString() - } - - suspend fun checkForUnreadMessages(): Boolean { - return messagesDao.getUnreadMessagesCount() >= 1 - } -} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/MessageDatabase.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/MessageDatabase.kt deleted file mode 100644 index 90e50ccc9..000000000 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/MessageDatabase.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2023 Treetracker - * - * 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. - */ -package org.greenstand.android.TreeTracker.models.messages.database - -import android.content.Context -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import org.greenstand.android.TreeTracker.database.Converters -import org.greenstand.android.TreeTracker.models.messages.database.entities.MessageEntity -import org.greenstand.android.TreeTracker.models.messages.database.entities.QuestionEntity -import org.greenstand.android.TreeTracker.models.messages.database.entities.SurveyEntity - -@Database( - version = 1, - exportSchema = true, - entities = [ - MessageEntity::class, - SurveyEntity::class, - QuestionEntity::class, - ], -) -@TypeConverters(Converters::class) -abstract class MessageDatabase : RoomDatabase() { - - abstract fun messagesDao(): MessagesDAO - - companion object { - - private var INSTANCE: MessageDatabase? = null - - fun getInstance(context: Context): MessageDatabase { - if (INSTANCE == null) { - synchronized(MessageDatabase::class) { - INSTANCE = Room.databaseBuilder( - context.applicationContext, - MessageDatabase::class.java, - DB_NAME - ).build() - } - } - return INSTANCE!! - } - - private const val DB_NAME = "treetracker.messages.db" - } -} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/MessagesDAO.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/MessagesDAO.kt deleted file mode 100644 index 38a761d75..000000000 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/database/MessagesDAO.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2023 Treetracker - * - * 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. - */ -package org.greenstand.android.TreeTracker.models.messages.database - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import kotlinx.coroutines.flow.Flow -import org.greenstand.android.TreeTracker.models.messages.database.entities.MessageEntity -import org.greenstand.android.TreeTracker.models.messages.database.entities.QuestionEntity -import org.greenstand.android.TreeTracker.models.messages.database.entities.SurveyEntity - -@Dao -interface MessagesDAO { - - /** - * Messages - */ - - @Query("DELETE FROM messages WHERE id = :id") - suspend fun deleteMessage(id: String) - - @Insert(onConflict = OnConflictStrategy.ABORT) - suspend fun insertMessage(messageEntity: MessageEntity): Long - - @Query("SELECT * FROM messages WHERE id = :id") - suspend fun getMessage(id: String): MessageEntity? - - @Query("SELECT MAX(composed_at) FROM messages WHERE wallet = :wallet") - suspend fun getLatestSyncTimeForWallet(wallet: String): String? - - @Query("SELECT * FROM messages WHERE wallet = :wallet") - suspend fun getMessagesForWallet(wallet: String): List - - @Query("SELECT * FROM messages WHERE wallet = :wallet") - fun getMessagesForWalletFlow(wallet: String): Flow> - - @Query("SELECT * FROM messages WHERE wallet = :wallet AND type = 'MESSAGE'") - fun getDirectMessagesForWallet(wallet: String): Flow> - - @Query("SELECT * FROM messages WHERE wallet = :wallet AND type = 'ANNOUNCE'") - fun getAnnouncementMessagesForWallet(wallet: String): Flow> - - @Query("UPDATE messages SET bundle_id = :bundleId WHERE id IN (:ids)") - suspend fun updateMessageBundleIds(ids: List, bundleId: String) - - @Query("SELECT * FROM messages WHERE should_upload = 1") - suspend fun getMessagesToUpload(): List - - @Query("SELECT id FROM messages WHERE should_upload = 1") - suspend fun getMessageIdsToUpload(): List - - @Query("SELECT * FROM messages WHERE id IN (:ids)") - suspend fun getMessagesByIds(ids: List): List - - @Query("UPDATE messages SET should_upload = 0 WHERE id IN (:ids)") - suspend fun markMessagesAsUploaded(ids: List) - - @Query("UPDATE messages SET is_read = 1 WHERE id IN (:id)") - suspend fun markMessageAsRead(id: List) - - @Query("UPDATE messages SET is_survey_complete = 1 WHERE id = :id") - suspend fun markSurveyMessageComplete(id: String?) - - @Query("SELECT COUNT(*) FROM messages WHERE is_read = 0") - suspend fun getUnreadMessagesCount(): Int - - @Query("SELECT COUNT(*) FROM messages WHERE wallet = (:wallet) AND is_read = 0") - suspend fun getUnreadMessageCountForWallet(wallet: String): Int - - /** - * Surveys - */ - - @Query("DELETE FROM surveys WHERE id = :id") - suspend fun deleteSurvey(id: String) - - @Insert(onConflict = OnConflictStrategy.ABORT) - suspend fun insertSurvey(surveyEntity: SurveyEntity): Long - - @Query("SELECT * FROM surveys WHERE id = :id") - suspend fun getSurvey(id: String?): SurveyEntity? - - /** - * Questions - */ - - @Query("DELETE FROM questions WHERE _id IN (:ids)") - suspend fun deleteQuestions(ids: List) - - @Insert(onConflict = OnConflictStrategy.ABORT) - suspend fun insertQuestion(questionEntity: QuestionEntity): Long - - @Query("SELECT * FROM questions WHERE survey_id = :surveyId") - suspend fun getQuestionsForSurvey(surveyId: String): List -} \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/organization/OrgRepo.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/organization/OrgRepo.kt index d70deed6d..3b0113092 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/organization/OrgRepo.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/models/organization/OrgRepo.kt @@ -18,8 +18,8 @@ package org.greenstand.android.TreeTracker.models.organization import com.google.gson.Gson import com.google.gson.JsonParser import com.google.gson.reflect.TypeToken -import org.greenstand.android.TreeTracker.database.TreeTrackerDAO -import org.greenstand.android.TreeTracker.database.entity.OrganizationEntity +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO +import org.greenstand.android.TreeTracker.database.app.entity.OrganizationEntity import org.greenstand.android.TreeTracker.models.NavRoute import org.greenstand.android.TreeTracker.preferences.PrefKey import org.greenstand.android.TreeTracker.preferences.PrefKeys diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/user/UserRepo.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/user/UserRepo.kt index ea28f6e0e..fa139420e 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/user/UserRepo.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/models/user/UserRepo.kt @@ -21,20 +21,20 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import org.greenstand.android.TreeTracker.analytics.Analytics import org.greenstand.android.TreeTracker.analytics.ExceptionDataCollector -import org.greenstand.android.TreeTracker.database.TreeTrackerDAO -import org.greenstand.android.TreeTracker.database.entity.UserEntity +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO +import org.greenstand.android.TreeTracker.database.app.entity.UserEntity +import org.greenstand.android.TreeTracker.database.messages.dao.MessageDao import org.greenstand.android.TreeTracker.models.location.LocationUpdateManager -import org.greenstand.android.TreeTracker.models.messages.database.MessagesDAO import org.greenstand.android.TreeTracker.models.user.User import org.greenstand.android.TreeTracker.utilities.TimeProvider -import java.util.* +import java.util.UUID class UserRepo( private val locationUpdateManager: LocationUpdateManager, private val dao: TreeTrackerDAO, private val analytics: Analytics, private val timeProvider: TimeProvider, - private val messagesDao: MessagesDAO, + private val messageDao: MessageDao, private val exceptionDataCollector: ExceptionDataCollector, ) { @@ -56,7 +56,7 @@ class UserRepo( } suspend fun checkForUnreadMessagesPerUser(wallet: String): Boolean { - return messagesDao.getUnreadMessageCountForWallet(wallet) >= 1 + return messageDao.haveUnreadMessageCountForWallet(wallet) } suspend fun getPowerUser(): User? { diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/orgpicker/AddOrgScreen.kt b/app/src/main/java/org/greenstand/android/TreeTracker/orgpicker/AddOrgScreen.kt index 85095b22a..fa42dc4bf 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/orgpicker/AddOrgScreen.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/orgpicker/AddOrgScreen.kt @@ -74,7 +74,9 @@ fun AddOrgScreen(viewModel: AddOrgViewModel = viewModel(factory = LocalViewModel } ) { Column( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .padding(it), horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(modifier = Modifier.height(120.dp)) @@ -82,7 +84,12 @@ fun AddOrgScreen(viewModel: AddOrgViewModel = viewModel(factory = LocalViewModel value = state.orgName, padding = PaddingValues(4.dp), onValueChange = { updatedName -> viewModel.updateOrgName(updatedName) }, - placeholder = { Text(text = stringResource(id = R.string.organization), color = Color.White) }, + placeholder = { + Text( + text = stringResource(id = R.string.organization), + color = Color.White + ) + }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Text, imeAction = ImeAction.Go, diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/orgpicker/OrgPickerScreen.kt b/app/src/main/java/org/greenstand/android/TreeTracker/orgpicker/OrgPickerScreen.kt index bfae39383..3986ee89b 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/orgpicker/OrgPickerScreen.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/orgpicker/OrgPickerScreen.kt @@ -22,8 +22,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.GridCells -import androidx.compose.foundation.lazy.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items import androidx.compose.material.Scaffold import androidx.compose.material.Text @@ -85,8 +86,10 @@ fun OrgPickerScreen(viewModel: OrgPickerViewModel = viewModel(factory = LocalVie }, ) { LazyVerticalGrid( - cells = GridCells.Fixed(2), - modifier = Modifier.fillMaxSize(), + columns = GridCells.Fixed(2), + modifier = Modifier + .fillMaxSize() + .padding(it), horizontalArrangement = Arrangement.SpaceEvenly, verticalArrangement = Arrangement.Center ) { diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/permissions/Permissions.kt b/app/src/main/java/org/greenstand/android/TreeTracker/permissions/Permissions.kt index d09adf0fd..02cbd35d6 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/permissions/Permissions.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/permissions/Permissions.kt @@ -35,7 +35,9 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState +import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberMultiplePermissionsState +import com.google.accompanist.permissions.shouldShowRationale import org.greenstand.android.TreeTracker.R import org.greenstand.android.TreeTracker.permissions.PermissionItemsState import org.greenstand.android.TreeTracker.permissions.PermissionViewModel @@ -77,8 +79,8 @@ fun PermissionRequest( when (perm.permission) { Manifest.permission.CAMERA -> { when { - perm.hasPermission -> {} - perm.shouldShowRationale -> { + perm.status.isGranted -> {} + perm.status.shouldShowRationale -> { CustomDialog( title = stringResource(R.string.accept_camera_permission_header), textContent = stringResource(R.string.accept_camera_permission_message), @@ -98,15 +100,17 @@ fun PermissionRequest( } Manifest.permission.ACCESS_FINE_LOCATION -> { when { - perm.hasPermission -> { + perm.status.isGranted -> { viewModel.isLocationEnabled() if (state.isLocationEnabled == false) { enableLocation() } } - perm.shouldShowRationale -> { + + perm.status.shouldShowRationale -> { LocationRationaleDialog(navController = navController, perm = perm) } + else -> { PermissionDeniedPermanentlyDialog(navController) } @@ -114,15 +118,17 @@ fun PermissionRequest( } Manifest.permission.ACCESS_COARSE_LOCATION -> { when { - perm.hasPermission -> { + perm.status.isGranted -> { viewModel.isLocationEnabled() if (state.isLocationEnabled == false) { enableLocation() } } - perm.shouldShowRationale -> { + + perm.status.shouldShowRationale -> { LocationRationaleDialog(navController = navController, perm = perm) } + else -> { PermissionDeniedPermanentlyDialog(navController) } diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/sessionnote/SessionNoteScreen.kt b/app/src/main/java/org/greenstand/android/TreeTracker/sessionnote/SessionNoteScreen.kt index e9ed697ac..d8dfbb15e 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/sessionnote/SessionNoteScreen.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/sessionnote/SessionNoteScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.Scaffold @@ -67,7 +68,9 @@ fun SessionNoteScreen(viewModel: SessionNoteViewModel = viewModel(factory = Loca } ) { Column( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .padding(it), horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(modifier = Modifier.height(120.dp)) @@ -75,7 +78,12 @@ fun SessionNoteScreen(viewModel: SessionNoteViewModel = viewModel(factory = Loca value = state.note, padding = PaddingValues(4.dp), onValueChange = { updatedNote -> viewModel.updateNote(updatedNote) }, - placeholder = { Text(text = stringResource(id = R.string.add_note_to_session), color = Color.White) }, + placeholder = { + Text( + text = stringResource(id = R.string.add_note_to_session), + color = Color.White + ) + }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Text, imeAction = ImeAction.Go, diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/signup/CredentialEntryView.kt b/app/src/main/java/org/greenstand/android/TreeTracker/signup/CredentialEntryView.kt index 2824cf8ee..a42e3506b 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/signup/CredentialEntryView.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/signup/CredentialEntryView.kt @@ -124,6 +124,7 @@ fun CredentialEntryView(viewModel: SignupViewModel, state: SignUpState) { ) } } + is Credential.Phone -> { scope.launch { snackBarHostState.showSnackbar( @@ -148,6 +149,7 @@ fun CredentialEntryView(viewModel: SignupViewModel, state: SignUpState) { modifier = Modifier .fillMaxWidth() .fillMaxHeight(0.6f) + .padding(it) .verticalScroll(rememberScrollState()) ) { val navigateToWebPage: () -> Unit = { @@ -169,8 +171,23 @@ fun CredentialEntryView(viewModel: SignupViewModel, state: SignUpState) { } when (state.credential) { - is Credential.Email -> EmailTextField(state, viewModel, focusRequester, snackBarHostState, scope, context) - is Credential.Phone -> PhoneTextField(state, viewModel, focusRequester, snackBarHostState, scope, context) + is Credential.Email -> EmailTextField( + state, + viewModel, + focusRequester, + snackBarHostState, + scope, + context + ) + + is Credential.Phone -> PhoneTextField( + state, + viewModel, + focusRequester, + snackBarHostState, + scope, + context + ) } ViewWebMapText(isVisible = state.isInternetAvailable, onClick = navigateToWebPage) @@ -220,13 +237,25 @@ fun ViewWebMapText(isVisible: Boolean, onClick: () -> Unit) { } @Composable -private fun EmailTextField(state: SignUpState, viewModel: SignupViewModel, focusRequester: FocusRequester, snackBarHostState: SnackbarHostState, scope: CoroutineScope, context: Context) { +private fun EmailTextField( + state: SignUpState, + viewModel: SignupViewModel, + focusRequester: FocusRequester, + snackBarHostState: SnackbarHostState, + scope: CoroutineScope, + context: Context +) { val focusManager = LocalFocusManager.current BorderedTextField( value = state.email ?: "", padding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 8.dp, top = 8.dp), onValueChange = { updatedEmail -> viewModel.updateEmail(updatedEmail) }, - placeholder = { Text(text = stringResource(id = R.string.email_placeholder), color = Color.White) }, + placeholder = { + Text( + text = stringResource(id = R.string.email_placeholder), + color = Color.White + ) + }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Email, imeAction = ImeAction.Go, @@ -254,13 +283,25 @@ private fun EmailTextField(state: SignUpState, viewModel: SignupViewModel, focus } @Composable -private fun PhoneTextField(state: SignUpState, viewModel: SignupViewModel, focusRequester: FocusRequester, snackBarHostState: SnackbarHostState, scope: CoroutineScope, context: Context) { +private fun PhoneTextField( + state: SignUpState, + viewModel: SignupViewModel, + focusRequester: FocusRequester, + snackBarHostState: SnackbarHostState, + scope: CoroutineScope, + context: Context +) { val focusManager = LocalFocusManager.current BorderedTextField( value = state.phone ?: "", padding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 8.dp, top = 8.dp), onValueChange = { updatedPhone -> viewModel.updatePhone(updatedPhone) }, - placeholder = { Text(text = stringResource(id = R.string.phone_placeholder), color = Color.White) }, + placeholder = { + Text( + text = stringResource(id = R.string.phone_placeholder), + color = Color.White + ) + }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Phone, imeAction = ImeAction.Go, diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/signup/NameEntryView.kt b/app/src/main/java/org/greenstand/android/TreeTracker/signup/NameEntryView.kt index 110d7b348..c16a05fdd 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/signup/NameEntryView.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/signup/NameEntryView.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -108,6 +109,7 @@ fun NameEntryView(viewModel: SignupViewModel, state: SignUpState) { modifier = Modifier .fillMaxWidth() .fillMaxHeight(0.6f) + .padding(it) .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/splash/SplashScreenViewModel.kt b/app/src/main/java/org/greenstand/android/TreeTracker/splash/SplashScreenViewModel.kt index 32c727681..bc1938650 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/splash/SplashScreenViewModel.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/splash/SplashScreenViewModel.kt @@ -21,11 +21,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.greenstand.android.TreeTracker.analytics.ExceptionDataCollector import org.greenstand.android.TreeTracker.dashboard.TreesToSyncHelper +import org.greenstand.android.TreeTracker.domain.repository.MessageRepository import org.greenstand.android.TreeTracker.models.DeviceConfigUpdater import org.greenstand.android.TreeTracker.models.SessionTracker import org.greenstand.android.TreeTracker.models.UserRepo import org.greenstand.android.TreeTracker.models.location.LocationDataCapturer -import org.greenstand.android.TreeTracker.models.messages.MessagesRepo import org.greenstand.android.TreeTracker.models.organization.OrgRepo import org.greenstand.android.TreeTracker.usecases.CheckForInternetUseCase import org.koin.core.component.KoinComponent @@ -38,7 +38,7 @@ class SplashScreenViewModel( private val sessionTracker: SessionTracker, private val deviceConfigUpdater: DeviceConfigUpdater, private val locationDataCapturer: LocationDataCapturer, - private val messagesRepo: MessagesRepo, + private val messageRepository: MessageRepository, private val checkForInternetUseCase: CheckForInternetUseCase, private val orgRepo: OrgRepo, private val exceptionDataCollector: ExceptionDataCollector, @@ -51,7 +51,7 @@ class SplashScreenViewModel( orgJsonString?.let { orgRepo.addOrgFromJsonString(it) } if (checkForInternetUseCase.execute(Unit)) { - messagesRepo.syncMessages() + messageRepository.syncMessages() } userRepo.getPowerUser()?.let { @@ -78,7 +78,7 @@ class SplashScreenViewModel( class SplashScreenViewModelFactory(private val orgJsonString: String?) : ViewModelProvider.Factory, KoinComponent { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { + override fun create(modelClass: Class): T { return SplashScreenViewModel(orgJsonString, get(), get(), get(), get(), get(), get(), get(), get(), get()) as T } } \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/treeheight/TreeHeightColourSelection.kt b/app/src/main/java/org/greenstand/android/TreeTracker/treeheight/TreeHeightColourSelection.kt index 369542f08..2d491f421 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/treeheight/TreeHeightColourSelection.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/treeheight/TreeHeightColourSelection.kt @@ -87,7 +87,9 @@ fun TreeHeightScreen() { } ) { LazyColumn( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(it), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(20.dp), contentPadding = PaddingValues(start = 8.dp, end = 8.dp, top = 30.dp) diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CreateFakeTreesUseCase.kt b/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CreateFakeTreesUseCase.kt index 05c78c359..68bcae954 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CreateFakeTreesUseCase.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CreateFakeTreesUseCase.kt @@ -18,9 +18,9 @@ package org.greenstand.android.TreeTracker.usecases import android.content.Context import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.greenstand.android.TreeTracker.database.TreeTrackerDAO -import org.greenstand.android.TreeTracker.database.legacy.entity.PlanterCheckInEntity -import org.greenstand.android.TreeTracker.database.legacy.entity.PlanterInfoEntity +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO +import org.greenstand.android.TreeTracker.database.app.legacy.entity.PlanterCheckInEntity +import org.greenstand.android.TreeTracker.database.app.legacy.entity.PlanterInfoEntity import org.greenstand.android.TreeTracker.models.SessionTracker import org.greenstand.android.TreeTracker.models.Tree import org.greenstand.android.TreeTracker.models.location.LocationUpdateManager diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CreateLegacyTreeUseCase.kt b/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CreateLegacyTreeUseCase.kt index 6e92f7345..2e1d502af 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CreateLegacyTreeUseCase.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CreateLegacyTreeUseCase.kt @@ -17,9 +17,9 @@ package org.greenstand.android.TreeTracker.usecases import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.greenstand.android.TreeTracker.database.TreeTrackerDAO -import org.greenstand.android.TreeTracker.database.legacy.entity.TreeAttributeEntity -import org.greenstand.android.TreeTracker.database.legacy.entity.TreeCaptureEntity +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO +import org.greenstand.android.TreeTracker.database.app.legacy.entity.TreeAttributeEntity +import org.greenstand.android.TreeTracker.database.app.legacy.entity.TreeCaptureEntity import org.greenstand.android.TreeTracker.models.Tree import org.greenstand.android.TreeTracker.utilities.TimeProvider import timber.log.Timber @@ -46,10 +46,10 @@ class CreateLegacyTreeUseCase( accuracy = 0.0, // accuracy is a legacy remnant and not used. Pending table cleanup createAt = timeProvider.currentTime().epochSeconds, // legacy bulk pack uses seconds, not milliseconds ) - val attributeEntitites = params.tree.treeCaptureAttributes().map { + val attributeEntities = params.tree.treeCaptureAttributes().map { TreeAttributeEntity(it.key, it.value, -1) }.toList() Timber.d("Inserting TreeCapture entity $entity") - dao.insertTreeWithAttributes(entity, attributeEntitites) + dao.insertTreeWithAttributes(entity, attributeEntities) } } \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CreateTreeRequestUseCase.kt b/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CreateTreeRequestUseCase.kt index 998b6bf7a..9df912468 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CreateTreeRequestUseCase.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CreateTreeRequestUseCase.kt @@ -17,7 +17,7 @@ package org.greenstand.android.TreeTracker.usecases import org.greenstand.android.TreeTracker.api.models.requests.AttributeRequest import org.greenstand.android.TreeTracker.api.models.requests.NewTreeRequest -import org.greenstand.android.TreeTracker.database.TreeTrackerDAO +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO import org.greenstand.android.TreeTracker.utilities.DeviceUtils data class CreateTreeRequestParams( @@ -34,11 +34,8 @@ class CreateTreeRequestUseCase(private val dao: TreeTrackerDAO) : val planterCheckIn = dao.getPlanterCheckInById(treeCapture.planterCheckInId) val planterInfo = dao.getPlanterInfoById(planterCheckIn.planterInfoId) ?: throw IllegalStateException("No Planter Info") - - val attributesList = dao.getTreeAttributeByTreeCaptureId(treeCapture.id) - val attributesRequest = mutableListOf() - for (attribute in attributesList) { - attributesRequest.add(AttributeRequest(key = attribute.key, value = attribute.value)) + val attributesRequest = dao.getTreeAttributeByTreeCaptureId(treeCapture.id).map { + AttributeRequest(key = it.key, value = it.value) } return NewTreeRequest( diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CreateTreeUseCase.kt b/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CreateTreeUseCase.kt index d0731efe0..6cedcd9fb 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CreateTreeUseCase.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CreateTreeUseCase.kt @@ -18,8 +18,8 @@ package org.greenstand.android.TreeTracker.usecases import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.greenstand.android.TreeTracker.analytics.Analytics -import org.greenstand.android.TreeTracker.database.TreeTrackerDAO -import org.greenstand.android.TreeTracker.database.entity.TreeEntity +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO +import org.greenstand.android.TreeTracker.database.app.entity.TreeEntity import org.greenstand.android.TreeTracker.models.Tree import org.greenstand.android.TreeTracker.utilities.TimeProvider import timber.log.Timber diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/usecases/SyncDataUseCase.kt b/app/src/main/java/org/greenstand/android/TreeTracker/usecases/SyncDataUseCase.kt index 655f9dd26..dc27078f8 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/usecases/SyncDataUseCase.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/usecases/SyncDataUseCase.kt @@ -19,12 +19,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext -import org.greenstand.android.TreeTracker.database.TreeTrackerDAO +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO +import org.greenstand.android.TreeTracker.domain.repository.MessageRepository import org.greenstand.android.TreeTracker.models.DeviceConfigUploader import org.greenstand.android.TreeTracker.models.PlanterUploader import org.greenstand.android.TreeTracker.models.SessionUploader import org.greenstand.android.TreeTracker.models.TreeUploader -import org.greenstand.android.TreeTracker.models.messages.MessagesRepo import timber.log.Timber import kotlin.coroutines.coroutineContext @@ -35,7 +35,7 @@ class SyncDataUseCase( private val planterUploader: PlanterUploader, private val sessionUploader: SessionUploader, private val deviceConfigUploader: DeviceConfigUploader, - private val messagesRepo: MessagesRepo, + private val messageRepository: MessageRepository, ) : UseCase() { private val TAG = "SyncDataUseCase" @@ -45,7 +45,7 @@ class SyncDataUseCase( withContext(Dispatchers.IO) { executeIfContextActive("Message Sync") { - messagesRepo.syncMessages() + messageRepository.syncMessages() } executeIfContextActive("Device Config Upload") { diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/usecases/UploadLocationDataUseCase.kt b/app/src/main/java/org/greenstand/android/TreeTracker/usecases/UploadLocationDataUseCase.kt index 3b945e642..168de2205 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/usecases/UploadLocationDataUseCase.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/usecases/UploadLocationDataUseCase.kt @@ -22,7 +22,7 @@ import kotlinx.coroutines.withContext import org.greenstand.android.TreeTracker.api.ObjectStorageClient import org.greenstand.android.TreeTracker.api.models.requests.TracksRequest import org.greenstand.android.TreeTracker.api.models.requests.UploadBundle -import org.greenstand.android.TreeTracker.database.TreeTrackerDAO +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO import org.greenstand.android.TreeTracker.models.LocationData import org.greenstand.android.TreeTracker.utilities.md5 import timber.log.Timber diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/userselect/UserSelect.kt b/app/src/main/java/org/greenstand/android/TreeTracker/userselect/UserSelect.kt index bf18ebe05..d1fa25737 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/userselect/UserSelect.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/userselect/UserSelect.kt @@ -18,8 +18,9 @@ package org.greenstand.android.TreeTracker.userselect import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.GridCells -import androidx.compose.foundation.lazy.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items import androidx.compose.material.Scaffold import androidx.compose.runtime.Composable @@ -94,7 +95,7 @@ fun UserSelect( } ) { LazyVerticalGrid( - cells = GridCells.Fixed(2), + columns = GridCells.Fixed(2), modifier = Modifier.padding(it), // Padding for bottom bar. contentPadding = PaddingValues(start = 8.dp, end = 8.dp, top = 10.dp) ) { diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/utils/Utils.kt b/app/src/main/java/org/greenstand/android/TreeTracker/utils/Utils.kt index e3d44d236..d762d00b1 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/utils/Utils.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/utils/Utils.kt @@ -18,15 +18,12 @@ package org.greenstand.android.TreeTracker.utils import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext suspend fun List.runInParallel( dispatcher: CoroutineDispatcher = Dispatchers.IO, action: suspend (Data) -> Unit ) = withContext(dispatcher) { - this@runInParallel.map { - async { action(it) } - }.forEach { - it.await() - } + map { async { action(it) } }.awaitAll() } \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/walletselect/WalletSelectScreen.kt b/app/src/main/java/org/greenstand/android/TreeTracker/walletselect/WalletSelectScreen.kt index 8bed89070..19fd0e176 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/walletselect/WalletSelectScreen.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/walletselect/WalletSelectScreen.kt @@ -107,6 +107,7 @@ fun WalletSelectScreen( LazyColumn( modifier = Modifier .fillMaxSize() + .padding(it) .padding( start = 10.dp, top = 10.dp, diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/walletselect/addwallet/AddWalletScreen.kt b/app/src/main/java/org/greenstand/android/TreeTracker/walletselect/addwallet/AddWalletScreen.kt index c78c64672..14e30496b 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/walletselect/addwallet/AddWalletScreen.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/walletselect/addwallet/AddWalletScreen.kt @@ -78,6 +78,7 @@ fun AddWalletScreen( contentAlignment = Alignment.TopCenter, modifier = Modifier .fillMaxSize() + .padding(it) .padding(top = 120.dp) ) { BorderedTextField( diff --git a/app/src/test/java/org/greenstand/android/TreeTracker/dashboard/DashboardViewModelTest.kt b/app/src/test/java/org/greenstand/android/TreeTracker/dashboard/DashboardViewModelTest.kt index 49986d09b..c2703faa6 100644 --- a/app/src/test/java/org/greenstand/android/TreeTracker/dashboard/DashboardViewModelTest.kt +++ b/app/src/test/java/org/greenstand/android/TreeTracker/dashboard/DashboardViewModelTest.kt @@ -24,9 +24,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import org.greenstand.android.TreeTracker.MainCoroutineRule import org.greenstand.android.TreeTracker.analytics.Analytics -import org.greenstand.android.TreeTracker.database.TreeTrackerDAO +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO +import org.greenstand.android.TreeTracker.domain.repository.MessageRepository import org.greenstand.android.TreeTracker.models.location.LocationDataCapturer -import org.greenstand.android.TreeTracker.models.messages.MessagesRepo import org.greenstand.android.TreeTracker.models.organization.OrgRepo import org.greenstand.android.TreeTracker.usecases.CheckForInternetUseCase import org.greenstand.android.TreeTracker.utils.FakeFileGenerator @@ -60,7 +60,7 @@ class DashboardViewModelTest { @MockK(relaxed = true) private lateinit var orgRepo: OrgRepo @MockK(relaxed = true) - private lateinit var messagesRepo: MessagesRepo + private lateinit var messageRepository: MessageRepository @MockK(relaxed = true) private lateinit var checkForInternetUseCase: CheckForInternetUseCase @MockK(relaxed = true) @@ -76,10 +76,10 @@ class DashboardViewModelTest { coEvery { dao.getNonUploadedLegacyTreeCaptureImageCount() } returns 2 coEvery { dao.getNonUploadedTreeImageCount() } returns 4 coEvery { checkForInternetUseCase.execute(Unit) } returns true - coEvery { messagesRepo.syncMessages() } just Runs + coEvery { messageRepository.syncMessages() } just Runs coEvery { treesToSyncHelper.getTreeCountToSync() } returns 6 coEvery { orgRepo.getOrgs() } returns FakeFileGenerator.fakeOrganizationList - coEvery { messagesRepo.checkForUnreadMessages() } returns false + coEvery { messageRepository.haveUnreadMessages() } returns false every { workManager.enqueueUniqueWork(any(), any(), any()) } returns mockk() testSubject = DashboardViewModel( dao = dao, @@ -87,7 +87,7 @@ class DashboardViewModelTest { analytics = analytics, treesToSyncHelper = treesToSyncHelper, orgRepo = orgRepo, - messagesRepo = messagesRepo, + messageRepository = messageRepository, checkForInternetUseCase = checkForInternetUseCase, locationDataCapturer = locationDataCapturer ) @@ -95,15 +95,15 @@ class DashboardViewModelTest { @Test fun `syncMessages should call syncMessages on messagesRepo if there is internet connection`() = runBlocking { coEvery { checkForInternetUseCase.execute(Unit) } returns true - coEvery { messagesRepo.syncMessages() } just Runs + coEvery { messageRepository.syncMessages() } just Runs testSubject.syncMessages() - coVerify { messagesRepo.syncMessages() } + coVerify { messageRepository.syncMessages() } } @Test fun `syncMessages should not call syncMessages on messagesRepo if there is no internet connection`() = runBlocking { coEvery { checkForInternetUseCase.execute(Unit) } returns false testSubject.syncMessages() - coVerify(exactly = 0) { messagesRepo.syncMessages() } + coVerify(exactly = 0) { messageRepository.syncMessages() } } @Test fun `updateData should update the state with correct values querying totalTreesToSync`() = runBlocking { diff --git a/app/src/test/java/org/greenstand/android/TreeTracker/database/TreeTrackerDaoTest.kt b/app/src/test/java/org/greenstand/android/TreeTracker/database/TreeTrackerDaoTest.kt index 1527e117d..2326f46ce 100644 --- a/app/src/test/java/org/greenstand/android/TreeTracker/database/TreeTrackerDaoTest.kt +++ b/app/src/test/java/org/greenstand/android/TreeTracker/database/TreeTrackerDaoTest.kt @@ -21,6 +21,8 @@ import androidx.test.core.app.ApplicationProvider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking +import org.greenstand.android.TreeTracker.database.app.AppDatabase +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO import org.greenstand.android.TreeTracker.utils.* import org.junit.After import org.junit.Assert.* diff --git a/app/src/test/java/org/greenstand/android/TreeTracker/models/messages/MessagesRepoTest.kt b/app/src/test/java/org/greenstand/android/TreeTracker/domain/repository/MessageRepositoryTest.kt similarity index 76% rename from app/src/test/java/org/greenstand/android/TreeTracker/models/messages/MessagesRepoTest.kt rename to app/src/test/java/org/greenstand/android/TreeTracker/domain/repository/MessageRepositoryTest.kt index 887a408e5..9c56293d0 100644 --- a/app/src/test/java/org/greenstand/android/TreeTracker/models/messages/MessagesRepoTest.kt +++ b/app/src/test/java/org/greenstand/android/TreeTracker/domain/repository/MessageRepositoryTest.kt @@ -13,20 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.greenstand.android.TreeTracker.models.messages +package org.greenstand.android.TreeTracker.domain.repository import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.google.gson.Gson import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.* import org.greenstand.android.TreeTracker.MainCoroutineRule +import org.greenstand.android.TreeTracker.api.ObjectStorageClient +import org.greenstand.android.TreeTracker.data.repository.MessageRepositoryImpl +import org.greenstand.android.TreeTracker.database.messages.dao.MessageDao +import org.greenstand.android.TreeTracker.database.messages.entity.MessageEntity import org.greenstand.android.TreeTracker.models.UserRepo -import org.greenstand.android.TreeTracker.models.messages.database.MessagesDAO -import org.greenstand.android.TreeTracker.models.messages.database.entities.MessageEntity import org.greenstand.android.TreeTracker.models.messages.network.MessagesApiService import org.greenstand.android.TreeTracker.models.messages.network.responses.* import org.greenstand.android.TreeTracker.models.user.User @@ -36,9 +38,13 @@ import org.junit.rules.TestRule import org.junit.runner.RunWith import org.junit.runners.JUnit4 +/** + * Doesn't pass https://github.com/mockk/mockk/issues/957 + */ + @OptIn(ExperimentalCoroutinesApi::class) @RunWith(JUnit4::class) -class MessagesRepoTest { +class MessageRepositoryTest { @get:Rule val rule: TestRule = InstantTaskExecutorRule() @@ -47,33 +53,43 @@ class MessagesRepoTest { @get:Rule var mainCoroutineRule = MainCoroutineRule() - private lateinit var testDispatcher: TestCoroutineDispatcher + private lateinit var testDispatcher: TestDispatcher private lateinit var apiService: MessagesApiService private lateinit var userRepo: UserRepo private lateinit var timeProvider: TimeProvider - private lateinit var messagesDAO: MessagesDAO - private lateinit var messageUploader: MessageUploader + private lateinit var messageDao: MessageDao + private lateinit var surveyRepository: SurveyRepository + private lateinit var questionRepository: QuestionRepository + private lateinit var objectStorageClient: ObjectStorageClient + private lateinit var gson: Gson // system under test - private lateinit var messagesRepo: MessagesRepo + private lateinit var messageRepository: MessageRepository @Before fun setUp() { - testDispatcher = TestCoroutineDispatcher() + testDispatcher = StandardTestDispatcher() Dispatchers.setMain(testDispatcher) + messageDao = mockk() + surveyRepository = mockk() + questionRepository = mockk() + timeProvider = mockk() apiService = mockk() + objectStorageClient = mockk() userRepo = mockk() - timeProvider = mockk() - messagesDAO = mockk() - messageUploader = mockk() - - messagesRepo = MessagesRepo( - apiService, - userRepo, - timeProvider, - messagesDAO, - messageUploader + gson = mockk() + + messageRepository = MessageRepositoryImpl( + messageDao = messageDao, + surveyRepository = surveyRepository, + questionRepository = questionRepository, + timeProvider = timeProvider, + apiService = apiService, + objectStorageClient = objectStorageClient, + userRepo = userRepo, + gson = gson, + ioDispatcher = testDispatcher ) } @@ -99,7 +115,7 @@ class MessagesRepoTest { @Test fun `sync messages method fetches messages from api and saves them in database for every page and wallet correctly`() = - runBlocking { + runTest { // mock coEvery { @@ -107,15 +123,15 @@ class MessagesRepoTest { } returns userList coEvery { - messagesDAO.insertMessage(any()) + messageDao.insertItem(any()) } returns 0L coEvery { - messageUploader.uploadMessages() - } returns Unit + messageDao.getMessageIdsToUpload() + } returns emptyList() coEvery { - messagesDAO.getLatestSyncTimeForWallet(any()) + messageDao.getLatestSyncTimeForWallet(any()) } returns lastTimeMessageSynced for (i in userList.indices) { @@ -139,7 +155,8 @@ class MessagesRepoTest { } // perform - messagesRepo.syncMessages() + messageRepository.syncMessages() + advanceUntilIdle() // assert for (i in userList.indices) { @@ -156,7 +173,7 @@ class MessagesRepoTest { ) getMessageResponseList(limit, offset, i).forEach { - messagesDAO.insertMessage( + messageDao.insertItem( getMessageEntityFromResponse(it, i.toString()) ) } @@ -185,7 +202,7 @@ class MessagesRepoTest { offset: Int, userIndex: Int ): List { - return List(10) { + return List(10) { MessageResponse( id = ((limit + offset) + it + userIndex).toString(), type = MessageType.MESSAGE, diff --git a/app/src/test/java/org/greenstand/android/TreeTracker/messages/announcementmessage/AnnouncementViewModelTest.kt b/app/src/test/java/org/greenstand/android/TreeTracker/messages/announcementmessage/AnnouncementViewModelTest.kt index 61f7e5f3a..706f131dd 100644 --- a/app/src/test/java/org/greenstand/android/TreeTracker/messages/announcementmessage/AnnouncementViewModelTest.kt +++ b/app/src/test/java/org/greenstand/android/TreeTracker/messages/announcementmessage/AnnouncementViewModelTest.kt @@ -23,7 +23,7 @@ import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import org.greenstand.android.TreeTracker.MainCoroutineRule -import org.greenstand.android.TreeTracker.models.messages.MessagesRepo +import org.greenstand.android.TreeTracker.domain.repository.MessageRepository import org.greenstand.android.TreeTracker.utils.FakeFileGenerator.fakeAnnouncementMessage import org.junit.Assert.assertEquals import org.junit.Before @@ -39,18 +39,18 @@ class AnnouncementViewModelTest { var mainCoroutineRule = MainCoroutineRule() private val messageId = "" - private val messagesRepo = mockk(relaxed = true) + private val messageRepository = mockk(relaxed = true) private lateinit var announcementViewModel: AnnouncementViewModel @Before fun setup() { - coEvery { messagesRepo.getAnnouncementMessages(any()) } returns fakeAnnouncementMessage - announcementViewModel = AnnouncementViewModel(messageId, messagesRepo) + coEvery { messageRepository.getAnnouncementMessages(any()) } returns fakeAnnouncementMessage + announcementViewModel = AnnouncementViewModel(messageId, messageRepository) } @Test fun `verify messages repo gets the correct announcement message `() = runBlocking { - coVerify { messagesRepo.getAnnouncementMessages(messageId) } + coVerify { messageRepository.getAnnouncementMessages(messageId) } } @Test @@ -81,6 +81,6 @@ class AnnouncementViewModelTest { @Test fun `verify message repo marks message as read`() = runBlocking { - coVerify { messagesRepo.markMessageAsRead(messageId) } + coVerify { messageRepository.markMessageAsRead(messageId) } } } \ No newline at end of file diff --git a/app/src/test/java/org/greenstand/android/TreeTracker/messages/directmessages/ChatViewModelTest.kt b/app/src/test/java/org/greenstand/android/TreeTracker/messages/directmessages/ChatViewModelTest.kt index 70c81874b..3f3aa6fc1 100644 --- a/app/src/test/java/org/greenstand/android/TreeTracker/messages/directmessages/ChatViewModelTest.kt +++ b/app/src/test/java/org/greenstand/android/TreeTracker/messages/directmessages/ChatViewModelTest.kt @@ -23,16 +23,15 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.greenstand.android.TreeTracker.MainCoroutineRule +import org.greenstand.android.TreeTracker.domain.repository.MessageRepository import org.greenstand.android.TreeTracker.messages.ChatViewModel import org.greenstand.android.TreeTracker.models.UserRepo -import org.greenstand.android.TreeTracker.models.messages.MessagesRepo import org.greenstand.android.TreeTracker.utils.FakeFileGenerator import org.greenstand.android.TreeTracker.utils.getOrAwaitValueTest import org.junit.Assert.* import org.junit.Before import org.junit.Rule import org.junit.Test -import org.mockito.Mockito.* @ExperimentalCoroutinesApi class ChatViewModelTest { @@ -45,14 +44,14 @@ class ChatViewModelTest { private val userId = 2L private val otherChatIdentifier = "Mary" private val userRepo = mockk(relaxed = true) - private val messagesRepo = mockk(relaxed = true) + private val messageRepository = mockk(relaxed = true) private lateinit var testSubject: ChatViewModel @Before fun setup() { coEvery { userRepo.getUser(any()) } returns FakeFileGenerator.fakeUsers.first() - coEvery { messagesRepo.getDirectMessages(any(), any()) } returns flowOf(FakeFileGenerator.fakeDirectMessageList) - testSubject = ChatViewModel(userId, otherChatIdentifier, userRepo, messagesRepo) + coEvery { messageRepository.getDirectMessages(any(), any()) } returns flowOf(FakeFileGenerator.fakeDirectMessageList) + testSubject = ChatViewModel(userId, otherChatIdentifier, userRepo, messageRepository) } @Test fun `WHEN draft text is empty,returns empty string, THEN when draft text updates, returns correct data`() = runBlocking { @@ -66,7 +65,7 @@ class ChatViewModelTest { fun `Verify message repo saves message`() = runBlocking { testSubject.updateDraftText("Draft") testSubject.sendMessage() - coVerify { messagesRepo.saveMessage("some random text", "Mary", "Draft") } + coVerify { messageRepository.saveMessage("some random text", "Mary", "Draft") } } @Test fun `WHEN current user sends message THEN message is sent AND draft message is cleared`() = runBlocking { diff --git a/app/src/test/java/org/greenstand/android/TreeTracker/messages/individualmessagelist/IndividualMessageListViewModelTest.kt b/app/src/test/java/org/greenstand/android/TreeTracker/messages/individualmessagelist/IndividualMessageListViewModelTest.kt index 5556834b9..775c7d6d3 100644 --- a/app/src/test/java/org/greenstand/android/TreeTracker/messages/individualmessagelist/IndividualMessageListViewModelTest.kt +++ b/app/src/test/java/org/greenstand/android/TreeTracker/messages/individualmessagelist/IndividualMessageListViewModelTest.kt @@ -23,9 +23,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.greenstand.android.TreeTracker.MainCoroutineRule +import org.greenstand.android.TreeTracker.domain.repository.MessageRepository import org.greenstand.android.TreeTracker.messages.individualmeassagelist.IndividualMessageListViewModel import org.greenstand.android.TreeTracker.models.UserRepo -import org.greenstand.android.TreeTracker.models.messages.MessagesRepo import org.greenstand.android.TreeTracker.utils.FakeFileGenerator import org.greenstand.android.TreeTracker.utils.getOrAwaitValueTest import org.junit.Assert.assertEquals @@ -43,14 +43,14 @@ class IndividualMessageListViewModelTest { var mainCoroutineRule = MainCoroutineRule() private val userId = 2L private val userRepo = mockk(relaxed = true) - private val messagesRepo = mockk(relaxed = true) + private val messageRepository = mockk(relaxed = true) private lateinit var testSubject: IndividualMessageListViewModel @Before fun setup() { coEvery { userRepo.getUser(any()) } returns FakeFileGenerator.fakeUsers.first() - coEvery { messagesRepo.getMessageFlow(any()) } returns flowOf(FakeFileGenerator.messages) - testSubject = IndividualMessageListViewModel(userId, userRepo, messagesRepo) + coEvery { messageRepository.getMessageFlow(any()) } returns flowOf(FakeFileGenerator.messages) + testSubject = IndividualMessageListViewModel(userId, userRepo, messageRepository) } @Test diff --git a/app/src/test/java/org/greenstand/android/TreeTracker/messages/survey/SurveyViewModelTest.kt b/app/src/test/java/org/greenstand/android/TreeTracker/messages/survey/SurveyViewModelTest.kt index 6482ab3b2..9bf4bbd0a 100644 --- a/app/src/test/java/org/greenstand/android/TreeTracker/messages/survey/SurveyViewModelTest.kt +++ b/app/src/test/java/org/greenstand/android/TreeTracker/messages/survey/SurveyViewModelTest.kt @@ -23,8 +23,8 @@ import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import org.greenstand.android.TreeTracker.MainCoroutineRule +import org.greenstand.android.TreeTracker.domain.repository.MessageRepository import org.greenstand.android.TreeTracker.models.UserRepo -import org.greenstand.android.TreeTracker.models.messages.MessagesRepo import org.greenstand.android.TreeTracker.models.messages.Question import org.greenstand.android.TreeTracker.utils.FakeFileGenerator import org.junit.Assert @@ -41,20 +41,20 @@ class SurveyViewModelTest { @get:Rule var mainCoroutineRule = MainCoroutineRule() private val messageId = "message" - private val messagesRepo = mockk(relaxed = true) + private val messageRepository = mockk(relaxed = true) private val userRepo = mockk(relaxed = true) private lateinit var testSubject: SurveyViewModel @Before fun setup() { - coEvery { messagesRepo.getSurveyMessage(any()) } returns FakeFileGenerator.fakeSurveyMessage + coEvery { messageRepository.getSurveyMessage(any()) } returns FakeFileGenerator.fakeSurveyMessage coEvery { userRepo.getUserWithWallet(any()) } returns FakeFileGenerator.fakeUsers.first() - testSubject = SurveyViewModel(messageId, messagesRepo, userRepo) + testSubject = SurveyViewModel(messageId, messageRepository, userRepo) } @Test fun `verify message repo gets the correct survey Message`() = runBlocking { - coVerify { messagesRepo.getSurveyMessage(messageId) } + coVerify { messageRepository.getSurveyMessage(messageId) } } @Test fun `verify user Repo gets the correct user with wallet`() = runBlocking { @@ -88,7 +88,7 @@ class SurveyViewModelTest { @Test fun `WHEN current question is already first, go to previous question returns false`() = runBlocking { val questions = listOf(Question(prompt = "random", choices = listOf("one", "two"))) - coEvery { messagesRepo.getSurveyMessage(any()) } returns FakeFileGenerator.fakeSurveyMessage.copy(questions = questions) + coEvery { messageRepository.getSurveyMessage(any()) } returns FakeFileGenerator.fakeSurveyMessage.copy(questions = questions) val result = testSubject.goToPrevQuestion() Assert.assertFalse(result) } diff --git a/app/src/test/java/org/greenstand/android/TreeTracker/models/location/LocationDataCapturerTest.kt b/app/src/test/java/org/greenstand/android/TreeTracker/models/location/LocationDataCapturerTest.kt index a2b96e2cf..a04450739 100644 --- a/app/src/test/java/org/greenstand/android/TreeTracker/models/location/LocationDataCapturerTest.kt +++ b/app/src/test/java/org/greenstand/android/TreeTracker/models/location/LocationDataCapturerTest.kt @@ -27,7 +27,7 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk import io.mockk.verify -import org.greenstand.android.TreeTracker.database.TreeTrackerDAO +import org.greenstand.android.TreeTracker.database.app.TreeTrackerDAO import org.greenstand.android.TreeTracker.models.ConvergenceConfiguration import org.greenstand.android.TreeTracker.models.LocationDataConfig import org.greenstand.android.TreeTracker.models.SessionTracker diff --git a/app/src/test/java/org/greenstand/android/TreeTracker/splash/SplashScreenViewModelTest.kt b/app/src/test/java/org/greenstand/android/TreeTracker/splash/SplashScreenViewModelTest.kt index 39bab0862..7bc807c6c 100644 --- a/app/src/test/java/org/greenstand/android/TreeTracker/splash/SplashScreenViewModelTest.kt +++ b/app/src/test/java/org/greenstand/android/TreeTracker/splash/SplashScreenViewModelTest.kt @@ -23,11 +23,11 @@ import kotlinx.coroutines.runBlocking import org.greenstand.android.TreeTracker.MainCoroutineRule import org.greenstand.android.TreeTracker.analytics.ExceptionDataCollector import org.greenstand.android.TreeTracker.dashboard.TreesToSyncHelper +import org.greenstand.android.TreeTracker.domain.repository.MessageRepository import org.greenstand.android.TreeTracker.models.DeviceConfigUpdater import org.greenstand.android.TreeTracker.models.SessionTracker import org.greenstand.android.TreeTracker.models.UserRepo import org.greenstand.android.TreeTracker.models.location.LocationDataCapturer -import org.greenstand.android.TreeTracker.models.messages.MessagesRepo import org.greenstand.android.TreeTracker.models.organization.OrgRepo import org.greenstand.android.TreeTracker.usecases.CheckForInternetUseCase import org.greenstand.android.TreeTracker.utils.FakeFileGenerator @@ -63,7 +63,7 @@ class SplashScreenViewModelTest { private lateinit var locationDataCapturer: LocationDataCapturer @MockK(relaxed = true) - private lateinit var messagesRepo: MessagesRepo + private lateinit var messageRepository: MessageRepository @MockK(relaxed = true) private lateinit var checkForInternetUseCase: CheckForInternetUseCase @@ -86,7 +86,7 @@ class SplashScreenViewModelTest { sessionTracker = sessionTracker, deviceConfigUpdater = deviceConfigUpdater, locationDataCapturer = locationDataCapturer, - messagesRepo = messagesRepo, + messageRepository = messageRepository, checkForInternetUseCase = checkForInternetUseCase, orgRepo = orgRepo, exceptionDataCollector = exceptionDataCollector @@ -107,7 +107,7 @@ class SplashScreenViewModelTest { coVerify(exactly = 1) { deviceConfigUpdater.saveLatestConfig() } coVerify(exactly = 1) { orgRepo.init() } coVerify(exactly = 0) { orgRepo.addOrgFromJsonString(orgJsonString ?: "some string") } - coVerify(exactly = 1) { messagesRepo.syncMessages() } + coVerify(exactly = 1) { messageRepository.syncMessages() } coVerify(exactly = 1) { exceptionDataCollector.set(ExceptionDataCollector.POWER_USER_WALLET, user.wallet) } coVerify(exactly = 1) { treesToSyncHelper.refreshTreeCountToSync() } } @@ -126,7 +126,7 @@ class SplashScreenViewModelTest { coVerify(exactly = 1) { deviceConfigUpdater.saveLatestConfig() } coVerify(exactly = 1) { orgRepo.init() } coVerify(exactly = 0) { orgRepo.addOrgFromJsonString(orgJsonString ?: "some stirng") } - coVerify(exactly = 0) { messagesRepo.syncMessages() } + coVerify(exactly = 0) { messageRepository.syncMessages() } coVerify(exactly = 0) { exceptionDataCollector.set(ExceptionDataCollector.POWER_USER_WALLET, user.wallet) } coVerify(exactly = 0) { treesToSyncHelper.refreshTreeCountToSync() } } diff --git a/app/src/test/java/org/greenstand/android/TreeTracker/utils/FakeFileGenerator.kt b/app/src/test/java/org/greenstand/android/TreeTracker/utils/FakeFileGenerator.kt index 55330a739..12f1b3786 100644 --- a/app/src/test/java/org/greenstand/android/TreeTracker/utils/FakeFileGenerator.kt +++ b/app/src/test/java/org/greenstand/android/TreeTracker/utils/FakeFileGenerator.kt @@ -16,8 +16,17 @@ package org.greenstand.android.TreeTracker.utils import kotlinx.datetime.Instant -import org.greenstand.android.TreeTracker.database.entity.* -import org.greenstand.android.TreeTracker.database.legacy.entity.* +import org.greenstand.android.TreeTracker.database.app.entity.DeviceConfigEntity +import org.greenstand.android.TreeTracker.database.app.entity.LocationEntity +import org.greenstand.android.TreeTracker.database.app.entity.OrganizationEntity +import org.greenstand.android.TreeTracker.database.app.entity.SessionEntity +import org.greenstand.android.TreeTracker.database.app.entity.TreeEntity +import org.greenstand.android.TreeTracker.database.app.entity.UserEntity +import org.greenstand.android.TreeTracker.database.app.legacy.entity.LocationDataEntity +import org.greenstand.android.TreeTracker.database.app.legacy.entity.PlanterCheckInEntity +import org.greenstand.android.TreeTracker.database.app.legacy.entity.PlanterInfoEntity +import org.greenstand.android.TreeTracker.database.app.legacy.entity.TreeAttributeEntity +import org.greenstand.android.TreeTracker.database.app.legacy.entity.TreeCaptureEntity import org.greenstand.android.TreeTracker.models.messages.AnnouncementMessage import org.greenstand.android.TreeTracker.models.messages.DirectMessage import org.greenstand.android.TreeTracker.models.messages.Question @@ -101,7 +110,8 @@ object FakeFileGenerator { bundleId = null, photoPath = "lol", photoUrl = "anotherString", - powerUser = true + powerUser = true, + id = 1 ) val fakePlanterCheckInEntity = PlanterCheckInEntity( diff --git a/app/src/test/java/org/greenstand/android/TreeTracker/utils/LiveDataUtilTest.kt b/app/src/test/java/org/greenstand/android/TreeTracker/utils/LiveDataUtilTest.kt index 84336d329..e532681c5 100644 --- a/app/src/test/java/org/greenstand/android/TreeTracker/utils/LiveDataUtilTest.kt +++ b/app/src/test/java/org/greenstand/android/TreeTracker/utils/LiveDataUtilTest.kt @@ -37,7 +37,7 @@ fun LiveData.getOrAwaitValueTest( var data: T? = null val latch = CountDownLatch(1) val observer = object : Observer { - override fun onChanged(o: T?) { + override fun onChanged(o: T) { data = o latch.countDown() this@getOrAwaitValueTest.removeObserver(this) diff --git a/build.gradle b/build.gradle index 5350a2ffd..dd9637b8d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,19 +1,34 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.8.21' repositories { + maven { + url "https://jitpack.io" + } + maven { + url 'https://raw.github.com/Raizlabs/maven-releases/master/releases' + } + maven { + url 'http://oss.sonatype.org/content/repositories/snapshots' + allowInsecureProtocol = true + } + maven { + url "https://plugins.gradle.org/m2/" + } + maven { + url 'https://maven.google.com' + } google() - jcenter() - maven { url "https://plugins.gradle.org/m2/" } + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.0' - classpath 'com.google.gms:google-services:4.3.3' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.2' + classpath 'com.android.tools.build:gradle:8.0.1' + classpath 'com.google.gms:google-services:4.3.15' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.5' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5" + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.5.3" classpath 'org.jlleitschuh.gradle:ktlint-gradle:11.0.0' classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.18.0' classpath 'io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.18.0-RC2' @@ -34,19 +49,32 @@ allprojects { } } repositories { - google() - jcenter() maven { - url "https://maven.google.com" // Google's Maven repository + url "https://jitpack.io" + } + maven { + url 'https://raw.github.com/Raizlabs/maven-releases/master/releases' + } + maven { + url 'http://oss.sonatype.org/content/repositories/snapshots' + allowInsecureProtocol = true } - maven { url "https://oss.sonatype.org/content/repositories/snapshots" } + maven { + url "https://plugins.gradle.org/m2/" + } + maven { + url 'https://maven.google.com' + } + google() + mavenCentral() } ext { - compose_version = "1.1.1" - koin_version= "3.2.0-beta-1" + compose_version = "1.4.3" + compose_compiler_version = "1.4.7" + koin_version = '3.4.0' androidSupportVersion = '1.0.0' - retrofit2Version = '2.6.0' + retrofit2Version = '2.9.0' s3_production_identity_pool_id = "configure in treetracker.keys.properties" prod_treetracker_client_id = "configure in treetracker.keys.properties" prod_treetracker_client_secret = "configure in treetracker.keys.properties" diff --git a/gradle.properties b/gradle.properties index 988d1ed5e..24e5d6da7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,6 +14,7 @@ android.enableJetifier=true android.useAndroidX=true org.gradle.jvmargs=-Xmx1536m org.gradle.warning.mode=all +android.defaults.buildfeatures.buildconfig=true # When configured, Gradle will run in incubating parallel mode. diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ffed3a254..da1db5f04 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.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists