diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml index 371f2e2..b2b6f45 100644 --- a/.idea/appInsightsSettings.xml +++ b/.idea/appInsightsSettings.xml @@ -3,6 +3,20 @@ \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index efacb99..639c779 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -6,14 +6,13 @@ - diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 005afb3..f0c6ad0 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -40,6 +40,9 @@ + + diff --git a/.idea/misc.xml b/.idea/misc.xml index b2c751a..f7608ed 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,9 +1,4 @@ - - - - - + \ No newline at end of file diff --git a/.kotlin/errors/errors-1738346278539.log b/.kotlin/errors/errors-1738346278539.log new file mode 100644 index 0000000..38b1cc9 --- /dev/null +++ b/.kotlin/errors/errors-1738346278539.log @@ -0,0 +1,4 @@ +kotlin version: 2.0.20-RC +error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: + 1. Kotlin compile daemon is ready + diff --git a/.kotlin/errors/errors-1739362815060.log b/.kotlin/errors/errors-1739362815060.log new file mode 100644 index 0000000..38b1cc9 --- /dev/null +++ b/.kotlin/errors/errors-1739362815060.log @@ -0,0 +1,4 @@ +kotlin version: 2.0.20-RC +error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: + 1. Kotlin compile daemon is ready + diff --git a/.kotlin/errors/errors-1749473483135.log b/.kotlin/errors/errors-1749473483135.log new file mode 100644 index 0000000..38b1cc9 --- /dev/null +++ b/.kotlin/errors/errors-1749473483135.log @@ -0,0 +1,4 @@ +kotlin version: 2.0.20-RC +error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: + 1. Kotlin compile daemon is ready + diff --git a/app/build.gradle b/app/build.gradle index 306e11c..3244764 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,6 +11,8 @@ plugins { def googleMapsApiKey = System.getenv("GOOGLE_MAPS_API_KEY") ?: "" def baseUrl = System.getenv("BASE_URL") ?: "" +def userGuideUrl = System.getenv("USER_GUIDE_URL") ?: "" +def dataPrivacyUrl = System.getenv("DATA_PRIVACY_URL") ?: "" android { namespace 'org.technoserve.farmcollector' @@ -20,14 +22,16 @@ android { applicationId "org.technoserve.farmcollector" minSdk 21 targetSdk 35 - versionCode 53 - versionName "2.39" + versionCode 73 + versionName "2.51" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary true } buildConfigField "String", "GOOGLE_MAPS_API_KEY", "\"${googleMapsApiKey}\"" buildConfigField "String", "BASE_URL", "\"${baseUrl}\"" + buildConfigField "String", "USER_GUIDE_URL", "\"${userGuideUrl}\"" + buildConfigField "String", "DATA_PRIVACY_URL","\"${dataPrivacyUrl}\"" } buildTypes { @@ -51,15 +55,57 @@ android { } } + def debugUserGuideUrl = project.rootProject.file("local.properties").with { propertiesFile -> + if (propertiesFile.exists()) { + def properties = new Properties() + properties.load(propertiesFile.newInputStream()) + properties.getProperty("USER_GUIDE_URL", "default_debug_user_guide_url") + } else { + "default_debug_user_guide_url" + } + } + + def releaseUserGuideUrl = project.rootProject.file("local.properties").with { propertiesFile -> + if (propertiesFile.exists()) { + def properties = new Properties() + properties.load(propertiesFile.newInputStream()) + properties.getProperty("USER_GUIDE_URL", "default_release_user_guide_url") + } else { + "default_release_user_guide_url" + } + } + def debugDataPrivacyUrl = project.rootProject.file("local.properties").with { propertiesFile -> + if (propertiesFile.exists()) { + def properties = new Properties() + properties.load(propertiesFile.newInputStream()) + properties.getProperty("DATA_PRIVACY_URL", "default_data_privacy_url") + } else { + "default_data_privacy_url" + } + } + def releaseDataPrivacyUrl = project.rootProject.file("local.properties").with { propertiesFile -> + if (propertiesFile.exists()) { + def properties = new Properties() + properties.load(propertiesFile.newInputStream()) + properties.getProperty("DATA_PRIVACY_URL", "default_data_privacy_url") + } else { + "default_data_privacy_url" + } + } + debug { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard' buildConfigField "String", "BASE_URL", "\"$debugBaseUrl\"" + buildConfigField "String", "USER_GUIDE_URL", "\"$debugUserGuideUrl\"" + buildConfigField "String", "DATA_PRIVACY_URL", "\"$debugDataPrivacyUrl\"" } release { minifyEnabled false proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" buildConfigField "String", "BASE_URL", "\"$releaseBaseUrl\"" + buildConfigField "String", "USER_GUIDE_URL", "\"$releaseUserGuideUrl\"" + buildConfigField "String", "DATA_PRIVACY_URL", "\"$releaseDataPrivacyUrl\"" } } @@ -98,11 +144,6 @@ android { testOptions { unitTests { includeAndroidResources = true -// all { -// systemProperty 'robolectric.enabledSdks', '33' -// systemProperty 'robolectric.build.model', 'DEVICE' -// environment 'BUILD_FINGERPRINT', 'foo' -// } } } @@ -130,11 +171,11 @@ dependencies { implementation("androidx.compose.material3:material3:1.3.1") implementation("androidx.compose.material3:material3-window-size-class:1.3.1") implementation 'com.google.android.gms:play-services-ads-identifier:18.1.0' -// implementation 'androidx.work:work-runtime-ktx:2.10.0' implementation 'androidx.work:work-runtime-ktx:2.9.0' - // implementation 'androidx.work:work-runtime:2.8.0' + implementation "com.google.accompanist:accompanist-systemuicontroller:0.32.0" + // camera dependencies implementation 'androidx.camera:camera-core:1.4.0' @@ -212,39 +253,36 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:2.0.20-RC" - + implementation("androidx.datastore:datastore-preferences:1.0.0") // Testing Dependencies implementation 'androidx.navigation:navigation-testing:2.4.0-alpha04' implementation 'androidx.test.ext:junit-ktx:1.2.1' + implementation 'com.jakewharton.timber:timber:5.0.1' + testImplementation "androidx.test:core:1.5.0" testImplementation "org.mockito:mockito-core:5.7.0" testImplementation "org.mockito.kotlin:mockito-kotlin:5.3.1" testImplementation "io.mockk:mockk:1.13.7" - // testImplementation "org.robolectric:robolectric:4.10.3" testImplementation 'com.google.dagger:hilt-android-testing:2.48' testImplementation 'androidx.compose.ui:ui-test-junit4-android:1.7.5' testImplementation 'androidx.arch.core:core-testing:2.2.0' testImplementation "com.google.truth:truth:1.1.3" testImplementation "io.mockk:mockk:1.13.7" testImplementation "io.mockk:mockk-android:1.13.5" - //testImplementation "org.robolectric:robolectric:4.9" testImplementation("androidx.work:work-testing:2.8.1") // testImplementation("junit:junit:4.13.2") testImplementation("org.robolectric:robolectric:4.13") - - // testImplementation "org.robolectric:robolectric:4.11.1" testImplementation "androidx.test:core:1.5.0" testImplementation "androidx.test.ext:junit:1.1.5" testImplementation "androidx.compose.ui:ui-test-junit4:1.5.4" - androidTestImplementation "androidx.work:work-testing:2.7.1" androidTestImplementation "com.google.truth:truth:1.1.3" androidTestImplementation "androidx.work:work-testing:2.7.1" diff --git a/app/release/app-release.aab b/app/release/app-release.aab index 665ad7e..5f5362e 100644 Binary files a/app/release/app-release.aab and b/app/release/app-release.aab differ diff --git a/app/src/androidTest/java/org/technoserve/farmcollector/utils/LanguageSelectorKtTest.kt b/app/src/androidTest/java/org/technoserve/farmcollector/utils/LanguageSelectorKtTest.kt deleted file mode 100644 index 2459d92..0000000 --- a/app/src/androidTest/java/org/technoserve/farmcollector/utils/LanguageSelectorKtTest.kt +++ /dev/null @@ -1,119 +0,0 @@ -package org.technoserve.farmcollector.utils - -import android.annotation.SuppressLint -import android.content.Context -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.lifecycle.ViewModel -import io.mockk.Runs -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import org.junit.Rule -import org.junit.Test -import org.mockito.Mockito.verify -import org.technoserve.farmcollector.ui.screens.settings.LanguageSelector -import org.technoserve.farmcollector.viewmodels.LanguageViewModel - -/* - -// Mock Language class -data class Language(val displayName: String) - -// Mock ViewModel -class LanguageViewModel : ViewModel() { - private val _currentLanguage = MutableStateFlow( - org.technoserve.farmcollector.database.models.Language( - "English" - ) - ) - val currentLanguage: StateFlow get() = _currentLanguage - - fun selectLanguage(language: Language, context: Context) { - _currentLanguage.value = language - } -} - - -class LanguageSelectorKtTest{ - @get:Rule - val composeTestRule = createComposeRule() - - private val mockContext = mockk(relaxed = true) - - @Test - fun languageSelector_displaysCurrentLanguage() { - val mockViewModel = mockk(relaxed = true) - val languages = listOf( - org.technoserve.farmcollector.database.models.Language("English"), - org.technoserve.farmcollector.database.models.Language("French") - ) - val currentLanguage = MutableStateFlow(languages[0]) // English - - every { mockViewModel.currentLanguage } returns currentLanguage - - composeTestRule.setContent { - LanguageSelector(viewModel = mockViewModel, languages = languages) - } - - // Assert the current language is displayed - composeTestRule.onNodeWithText("English").assertExists() - } - - @Test - fun languageSelector_opensDropdownOnClick() { - val mockViewModel = mockk(relaxed = true) - val languages = listOf( - org.technoserve.farmcollector.database.models.Language("English"), - org.technoserve.farmcollector.database.models.Language("French") - ) - val currentLanguage = MutableStateFlow(languages[0]) // English - - every { mockViewModel.currentLanguage } returns currentLanguage - - composeTestRule.setContent { - LanguageSelector(viewModel = mockViewModel, languages = languages) - } - - // Click on the row to expand the dropdown menu - composeTestRule.onNodeWithText("English").performClick() - - // Assert that the dropdown is expanded and displays the available languages - composeTestRule.onNodeWithText("French").assertExists() - } - - @SuppressLint("CheckResult") - @Test - fun languageSelector_selectsLanguageOnClick() { - val mockViewModel = mockk(relaxed = true) - val languages = listOf( - org.technoserve.farmcollector.database.models.Language("English"), - org.technoserve.farmcollector.database.models.Language("French") - ) - val currentLanguage = MutableStateFlow(languages[0]) // English - - every { mockViewModel.currentLanguage } returns currentLanguage - every { mockViewModel.selectLanguage(any(), any()) } just Runs - - composeTestRule.setContent { - LanguageSelector(viewModel = mockViewModel, languages = languages) - } - - // Expand the dropdown - composeTestRule.onNodeWithText("English").performClick() - - // Select the "French" option - composeTestRule.onNodeWithText("French").performClick() - - // Verify that selectLanguage was called with the correct language - verify { mockViewModel.selectLanguage( - org.technoserve.farmcollector.database.models.Language( - "French" - ), mockContext) } - } -} - - */ \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5f5af76..8fd9e15 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,14 +7,18 @@ android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" tools:ignore="ProtectedPermissions" /> - - + + + @@ -39,7 +43,7 @@ android:networkSecurityConfig="@xml/network_security_config" tools:targetApi="31"> + + + + + Plot Visualization + + + + + + + + +
+
+ + +
+
Loading map...
+ + + + diff --git a/app/src/main/assets/leaflet_map.html b/app/src/main/assets/leaflet_map.html new file mode 100644 index 0000000..337a89c --- /dev/null +++ b/app/src/main/assets/leaflet_map.html @@ -0,0 +1,1378 @@ + + + + + + Farm Plot Map + + + + + + + +
+
+ + + + + +
+ +
+ + +
+ + +
+
Plot Size: 0 hectares
+
Accuracy: Calculating...
+
+
+ +
+

+ Please start capture first +

+
+ +
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/migrations/migration_19_20.sql b/app/src/main/assets/migrations/migration_19_20.sql index 99e73fe..8608b6a 100644 --- a/app/src/main/assets/migrations/migration_19_20.sql +++ b/app/src/main/assets/migrations/migration_19_20.sql @@ -1,54 +1,117 @@ - // 1. Create a new table `new_Farms` with `accuracyArray` field - db.execSQL( - """ - CREATE TABLE new_Farms ( - siteId INTEGER NOT NULL, - remote_id BLOB NOT NULL, - farmerPhoto TEXT NOT NULL, - farmerName TEXT NOT NULL, - memberId TEXT NOT NULL, - village TEXT NOT NULL, - district TEXT NOT NULL, - purchases REAL, - size REAL NOT NULL, - latitude TEXT NOT NULL, - longitude TEXT NOT NULL, - coordinates TEXT, - accuracyArray TEXT, -- For storing accuracy array (one element for point, multiple for polygon) - synced INTEGER NOT NULL DEFAULT 0, - scheduledForSync INTEGER NOT NULL DEFAULT 0, - createdAt INTEGER NOT NULL, - updatedAt INTEGER NOT NULL, - needsUpdate INTEGER NOT NULL DEFAULT 0, - id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - FOREIGN KEY (siteId) - REFERENCES CollectionSites (siteId) ON UPDATE NO ACTION - ON DELETE CASCADE - ) +-- // 1. Create a new table `new_Farms` with `accuracyArray` field +-- db.execSQL( +-- """ +-- CREATE TABLE new_Farms ( +-- siteId INTEGER NOT NULL, +-- remote_id BLOB NOT NULL, +-- farmerPhoto TEXT NOT NULL, +-- farmerName TEXT NOT NULL, +-- memberId TEXT NOT NULL, +-- village TEXT NOT NULL, +-- district TEXT NOT NULL, +-- purchases REAL, +-- size REAL NOT NULL, +-- latitude TEXT NOT NULL, +-- longitude TEXT NOT NULL, +-- coordinates TEXT, +-- accuracyArray TEXT, -- For storing accuracy array (one element for point, multiple for polygon) +-- synced INTEGER NOT NULL DEFAULT 0, +-- scheduledForSync INTEGER NOT NULL DEFAULT 0, +-- createdAt INTEGER NOT NULL, +-- updatedAt INTEGER NOT NULL, +-- needsUpdate INTEGER NOT NULL DEFAULT 0, +-- id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, +-- FOREIGN KEY (siteId) +-- REFERENCES CollectionSites (siteId) ON UPDATE NO ACTION +-- ON DELETE CASCADE +-- ) +-- """.trimIndent() +-- ) +-- +-- // 2. Copy existing data from `Farms` to `new_Farms`, initializing `accuracyArray` +-- db.execSQL( +-- """ +-- INSERT INTO new_Farms ( +-- siteId, remote_id, farmerPhoto, farmerName, memberId, +-- village, district, purchases, size, latitude, longitude, +-- coordinates, accuracyArray, synced, scheduledForSync, +-- createdAt, updatedAt, needsUpdate, id +-- ) +-- SELECT +-- siteId, remote_id, farmerPhoto, farmerName, memberId, +-- village, district, purchases, size, latitude, longitude, +-- coordinates, '[]' AS accuracyArray, -- Initialize new field as an empty array +-- synced, scheduledForSync, createdAt, updatedAt, needsUpdate, id +-- FROM Farms +-- """.trimIndent() +-- ) +-- +-- // 3. Drop the old `Farms` table +-- db.execSQL("DROP TABLE Farms") +-- +-- // 4. Rename the `new_Farms` table to `Farms` +-- db.execSQL("ALTER TABLE new_Farms RENAME TO Farms") +-- } + +try { + // 1. Create a new table `new_Farms` with `accuracyArray` field + db.execSQL( + """ + CREATE TABLE new_Farms ( + siteId INTEGER NOT NULL, + remote_id BLOB NOT NULL, + farmerPhoto TEXT NOT NULL, + farmerName TEXT NOT NULL, + memberId TEXT NOT NULL, + village TEXT NOT NULL, + district TEXT NOT NULL, + purchases REAL, + size REAL NOT NULL, + latitude TEXT NOT NULL, + longitude TEXT NOT NULL, + coordinates TEXT, + accuracyArray TEXT, -- For storing accuracy array (one element for point, multiple for polygon) + synced INTEGER NOT NULL DEFAULT 0, + scheduledForSync INTEGER NOT NULL DEFAULT 0, + createdAt INTEGER NOT NULL, + updatedAt INTEGER NOT NULL, + needsUpdate INTEGER NOT NULL DEFAULT 0, + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + FOREIGN KEY (siteId) + REFERENCES CollectionSites (siteId) ON UPDATE NO ACTION + ON DELETE CASCADE + ) """.trimIndent() - ) + ); + Log.d("Database", "Table new_Farms created successfully"); - // 2. Copy existing data from `Farms` to `new_Farms`, initializing `accuracyArray` - db.execSQL( - """ - INSERT INTO new_Farms ( - siteId, remote_id, farmerPhoto, farmerName, memberId, - village, district, purchases, size, latitude, longitude, - coordinates, accuracyArray, synced, scheduledForSync, - createdAt, updatedAt, needsUpdate, id - ) - SELECT - siteId, remote_id, farmerPhoto, farmerName, memberId, - village, district, purchases, size, latitude, longitude, - coordinates, '[]' AS accuracyArray, -- Initialize new field as an empty array - synced, scheduledForSync, createdAt, updatedAt, needsUpdate, id - FROM Farms + // 2. Copy existing data from `Farms` to `new_Farms`, initializing `accuracyArray` + db.execSQL( + """ + INSERT INTO new_Farms ( + siteId, remote_id, farmerPhoto, farmerName, memberId, + village, district, purchases, size, latitude, longitude, + coordinates, accuracyArray, synced, scheduledForSync, + createdAt, updatedAt, needsUpdate, id + ) + SELECT + siteId, remote_id, farmerPhoto, farmerName, memberId, + village, district, purchases, size, latitude, longitude, + coordinates, '[]' AS accuracyArray, -- Initialize new field as an empty array + synced, scheduledForSync, createdAt, updatedAt, needsUpdate, id + FROM Farms """.trimIndent() - ) + ); + Log.d("Database", "Data copied from Farms to new_Farms successfully"); + + // 3. Drop the old `Farms` table + db.execSQL("DROP TABLE IF EXISTS Farms"); + Log.d("Database", "Table Farms dropped successfully"); - // 3. Drop the old `Farms` table - db.execSQL("DROP TABLE Farms") + // 4. Rename the `new_Farms` table to `Farms` + db.execSQL("ALTER TABLE new_Farms RENAME TO Farms"); + Log.d("Database", "Table new_Farms renamed to Farms successfully"); - // 4. Rename the `new_Farms` table to `Farms` - db.execSQL("ALTER TABLE new_Farms RENAME TO Farms") - } +} catch (SQLException e) { + Log.e("Database", "Error during table migration", e); +} \ No newline at end of file diff --git a/app/src/main/assets/migrations/migration_20_21.sql b/app/src/main/assets/migrations/migration_20_21.sql new file mode 100644 index 0000000..4f7a2a1 --- /dev/null +++ b/app/src/main/assets/migrations/migration_20_21.sql @@ -0,0 +1 @@ +ALTER TABLE CollectionSites ADD COLUMN commodity TEXT NOT NULL DEFAULT 'coffee'; diff --git a/app/src/main/assets/script.js b/app/src/main/assets/script.js new file mode 100644 index 0000000..bbbd339 --- /dev/null +++ b/app/src/main/assets/script.js @@ -0,0 +1,25 @@ +const CACHE_NAME = 'leaflet-map-cache-v1'; + +self.addEventListener('install', event => { + event.waitUntil( + caches.open(CACHE_NAME).then(cache => { + console.log('Opened cache'); + return cache.addAll([]); + }) + ); +}); + +self.addEventListener('fetch', event => { + if (event.request.url.includes('tile.openstreetmap.org')) { + event.respondWith( + caches.match(event.request).then(response => { + return response || fetch(event.request).then(networkResponse => { + return caches.open(CACHE_NAME).then(cache => { + cache.put(event.request, networkResponse.clone()); + return networkResponse; + }); + }); + }) + ); + } +}); diff --git a/app/src/main/java/org/technoserve/farmcollector/FarmCollectorApp.kt b/app/src/main/java/org/technoserve/farmcollector/FarmCollectorApp.kt index b55f1c8..a110554 100644 --- a/app/src/main/java/org/technoserve/farmcollector/FarmCollectorApp.kt +++ b/app/src/main/java/org/technoserve/farmcollector/FarmCollectorApp.kt @@ -10,46 +10,63 @@ import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import org.technoserve.farmcollector.database.helpers.ContextProvider import org.technoserve.farmcollector.database.sync.SyncWorker +import org.technoserve.farmcollector.utils.BackupPreferences import java.util.concurrent.TimeUnit +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking /** * * This class initializes WorkManager and sets up a periodic sync job to fetch and update data from the server. * */ -//class FarmCollectorApp : Application(), Configuration.Provider { -class FarmCollectorApp : Application(){ +class FarmCollectorApp : Application() { override fun onCreate() { super.onCreate() ContextProvider.initialize(this) - initializeWorkManager() + observeBackupPreference() // colors Start observing backup setting } + /** + * colors Observe user's backup setting and enable WorkManager only when backup is ON + */ + private fun observeBackupPreference() { + val isBackupEnabled = runBlocking { BackupPreferences.isBackupEnabled(this@FarmCollectorApp).first() } + + if (isBackupEnabled) { + initializeWorkManager() // colors Start WorkManager if backup is enabled + } else { + cancelWorkManager() // colors Stop WorkManager if backup is disabled + } + } + + /** + * colors Initialize WorkManager when backup is enabled + */ private fun initializeWorkManager() { - // Build the constraints for the work val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) // Requires a connected network + .setRequiredNetworkType(NetworkType.CONNECTED) .build() - // Create the periodic work request val workRequest = PeriodicWorkRequestBuilder(2, TimeUnit.MINUTES) .setConstraints(constraints) .build() - // Enqueue the periodic work with a unique name to avoid duplicate schedules WorkManager.getInstance(this).enqueueUniquePeriodicWork( - "sync_work_tag", // Unique name for the work - ExistingPeriodicWorkPolicy.UPDATE, // Replace if already exists + "sync_work_tag", + ExistingPeriodicWorkPolicy.UPDATE, workRequest ) - Log.d("WorkManager", "WorkManager is initialized successfully") + Log.d("WorkManager", "WorkManager started because backup is enabled") } -// // Provide the WorkManager configuration -// override val workManagerConfiguration: Configuration -// get() = Configuration.Builder() -// .setMinimumLoggingLevel(Log.DEBUG) // Set logging level -// .build() -} \ No newline at end of file + /** + * colors Cancel WorkManager when backup is disabled + */ + private fun cancelWorkManager() { + WorkManager.getInstance(this).cancelUniqueWork("sync_work_tag") + Log.d("WorkManager", "WorkManager canceled because backup is disabled") + } +} diff --git a/app/src/main/java/org/technoserve/farmcollector/MainActivity.kt b/app/src/main/java/org/technoserve/farmcollector/MainActivity.kt index 00000c6..df767a1 100644 --- a/app/src/main/java/org/technoserve/farmcollector/MainActivity.kt +++ b/app/src/main/java/org/technoserve/farmcollector/MainActivity.kt @@ -6,7 +6,10 @@ import android.app.Activity import android.app.Application import android.content.Intent import android.net.Uri +import android.os.Build import android.os.Bundle +import android.util.Log +import android.webkit.WebView import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler @@ -26,6 +29,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -34,12 +38,24 @@ import androidx.navigation.compose.rememberNavController //import androidx.navigation.navArgument import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberMultiplePermissionsState +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.reflect.TypeToken +import kotlinx.coroutines.delay +import org.json.JSONObject +import org.technoserve.farmcollector.database.helpers.PreferencesManager import org.technoserve.farmcollector.viewmodels.AppUpdateViewModel //import org.technoserve.farmcollector.viewmodels.ExitConfirmationDialog import org.technoserve.farmcollector.viewmodels.FarmViewModel import org.technoserve.farmcollector.viewmodels.FarmViewModelFactory import org.technoserve.farmcollector.viewmodels.UpdateAlert import org.technoserve.farmcollector.database.helpers.map.LocationHelper +//import org.technoserve.farmcollector.database.mappers.CoordinatesDeserializer +import org.technoserve.farmcollector.database.models.Farm +import org.technoserve.farmcollector.ui.components.UserGuideScreen import org.technoserve.farmcollector.viewmodels.MapViewModel import org.technoserve.farmcollector.ui.screens.farms.AddFarm import org.technoserve.farmcollector.ui.screens.collectionsites.AddSite @@ -50,10 +66,18 @@ import org.technoserve.farmcollector.ui.screens.settings.ScreenWithSidebar import org.technoserve.farmcollector.ui.screens.map.SetPolygon import org.technoserve.farmcollector.ui.screens.settings.SettingsScreen import org.technoserve.farmcollector.ui.screens.farms.UpdateFarmForm +import org.technoserve.farmcollector.ui.screens.map.PlotVisualizationApp +import org.technoserve.farmcollector.ui.screens.map.WebViewPage +import org.technoserve.farmcollector.ui.screens.map.cacheMapInBackground import org.technoserve.farmcollector.ui.theme.FarmCollectorTheme import org.technoserve.farmcollector.viewmodels.LanguageViewModel import org.technoserve.farmcollector.viewmodels.LanguageViewModelFactory +import java.io.Serializable +import java.lang.reflect.Type +import java.time.Instant import java.util.Locale +import java.util.UUID +import org.technoserve.farmcollector.ui.screens.privacy.PrivacyPolicyScreen /** @@ -69,19 +93,20 @@ import java.util.Locale * const val UPDATE_FARM = "updateFarm/{farmId}" * */ - - object Routes { const val HOME = "home" const val SITE_LIST = "siteList" const val FARM_LIST = "farmList/{siteId}" - const val ADD_FARM = "addFarm/{siteId}" + const val ADD_FARM = "addFarm/{siteId}/{plotDataJson}" const val ADD_SITE = "addSite" const val UPDATE_FARM = "updateFarm/{farmId}" - const val SET_POLYGON = "setPolygon" + const val SET_POLYGON = "setPolygon/{siteId}" const val SETTINGS = "settings" + const val PRIVACY_POLICY = "privacy_policy" } +var loadURL = "file:///android_asset/leaflet_map.html" + /** * MainActivity is the entry point for the Android app. It sets up the navigation graph, * manages permissions, and initializes the language and map view models. @@ -100,12 +125,28 @@ class MainActivity : ComponentActivity() { getSharedPreferences("FarmCollector", MODE_PRIVATE) } - @SuppressLint("InlinedApi") @OptIn(ExperimentalPermissionsApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + // Preload the map in the background + cacheMapInBackground( + context = this, + mapUrl = "file:///android_asset/leaflet_map.html" + ) + // Preload the map in the background + cacheMapInBackground( + context = this, + mapUrl = "file:///android_asset/index.html" + ) + + + // Enable WebView debugging + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + WebView.setWebContentsDebuggingEnabled(true) + } + locationHelper = LocationHelper(this) val darkMode = mutableStateOf(sharedPreferences.getBoolean("dark_mode", false)) @@ -129,52 +170,24 @@ class MainActivity : ComponentActivity() { // Apply language preference when the activity starts applyLanguagePreference() + val preferencesManager = PreferencesManager(this) + setContent { val navController = rememberNavController() val context = LocalContext.current var canExitApp by remember { mutableStateOf(false) } -// var showExitToast by remember { mutableStateOf(false) } val currentLanguage by languageViewModel.currentLanguage.collectAsState() val appUpdateViewModel: AppUpdateViewModel = viewModel() val updateAvailable by appUpdateViewModel.updateAvailable.collectAsState() - // var showExitDialog by remember { mutableStateOf(false) } - // Initialize update check LaunchedEffect(Unit) { appUpdateViewModel.initializeAppUpdateCheck(context as Activity) } - -// // Handle back press -// BackHandler { -// if (canExitApp) { -// // Exit the app -// (context as? Activity)?.finish() -// } else { -// showExitToast = true -// canExitApp = true -// } -// } -// -// -// // Show exit toast with delay for reset -// if (showExitToast) { -// Toast.makeText(context, "Press back again to exit", Toast.LENGTH_SHORT).show() -// -// // Delay resetting `showExitToast` and `canExitApp` -// LaunchedEffect(showExitToast) { -// kotlinx.coroutines.delay(2000) // 2 seconds delay -// showExitToast = false -// canExitApp = false -// } -// } - - - // Update Alert UpdateAlert( showDialog = updateAvailable, @@ -187,15 +200,6 @@ class MainActivity : ComponentActivity() { } ) -// // Exit Confirmation -// ExitConfirmationDialog( -// showDialog = showExitDialog, -// onDismiss = { showExitDialog = false }, -// onConfirm = { (context as? Activity)?.finish() } -// ) - - - LaunchedEffect(currentLanguage) { languageViewModel.updateLocale(context = applicationContext, Locale(currentLanguage.code)) } @@ -229,20 +233,8 @@ class MainActivity : ComponentActivity() { navController = navController, startDestination = Routes.HOME, ) { -// composable(Routes.HOME) { -// BackHandler(enabled = canExitApp) { -// (context as? Activity)?.finish() -// } -// LaunchedEffect(Unit) { -// canExitApp = true -// } -// Home(navController, languageViewModel, languages) -// } composable(Routes.HOME) { - //val context = LocalContext.current - // var canExitApp by remember { mutableStateOf(false) } var showExitToast by remember { mutableStateOf(false) } - // Handle back press with confirmation BackHandler { if (canExitApp) { @@ -257,11 +249,15 @@ class MainActivity : ComponentActivity() { // Show exit toast and reset the state after a delay if (showExitToast) { // Show the toast - Toast.makeText(context, "Press back again to exit", Toast.LENGTH_SHORT).show() + Toast.makeText( + context, + context.getString(R.string.press_back_again), + Toast.LENGTH_SHORT + ).show() // Reset `showExitToast` and `canExitApp` after 2 seconds LaunchedEffect(Unit) { - kotlinx.coroutines.delay(2000) // 2 seconds delay + delay(2000) // 2 seconds delay showExitToast = false canExitApp = false } @@ -271,10 +267,25 @@ class MainActivity : ComponentActivity() { Home(navController, languageViewModel, languages) } + composable("userGuideScreen"){ + UserGuideScreen(navController) + } + composable(Routes.SITE_LIST) { LaunchedEffect(Unit) { canExitApp = false } +// // Check if user has agreed to terms +// if (preferencesManager.hasAgreedToTerms) { +// ScreenWithSidebar(navController) { +// CollectionSiteList(navController) +// } +// } else { +// // Redirect to privacy policy screen if not agreed +// navController.navigate(Routes.PRIVACY_POLICY) { +// popUpTo(Routes.HOME) { inclusive = true } // Clear back stack +// } +// } ScreenWithSidebar(navController) { CollectionSiteList(navController) } @@ -288,20 +299,42 @@ class MainActivity : ComponentActivity() { ScreenWithSidebar(navController) { FarmList( navController = navController, - siteId = siteId.toLong(), + siteId = siteId.toLong() ) } } } - composable(Routes.ADD_FARM) { backStackEntry -> - val siteId = backStackEntry.arguments?.getString("siteId") - LaunchedEffect(Unit) { - canExitApp = false - } - if (siteId != null) { - AddFarm(navController = navController, siteId = siteId.toLong()) - } + + + composable( + route = "addFarm/{siteId}?plotDataJson={plotDataJson}", + arguments = listOf( + navArgument("siteId") { type = NavType.LongType }, + navArgument("plotDataJson") { + type = + NavType.StringType + nullable = true + } + ) + ) { backStackEntry -> + val siteId = backStackEntry.arguments?.getLong("siteId") ?: 0L + val plotDataJson = backStackEntry.arguments?.getString("plotDataJson") ?:"" + Log.d("Navigation", "Received siteId: $siteId, plotDataJson: $plotDataJson") + Log.d("Navigation", "Received siteId: $siteId, plotDataJson: $plotDataJson") + + val plotData: Farm? = if (plotDataJson.isNotEmpty()) { + try { + val gson = Gson() + gson.fromJson(plotDataJson, Farm::class.java) + } catch (e: Exception) { + Log.e("Navigation", "Error deserializing plot data: ${e.message}") + null + } + } else null + + AddFarm(navController = navController, siteId = siteId, plotData = plotData, mapViewModel=viewModel) } + composable(Routes.ADD_SITE) { LaunchedEffect(Unit) { canExitApp = false @@ -324,15 +357,19 @@ class MainActivity : ComponentActivity() { composable(Routes.SET_POLYGON, arguments = listOf( + navArgument("siteId") { type = NavType.LongType }, navArgument("coordinates") { type = NavType.StringType }, navArgument("accuracyArray") { type = NavType.StringType } ) ) { backStackEntry -> + val siteId = backStackEntry.arguments?.getLong("siteId") ?: 0L LaunchedEffect(Unit) { canExitApp = false } - SetPolygon(navController, viewModel) + PlotVisualizationApp(navController, viewModel,siteId,languageViewModel) } + + composable(Routes.SETTINGS) { LaunchedEffect(Unit) { canExitApp = false @@ -344,6 +381,15 @@ class MainActivity : ComponentActivity() { languages, ) } + + composable(Routes.PRIVACY_POLICY) { + PrivacyPolicyScreen(BuildConfig.DATA_PRIVACY_URL, onAgree = { + navController.navigate(Routes.ADD_SITE) { + popUpTo(Routes.HOME) { inclusive = true } // Optional: clear back stack + } + + }) + } } } } diff --git a/app/src/main/java/org/technoserve/farmcollector/MapApplication.kt b/app/src/main/java/org/technoserve/farmcollector/MapApplication.kt index 806f52b..60756a5 100644 --- a/app/src/main/java/org/technoserve/farmcollector/MapApplication.kt +++ b/app/src/main/java/org/technoserve/farmcollector/MapApplication.kt @@ -3,5 +3,5 @@ package org.technoserve.farmcollector import android.app.Application import dagger.hilt.android.HiltAndroidApp -@HiltAndroidApp -class MapApplication : Application() \ No newline at end of file +//@HiltAndroidApp +//class MapApplication : Application() \ No newline at end of file diff --git a/app/src/main/java/org/technoserve/farmcollector/database/AppDatabase.kt b/app/src/main/java/org/technoserve/farmcollector/database/AppDatabase.kt index e87ebb3..3e36167 100644 --- a/app/src/main/java/org/technoserve/farmcollector/database/AppDatabase.kt +++ b/app/src/main/java/org/technoserve/farmcollector/database/AppDatabase.kt @@ -1,6 +1,7 @@ package org.technoserve.farmcollector.database import android.content.Context +import android.util.Log import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase @@ -13,8 +14,10 @@ import org.technoserve.farmcollector.database.converters.DateConverter import org.technoserve.farmcollector.database.dao.FarmDAO import org.technoserve.farmcollector.database.helpers.ContextProvider import org.technoserve.farmcollector.database.helpers.MigrationHelper +import org.technoserve.farmcollector.database.mappers.CommodityConverter import org.technoserve.farmcollector.database.models.CollectionSite import org.technoserve.farmcollector.database.models.Farm +import timber.log.Timber /** * This class is used to create app database and to run migrations from one db version to another @@ -27,8 +30,10 @@ import org.technoserve.farmcollector.database.models.Farm * */ -@Database(entities = [Farm::class, CollectionSite::class], version = 20, exportSchema = true) -@TypeConverters(CoordinateListConvert::class, AccuracyListConvert::class, DateConverter::class) +@Database(entities = [Farm::class, CollectionSite::class], version = 21, exportSchema = true) +@TypeConverters(CoordinateListConvert::class, AccuracyListConvert::class, DateConverter::class, + CommodityConverter::class +) abstract class AppDatabase : RoomDatabase() { abstract fun farmsDAO(): FarmDAO @@ -85,6 +90,14 @@ abstract class AppDatabase : RoomDatabase() { } } + val MIGRATION_20_21 = object : Migration(20, 21) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE CollectionSites ADD COLUMN commodity TEXT NOT NULL DEFAULT 'coffee'") + Timber.d("Migration 20_21: Column 'commodity' added to CollectionSites") + } + } + + fun getInstance(context: Context): AppDatabase { synchronized(this) { var instance = INSTANCE @@ -101,7 +114,8 @@ abstract class AppDatabase : RoomDatabase() { MIGRATION_16_17, MIGRATION_17_18, MIGRATION_18_19, - MIGRATION_19_20 + MIGRATION_19_20, + MIGRATION_20_21 ) .build() diff --git a/app/src/main/java/org/technoserve/farmcollector/database/FarmRepository.kt b/app/src/main/java/org/technoserve/farmcollector/database/FarmRepository.kt new file mode 100644 index 0000000..00eacbe --- /dev/null +++ b/app/src/main/java/org/technoserve/farmcollector/database/FarmRepository.kt @@ -0,0 +1,209 @@ +package org.technoserve.farmcollector.database + +import android.content.ContentValues.TAG +import android.util.Log +import androidx.lifecycle.LiveData +import org.technoserve.farmcollector.database.dao.FarmDAO +import org.technoserve.farmcollector.database.models.CollectionSite +import org.technoserve.farmcollector.database.models.Farm + +class FarmRepository(private val farmDAO: FarmDAO) { + + val readAllSites: LiveData> = farmDAO.getSites() + val readData: LiveData> = farmDAO.getData() + fun readAllFarms(siteId: Long): LiveData> { + return farmDAO.getAll(siteId) + } + + fun getAllFarms(): List { + return farmDAO.getAllFarms() + } + + fun getAllSites(): List{ + return farmDAO.getAllSites() + } + + fun readAllFarmsSync(siteId: Long): List { + return farmDAO.getAllSync(siteId) + } + + fun readFarm(farmId: Long): LiveData> { + return farmDAO.getFarmById(farmId) + } + suspend fun addFarm(farm: Farm) { + try { + // Step 1: Ensure that the CollectionSite exists for the farm's siteId + val collectionSite = farmDAO.getCollectionSiteById(farm.siteId) + if (collectionSite == null) { + Log.e(TAG, "Failed to insert farm. CollectionSite with siteId ${farm.siteId} does not exist.") + return // Exit if the CollectionSite doesn't exist + } + + // Step 2: Check if the farm already exists + val existingFarm = isFarmDuplicate(farm) + if (existingFarm == null) { + Log.d(TAG, "Attempting to insert new farm: $farm") + val insertResult = farmDAO.insert(farm) + + if (insertResult != -1L) { + Log.d(TAG, "New farm inserted successfully: $farm") + } else { + Log.e(TAG, "Farm insertion failed, insertResult: $insertResult") + } + } else { + Log.d(TAG, "Farm already exists: $existingFarm") + + // Step 3: Check if the farm needs an update + if (farmNeedsUpdate(existingFarm, farm)) { + Log.d(TAG, "Updating existing farm: $farm") + farmDAO.update(farm) + } else { + Log.d(TAG, "No update needed for farm: $farm") + } + } + } catch (e: Exception) { + Log.e(TAG, "Error during farm insertion or update: ${e.message}", e) + } + } + + + + private suspend fun addFarms(farms: List) { + farmDAO.insertAllIfNotExists(farms) + } + + + suspend fun addSite(site: CollectionSite) : Boolean { + // Check if the site already exists + val existingSite = isSiteDuplicate(site) + + if (existingSite == null) { + Log.d(TAG, "Attempting to insert new site: $site") + val insertResult = farmDAO.insertSite(site) + Log.d(TAG, "Insert operation result: $insertResult") + if (insertResult != -1L) { + Log.d(TAG, "New site inserted: $site") + return true + } else { + Log.d(TAG, "Insertion was ignored (likely due to conflict strategy)") + return false + } + } else { + Log.d(TAG, "Site already exists: $existingSite") + return false + } + } + + + fun getLastFarm(): LiveData> { + return farmDAO.getLastFarm() + } + suspend fun getFarmBySiteId(siteId: Long): Farm? { + return farmDAO.getFarmBySiteId(siteId) + } + + + suspend fun updateFarm(farm: Farm) { + farmDAO.update(farm) + } + private suspend fun updateFarms(farms: List) { + farms.forEach { updateFarm(it) } + } + + suspend fun updateSite(site: CollectionSite) { + farmDAO.updateSite(site) + } + + + suspend fun deleteFarm(farm: Farm) { + farmDAO.delete(farm) + } + + suspend fun deleteFarmById(farm: Farm) { + farmDAO.deleteFarmByRemoteId(farm.remoteId) + } + + + suspend fun deleteAllFarms() { + farmDAO.deleteAll() + } + + suspend fun updateSyncStatus(id: Long) { + farmDAO.updateSyncStatus(id) + } + + suspend fun updateSyncListStatus(ids: List) { + farmDAO.updateSyncListStatus(ids) + } + + suspend fun deleteList(ids: List) { + farmDAO.deleteList(ids) + } + + suspend fun deleteListSite(ids: List) { + farmDAO.deleteListSite(ids) + } + + suspend fun isFarmDuplicateBoolean(farm: Farm): Boolean { + return farmDAO.getFarmByDetails( + farm.remoteId, + farm.farmerName, + farm.village, + farm.district + ) != null + } + + suspend fun isFarmDuplicate(farm: Farm): Farm? { + return farmDAO.getFarmByDetails( + farm.remoteId, + farm.farmerName, + farm.village, + farm.district + ) + } + + suspend fun isSiteDuplicate(collectionSite: CollectionSite): CollectionSite? { + return farmDAO.getSiteByDetails( + collectionSite.siteId, + collectionSite.district, + collectionSite.name, + collectionSite.village + ) + } + + // Function to fetch a farm by remote ID, farmer name, and address + suspend fun getFarmByDetails(farm: Farm): Farm? { + return farmDAO.getFarmByDetails( + farm.remoteId, + farm.farmerName, + farm.village, + farm.district + ) + } + + fun farmNeedsUpdate(existingFarm: Farm, newFarm: Farm): Boolean { + return existingFarm.farmerName != newFarm.farmerName || + existingFarm.size != newFarm.size || + existingFarm.village != newFarm.village || + existingFarm.district != newFarm.district + } + + fun isDuplicateFarm(existingFarm: Farm, newFarm: Farm): Boolean { + return existingFarm.farmerName == newFarm.farmerName && + existingFarm.size == newFarm.size && + existingFarm.village == newFarm.village && + existingFarm.district == newFarm.district + } + + + fun farmNeedsUpdateImport(newFarm: Farm): Boolean { + return newFarm.farmerName.isEmpty() || + newFarm.district.isEmpty() || + newFarm.village.isEmpty() || + newFarm.latitude == "0.0" || + newFarm.longitude == "0.0" || + newFarm.size == 0.0f || + newFarm.remoteId.toString().isEmpty() + // || newFarm.coordinates.isNullOrEmpty() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/technoserve/farmcollector/database/dao/FarmDAO.kt b/app/src/main/java/org/technoserve/farmcollector/database/dao/FarmDAO.kt index 3e3cf7f..81e5658 100644 --- a/app/src/main/java/org/technoserve/farmcollector/database/dao/FarmDAO.kt +++ b/app/src/main/java/org/technoserve/farmcollector/database/dao/FarmDAO.kt @@ -8,6 +8,7 @@ import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction import androidx.room.Update +import kotlinx.coroutines.flow.Flow import org.technoserve.farmcollector.database.models.CollectionSite import org.technoserve.farmcollector.database.models.Farm import java.util.UUID @@ -56,6 +57,9 @@ interface FarmDAO { @Query("SELECT * FROM CollectionSites WHERE siteId = :siteId") suspend fun getSiteById(siteId: Long): CollectionSite? + @Query("SELECT * FROM CollectionSites WHERE siteId = :siteId") + fun getSiteByIdNew(siteId: Long): Flow + @Update fun updateSite(site: CollectionSite) @@ -156,5 +160,7 @@ interface FarmDAO { @Query("SELECT * FROM CollectionSites LIMIT :limit OFFSET :offset") fun getCollectionSites(offset: Int, limit: Int): List + @Query("SELECT * FROM farms WHERE id = :id") + fun getFarm(id: Long): Farm? } \ No newline at end of file diff --git a/app/src/main/java/org/technoserve/farmcollector/database/helpers/MigrationHelper.kt b/app/src/main/java/org/technoserve/farmcollector/database/helpers/MigrationHelper.kt index 431d28f..bd8faed 100644 --- a/app/src/main/java/org/technoserve/farmcollector/database/helpers/MigrationHelper.kt +++ b/app/src/main/java/org/technoserve/farmcollector/database/helpers/MigrationHelper.kt @@ -1,7 +1,9 @@ package org.technoserve.farmcollector.database.helpers import android.content.Context +import android.database.sqlite.SQLiteException import androidx.sqlite.db.SupportSQLiteDatabase +import timber.log.Timber /** * This class helps to run the migrations of a database from one version to another @@ -12,7 +14,14 @@ class MigrationHelper(private val context: Context) { sql.split(";").forEach { statement -> val trimmed = statement.trim() if (trimmed.isNotEmpty()) { - database.execSQL(trimmed) + //database.execSQL(trimmed) + try { + database.execSQL(trimmed) + } catch (e: SQLiteException) { + // Log the error, potentially clear the database or offer a recovery mechanism + Timber.e(e, "Error executing migration SQL") + // Example: database.execSQL("DELETE FROM Farm") // Clear corrupt data + } } } } diff --git a/app/src/main/java/org/technoserve/farmcollector/database/helpers/PreferencesManager.kt b/app/src/main/java/org/technoserve/farmcollector/database/helpers/PreferencesManager.kt new file mode 100644 index 0000000..67175b5 --- /dev/null +++ b/app/src/main/java/org/technoserve/farmcollector/database/helpers/PreferencesManager.kt @@ -0,0 +1,17 @@ +package org.technoserve.farmcollector.database.helpers + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit + + +class PreferencesManager(context: Context) { + private val sharedPreferences: SharedPreferences = + context.getSharedPreferences("privacy_policy", Context.MODE_PRIVATE) + + private val AGREED_KEY = "has_agreed_to_privacy_policy_terms" + + var hasAgreedToTerms: Boolean + get() = sharedPreferences.getBoolean(AGREED_KEY, false) + set(value) = sharedPreferences.edit { putBoolean(AGREED_KEY, value) } +} \ No newline at end of file diff --git a/app/src/main/java/org/technoserve/farmcollector/database/helpers/map/JavascriptInterface.kt b/app/src/main/java/org/technoserve/farmcollector/database/helpers/map/JavascriptInterface.kt new file mode 100644 index 0000000..9b68144 --- /dev/null +++ b/app/src/main/java/org/technoserve/farmcollector/database/helpers/map/JavascriptInterface.kt @@ -0,0 +1,244 @@ +package org.technoserve.farmcollector.database.helpers.map + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.webkit.JavascriptInterface +import android.webkit.WebView +import androidx.compose.runtime.MutableState +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavController +import com.google.gson.Gson +import org.json.JSONObject +import org.technoserve.farmcollector.R +import org.technoserve.farmcollector.database.AppDatabase +import org.technoserve.farmcollector.database.models.Farm +import org.technoserve.farmcollector.ui.composes.ConfirmDialog +import org.technoserve.farmcollector.utils.convertSize +import org.technoserve.farmcollector.viewmodels.MapViewModel +import java.net.URLEncoder +import java.util.UUID + +/** + * This class provides a JavaScript interface for the WebView + */ +class JavaScriptInterface( + private val context: Context, + private val navController: NavController, + private val mapViewModel: MapViewModel +) { + fun parseCoordinates(coordinatesString: String): List> { + val result = mutableListOf>() + val cleanedString = coordinatesString.trim().removeSurrounding("\"", "").replace(" ", "") + + if (cleanedString.isNotEmpty()) { + // Check if the coordinates are in polygon or point format + val isPolygon = cleanedString.startsWith("[[") && cleanedString.endsWith("]]") + val isPoint = cleanedString.startsWith("[") && cleanedString.endsWith("]") && !isPolygon + + if (isPolygon) { + // Handle Polygon Format + val pairs = + cleanedString + .removePrefix("[[") + .removeSuffix("]]") + .split("],[") + .map { it.split(",") } + for (pair in pairs) { + if (pair.size == 2) { + try { + val lat = pair[0].toDouble() + val lon = pair[1].toDouble() + result.add(Pair(lat, lon)) + } catch (e: NumberFormatException) { + println("Error parsing polygon coordinate pair: ${pair.joinToString(",")}") + } + } + } + + // Ensure polygon is closed + if (result.isNotEmpty() && result.first() != result.last()) { + result.add(result.first()) + } + + } else if (isPoint) { + // Handle Point Format + val coords = cleanedString.removePrefix("[").removeSuffix("]").split(", ") + if (coords.size == 2) { + try { + val lat = coords[1].toDouble() + val lon = coords[0].toDouble() + result.add(Pair(lat, lon)) + } catch (e: NumberFormatException) { + println("Error parsing point coordinate pair: ${coords.joinToString(",")}") + } + } + } else { + println("Unrecognized coordinates format: $coordinatesString") + } + } + return result + } + + @JavascriptInterface + fun receivePlotData(plotDataJson: String) { + Log.d("JavaScriptInterface", "Received Plot Data: $plotDataJson") + val sharedPref = context.getSharedPreferences("FarmCollector", Context.MODE_PRIVATE) + + try { + // Parse the JSON string into a JSONObject + val jsonObject = JSONObject(plotDataJson) + + println("JavaScriptInterface Parsed JSON Object: $jsonObject") + + // Extract the coordinates as a string + val coordinatesArray = jsonObject.getJSONArray("coordinates") + val coordinatesString = coordinatesArray.toString() + + // Use the parseCoordinates function to parse the coordinates + val parsedCoordinates = parseCoordinates(coordinatesString) + + // Map the parsed data to the Farm object + val farmData = Farm( + siteId = jsonObject.getLong("siteId"), + remoteId = jsonObject.optString("remoteId").takeIf { it.isNotBlank() }?.let { UUID.fromString(it) } ?: UUID.randomUUID(), + farmerPhoto = jsonObject.getString("farmerPhoto"), + farmerName = jsonObject.getString("farmerName")?: "Farmer", + memberId = jsonObject.getString("memberId"), + village = jsonObject.getString("village"), + district = jsonObject.getString("district"), + purchases = jsonObject.optString("purchases")?.toFloatOrNull(), + size = jsonObject.getDouble("size").toFloat(), + latitude = jsonObject.getString("latitude"), + longitude = jsonObject.getString("longitude"), + coordinates = parsedCoordinates, + accuracyArray = jsonObject.optJSONArray("accuracyArray")?.let { array -> + List(array.length()) { i -> array.getDouble(i).toFloat() } + }, + createdAt = System.currentTimeMillis(), + updatedAt = System.currentTimeMillis() , + synced = false, + scheduledForSync = false, + needsUpdate = false + ) + + Log.d("JavaScriptInterface", "Parsed Plot Data: $farmData") + +// mapViewModel.updatePlotData(farmData) + mapViewModel.updatePlotData( + siteId = farmData.siteId, + coordinates = parsedCoordinates, + latitude = farmData.latitude, + longitude = farmData.longitude, + size = farmData.size, + accuracyArray = farmData.accuracyArray as List? + ) + + + val gson = Gson() + val farmDataJson = gson.toJson(farmData) + if(farmDataJson == null){ + Log.d("JavaScriptInterface", "Farm Data Json is null") + navController.navigate("addFarm/${farmData.siteId}") + } + val encodedFarmDataJson = URLEncoder.encode(farmDataJson, "UTF-8") // Encode J SON to avoid special character issues' + + val calculatedArea: Double = farmData.size?.toDouble() ?: 0.0 + val enteredArea = sharedPref.getString("plot_size", "0.0")?.toDoubleOrNull() ?: 0.0 + val selectedUnit = sharedPref.getString("selectedUnit", "Ha") ?: "Ha" + val enteredAreaConverted = convertSize(enteredArea, selectedUnit) + + // Navigate to the AddFarm composable on the main thread + Handler(Looper.getMainLooper()).post { + mapViewModel.showAreaDialog(calculatedArea = calculatedArea.toString(), enteredArea = enteredAreaConverted.toString())// Update the dialog state in the ViewModel + } + + } catch (e: Exception) { + Log.e("JavaScriptInterface", "Error parsing plot data: ${e.message}", e) + } + } + + + + + + @JavascriptInterface + fun getPlots(): String { + return Gson().toJson(AppDatabase.getInstance(context).farmsDAO().getAllFarms()) + } + + fun parseCoordinatesVisualize(coordinatesString: String): List> { + if (coordinatesString.isEmpty()) return emptyList() + + // Regex to extract (lat, lng) pairs + val regex = "\\(([^,]+), ([^\\)]+)\\)".toRegex() + val matches = regex.findAll(coordinatesString) + + // Convert matches to List> + return matches.map { match -> + val lat = match.groupValues[1].toDouble() + val lng = match.groupValues[2].toDouble() + Pair(lat, lng) + }.toList() + } + + @JavascriptInterface + fun getSelectedPlot(id: Long): String { + val farm = AppDatabase.getInstance(context).farmsDAO().getFarm(id) + Log.d("Selected farm", "getSelectedPlot: $farm") + + if (farm != null) { + + // Determine coordinates + val coordinates = farm.coordinates?.toString()?.takeIf { it.isNotEmpty() } + ?: "[${farm.latitude}, ${farm.longitude}]" // Use latitude and longitude if coordinates are null + + // Parse coordinates + val parsedCoordinates = parseCoordinatesVisualize(coordinates) + + Log.d("Selected Coordinates", "getSelectedPlot: $parsedCoordinates") + + // Create a new farm object with parsed coordinates + val updatedFarm = farm.copy(coordinates = parsedCoordinates) + + // Return JSON representation + return Gson().toJson(updatedFarm) + } else { + return "{}" // Return an empty JSON object if no farm is found + } + } + + + @JavascriptInterface + fun showClearMapDialog() { + Handler(Looper.getMainLooper()).post { + mapViewModel.showClearDialog() + } + } + + @JavascriptInterface + fun showConfirmPolygonDialog() { + Log.d("JavaScriptInterface", "showConfirmPolygonDialog triggered") + Handler(Looper.getMainLooper()).post { + mapViewModel.showConfirmPolygonDialog() + Log.d("MapViewModel", "showConfirmPolygonDialog() executed in ViewModel") + } + } + + @JavascriptInterface + fun showInsufficientPointsDialog() { + Handler(Looper.getMainLooper()).post { + mapViewModel.showAlertDialog() + } + } + + @JavascriptInterface + fun showPolygonTooSmallDialog() { + Handler(Looper.getMainLooper()).post { + mapViewModel.showInvalidPolygonDialog() + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/technoserve/farmcollector/database/helpers/map/LocationHelper.kt b/app/src/main/java/org/technoserve/farmcollector/database/helpers/map/LocationHelper.kt index 131b44d..161b355 100644 --- a/app/src/main/java/org/technoserve/farmcollector/database/helpers/map/LocationHelper.kt +++ b/app/src/main/java/org/technoserve/farmcollector/database/helpers/map/LocationHelper.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.suspendCancellableCoroutine import org.technoserve.farmcollector.R import org.technoserve.farmcollector.database.models.map.LocationState +import org.technoserve.farmcollector.ui.screens.farms.siteID import org.technoserve.farmcollector.viewmodels.MapViewModel import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -228,7 +229,7 @@ class LocationHelper(private val context: Context) : SensorEventListener { if (enteredSize >= 4f) { navController.currentBackStackEntry?.arguments?.putParcelable("farmData", null) - navController.navigate("setPolygon") + navController.navigate("setPolygon/${siteID}") mapViewModel.clearCoordinates() } } diff --git a/app/src/main/java/org/technoserve/farmcollector/database/mappers/CommodityConverter.kt b/app/src/main/java/org/technoserve/farmcollector/database/mappers/CommodityConverter.kt new file mode 100644 index 0000000..0b11653 --- /dev/null +++ b/app/src/main/java/org/technoserve/farmcollector/database/mappers/CommodityConverter.kt @@ -0,0 +1,16 @@ +package org.technoserve.farmcollector.database.mappers + +import androidx.room.TypeConverter +import org.technoserve.farmcollector.database.models.Commodity + +class CommodityConverter { + @TypeConverter + fun fromCommodity(commodity: Commodity): String { + return commodity.displayName + } + + @TypeConverter + fun toCommodity(value: String): Commodity { + return Commodity.fromDisplayName(value) + } +} diff --git a/app/src/main/java/org/technoserve/farmcollector/database/models/CollectionSite.kt b/app/src/main/java/org/technoserve/farmcollector/database/models/CollectionSite.kt index 3fb0713..de54b0d 100644 --- a/app/src/main/java/org/technoserve/farmcollector/database/models/CollectionSite.kt +++ b/app/src/main/java/org/technoserve/farmcollector/database/models/CollectionSite.kt @@ -5,7 +5,9 @@ import androidx.room.Entity import androidx.room.PrimaryKey import androidx.room.TypeConverters import org.technoserve.farmcollector.database.converters.DateConverter -/* +import org.technoserve.farmcollector.database.mappers.CommodityConverter + +/** * * This class represents a collection site, with additional fields for agent name, phone number, email, village, * district, and timestamps for created and updated at. @@ -33,9 +35,27 @@ data class CollectionSite( @ColumnInfo(name = "updatedAt") @TypeConverters(DateConverter::class) var updatedAt: Long, + @ColumnInfo(name = "commodity") + @TypeConverters(CommodityConverter::class) + var commodity: Commodity = Commodity.COFFEE, ) { @PrimaryKey(autoGenerate = true) var siteId: Long = 0L } +enum class Commodity(val displayName: String) { + COFFEE("coffee"), + COCOA("cocoa"); + + companion object { + fun fromDisplayName(name: String): Commodity { + return Commodity.entries.firstOrNull { it.displayName.equals(name, ignoreCase = true) } + ?: COFFEE // default + } + + fun displayNames(): List { + return Commodity.entries.map { it.displayName } + } + } +} diff --git a/app/src/main/java/org/technoserve/farmcollector/database/models/Farm.kt b/app/src/main/java/org/technoserve/farmcollector/database/models/Farm.kt index 29e22bf..8784dac 100644 --- a/app/src/main/java/org/technoserve/farmcollector/database/models/Farm.kt +++ b/app/src/main/java/org/technoserve/farmcollector/database/models/Farm.kt @@ -28,7 +28,7 @@ TypeConverters: Specifies the converters that will be used to convert certain da the database. */ - +@Parcelize @Entity( tableName = "Farms", foreignKeys = [ @@ -40,128 +40,27 @@ the database. ), ], ) -@Parcelize -@TypeConverters(CoordinateListConvert::class, AccuracyListConvert::class) +@TypeConverters(CoordinateListConvert::class, AccuracyListConvert::class, DateConverter::class) data class Farm( - @ColumnInfo(name = "siteId") - var siteId: Long, - @ColumnInfo(name = "remote_id") - var remoteId: UUID = UUID.randomUUID(), - @ColumnInfo(name = "farmerPhoto") - var farmerPhoto: String, - @ColumnInfo(name = "farmerName") - var farmerName: String, - @ColumnInfo(name = "memberId") - var memberId: String, - @ColumnInfo(name = "village") - var village: String, - @ColumnInfo(name = "district") - var district: String, - @ColumnInfo(name = "purchases") - var purchases: Float?, - @ColumnInfo(name = "size") - var size: Float, - @ColumnInfo(name = "latitude") - var latitude: String, - @ColumnInfo(name = "longitude") - var longitude: String, - @ColumnInfo(name = "coordinates") - @TypeConverters(CoordinateListConvert::class) - var coordinates: List>?, - @ColumnInfo(name = "accuracyArray") // New field - @TypeConverters(AccuracyListConvert::class) - var accuracyArray: List?, // List to store accuracies - @ColumnInfo(name = "synced", defaultValue = "0") - val synced: Boolean = false, - @ColumnInfo(name = "scheduledForSync", defaultValue = "0") - val scheduledForSync: Boolean = false, - @ColumnInfo(name = "createdAt") - @TypeConverters(DateConverter::class) - val createdAt: Long, - @ColumnInfo(name = "updatedAt") - @TypeConverters(DateConverter::class) - var updatedAt: Long, - @ColumnInfo(name = "needsUpdate", defaultValue = "0") - var needsUpdate: Boolean = false, -) : Parcelable { - @PrimaryKey(autoGenerate = true) - var id: Long = 0L - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Farm - - return id == other.id - } - - override fun hashCode(): Int = id.hashCode() - - constructor(parcel: Parcel) : this( - parcel.readLong(), - UUID.fromString(parcel.readString()), - parcel.readString()!!, - parcel.readString()!!, - parcel.readString()!!, - parcel.readString()!!, - parcel.readString()!!, - parcel.readValue(Float::class.java.classLoader) as? Float, - parcel.readFloat(), - parcel.readString()!!, - parcel.readString()!!, - parcel.createTypedArrayList(ParcelablePair.CREATOR)?.map { Pair(it.first, it.second) }, - parcel.createFloatArray()?.toList(), // Read accuracyArray as a List - parcel.readByte() != 0.toByte(), - parcel.readByte() != 0.toByte(), - parcel.readLong(), - parcel.readLong(), - parcel.readByte() != 0.toByte() - ) { - id = parcel.readLong() - } - - override fun describeContents(): Int = 0 - - companion object : Parceler { - - override fun Farm.write(parcel: Parcel, flags: Int) { - parcel.writeLong(siteId) - parcel.writeString(remoteId.toString()) - parcel.writeString(farmerPhoto) - parcel.writeString(farmerName) - parcel.writeString(memberId) - parcel.writeString(village) - parcel.writeString(district) - parcel.writeValue(purchases) - parcel.writeFloat(size) - parcel.writeString(latitude) - parcel.writeString(longitude) - parcel.writeTypedList(coordinates?.map { - it.first?.let { it1 -> - it.second?.let { it2 -> - ParcelablePair( - it1, - it2 - ) - } - } - }) - parcel.writeFloatArray( - accuracyArray?.filterNotNull()?.toFloatArray() - ) // Write accuracyArray - parcel.writeByte(if (synced) 1 else 0) - parcel.writeByte(if (scheduledForSync) 1 else 0) - parcel.writeLong(createdAt) - parcel.writeLong(updatedAt) - parcel.writeByte(if (needsUpdate) 1 else 0) - parcel.writeLong(id) - } - - override fun create(parcel: Parcel): Farm { - return Farm(parcel) - } - } -} + @ColumnInfo(name = "siteId") var siteId: Long = 0L, + @ColumnInfo(name = "remote_id") var remoteId: UUID = UUID.randomUUID(), + @ColumnInfo(name = "farmerPhoto") var farmerPhoto: String = "", + @ColumnInfo(name = "farmerName") var farmerName: String = "", + @ColumnInfo(name = "memberId") var memberId: String = "", + @ColumnInfo(name = "village") var village: String = "", + @ColumnInfo(name = "district") var district: String = "", + @ColumnInfo(name = "purchases") var purchases: Float? = null, + @ColumnInfo(name = "size") var size: Float = 0f, + @ColumnInfo(name = "latitude") var latitude: String = "", + @ColumnInfo(name = "longitude") var longitude: String = "", + @ColumnInfo(name = "coordinates") var coordinates: List>? = emptyList(), + @ColumnInfo(name = "accuracyArray") var accuracyArray: List? = emptyList(), + @ColumnInfo(name = "synced", defaultValue = "0") val synced: Boolean = false, + @ColumnInfo(name = "scheduledForSync", defaultValue = "0") val scheduledForSync: Boolean = false, + @ColumnInfo(name = "createdAt") val createdAt: Long = System.currentTimeMillis(), + @ColumnInfo(name = "updatedAt") var updatedAt: Long = System.currentTimeMillis(), + @ColumnInfo(name = "needsUpdate", defaultValue = "0") var needsUpdate: Boolean = false, + @PrimaryKey(autoGenerate = true) var id: Long = 0L +) : Parcelable diff --git a/app/src/main/java/org/technoserve/farmcollector/database/models/ParcelableFarmData.kt b/app/src/main/java/org/technoserve/farmcollector/database/models/ParcelableFarmData.kt index 0fc1766..6b5aaae 100644 --- a/app/src/main/java/org/technoserve/farmcollector/database/models/ParcelableFarmData.kt +++ b/app/src/main/java/org/technoserve/farmcollector/database/models/ParcelableFarmData.kt @@ -2,7 +2,7 @@ package org.technoserve.farmcollector.database.models import android.os.Parcel import android.os.Parcelable -/* +/** * Parcelable class representing a pair of a Farm and a String representing the view. * Used to pass data between Activities or Fragments. * diff --git a/app/src/main/java/org/technoserve/farmcollector/database/models/ParcelablePair.kt b/app/src/main/java/org/technoserve/farmcollector/database/models/ParcelablePair.kt index 2d9cab0..f30dd71 100644 --- a/app/src/main/java/org/technoserve/farmcollector/database/models/ParcelablePair.kt +++ b/app/src/main/java/org/technoserve/farmcollector/database/models/ParcelablePair.kt @@ -2,7 +2,7 @@ package org.technoserve.farmcollector.database.models import android.os.Parcel import android.os.Parcelable -/* +/** * This class is used to transfer data between activities/fragments and Parcelable objects. * * @param first First value diff --git a/app/src/main/java/org/technoserve/farmcollector/database/models/ParsedFarms.kt b/app/src/main/java/org/technoserve/farmcollector/database/models/ParsedFarms.kt index a961f3c..a077569 100644 --- a/app/src/main/java/org/technoserve/farmcollector/database/models/ParsedFarms.kt +++ b/app/src/main/java/org/technoserve/farmcollector/database/models/ParsedFarms.kt @@ -6,7 +6,7 @@ package org.technoserve.farmcollector.database.models * @param validFarms A list of valid farms * @param invalidFarms A list of invalid farms (e.g., farms with missing or invalid data) * - * @author 13612333 (Rajat) + * @author */ data class ParsedFarms( val validFarms: List, diff --git a/app/src/main/java/org/technoserve/farmcollector/database/sync/remote/ApiService.kt b/app/src/main/java/org/technoserve/farmcollector/database/sync/remote/ApiService.kt index a275f5d..c596c68 100644 --- a/app/src/main/java/org/technoserve/farmcollector/database/sync/remote/ApiService.kt +++ b/app/src/main/java/org/technoserve/farmcollector/database/sync/remote/ApiService.kt @@ -1,10 +1,14 @@ package org.technoserve.farmcollector.database.sync.remote +import okhttp3.ResponseBody import org.technoserve.farmcollector.database.models.DeviceFarmDto import org.technoserve.farmcollector.database.models.FarmRequest import retrofit2.Response import retrofit2.http.Body +import retrofit2.http.GET import retrofit2.http.POST +import retrofit2.http.Streaming +import retrofit2.http.Url /** * this is the implementation of the API interface that will be used to connect to the device farm server TO sync the farm plots to remote server @@ -26,4 +30,9 @@ interface ApiService { @POST("/api/farm/restore/") suspend fun getFarmsByDeviceId(@Body request: FarmRequest): List + + // Use the @Url parameter to pass the full URL of the PDF file. + @Streaming + @GET + suspend fun downloadUserGuide(@Url fileUrl: String): Response } diff --git a/app/src/main/java/org/technoserve/farmcollector/repositories/FarmRepository.kt b/app/src/main/java/org/technoserve/farmcollector/repositories/FarmRepository.kt index 17af673..966c90f 100644 --- a/app/src/main/java/org/technoserve/farmcollector/repositories/FarmRepository.kt +++ b/app/src/main/java/org/technoserve/farmcollector/repositories/FarmRepository.kt @@ -4,6 +4,7 @@ import android.content.ContentValues.TAG import android.util.Log import androidx.lifecycle.LiveData import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext import org.technoserve.farmcollector.database.dao.FarmDAO import org.technoserve.farmcollector.database.models.CollectionSite @@ -38,7 +39,10 @@ class FarmRepository(private val farmDAO: FarmDAO) { fun readAllFarmsSync(siteId: Long): List { return farmDAO.getAllSync(siteId) } - + // GET SITE BY SITE ID + fun getSiteByIdNew(siteId: Long): Flow { + return farmDAO.getSiteByIdNew(siteId) + } suspend fun addFarm(farm: Farm) { try { farmDAO.getCollectionSiteById(farm.siteId) diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/components/BackupConfirmationDialog.kt b/app/src/main/java/org/technoserve/farmcollector/ui/components/BackupConfirmationDialog.kt new file mode 100644 index 0000000..b63eff8 --- /dev/null +++ b/app/src/main/java/org/technoserve/farmcollector/ui/components/BackupConfirmationDialog.kt @@ -0,0 +1,48 @@ +package org.technoserve.farmcollector.ui.components + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.technoserve.farmcollector.R + +@Composable +fun BackupConfirmationDialog( + isEnablingBackup: Boolean, + onConfirm: () -> Unit, + onCancel: () -> Unit +) { + AlertDialog( + onDismissRequest = onCancel, + title = { + Text( + text = stringResource( + id = if (isEnablingBackup) R.string.enable_backup_title else R.string.disable_backup_title + ) + ) + }, + text = { + Column { + Text( + text = stringResource( + id = if (isEnablingBackup) R.string.enable_backup_message else R.string.disable_backup_message + ) + ) + Spacer(modifier = Modifier.height(10.dp)) + Text(text = stringResource(id = R.string.proceed_question)) + } + }, + confirmButton = { + Button(onClick = onConfirm) { + Text(text = stringResource(id = R.string.confirm)) + } + }, + dismissButton = { + Button(onClick = onCancel) { + Text(text = stringResource(id = R.string.cancel)) + } + } + ) +} diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/components/BackupPromptDialog.kt b/app/src/main/java/org/technoserve/farmcollector/ui/components/BackupPromptDialog.kt new file mode 100644 index 0000000..611a2bd --- /dev/null +++ b/app/src/main/java/org/technoserve/farmcollector/ui/components/BackupPromptDialog.kt @@ -0,0 +1,73 @@ +package org.technoserve.farmcollector.ui.components + +import android.content.Context +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import androidx.navigation.NavController +import org.technoserve.farmcollector.utils.BackupPreferences +import org.technoserve.farmcollector.R + +/** + * A dialog that prompts the user to enable or disable data backup. + * + * @param context The context of the application. + * @param navController The navigation controller to handle navigation actions. + * @param showDialog A boolean indicating whether the dialog should be shown. + * @param onDismiss A callback function to be called when the dialog is dismissed. + */ + + +@Composable +fun BackupPromptDialog( + context: Context, + navController: NavController, + showDialog: Boolean, + onDismiss: () -> Unit +) { + val coroutineScope = rememberCoroutineScope() + + if (showDialog) { + AlertDialog( + onDismissRequest = { /* User must make a choice */ }, + title = { Text(stringResource(id = R.string.enable_data_backup_title)) }, + text = { + Column { + Text(stringResource(id = R.string.enable_backup_message)) + Spacer(modifier = Modifier.height(8.dp)) + Text(stringResource(id = R.string.disable_backup_message)) + } + }, + confirmButton = { + Button( + onClick = { + coroutineScope.launch { + BackupPreferences.saveBackupChoice(context, true) // Save user's choice + onDismiss() // Close the dialog + navController.navigate("siteList") // Navigate after selection + } + } + ) { + Text(stringResource(id = R.string.enable_backup)) + } + }, + dismissButton = { + Button( + onClick = { + coroutineScope.launch { + BackupPreferences.saveBackupChoice(context, false) // Save user's choice + onDismiss() // Close the dialog + navController.navigate("siteList") // Navigate after selection + } + } + ) { + Text(stringResource(id = R.string.disable_backup)) + } + } + ) + } +} diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/components/FarmCard.kt b/app/src/main/java/org/technoserve/farmcollector/ui/components/FarmCard.kt index 618c409..3e398da 100644 --- a/app/src/main/java/org/technoserve/farmcollector/ui/components/FarmCard.kt +++ b/app/src/main/java/org/technoserve/farmcollector/ui/components/FarmCard.kt @@ -44,115 +44,6 @@ import org.technoserve.farmcollector.ui.screens.farms.formatInput * @param onCardClick A callback to be invoked when the card is clicked. * @param onDeleteClick A callback to be invoked when the delete icon is clicked. */ -//@Composable -//fun FarmCard( -// farm: Farm, -// onCardClick: () -> Unit, -// onDeleteClick: () -> Unit, -//) { -// val textColor = MaterialTheme.colorScheme.onBackground -// Column( -// modifier = -// Modifier -// .fillMaxSize() -// .padding(top = 8.dp), -// verticalArrangement = Arrangement.Center, -// horizontalAlignment = Alignment.CenterHorizontally, -// ) { -// ElevatedCard( -// elevation = -// CardDefaults.cardElevation( -// defaultElevation = 6.dp, -// ), -// modifier = -// Modifier -// .background(MaterialTheme.colorScheme.background) -// .fillMaxWidth() -// .padding(8.dp), -// onClick = { -// onCardClick() -// }, -// ) { -// Column( -// modifier = -// Modifier -// .background(MaterialTheme.colorScheme.background) -// .padding(16.dp), -// ) { -// Row( -// horizontalArrangement = Arrangement.SpaceBetween, -// verticalAlignment = Alignment.CenterVertically, -// modifier = Modifier.fillMaxWidth(), -// ) { -// Text( -// text = farm.farmerName, -// style = -// MaterialTheme.typography.bodySmall.copy( -// fontSize = 18.sp, -// fontWeight = FontWeight.Bold, -// color = textColor, -// ), -// modifier = -// Modifier -// .weight(1.1f) -// .padding(bottom = 4.dp), -// ) -// Text( -// text = "${stringResource(id = R.string.size)}: ${formatInput(farm.size.toString())} ${ -// stringResource(id = R.string.ha) -// }", -// style = MaterialTheme.typography.bodySmall.copy(color = textColor), -// modifier = -// Modifier -// .weight(0.9f) -// .padding(bottom = 4.dp), -// ) -// IconButton( -// onClick = { -// onDeleteClick() -// }, -// modifier = -// Modifier -// .size(24.dp) -// .padding(4.dp), -// ) { -// Icon( -// imageVector = Icons.Default.Delete, -// contentDescription = "Delete", -// tint = Color.Red, -// ) -// } -// } -// Row( -// verticalAlignment = Alignment.CenterVertically, -// modifier = Modifier.fillMaxWidth(), -// ) { -// Text( -// text = "${stringResource(id = R.string.village)}: ${farm.village}", -// style = MaterialTheme.typography.bodySmall.copy(color = textColor), -// modifier = Modifier.weight(1f), -// ) -// Text( -// text = "${stringResource(id = R.string.district)}: ${farm.district}", -// style = MaterialTheme.typography.bodySmall.copy(color = textColor), -// modifier = Modifier.weight(1f), -// ) -// } -// -// // Show the label if the farm needs an update -// if (farm.needsUpdate) { -// Text( -// text = stringResource(id = R.string.needs_update), -// color = Color.Blue, -// fontWeight = FontWeight.Bold, -// fontSize = 12.sp, // Adjust font size -// modifier = Modifier.padding(top = 4.dp) -// ) -// } -// } -// } -// } -//} @Composable fun FarmCard( diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/components/FarmForm.kt b/app/src/main/java/org/technoserve/farmcollector/ui/components/FarmForm.kt index 0226cbe..6ba0974 100644 --- a/app/src/main/java/org/technoserve/farmcollector/ui/components/FarmForm.kt +++ b/app/src/main/java/org/technoserve/farmcollector/ui/components/FarmForm.kt @@ -5,8 +5,14 @@ import android.app.Activity import android.app.Application import android.content.Context import android.content.Intent +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.provider.Settings +import android.util.Log import android.view.KeyEvent import android.widget.Toast +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -16,6 +22,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions @@ -23,6 +30,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox @@ -35,9 +43,11 @@ import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -59,8 +69,13 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import com.google.android.gms.maps.model.LatLngBounds +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.technoserve.farmcollector.R import org.technoserve.farmcollector.database.helpers.map.LocationHelper +import org.technoserve.farmcollector.database.models.Commodity +import org.technoserve.farmcollector.database.models.Farm import org.technoserve.farmcollector.database.models.map.LocationState import org.technoserve.farmcollector.viewmodels.MapViewModel import org.technoserve.farmcollector.utils.map.getCenterOfPolygon @@ -69,7 +84,7 @@ import org.technoserve.farmcollector.ui.screens.farms.addFarm import org.technoserve.farmcollector.ui.screens.farms.formatInput import org.technoserve.farmcollector.ui.screens.farms.isLocationEnabled import org.technoserve.farmcollector.ui.screens.farms.promptEnableLocation -import org.technoserve.farmcollector.ui.screens.farms.readStoredValue +//import org.technoserve.farmcollector.ui.screens.farms.readStoredValue import org.technoserve.farmcollector.ui.screens.farms.toLatLngList import org.technoserve.farmcollector.ui.screens.farms.truncateToDecimalPlaces import org.technoserve.farmcollector.ui.screens.farms.validateNumber @@ -86,6 +101,20 @@ import java.util.regex.Pattern * FarmForm.kt * */ + +@Composable +fun FarmCommodityText(siteId: Long,farmViewModel: FarmViewModel) { + print("Site ID: $siteId") + val siteState by farmViewModel.getSiteByIdNew(siteId).collectAsState(initial = null) + + siteState?.let { site -> + println("Site: $site") + Text(text = "Commodity: ${site.commodity.displayName}") + } ?: Text("Loading commodity...") +} + + + @SuppressLint("MissingPermission") @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @Composable @@ -93,26 +122,17 @@ fun FarmForm( navController: NavController, siteId: Long, coordinatesData: List>?, - accuracyArrayData: List? + accuracyArrayData: List?, + mapViewModel: MapViewModel ) { val context = LocalContext.current as Activity var isValid by remember { mutableStateOf(true) } - var farmerName by rememberSaveable { mutableStateOf("") } - var memberId by rememberSaveable { mutableStateOf("") } - val farmerPhoto by rememberSaveable { mutableStateOf("") } - var village by rememberSaveable { mutableStateOf("") } - var district by rememberSaveable { mutableStateOf("") } - var latitude by rememberSaveable { mutableStateOf("") } - var longitude by rememberSaveable { mutableStateOf("") } - var accuracyArray by rememberSaveable { mutableStateOf(listOf()) } val items = listOf("Ha", "Acres", "Sqm", "Timad", "Fichesa", "Manzana", "Tarea") var expanded by remember { mutableStateOf(false) } val sharedPref = context.getSharedPreferences("FarmCollector", Context.MODE_PRIVATE) val farmViewModel: FarmViewModel = viewModel( factory = FarmViewModelFactory(context.applicationContext as Application) ) - val mapViewModel: MapViewModel = viewModel() - var size by rememberSaveable { mutableStateOf(readStoredValue(sharedPref)) } var selectedUnit by rememberSaveable { mutableStateOf( sharedPref.getString( @@ -121,6 +141,44 @@ fun FarmForm( ) ?: items[0] ) } + // Collect plotData from ViewModel + val farmData by mapViewModel.plotData.collectAsState() + + // ✅ Use state variables linked to ViewModel + var farmerName by remember { mutableStateOf(farmData.farmerName) } + var memberId by remember { mutableStateOf(farmData.memberId) } + var farmerPhoto by remember { mutableStateOf(farmData.farmerPhoto) } + var village by remember { mutableStateOf(farmData.village) } + var district by remember { mutableStateOf(farmData.district) } + var coordinates by remember { mutableStateOf(farmData.coordinates) } + var latitude by remember { mutableStateOf(farmData.latitude) } + var longitude by remember { mutableStateOf(farmData.longitude) } + var size by remember { mutableStateOf(truncateToDecimalPlaces(farmData.size.takeIf { it != 0f }?.toString().orEmpty(), 9)) } + var accuracyArray by remember { mutableStateOf(farmData.accuracyArray) } + // Handle Back Press to Clear Form Only on Back Navigation + BackHandler { + mapViewModel.submitForm() // Clears form ONLY when the user presses back + navController.popBackStack() // Navigate back + } + + // Ensure state updates when farmData changes + LaunchedEffect(farmData) { + Log.d("SITE ID", "$siteId") + Log.d("FarmDataChanged", "FarmData changed: $farmData") + farmerName = farmData.farmerName + memberId = farmData.memberId + farmerPhoto = farmData.farmerPhoto + village = farmData.village + district = farmData.district + coordinates = farmData.coordinates + latitude = farmData.latitude + longitude = farmData.longitude + size = farmData.size.toString() + accuracyArray = farmData.accuracyArray + } + + + var isValidSize by remember { mutableStateOf(true) } var isFormSubmitted by remember { mutableStateOf(false) } val scientificNotationPattern = Pattern.compile("([+-]?\\d*\\.?\\d+)[eE][+-]?\\d+") @@ -135,17 +193,26 @@ fun FarmForm( val locationHelper = LocationHelper(context) var locationState by remember { mutableStateOf(null) } + // Add a state to track permission denial attempts + var permissionDenialCount by remember { mutableStateOf(0) } + + // Add loading state + var isLoadingLocation by remember { mutableStateOf(false) } + // Add rememberCoroutineScope at the start of your composable + val coroutineScope = rememberCoroutineScope() + LaunchedEffect(locationHelper) { locationHelper.locationState.collect { state -> locationState = state } } + val lifecycleOwner = LocalLifecycleOwner.current DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) { - size = sharedPref.getString("plot_size", "") ?: "" + size = sharedPref.getString("plot_size", "") ?: "" selectedUnit = sharedPref.getString("selectedUnit", "Ha") ?: "Ha" with(sharedPref.edit()) { remove("plot_size") @@ -192,6 +259,7 @@ fun FarmForm( } fun saveFarm() { + Log.d("Save Farm", " Data to save $farmData") // Validate size input if the size is empty we use the default size 0 if (size.isEmpty()) { size = "0.0" @@ -201,8 +269,8 @@ fun FarmForm( val coordinatesSize = coordinatesData?.size ?: 0 val finalAccuracyArray = when { - accuracyArray.isEmpty() -> emptyList() - coordinatesSize == 0 -> listOf(accuracyArray[0]) + accuracyArrayData?.isEmpty() == true -> emptyList() + coordinatesSize == 0 -> listOf(accuracyArrayData?.get(0)) else -> { val result = accuracyArrayData!!.toMutableList() if (coordinatesSize > 1) { @@ -230,6 +298,8 @@ fun FarmForm( val returnIntent = Intent() context.setResult(Activity.RESULT_OK, returnIntent) navController.navigate("farmList/${siteId}") + // Submit the form + mapViewModel.submitForm() // Submits and clears the form } if (showDialog.value) { AlertDialog( @@ -252,7 +322,7 @@ fun FarmForm( TextButton(onClick = { showDialog.value = false - navController.navigate("setPolygon") + navController.navigate("setPolygon/$siteId") }) { Text(text = stringResource(id = R.string.set_polygon)) } @@ -308,6 +378,11 @@ fun FarmForm( .padding(16.dp) .verticalScroll(state = scrollState) ) { + FarmCommodityText( + siteId = siteId, + farmViewModel = farmViewModel + ) + Spacer(modifier = Modifier.height(16.dp)) TextField( singleLine = true, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), @@ -317,6 +392,7 @@ fun FarmForm( value = farmerName, onValueChange = { farmerName = it + mapViewModel.updatePlotData(farmerName = it) isfarmerNameValid = farmerName.isNotBlank() && farmerName.matches(textWithNumbersRegex) }, @@ -360,7 +436,9 @@ fun FarmForm( onDone = { focusRequester1.requestFocus() } ), value = memberId, - onValueChange = { memberId = it }, + onValueChange = { memberId = it + mapViewModel.updatePlotData(memberId = it) + }, label = { Text(stringResource(id = R.string.member_id), color = inputLabelColor) }, colors = TextFieldDefaults.colors( focusedContainerColor = MaterialTheme.colorScheme.background, @@ -386,6 +464,7 @@ fun FarmForm( value = village, onValueChange = { village = it + mapViewModel.updatePlotData(village = it) isvillageValid = village.isNotBlank() && village.matches(textWithNumbersRegex) }, label = { @@ -425,6 +504,7 @@ fun FarmForm( value = district, onValueChange = { district = it + mapViewModel.updatePlotData(district = it) isDistrictValid = district.isNotBlank() && district.matches(textWithNumbersRegex) }, label = { @@ -462,6 +542,25 @@ fun FarmForm( ) { TextField( singleLine = true, +// value = truncateToDecimalPlaces(size, 9), +//// value = formatInput(size.takeIf { it != "0.0" }?.toString().orEmpty()), +// onValueChange = { inputValue -> +// val formattedValue = when { +// validateSize(inputValue.takeIf { it != "0.0" }?.toString().orEmpty()) -> inputValue.takeIf { it != "0.0" }?.toString().orEmpty() +// scientificNotationPattern.matcher(inputValue).matches() -> { +// truncateToDecimalPlaces(formatInput(inputValue), 9) +// } +// else -> inputValue.takeIf { it != "0.0" }?.toString().orEmpty() +// } +// size = formattedValue +// isValidSize = validateSize(formattedValue) +// with(sharedPref.edit()) { +// putString("plot_size", formattedValue) +// apply() +// } +// }, + + value = truncateToDecimalPlaces(size, 9), onValueChange = { inputValue -> val formattedValue = when { @@ -674,8 +773,21 @@ fun FarmForm( showLocationDialog.value = true }, onPermissionsGranted = { + // Reset denial count on successful permission grant + permissionDenialCount = 0 showPermissionRequest.value = false }, + onPermissionsDenied = { + // Optional: Additional handling for denied permissions + if (permissionDenialCount > 1) { + // You can add a toast or snackbar explaining why permissions are needed + Toast.makeText( + context, + R.string.location_permission_required_for_this_feature, + Toast.LENGTH_LONG + ).show() + } + }, showLocationDialogNew = showLocationDialogNew, hasToShowDialog = showLocationDialogNew.value ) @@ -690,35 +802,57 @@ fun FarmForm( /** * Function to handle location permission and coordinate calculation */ - fun handleLocationAndNavigate(size: String, selectedUnit: String) { + + suspend fun handleLocationAndNavigate(size: String, selectedUnit: String) { val enteredSize = size.toDoubleOrNull()?.let { convertSize(it, selectedUnit).toFloat() } ?: 0f + if (coordinatesData?.isNotEmpty() == true && latitude.isBlank() && longitude.isBlank()) { val center = coordinatesData.toLatLngList().getCenterOfPolygon() val bounds: LatLngBounds = center latitude = roundToDecimalPlaces(bounds.northeast.longitude.toString().toDouble()) longitude = roundToDecimalPlaces(bounds.southwest.latitude.toString().toDouble()) } - locationHelper.requestLocationPermissionAndUpdateCoordinates( - enteredSize = enteredSize, - navController = navController, - mapViewModel = mapViewModel, - onLocationResult = { newLatitude, newLongitude, accuracy -> - latitude = newLatitude - longitude = newLongitude - accuracyArray = accuracyArray + accuracy.toFloat() - } - ) + // Ensure permission request runs asynchronously + withContext(Dispatchers.Main) { // Runs on the UI thread + locationHelper.requestLocationPermissionAndUpdateCoordinates( + enteredSize = enteredSize, + navController = navController, + mapViewModel = mapViewModel, + onLocationResult = { newLatitude, newLongitude, accuracy -> + latitude = newLatitude + longitude = newLongitude + accuracyArrayData + } + ) + } } Button( onClick = { if (isLocationEnabled(context)) { - handleLocationAndNavigate(size, selectedUnit) + isLoadingLocation = true // Start loading + // Wrap the location handling in a coroutine + coroutineScope.launch { + try { + handleLocationAndNavigate(size, selectedUnit) + } finally { + isLoadingLocation = false // Stop loading regardless of result + } + } + } else { + permissionDenialCount++ + if (permissionDenialCount <= 2) { + showLocationDialog.value = true + showPermissionRequest.value = true + } else { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + val uri: Uri = Uri.fromParts("package", context.packageName, null) + intent.data = uri + context.startActivity(intent) + } } - else - showPermissionRequest.value = true }, modifier = Modifier .background(MaterialTheme.colorScheme.background) @@ -726,22 +860,42 @@ fun FarmForm( .fillMaxWidth(0.7f) .height(50.dp) .padding(bottom = 5.dp), - enabled = size.isNotBlank() + enabled = size.isNotBlank() && !isLoadingLocation // Disable button while loading ) { - val enteredSize = - size.toDoubleOrNull()?.let { convertSize(it, selectedUnit).toFloat() } ?: 0f + val enteredSize = size.toDoubleOrNull()?.let { + convertSize(it, selectedUnit).toFloat() + } ?: 0f - Text( - text = if (enteredSize >= 4f) { - stringResource(id = R.string.set_polygon) - } else { - stringResource(id = R.string.get_coordinates) + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + if (isLoadingLocation) { + CircularProgressIndicator( + modifier = Modifier + .size(18.dp) + .padding(end = 8.dp), + color = MaterialTheme.colorScheme.primary, + strokeWidth = 2.dp + ) } - ) + Text( + text = if (isLoadingLocation) { + stringResource(id = R.string.fetching_location) + } else if (enteredSize >= 4f) { + stringResource(id = R.string.set_polygon) + } else { + stringResource(id = R.string.get_coordinates) + } + ) + } } + + Button( onClick = { isFormSubmitted = true + // Finding the center of the polygon captured if (coordinatesData?.isNotEmpty() == true && latitude.isBlank() && longitude.isBlank()) { val center = coordinatesData.toLatLngList().getCenterOfPolygon() @@ -771,8 +925,3 @@ fun FarmForm( } } } - -@Composable -fun isTablet(): Boolean { - return LocalConfiguration.current.screenWidthDp > 600 -} diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/components/FarmListHeader.kt b/app/src/main/java/org/technoserve/farmcollector/ui/components/FarmListHeader.kt index 063a656..79bc8b5 100644 --- a/app/src/main/java/org/technoserve/farmcollector/ui/components/FarmListHeader.kt +++ b/app/src/main/java/org/technoserve/farmcollector/ui/components/FarmListHeader.kt @@ -1,23 +1,39 @@ package org.technoserve.farmcollector.ui.components import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.DropdownMenu import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar @@ -29,8 +45,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -53,7 +72,11 @@ fun FarmListHeader( onBackClicked: () -> Unit, showSearch: Boolean, showRestore: Boolean, - onRestoreClicked: () -> Unit + onRestoreClicked: () -> Unit, + isBackupEnabled: Boolean, // Backup toggle state + showLastSync: Boolean, // Boolean to show/hide last sync time + lastSyncTime: String, // Last sync timestamp + onBackupToggleClicked: (Boolean) -> Unit // Callback for toggling backup ) { // State to hold the search query var searchQuery by remember { mutableStateOf("") } @@ -61,73 +84,201 @@ fun FarmListHeader( // State to determine if the search mode is active var isSearchVisible by remember { mutableStateOf(false) } + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + var isLastSyncDropdownVisible by remember { mutableStateOf(false) } + // Adjust sizes based on screen width + val iconSize = if (screenWidth < 450.dp) 24.dp else 24.dp + val switchScale = if (screenWidth < 450.dp) 0.6f else 0.8f + val horizontalPadding = if (screenWidth < 450.dp) 8.dp else 12.dp + val titleFontSize = if (screenWidth < 450.dp) 16.sp else 18.sp + val backupTextStyle = if (screenWidth < 450.dp) { + MaterialTheme.typography.bodySmall + } else { + MaterialTheme.typography.bodyMedium + } + TopAppBar( modifier = Modifier + .fillMaxWidth() .background(MaterialTheme.colorScheme.primary) - .fillMaxWidth(), + .padding(horizontal = horizontalPadding), navigationIcon = { - IconButton(onClick = { - if (isSearchVisible) { - // Exit search mode, clear search query - searchQuery = "" - onSearchQueryChanged("") - isSearchVisible = false - } else { - // Navigate back normally - onBackClicked() - } - }) { + IconButton( + onClick = { + if (isSearchVisible) { + onSearchQueryChanged("") + } else { + onBackClicked() + } + }, + modifier = Modifier.size(if (screenWidth < 450.dp) 32.dp else 32.dp) + ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", - tint = MaterialTheme.colorScheme.onPrimary + contentDescription = stringResource(id = R.string.back), + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.size(iconSize) ) } }, title = { - Text( - text = title, - color = MaterialTheme.colorScheme.onPrimary, - fontSize = 22.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = title, + color = MaterialTheme.colorScheme.onPrimary, + fontSize = titleFontSize, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .weight(1f) + .statusBarsPadding(), +// .padding(WindowInsets.safeDrawing.asPaddingValues()) // Ensures title is within the safe area + ) + + if (showLastSync) { + if (screenWidth >= 450.dp) { + // Show regular column on larger screens + Column( + modifier = Modifier.padding(end = 4.dp), + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(id = R.string.last_synced), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimary + ) + Text( + text = lastSyncTime, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimary, + fontWeight = FontWeight.Bold + ) + } + } else { + // Show dropdown on small screens + Box { + IconButton( + onClick = { isLastSyncDropdownVisible = !isLastSyncDropdownVisible }, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = stringResource(id = R.string.last_synced), + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.size(iconSize) + ) + } + DropdownMenu( + expanded = isLastSyncDropdownVisible, + onDismissRequest = { isLastSyncDropdownVisible = false }, + modifier = Modifier + .background(MaterialTheme.colorScheme.surface) + .width(IntrinsicSize.Min) + ) { + Column( + modifier = Modifier.padding(8.dp), + horizontalAlignment = Alignment.Start + ) { + Text( + text = stringResource(id = R.string.last_synced), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = lastSyncTime, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Bold + ) + } + } + } + } + } + } }, actions = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End, + modifier = Modifier.padding(start = if (screenWidth < 450.dp) 2.dp else 4.dp) + ) { + if (showLastSync) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(end = if (screenWidth < 450.dp) 2.dp else 4.dp) + ) { + Text( + text = stringResource(id = R.string.backup_now), + style = backupTextStyle, + color = MaterialTheme.colorScheme.onPrimary + ) + Spacer(modifier = Modifier.width(1.dp)) + Switch( + checked = isBackupEnabled, + onCheckedChange = onBackupToggleClicked, + thumbContent = { + Icon( + imageVector = if (isBackupEnabled) Icons.Filled.Check else Icons.Filled.Close, + contentDescription = stringResource(id = R.string.backup_now), + modifier = Modifier.size(SwitchDefaults.IconSize) + ) + }, + modifier = Modifier.scale(switchScale), + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colorScheme.primary, + checkedTrackColor = MaterialTheme.colorScheme.tertiary, + uncheckedThumbColor = MaterialTheme.colorScheme.error, + uncheckedTrackColor = MaterialTheme.colorScheme.errorContainer + ) + ) + } + } - if (showRestore) { - IconButton( - onClick = { onRestoreClicked() }, - modifier = Modifier.size(36.dp) - ) { - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = "Restore", - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onPrimary - ) + // Rest of the actions remain the same + if (showRestore) { + IconButton( + onClick = onRestoreClicked, + modifier = Modifier.size(if (screenWidth < 450.dp) 32.dp else 32.dp) + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = stringResource(id = R.string.restore), + modifier = Modifier.size(iconSize), + tint = MaterialTheme.colorScheme.onPrimary + ) + } } - } - if (showSearch) { - IconButton(onClick = { - isSearchVisible = !isSearchVisible - }, modifier = Modifier.size(36.dp)) { - Icon( - imageVector = Icons.Default.Search, - contentDescription = "Search", - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onPrimary - ) + + if (showSearch) { + IconButton( + onClick = { isSearchVisible = !isSearchVisible }, + modifier = Modifier.size(if (screenWidth < 450.dp) 32.dp else 32.dp) + ) { + Icon( + imageVector = Icons.Default.Search, + contentDescription = stringResource(id = R.string.search), + modifier = Modifier.size(iconSize), + tint = MaterialTheme.colorScheme.onPrimary + ) + } } } - }, + } ) + // Show search field when search mode is active if (isSearchVisible) { Box( modifier = Modifier .padding(top = 54.dp) + .background(MaterialTheme.colorScheme.background) .fillMaxWidth(), contentAlignment = Alignment.Center // Center the Row within the Box ) { @@ -145,6 +296,7 @@ fun FarmListHeader( modifier = Modifier .fillMaxWidth() // Center with a smaller width .padding(8.dp) + .background(MaterialTheme.colorScheme.background) .clip(RoundedCornerShape(0.dp)), // Add rounded corners placeholder = { Text( @@ -185,9 +337,14 @@ fun FarmListHeader( cursorColor = MaterialTheme.colorScheme.onSurface, focusedTextColor = MaterialTheme.colorScheme.onSurface, errorCursorColor = Color.Red, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = MaterialTheme.colorScheme.onSurfaceVariant, - errorIndicatorColor = Color.Red + //Ensure Border Always Stays Visible + focusedIndicatorColor = MaterialTheme.colorScheme.onSurfaceVariant, // Border when focused + unfocusedIndicatorColor = MaterialTheme.colorScheme.onSurfaceVariant, // Border when unfocused + errorIndicatorColor = Color.Red, // Border when error state + //Add Background Colors + focusedContainerColor = MaterialTheme.colorScheme.background, // Background when focused + unfocusedContainerColor = MaterialTheme.colorScheme.background, // Background when not focused + errorContainerColor = Color.Red// Light red for error state ), shape = RoundedCornerShape(0.dp) ) diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/components/FarmListHeaderPlots.kt b/app/src/main/java/org/technoserve/farmcollector/ui/components/FarmListHeaderPlots.kt index f38adf1..9d87bfc 100644 --- a/app/src/main/java/org/technoserve/farmcollector/ui/components/FarmListHeaderPlots.kt +++ b/app/src/main/java/org/technoserve/farmcollector/ui/components/FarmListHeaderPlots.kt @@ -1,25 +1,39 @@ package org.technoserve.farmcollector.ui.components -import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar @@ -31,15 +45,18 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.technoserve.farmcollector.R -/* +/** * This function is used to display the header for the farm list with search, export, share, and import buttons * @param title: The title of the header * @param onBackClicked: A function to be called when the back button is clicked @@ -64,21 +81,114 @@ fun FarmListHeaderPlots( showExport: Boolean, showShare: Boolean, showSearch: Boolean, - onRestoreClicked: () -> Unit + onRestoreClicked: () -> Unit, + isBackupEnabled: Boolean, + showLastSync: Boolean, + lastSyncTime: String, + onBackupToggleClicked: (Boolean) -> Unit ) { - var searchQuery by remember { mutableStateOf("") } var isSearchVisible by remember { mutableStateOf(false) } var isImportDisabled by remember { mutableStateOf(false) } + var isMenuExpanded by remember { mutableStateOf(false) } + var isInfoExpanded by remember { mutableStateOf(false) } + + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + + // Adjust sizes based on screen width + val iconSize = if (screenWidth < 450.dp) 24.dp else 24.dp + val switchScale = if (screenWidth < 450.dp) 0.6f else 0.8f + val horizontalPadding = if (screenWidth < 450.dp) 8.dp else 12.dp + val titleFontSize = if (screenWidth < 450.dp) 16.sp else 18.sp + val backupTextStyle = if (screenWidth < 450.dp) { + MaterialTheme.typography.bodySmall + } else { + MaterialTheme.typography.bodyMedium + } TopAppBar( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.primary) + .padding(horizontal = horizontalPadding), title = { - Text( - text = title, - fontSize = 22.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = title, + color = MaterialTheme.colorScheme.onPrimary, + fontSize = titleFontSize, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .weight(1f) + .statusBarsPadding() + ) + + // Info Icon for Last Sync + if (showLastSync) { + if (screenWidth >= 450.dp) { + // Show regular column on larger screens + Column( + modifier = Modifier.padding(end = 4.dp), + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(id = R.string.last_synced), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimary + ) + Text( + text = lastSyncTime, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimary, + fontWeight = FontWeight.Bold + ) + } + } else { + Box { + IconButton( + onClick = { isInfoExpanded = !isInfoExpanded }, + modifier = Modifier.size(iconSize) + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = stringResource(id = R.string.last_synced), + tint = MaterialTheme.colorScheme.onPrimary + ) + } + + DropdownMenu( + expanded = isInfoExpanded, + onDismissRequest = { isInfoExpanded = false }, + modifier = Modifier + .background(MaterialTheme.colorScheme.surface) + ) { + Column( + modifier = Modifier.padding(8.dp), + horizontalAlignment = Alignment.Start + ) { + Text( + text = stringResource(id = R.string.last_synced), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = lastSyncTime, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Bold + ) + } + } + } + } + } + } }, navigationIcon = { IconButton(onClick = { @@ -92,78 +202,180 @@ fun FarmListHeaderPlots( }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", + contentDescription = stringResource(id = R.string.back), tint = MaterialTheme.colorScheme.onPrimary ) } }, actions = { - Row( - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.horizontalScroll(rememberScrollState()) - ) { - IconButton( - onClick = { onRestoreClicked() }, - modifier = Modifier.size(36.dp) + // Always Visible Icons: Backup, Restore, and Search + if (showLastSync) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(end = 4.dp) ) { - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = "Restore", - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onPrimary + Text( + text = stringResource(id = R.string.backup_now), + style = backupTextStyle, + color = MaterialTheme.colorScheme.onPrimary ) - } - if (showExport) { - IconButton(onClick = onExportClicked, modifier = Modifier.size(36.dp)) { - Icon( - painter = painterResource(id = R.drawable.save), - contentDescription = "Export", - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onPrimary, + Spacer(modifier = Modifier.width(1.dp)) + Switch( + checked = isBackupEnabled, + onCheckedChange = onBackupToggleClicked, + thumbContent = { + Icon( + imageVector = if (isBackupEnabled) Icons.Filled.Check else Icons.Filled.Close, + contentDescription = stringResource(id = R.string.backup_now), + modifier = Modifier.size(SwitchDefaults.IconSize) + ) + }, + modifier = Modifier.scale(switchScale), + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colorScheme.primary, + checkedTrackColor = MaterialTheme.colorScheme.tertiary, + uncheckedThumbColor = MaterialTheme.colorScheme.error, + uncheckedTrackColor = MaterialTheme.colorScheme.errorContainer ) - } + ) } - if (showShare) { - IconButton(onClick = onShareClicked, modifier = Modifier.size(36.dp)) { + if (screenWidth >= 450.dp) { + if (showExport) { + IconButton(onClick = onExportClicked, modifier = Modifier.size(36.dp)) { + Icon( + painter = painterResource(id = R.drawable.save), + contentDescription = "Export", + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } + } + if (showShare) { + IconButton(onClick = onShareClicked, modifier = Modifier.size(36.dp)) { + Icon( + imageVector = Icons.Default.Share, + contentDescription = "Share", + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onPrimary + ) + } + } + IconButton( + onClick = { + if (!isImportDisabled) { + onImportClicked() + } + }, + modifier = Modifier.size(36.dp) + ) { Icon( - imageVector = Icons.Default.Share, - contentDescription = "Share", + painter = painterResource(id = R.drawable.icons8_import_file_48), + contentDescription = "Import", modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onPrimary + tint = MaterialTheme.colorScheme.onPrimary, ) } } - IconButton( - onClick = { - if (!isImportDisabled) { - onImportClicked() - } - }, - modifier = Modifier.size(36.dp) - ) { + + IconButton(onClick = onRestoreClicked, modifier = Modifier.size(36.dp)) { Icon( - painter = painterResource(id = R.drawable.icons8_import_file_48), - contentDescription = "Import", - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onPrimary, + imageVector = Icons.Default.Refresh, + contentDescription = stringResource(id = R.string.restore), + modifier = Modifier.size(iconSize), + tint = MaterialTheme.colorScheme.onPrimary ) } + + + if (showSearch) { - IconButton(onClick = { - isSearchVisible = !isSearchVisible - }, modifier = Modifier.size(36.dp)) { + IconButton( + onClick = { isSearchVisible = !isSearchVisible }, + modifier = Modifier.size(36.dp) + ) { Icon( imageVector = Icons.Default.Search, - contentDescription = "Search", - modifier = Modifier.size(24.dp), + contentDescription = stringResource(id = R.string.search), + modifier = Modifier.size(iconSize), tint = MaterialTheme.colorScheme.onPrimary ) } } + + if (screenWidth <= 450.dp) { + + // Move Export, Share, and Import to a dropdown on small screens + Box { + IconButton( + onClick = { isMenuExpanded = !isMenuExpanded }, + modifier = Modifier.size(iconSize) + ) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = "More Options", + tint = MaterialTheme.colorScheme.onPrimary + ) + } + + DropdownMenu( + expanded = isMenuExpanded, + onDismissRequest = { isMenuExpanded = false }, + modifier = Modifier + .background(MaterialTheme.colorScheme.surface) // Dropdown aligned to the right + ) { + if (showExport) { + DropdownMenuItem( + text = { Text(stringResource(id = R.string.export)) }, + onClick = { + onExportClicked() + isMenuExpanded = false + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.save), + contentDescription = stringResource(id = R.string.export), + tint = MaterialTheme.colorScheme.onPrimary + ) + } + ) + } + if (showShare) { + DropdownMenuItem( + text = { Text(stringResource(id = R.string.share)) }, + onClick = { + onShareClicked() + isMenuExpanded = false + }, + leadingIcon = { + Icon( + imageVector = Icons.Default.Share, + contentDescription = stringResource(id = R.string.share), + tint = MaterialTheme.colorScheme.onPrimary + ) + } + ) + } + DropdownMenuItem( + text = { Text(stringResource(id = R.string.import_)) }, + onClick = { + onImportClicked() + isMenuExpanded = false + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.icons8_import_file_48), + contentDescription = stringResource(id = R.string.import_), + tint = MaterialTheme.colorScheme.onPrimary + ) + } + ) + } + } + } } - }, + } ) + if (isSearchVisible) { Box( modifier = Modifier @@ -219,9 +431,12 @@ fun FarmListHeaderPlots( cursorColor = MaterialTheme.colorScheme.onSurface, focusedTextColor = MaterialTheme.colorScheme.onSurface, errorCursorColor = Color.Red, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = MaterialTheme.colorScheme.onSurfaceVariant, - errorIndicatorColor = Color.Red + focusedIndicatorColor = MaterialTheme.colorScheme.onSurfaceVariant, // Border when focused + unfocusedIndicatorColor = MaterialTheme.colorScheme.onSurfaceVariant, // Border when unfocused + errorIndicatorColor = Color.Red, // Border when error state + focusedContainerColor = MaterialTheme.colorScheme.background, // Background when focused + unfocusedContainerColor = MaterialTheme.colorScheme.background, // Background when not focused + errorContainerColor = Color.Red// Light red for error state ), shape = RoundedCornerShape(0.dp) ) @@ -229,4 +444,5 @@ fun FarmListHeaderPlots( } } } -} \ No newline at end of file +} + diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/components/SiteCard.kt b/app/src/main/java/org/technoserve/farmcollector/ui/components/SiteCard.kt index 8088e84..bd51b37 100644 --- a/app/src/main/java/org/technoserve/farmcollector/ui/components/SiteCard.kt +++ b/app/src/main/java/org/technoserve/farmcollector/ui/components/SiteCard.kt @@ -69,6 +69,7 @@ fun SiteCard( val showDeleteDialog = remember { mutableStateOf(false) } val showUndoSnackbar = remember { mutableStateOf(false) } + // Handle the delete dialog and undo snackbar SiteDeleteAllDialogPresenter( showDeleteDialog = showDeleteDialog, diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/components/SiteForm.kt b/app/src/main/java/org/technoserve/farmcollector/ui/components/SiteForm.kt index fa0305f..b277f9b 100644 --- a/app/src/main/java/org/technoserve/farmcollector/ui/components/SiteForm.kt +++ b/app/src/main/java/org/technoserve/farmcollector/ui/components/SiteForm.kt @@ -7,13 +7,19 @@ import android.content.Intent import android.view.KeyEvent import android.widget.Toast import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.toggleable +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll @@ -21,9 +27,16 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TextField @@ -33,6 +46,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -41,12 +55,18 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import org.technoserve.farmcollector.R +import org.technoserve.farmcollector.database.models.Commodity import org.technoserve.farmcollector.ui.screens.collectionsites.addSite import org.technoserve.farmcollector.utils.isSystemInDarkTheme import org.technoserve.farmcollector.viewmodels.FarmViewModel @@ -113,6 +133,10 @@ fun SiteForm(navController: NavController) { val inputTextColor = if (isDarkTheme) Color.White else Color.Black val inputBorder = if (isDarkTheme) Color.LightGray else Color.DarkGray + var selectedCommodity by remember { mutableStateOf(Commodity.COFFEE) } + + var privacyPolicyAccepted by remember { mutableStateOf(false) } + Column( modifier = Modifier .fillMaxWidth() @@ -120,6 +144,13 @@ fun SiteForm(navController: NavController) { .padding(16.dp) .verticalScroll(state = scrollState) ) { + + CommodityDropdownField( + commodities = Commodity.entries, + selectedCommodity = selectedCommodity, + onCommoditySelected = { selectedCommodity = it } + ) + Spacer(modifier = Modifier.height(16.dp)) Row { TextField( singleLine = true, @@ -416,9 +447,72 @@ fun SiteForm(navController: NavController) { .focusRequester(focusRequester6) ) Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .toggleable( + value = privacyPolicyAccepted, + onValueChange = { privacyPolicyAccepted = it }, + role = Role.Checkbox + ), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = privacyPolicyAccepted, + onCheckedChange = { privacyPolicyAccepted = it } + ) + Spacer(modifier = Modifier.width(8.dp)) + //Text(text = stringResource(R.string.accept_privacy_policy)) + Column { + Text( + text = buildAnnotatedString { + append(stringResource(R.string.accept_privacy_policy)) + append(" ") + withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary, textDecoration = TextDecoration.Underline)) { + // Make this part clickable to open the policy + append(stringResource(R.string.privacy_policy_link_text)) + } + }, + modifier = Modifier.clickable { + // Navigate to the PrivacyPolicyWebView composable + navController.navigate("privacy_policy") + } + ) + } + } + Spacer(modifier = Modifier.height(8.dp)) +// Button( +// onClick = { +// if (validateForm() && (phoneNumber.isEmpty() || isValidPhoneNumber(phoneNumber))) { +// addSite( +// farmViewModel, +// name, +// agentName, +// phoneNumber, +// email, +// village, +// district, +// selectedCommodity +// ) +// val returnIntent = Intent() +// context.setResult(Activity.RESULT_OK, returnIntent) +// navController.navigate("siteList") +// Toast.makeText(context, R.string.site_added_successfully, Toast.LENGTH_SHORT) +// .show() +// } else { +// Toast.makeText(context, fillForm, Toast.LENGTH_SHORT).show() +// } +// }, +// modifier = Modifier +// .fillMaxWidth() +// .height(50.dp) +// ) { +// Text(text = stringResource(id = R.string.add_site)) +// } Button( onClick = { - if (validateForm() && (phoneNumber.isEmpty() || isValidPhoneNumber(phoneNumber))) { + // Modify the condition to also check if privacyPolicyAccepted is true + if (validateForm() && (phoneNumber.isEmpty() || isValidPhoneNumber(phoneNumber)) && privacyPolicyAccepted) { addSite( farmViewModel, name, @@ -426,7 +520,8 @@ fun SiteForm(navController: NavController) { phoneNumber, email, village, - district + district, + selectedCommodity ) val returnIntent = Intent() context.setResult(Activity.RESULT_OK, returnIntent) @@ -434,7 +529,13 @@ fun SiteForm(navController: NavController) { Toast.makeText(context, R.string.site_added_successfully, Toast.LENGTH_SHORT) .show() } else { - Toast.makeText(context, fillForm, Toast.LENGTH_SHORT).show() + // Provide a more specific message if the checkbox isn't checked + val message = if (!privacyPolicyAccepted) { + context.getString(R.string.please_accept_privacy_policy) // Define this string resource + } else { + fillForm + } + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } }, modifier = Modifier @@ -444,4 +545,49 @@ fun SiteForm(navController: NavController) { Text(text = stringResource(id = R.string.add_site)) } } -} \ No newline at end of file +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CommodityDropdownField( + label: String = "Select Commodity", + commodities: List, + selectedCommodity: Commodity, + onCommoditySelected: (Commodity) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded } + ) { + OutlinedTextField( + value = selectedCommodity.displayName, + onValueChange = {}, + readOnly = true, + label = { Text(label) }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) + }, + modifier = Modifier + .menuAnchor() + .fillMaxWidth() + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + commodities.forEach { commodity -> + DropdownMenuItem( + text = { Text(commodity.displayName) }, + onClick = { + onCommoditySelected(commodity) + expanded = false + } + ) + } + } + } +} + diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/components/UserGuideScreen.kt b/app/src/main/java/org/technoserve/farmcollector/ui/components/UserGuideScreen.kt new file mode 100644 index 0000000..f8d0aa7 --- /dev/null +++ b/app/src/main/java/org/technoserve/farmcollector/ui/components/UserGuideScreen.kt @@ -0,0 +1,189 @@ +package org.technoserve.farmcollector.ui.components + + +import android.os.Build +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import kotlinx.coroutines.launch +import okhttp3.OkHttpClient +import org.technoserve.farmcollector.BuildConfig +import org.technoserve.farmcollector.R +import org.technoserve.farmcollector.database.sync.remote.ApiService +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit + + + +@RequiresApi(Build.VERSION_CODES.Q) +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun UserGuideScreen(navController: NavController) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + // Retrofit API Setup + val retrofit = Retrofit.Builder() + .baseUrl(BuildConfig.BASE_URL) + .client( + OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .addInterceptor { chain -> + val request = chain.request().newBuilder() + .addHeader("Accept", "application/pdf") + .build() + chain.proceed(request) + } + .build() + ) + .addConverterFactory(GsonConverterFactory.create()) + .build() + + val api = retrofit.create(ApiService::class.java) + + // Document launcher for file selection + val documentLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.CreateDocument("application/pdf"), + onResult = { selectedUri -> + if (selectedUri != null) { + coroutineScope.launch { + try { + val fileUrl = BuildConfig.USER_GUIDE_URL + .replace("/edit?usp=sharing", "/export?format=pdf") + + val response = api.downloadUserGuide(fileUrl) + + if (response.isSuccessful) { + response.body()?.byteStream()?.use { inputStream -> + context.contentResolver.openOutputStream(selectedUri)?.use { outputStream -> + inputStream.copyTo(outputStream) + } ?: throw Exception("Unable to open output stream.") + } ?: throw Exception("Response body is null.") + + Toast.makeText( + context, + context.getString(R.string.download_success), + Toast.LENGTH_LONG + ).show() + navController.popBackStack() + } else { + throw Exception("HTTP error: ${response.code()}") + } + } catch (e: Exception) { + Toast.makeText( + context, + context.getString(R.string.download_failed, e.localizedMessage), + Toast.LENGTH_LONG + ).show() + } + } + } else { + Toast.makeText( + context, + context.getString(R.string.no_location_selected), + Toast.LENGTH_SHORT + ).show() + } + } + ) + + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = stringResource(R.string.user_guide_title)) }, + navigationIcon = { + IconButton(onClick = { + navController.popBackStack() + }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(id = R.string.back), + tint = MaterialTheme.colorScheme.onPrimary + ) + } + }, + + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(horizontal = 16.dp, vertical = 24.dp), // Added more vertical padding + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // Welcome message text with more emphasis + Text( + text = stringResource(R.string.welcome_message), + style = MaterialTheme.typography.bodyLarge, // More prominent style + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onBackground, // Ensures good contrast + modifier = Modifier.padding(bottom = 32.dp) // Extra padding for spacing + ) + + // User Guide Button with more prominent design + Button( + onClick = { + documentLauncher.launch("TerraTrac Mobile Application User Guide.pdf") + }, + modifier = Modifier + .fillMaxWidth() // Makes the button expand to full width + .height(56.dp), // Consistent height for the button + shape = MaterialTheme.shapes.medium, // Rounded corners for the button + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, // Custom background color + contentColor = MaterialTheme.colorScheme.onPrimary // Custom text color + ) // Custom background color + ) { + Icon( + painter = painterResource(id = R.drawable.save), + contentDescription = stringResource(id = R.string.download_user_guide), + tint = MaterialTheme.colorScheme.onPrimary // Makes the icon visible on the button background + ) + Spacer(modifier = Modifier.width(12.dp)) // Increased spacing between the icon and text + Text( + text = stringResource(R.string.download_user_guide), + style = MaterialTheme.typography.bodySmall, // A more appropriate text style for a button + color = MaterialTheme.colorScheme.onPrimary // Ensure the text contrasts well + ) + } + } + + + } +} diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/screens/FarmList.kt b/app/src/main/java/org/technoserve/farmcollector/ui/screens/FarmList.kt new file mode 100644 index 0000000..5446d5f --- /dev/null +++ b/app/src/main/java/org/technoserve/farmcollector/ui/screens/FarmList.kt @@ -0,0 +1,2633 @@ +package org.technoserve.farmcollector.ui.screens + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.Application +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.os.Looper +import android.os.Parcel +import android.os.Parcelable +import android.view.KeyEvent +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateListOf +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.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.FileProvider +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import com.google.android.gms.location.LocationCallback +import com.google.android.gms.location.LocationRequest +import com.google.android.gms.location.LocationResult +import com.google.android.gms.location.LocationServices +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.joda.time.Instant +import org.technoserve.farmcollector.R +//import org.technoserve.farmcollector.database.Farm +//import org.technoserve.farmcollector.database.FarmViewModel +//import org.technoserve.farmcollector.database.FarmViewModelFactory +//import org.technoserve.farmcollector.hasLocationPermission +import org.technoserve.farmcollector.utils.convertSize +import java.io.BufferedWriter +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.OutputStream +import java.io.OutputStreamWriter +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.Objects +import java.util.regex.Pattern +//import org.technoserve.farmcollector.database.RestoreStatus +//import org.technoserve.farmcollector.database.sync.DeviceIdUtil +import org.technoserve.farmcollector.ui.composes.isValidPhoneNumber +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.ui.Alignment.Companion.BottomEnd +import androidx.compose.ui.draw.clip +import org.technoserve.farmcollector.database.models.Farm +import org.technoserve.farmcollector.ui.screens.farms.LocationPermissionRequest +import org.technoserve.farmcollector.ui.screens.farms.formatInput +import org.technoserve.farmcollector.ui.screens.farms.isLocationEnabled +import org.technoserve.farmcollector.ui.screens.farms.promptEnableLocation +//import org.technoserve.farmcollector.map.MapViewModel +import org.technoserve.farmcollector.ui.screens.farms.truncateToDecimalPlaces +import org.technoserve.farmcollector.ui.screens.farms.validateNumber +import org.technoserve.farmcollector.ui.screens.farms.validateSize +import org.technoserve.farmcollector.utils.DeviceIdUtil +import org.technoserve.farmcollector.utils.hasLocationPermission +import org.technoserve.farmcollector.viewmodels.FarmViewModel +import org.technoserve.farmcollector.viewmodels.FarmViewModelFactory +import org.technoserve.farmcollector.viewmodels.MapViewModel +import org.technoserve.farmcollector.viewmodels.RestoreStatus + + +var siteID = 0L + +enum class Action { + Export, + Share, +} + +private const val KEY_HAS_NEW_POLYGON = "has_new_polygon" + +data class ParcelablePair(val first: Double, val second: Double) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readDouble(), + parcel.readDouble() + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeDouble(first) + parcel.writeDouble(second) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): ParcelablePair { + return ParcelablePair(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} + +data class ParcelableFarmData(val farm: Farm, val view: String) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readParcelable(Farm::class.java.classLoader)!!, + parcel.readString()!! + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(farm, flags) + parcel.writeString(view) + } + + override fun describeContents(): Int = 0 + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): ParcelableFarmData { + return ParcelableFarmData(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} + +@Composable +fun KeepPolygonDialog( + onDismiss: () -> Unit, + onKeepExisting: () -> Unit, + onCaptureNew: () -> Unit, +) { + + val mapViewModel: MapViewModel = viewModel() + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text(text = stringResource(id=R.string.update_polygon), color = MaterialTheme.colorScheme.onBackground ) + }, + text = { + Text(text = stringResource(id=R.string.keep_existing_polygon_or_capture_new), color = MaterialTheme.colorScheme.onBackground ) + }, + confirmButton = { + Button(onClick = onKeepExisting, modifier = Modifier.background(MaterialTheme.colorScheme.background),colors = ButtonDefaults.buttonColors()) { + Text(text = stringResource(id=R.string.keep_existing), color = MaterialTheme.colorScheme.onBackground ) + } + }, + dismissButton = { + Button(onClick = { + mapViewModel.clearCoordinates() // Clear coordinates + onCaptureNew() + }, modifier = Modifier.background(MaterialTheme.colorScheme.background),colors = ButtonDefaults.buttonColors()) { + Text(text = stringResource(id=R.string.capture_new), color = MaterialTheme.colorScheme.onBackground ) + } + }, + containerColor = MaterialTheme.colorScheme.background, // Background that adapts to light/dark + tonalElevation = 6.dp // Adds a subtle shadow for better UX + ) +} + + +@Composable +fun isSystemInDarkTheme(): Boolean { + val context = LocalContext.current + val sharedPreferences = context.getSharedPreferences("theme_mode", Context.MODE_PRIVATE) + return sharedPreferences.getBoolean("dark_mode", false) +} + +@Composable +fun FormatSelectionDialog( + onDismiss: () -> Unit, + onFormatSelected: (String) -> Unit, +) { + var selectedFormat by remember { mutableStateOf("CSV") } + + AlertDialog( + onDismissRequest = { onDismiss() }, + title = { Text(text = stringResource(R.string.select_file_format)) }, + text = { + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = selectedFormat == "CSV", + onClick = { selectedFormat = "CSV" }, + ) + Text(stringResource(R.string.csv)) + } + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = selectedFormat == "GeoJSON", + onClick = { selectedFormat = "GeoJSON" }, + ) + Text(stringResource(R.string.geojson)) + } + } + }, + confirmButton = { + Button( + onClick = { + onFormatSelected(selectedFormat) + onDismiss() + }, + ) { + Text(stringResource(R.string.confirm)) + } + }, + dismissButton = { + Button(onClick = { onDismiss() }) { + Text(stringResource(R.string.cancel)) + } + }, + containerColor = MaterialTheme.colorScheme.background, // Background that adapts to light/dark + tonalElevation = 6.dp // Adds a subtle shadow for better UX + ) +} + +@Composable +fun ConfirmationDialog( + listItems: List, + action: Action, + onConfirm: () -> Unit, + onDismiss: () -> Unit, +) { + fun validateFarms(farms: List): Pair> { + val incompleteFarms = + farms.filter { farm -> + farm.farmerName.isEmpty() || + farm.district.isEmpty()|| + farm.village.isEmpty() || + farm.latitude == "0.0" || + farm.longitude == "0.0" || + farm.size == 0.0f || + farm.remoteId.toString().isEmpty() + } + return Pair(farms.size, incompleteFarms) + } + val (totalFarms, incompleteFarms) = validateFarms(listItems) + val message = + when (action) { + Action.Export -> stringResource(R.string.confirm_export, totalFarms, incompleteFarms.size) + Action.Share -> stringResource(R.string.confirm_share, totalFarms, incompleteFarms.size) + } + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(text = stringResource(R.string.confirm)) }, + text = { Text(text = message) }, + confirmButton = { + Button(onClick = { + onConfirm() + onDismiss() + }) { + Text(text = stringResource(R.string.yes)) + } + }, + dismissButton = { + Button(onClick = { onDismiss() }) { + Text(text = stringResource(R.string.no)) + } + }, + containerColor = MaterialTheme.colorScheme.background, // Background that adapts to light/dark + tonalElevation = 6.dp // Adds a subtle shadow for better UX + ) +} + +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) +@RequiresApi(Build.VERSION_CODES.N) +@Composable +fun FarmList( + navController: NavController, + siteId: Long, +) { + siteID = siteId + val context = LocalContext.current + val farmViewModel: FarmViewModel = + viewModel( + factory = FarmViewModelFactory(context.applicationContext as Application), + ) + val selectedIds = remember { mutableStateListOf() } + // Create a mutable state for the selected farm + val selectedFarm = remember { mutableStateOf(null) } + val showDeleteDialog = remember { mutableStateOf(false) } + val listItems by farmViewModel.readAllData(siteId).observeAsState(listOf()) + val cwsListItems by farmViewModel.readAllSites.observeAsState(listOf()) + // var showExportDialog by remember { mutableStateOf(false) } + var showFormatDialog by remember { mutableStateOf(false) } + var action by remember { mutableStateOf(null) } + val activity = context as Activity + var exportFormat by remember { mutableStateOf("") } + + var showImportDialog by remember { mutableStateOf(false) } + var showConfirmationDialog by remember { mutableStateOf(false) } + val (searchQuery, setSearchQuery) = remember { mutableStateOf("") } + + var selectedTabIndex by remember { mutableStateOf(0) } + val tabs = + listOf( + stringResource(id = R.string.all), + stringResource(id = R.string.needs_update), +// stringResource(id = R.string.no_update_needed), + ) + val pagerState = rememberPagerState(pageCount = { tabs.size }) + val coroutineScope = rememberCoroutineScope() + + + // State to manage the loading status + val isLoading = remember { mutableStateOf(true) } + var deviceId by remember { mutableStateOf("") } + // State variable to observe restore status + val restoreStatus by farmViewModel.restoreStatus.observeAsState() + + var phone by remember { mutableStateOf("") } + var email by remember { mutableStateOf("") } + var showRestorePrompt by remember { mutableStateOf(false) } + var finalMessage by remember { mutableStateOf("") } + var showFinalMessage by remember { mutableStateOf(false) } + + + val isDarkTheme = isSystemInDarkTheme() + val backgroundColor = if (isDarkTheme) Color.Black else Color.White + val inputLabelColor = if (isDarkTheme) Color.LightGray else Color.DarkGray + val inputTextColor = if (isDarkTheme) Color.White else Color.Black + val inputBorder = if (isDarkTheme) Color.LightGray else Color.DarkGray + + LaunchedEffect(Unit) { + deviceId = DeviceIdUtil.getDeviceId(context) + } + + // Simulate a network request or data loading + LaunchedEffect(Unit) { + // Simulate a delay for loading + delay(2000) // Adjust the delay as needed + // After loading data, set isLoading to false + isLoading.value = false + + } + + fun createFileForSharing(): File? { + // Get the current date and time + val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + val getSiteById = cwsListItems.find { it.siteId == siteID } + val siteName = getSiteById?.name ?: "SiteName" + val filename = + if (exportFormat == "CSV") "farms_${siteName}_$timestamp.csv" else "farms_${siteName}_$timestamp.geojson" + val mimeType = if (exportFormat == "CSV") "text/csv" else "application/geo+json" + // Get the Downloads directory + val downloadsDir = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + val file = File(downloadsDir, filename) + + try { + file.bufferedWriter().use { writer -> + if (exportFormat == "CSV") { + writer.write( + "remote_id,farmer_name,member_id,collection_site,agent_name,farm_village,farm_district,farm_size,latitude,longitude,polygon,accuracyArray,created_at,updated_at\n", + ) + listItems.forEach { farm -> + val regex = "\\(([^,]+), ([^)]+)\\)".toRegex() + val matches = regex.findAll(farm.coordinates.toString()) + val reversedCoordinates = + matches + .map { match -> + val (lat, lon) = match.destructured + "[$lon, $lat]" + }.toList() + .let { coordinates -> + if (coordinates.isNotEmpty()) { + // Always include brackets, even for a single point + coordinates.joinToString(", ", prefix = "[", postfix = "]") + } else { + "" + } + } + + val line = + "${farm.remoteId},\"${farm.farmerName.split(" ").joinToString(" ") }\",${farm.memberId},\"${getSiteById?.name}\",\"${getSiteById?.agentName}\",\"${farm.village}\",\"${farm.district}\",${farm.size},${farm.latitude},${farm.longitude},\"${reversedCoordinates}\",\"${farm.accuracyArray}\",${ + Date( + farm.createdAt, + ) + },${Date(farm.updatedAt)}\n" + writer.write(line) + } + } else { + val geoJson = + buildString { + append("{\"type\": \"FeatureCollection\", \"features\": [") + listItems.forEachIndexed { index, farm -> + val regex = "\\(([^,]+), ([^)]+)\\)".toRegex() + val matches = regex.findAll(farm.coordinates.toString()) + val geoJsonCoordinates = + matches + .map { match -> + val (lat, lon) = match.destructured + "[$lon, $lat]" + }.joinToString(", ", prefix = "[", postfix = "]") + val latitude = + farm.latitude.toDoubleOrNull()?.takeIf { it != 0.0 } ?: 0.0 + val longitude = + farm.longitude.toDoubleOrNull()?.takeIf { it != 0.0 } ?: 0.0 + + val feature = + """ + { + "type": "Feature", + "properties": { + "remote_id": "${farm.remoteId ?: ""}", + "farmer_name":"${farm.farmerName.split(" ").joinToString(" ") ?: ""}", + "member_id": "${farm.memberId ?: ""}", + "collection_site": "${getSiteById?.name ?: ""}", + "agent_name": "${getSiteById?.agentName ?: ""}", + "farm_village": "${farm.village ?: ""}", + "farm_district": "${farm.district ?: ""}", + "farm_size": ${farm.size ?: 0.0}, + "latitude": $latitude, + "longitude": $longitude, + "accuracyArray": "${farm.accuracyArray ?: ""}", + "created_at": "${farm.createdAt?.let { Date(it) } ?: "null"}", + "updated_at": "${farm.updatedAt?.let { Date(it) } ?: "null"} + }, + "geometry": { + "type": "${if ((farm.coordinates?.size ?: 0) > 1) "Polygon" else "Point"}", + "coordinates": ${if ((farm.coordinates?.size ?: 0) > 1) "[$geoJsonCoordinates]" else "[]"} + } + } + """.trimIndent() + append(feature) + if (index < listItems.size - 1) append(",") + } + append("]}") + } + writer.write(geoJson) + } + } + return file + } catch (e: IOException) { + Toast.makeText(context, R.string.error_export_msg, Toast.LENGTH_SHORT).show() + return null + } + } + + fun createFile( + context: Context, + uri: Uri, + ): Boolean { + // Get the current date and time + val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + val getSiteById = cwsListItems.find { it.siteId == siteID } + val siteName = getSiteById?.name ?: "SiteName" + val filename = + if (exportFormat == "CSV") "farms_${siteName}_$timestamp.csv" else "farms_${siteName}_$timestamp.geojson" + + try { + context.contentResolver.openOutputStream(uri)?.use { outputStream -> + BufferedWriter(OutputStreamWriter(outputStream)).use { writer -> + if (exportFormat == "CSV") { + writer.write( + "remote_id,farmer_name,member_id,collection_site,agent_name,farm_village,farm_district,farm_size,latitude,longitude,polygon,accuracyArray,created_at,updated_at\n", + ) + listItems.forEach { farm -> + val regex = "\\(([^,]+), ([^)]+)\\)".toRegex() + val matches = regex.findAll(farm.coordinates.toString()) + val reversedCoordinates = + matches + .map { match -> + val (lat, lon) = match.destructured + "[$lon, $lat]" + }.toList() + .let { coordinates -> + if (coordinates.isNotEmpty()) { + // Always include brackets, even for a single point + coordinates.joinToString( + ", ", + prefix = "[", + postfix = "]" + ) + } else { +// val lon = farm.longitude ?: "0.0" +// val lat = farm.latitude ?: "0.0" +// "[$lon, $lat]" + "" + } + } + + val line = + "${farm.remoteId},\"${farm.farmerName.split(" ").joinToString(" ") }\",${farm.memberId},${getSiteById?.name},\"${getSiteById?.agentName}\",\"${farm.village}\",\"${farm.district}\",${farm.size},${farm.latitude},${farm.longitude},\"${reversedCoordinates}\",\"${farm.accuracyArray}\",${ + Date(farm.createdAt) + },${Date(farm.updatedAt)}\n" + writer.write(line) + } + } else { + val geoJson = + buildString { + append("{\"type\": \"FeatureCollection\", \"features\": [") + listItems.forEachIndexed { index, farm -> + val regex = "\\(([^,]+), ([^)]+)\\)".toRegex() + val matches = regex.findAll(farm.coordinates.toString()) + val geoJsonCoordinates = + matches + .map { match -> + val (lat, lon) = match.destructured + "[$lon, $lat]" + }.joinToString(", ", prefix = "[", postfix = "]") + // Ensure latitude and longitude are not null + val latitude = + farm.latitude.toDoubleOrNull()?.takeIf { it != 0.0 } ?: 0.0 + val longitude = + farm.longitude.toDoubleOrNull()?.takeIf { it != 0.0 } ?: 0.0 + + val feature = + """ + { + "type": "Feature", + "properties": { + "remote_id": "${farm.remoteId ?: ""}", + "farmer_name": "${farm.farmerName.split(" ").joinToString(" ") ?: ""}", + "member_id": "${farm.memberId ?: ""}", + "collection_site": "${getSiteById?.name ?: ""}", + "agent_name": "${getSiteById?.agentName ?: ""}", + "farm_village": "${farm.village ?: ""}", + "farm_district": "${farm.district ?: ""}", + "farm_size": ${farm.size ?: 0.0}, + "latitude": $latitude, + "longitude": $longitude, + "accuracyArray": "${farm.accuracyArray ?: ""}", + "created_at": "${farm.createdAt?.let { Date(it) } ?: "null"}", + "updated_at": "${farm.updatedAt?.let { Date(it) } ?: "null"}" + + }, + "geometry": { + "type": "${if ((farm.coordinates?.size ?: 0) > 1) "Polygon" else "Point"}", + "coordinates": ${if ((farm.coordinates?.size ?: 0) > 1) "[$geoJsonCoordinates]" else "[]"} + } + } + """.trimIndent() + append(feature) + if (index < listItems.size - 1) append(",") + } + append("]}") + } + writer.write(geoJson) + } + } + } + return true + } catch (e: IOException) { + Toast.makeText(context, R.string.error_export_msg, Toast.LENGTH_SHORT).show() + return false + } + } + + val createDocumentLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + result.data?.data?.let { uri -> + val context = activity?.applicationContext + if (context != null && createFile(context, uri)) { + Toast.makeText(context, R.string.success_export_msg, Toast.LENGTH_SHORT) + .show() + } + } + } + } + + fun initiateFileCreation(activity: Activity) { + val mimeType = if (exportFormat == "CSV") "text/csv" else "application/geo+json" + val intent = + Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = mimeType + val getSiteById = cwsListItems.find { it.siteId == siteID } + val siteName = getSiteById?.name ?: "SiteName" + val timestamp = + SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + val filename = + if (exportFormat == "CSV") "farms_${siteName}_$timestamp.csv" else "farms_${siteName}_$timestamp.geojson" + putExtra(Intent.EXTRA_TITLE, filename) + } + createDocumentLauncher.launch(intent) + } + + // Function to share the file + fun shareFile(file: File) { + val fileURI: Uri = + context.let { + FileProvider.getUriForFile( + it, + context.applicationContext.packageName.toString() + ".provider", + file, + ) + } + + val shareIntent = + Intent(Intent.ACTION_SEND).apply { + type = if (exportFormat == "CSV") "text/csv" else "application/geo+json" + putExtra(Intent.EXTRA_SUBJECT, "Farm Data") + putExtra(Intent.EXTRA_TEXT, "Sharing the farm data file.") + putExtra(Intent.EXTRA_STREAM, fileURI) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + val chooserIntent = Intent.createChooser(shareIntent, "Share file") + activity.startActivity(chooserIntent) + } + + fun exportFile(activity: Activity) { + showConfirmationDialog = true + } + + // Function to handle the share action + fun shareFileAction() { + showConfirmationDialog = true + } + + if (showFormatDialog) { + FormatSelectionDialog( + onDismiss = { showFormatDialog = false }, + onFormatSelected = { format -> + exportFormat = format + showFormatDialog = false + when (action) { + Action.Export -> exportFile(activity) + Action.Share -> shareFileAction() + else -> {} + } + }, + ) + } + if (showConfirmationDialog) { + ConfirmationDialog( + listItems, + action = action!!, // Ensure action is not null + onConfirm = { + when (action) { + Action.Export -> initiateFileCreation(activity) + Action.Share -> { + val file = createFileForSharing() + if (file != null) { + shareFile(file) + } + } + + else -> {} + } + }, + onDismiss = { showConfirmationDialog = false }, + ) + } + if (showImportDialog) { + println("site ID am Using: $siteId") + // ImportFileDialog( siteId,onDismiss = { showImportDialog = false ; refreshTrigger = !refreshTrigger},navController = navController) + ImportFileDialog( + siteId, + onDismiss = { showImportDialog = false }, + navController = navController + ) + } + + fun onDelete() { + selectedFarm.value?.let { farm -> + val toDelete = + mutableListOf().apply { + addAll(selectedIds) + add(farm.id) + } + farmViewModel.deleteList(toDelete) + selectedIds.removeAll(selectedIds) + farmViewModel.deleteFarmById(farm) + selectedFarm.value = null + selectedIds.removeAll(selectedIds) + showDeleteDialog.value = false + } + } + +// +// Column( +// modifier = Modifier +// .fillMaxSize() +// .padding(16.dp) +// ) { + + // Function to show data or no data message + @Composable + fun showDataContent() { + val hasData = listItems.isNotEmpty() // Check if there's data available + + if (hasData) { + Column { + // Only show the TabRow and HorizontalPager if there is data + TabRow( + selectedTabIndex = pagerState.currentPage, + modifier = Modifier.background(MaterialTheme.colorScheme.surface), + contentColor = MaterialTheme.colorScheme.onSurface, + indicator = { tabPositions -> + TabRowDefaults.Indicator( + Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]).height(3.dp), + color = MaterialTheme.colorScheme.onPrimary // Color for the indicator + ) + }, + divider = { HorizontalDivider() } + ) { + tabs.forEachIndexed { index, title -> + Tab( + selected = pagerState.currentPage == index, + onClick = { + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + }, + text = { Text(title) }, + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + HorizontalPager( + state = pagerState, + modifier = Modifier.weight(1f).fillMaxWidth(), + ) { page -> + val filteredListItems = when (page) { + 1 -> listItems.filter { it.needsUpdate } + else -> listItems + }.filter { + it.farmerName.contains(searchQuery, ignoreCase = true) + } + if (filteredListItems.isNotEmpty() || searchQuery.isNotEmpty()) { + // Show the list only when loading is complete + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 90.dp) + ) { + val filteredList = filteredListItems.filter { + it.farmerName.contains(searchQuery, ignoreCase = true) + } + + if (filteredList.isEmpty()) { + item { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + ) { + Text( + text = stringResource(R.string.no_results_found), + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium, + ) + } + } + } else { + items(filteredList) { farm -> + FarmCard( + farm = farm, + onCardClick = { + navController.currentBackStackEntry?.arguments?.apply { + putParcelableArrayList( + "coordinates", + farm.coordinates?.map { + it.first?.let { it1 -> + it.second?.let { it2 -> + ParcelablePair( + it1, it2 + ) + } + } + }?.let { ArrayList(it) } + ) + putParcelable( + "farmData", + ParcelableFarmData(farm, "view") + ) + } + navController.navigate(route = "setPolygon") + }, + onDeleteClick = { + selectedIds.add(farm.id) + selectedFarm.value = farm + showDeleteDialog.value = true + } + ) + Spacer(modifier = Modifier.height(16.dp)) + } + } + } + } else { + Spacer(modifier = Modifier.height(8.dp)) + Image( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally) + .padding(16.dp, 8.dp), + painter = painterResource(id = R.drawable.no_data2), + contentDescription = null + ) + } + } + } + } else { + // Display a message or image indicating no data available + Spacer(modifier = Modifier.height(8.dp)) + Column(modifier = Modifier.fillMaxSize()) { + Image( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally) + .padding(16.dp, 8.dp), + painter = painterResource(id = R.drawable.no_data2), + contentDescription = null + ) + } + } + } + Scaffold( + topBar = { + FarmListHeaderPlots( + title = stringResource(id = R.string.farm_list), + onAddFarmClicked = { navController.navigate("addFarm/${siteId}") }, + onBackClicked = { navController.navigate("siteList") }, + onBackSearchClicked = { navController.navigate("farmList/${siteId}") }, + onExportClicked = { + action = Action.Export + showFormatDialog = true + }, + onShareClicked = { + action = Action.Share + showFormatDialog = true + }, + onSearchQueryChanged = setSearchQuery, + onImportClicked = { showImportDialog = true }, + showAdd = true, + showExport = listItems.isNotEmpty(), + showShare = listItems.isNotEmpty(), + showSearch = listItems.isNotEmpty(), + onRestoreClicked = { + farmViewModel.restoreData( + deviceId = deviceId, + phoneNumber = "", + email = "", + farmViewModel = farmViewModel + ) { success -> + if (success) { + finalMessage = context.getString(R.string.data_restored_successfully) + showFinalMessage = true + } else { + showFinalMessage = true + showRestorePrompt = true + } + } + } + + ) + }, + floatingActionButton = { + Box( + modifier = Modifier + .fillMaxSize() + ) { + FloatingActionButton( + onClick = { + val sharedPref = + context.getSharedPreferences("FarmCollector", Context.MODE_PRIVATE) + sharedPref.edit().remove("plot_size").remove("selectedUnit").apply() + navController.navigate("addFarm/${siteId}") + }, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(end = 0.dp, bottom = 48.dp) + .background(MaterialTheme.colorScheme.background).align(BottomEnd) + ) { + Icon(Icons.Default.Add, contentDescription = "Add Farm in a Site") + } + } + }, + content = { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + showDataContent() + } + } + ) + + when (restoreStatus) { + is RestoreStatus.InProgress -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + is RestoreStatus.Success -> { + Column( + modifier = Modifier + .padding(top=72.dp) + .fillMaxSize() + ) { + // Display a completion message + val status = restoreStatus as RestoreStatus.Success +// Text( +// text = stringResource( +// R.string.restoration_completed, +// status.addedCount, +// status.sitesCreated +// ), +// modifier = Modifier +// .padding(16.dp) +// .fillMaxWidth(), +// textAlign = TextAlign.Center, +// style = MaterialTheme.typography.bodyMedium +// ) + + if (showFinalMessage) { + // Show the toast + Toast.makeText( + context, + context.getString( + R.string.restoration_completed, + status.addedCount, + status.sitesCreated + ), + Toast.LENGTH_LONG + ).show() + } + showFinalMessage=false + showRestorePrompt = false // Hide the restore prompt if restoration is successful + // showDataContent() + } + } + + is RestoreStatus.Error -> { + // Display an error message + val status = restoreStatus as RestoreStatus.Error + + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + if (showRestorePrompt) { + Column( + modifier = Modifier +// .fillMaxSize() + .background(MaterialTheme.colorScheme.background.copy(alpha = 0.7f)) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { +// Text( +// text = stringResource(id = R.string.no_data_available), +// modifier = Modifier.padding(bottom = 16.dp), +// textAlign = TextAlign.Center, +// style = MaterialTheme.typography.bodyMedium +// ) + + if(showFinalMessage) { + // Show the toast with the final message + Toast.makeText( + context, + context.getString( + R.string.no_data_found, + ), + Toast.LENGTH_LONG // Duration of the toast (LONG or SHORT) + ).show() + } + + showFinalMessage = false + TextField( + value = phone, + onValueChange = { phone = it }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { + Text( + stringResource(id = R.string.phone_number,), + color = inputLabelColor + ) + }, + supportingText = { + if (phone.isNotEmpty() && !isValidPhoneNumber(phone)) Text( + stringResource(R.string.error_invalid_phone_number, phone) + ) + }, + isError = phone.isNotEmpty() && !isValidPhoneNumber(phone), + colors = TextFieldDefaults.colors( + errorLeadingIconColor = Color.Red, + cursorColor = inputTextColor, + errorCursorColor = Color.Red, + focusedIndicatorColor = inputBorder, + unfocusedIndicatorColor = inputBorder, + errorIndicatorColor = Color.Red + ) + + ) + TextField( + value = email, + onValueChange = { email = it }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { + Text( + stringResource(id = R.string.email), + color = inputLabelColor + ) + }, + supportingText = { + if (email.isNotEmpty() && !android.util.Patterns.EMAIL_ADDRESS.matcher( + email + ).matches() + ) + Text(stringResource(R.string.error_invalid_email_address)) + }, + isError = email.isNotEmpty() && !android.util.Patterns.EMAIL_ADDRESS.matcher( + email + ).matches(), + colors = TextFieldDefaults.colors( + errorLeadingIconColor = Color.Red, + cursorColor = inputTextColor, + errorCursorColor = Color.Red, + focusedIndicatorColor = inputBorder, + unfocusedIndicatorColor = inputBorder, + errorIndicatorColor = Color.Red + ), + ) + + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Button( + onClick = { + showRestorePrompt = false + showFinalMessage = false + }, + modifier = Modifier.weight(1f) + ) { + Text(stringResource(id = R.string.cancel)) + } + + Spacer(modifier = Modifier.width(8.dp)) + Button( + onClick = { + if (phone.isNotBlank() || email.isNotBlank()) { + showRestorePrompt = + false // Hide the restore prompt on retry + farmViewModel.restoreData( + deviceId = deviceId, + phoneNumber = phone, + email = email, + farmViewModel = farmViewModel + ) { success -> + finalMessage = if (success) { + context.getString(R.string.data_restored_successfully) + } else { + context.getString(R.string.no_data_found) + } + showFinalMessage = true + } + } + }, + enabled = email.isNotBlank() || phone.isNotBlank(), + modifier = Modifier.weight(1f) + ) { + Text(context.getString(R.string.restore_data)) + } + } + } + } else { + // Display a message indicating no data available +// Text( +// text = finalMessage, +// modifier = Modifier.padding(16.dp), +// textAlign = TextAlign.Center, +// style = MaterialTheme.typography.bodyMedium +// ) + + if (showFinalMessage) { + // Show the toast + Toast.makeText( + context, + finalMessage, + Toast.LENGTH_LONG + ).show() + } + + // showDataContent() + } + } + } + + null -> { + if (isLoading.value) { + // Show loader while data is loading + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } else { + Column( + modifier = Modifier + .padding(top=48.dp) +// .fillMaxSize() + ) { + // Display data or no data message if loading is complete + // showDataContent() + } + } + } + } + + if (showDeleteDialog.value) { + DeleteAllDialogPresenter(showDeleteDialog, onProceedFn = { onDelete() }) + } +} + + + + +@RequiresApi(Build.VERSION_CODES.N) +@Composable +fun ImportFileDialog( + siteId: Long, + onDismiss: () -> Unit, + navController: NavController, +) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + val farmViewModel: FarmViewModel = viewModel() + var selectedFileType by remember { mutableStateOf("") } + var isDropdownMenuExpanded by remember { mutableStateOf(false) } + // var importCompleted by remember { mutableStateOf(false) } + + // Create a launcher to handle the file picker result + val importLauncher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent(), + ) { uri: Uri? -> + uri?.let { + coroutineScope.launch { + try { + val result = farmViewModel.importFile(context, it, siteId) + Toast.makeText(context, result.message, Toast.LENGTH_SHORT).show() + navController.navigate("farmList/$siteId") // Navigate to the refreshed farm list + onDismiss() // Dismiss the dialog after import is complete + } catch (e: Exception) { + Toast.makeText(context, R.string.import_failed, Toast.LENGTH_SHORT).show() + } + } + } + } + + // Create a launcher to handle the file creation result + val createDocumentLauncher = + rememberLauncherForActivityResult( + ActivityResultContracts.CreateDocument(), + ) { uri: Uri? -> + uri?.let { + // Get the template content based on the selected file type + val templateContent = farmViewModel.getTemplateContent(selectedFileType) + // Save the template content to the created document + coroutineScope.launch { + try { + farmViewModel.saveFileToUri(context, it, templateContent) + } catch (e: Exception) { + Toast.makeText(context, R.string.template_download_failed, Toast.LENGTH_SHORT).show() + } + onDismiss() // Dismiss the dialog + } + } + } + + // Function to download the template file + fun downloadTemplate() { + coroutineScope.launch { + try { + // Prompt the user to select where to save the file + createDocumentLauncher.launch( + when (selectedFileType) { + "csv" -> "farm_template.csv" + "geojson" -> "farm_template.geojson" + else -> throw IllegalArgumentException("Unsupported file type: $selectedFileType") + }, + ) + } catch (e: Exception) { + Toast.makeText(context, R.string.template_download_failed, Toast.LENGTH_SHORT).show() + } + } + } + + AlertDialog( + onDismissRequest = { +// onDismiss() + }, + title = { Text(text = stringResource(R.string.import_file)) }, + text = { + Column( + modifier = + Modifier + .padding(8.dp) + .fillMaxWidth(), + ) { + Box( + modifier = + Modifier + .fillMaxWidth() + .border(1.dp, Color.Gray, RoundedCornerShape(4.dp)) + .clickable { isDropdownMenuExpanded = true } + .padding(16.dp), + ) { + Text( + text = if (selectedFileType.isNotEmpty()) selectedFileType else stringResource(R.string.select_file_type), + color = if (selectedFileType.isNotEmpty()) Color.Black else Color.Gray, + ) + DropdownMenu( + expanded = isDropdownMenuExpanded, + onDismissRequest = { isDropdownMenuExpanded = false }, + ) { + DropdownMenuItem(onClick = { + selectedFileType = "csv" + isDropdownMenuExpanded = false + }, text = { Text("CSV") }) + DropdownMenuItem(onClick = { + selectedFileType = "geojson" + isDropdownMenuExpanded = false + }, text = { Text("GeoJSON") }) + } + } + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = { downloadTemplate() }, + enabled = selectedFileType.isNotEmpty(), + modifier = Modifier.fillMaxWidth().padding(8.dp), + ) { + Text(stringResource(R.string.download_template)) + } + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(R.string.select_file_to_import), + modifier = Modifier.padding(bottom = 8.dp), + ) + } + }, + confirmButton = { + Button(onClick = { + importLauncher.launch("*/*") + }) { + Text(stringResource(R.string.select_file)) + } + }, + dismissButton = { + Button(onClick = { onDismiss() }) { + Text(stringResource(R.string.cancel)) + } + }, + containerColor = MaterialTheme.colorScheme.background, // Background that adapts to light/dark + tonalElevation = 6.dp // Adds a subtle shadow for better UX + ) +} + + + +@Composable +fun DeleteAllDialogPresenter( + showDeleteDialog: MutableState, + onProceedFn: () -> Unit, +) { + if (showDeleteDialog.value) { + AlertDialog( + modifier = Modifier.padding(horizontal = 32.dp), + onDismissRequest = { showDeleteDialog.value = false }, + title = { Text(text = stringResource(id = R.string.delete_this_item)) }, + text = { + Column { + Text(stringResource(id = R.string.are_you_sure)) + Text(stringResource(id = R.string.item_will_be_deleted)) + } + }, + confirmButton = { + TextButton(onClick = { onProceedFn() }) { + Text(text = stringResource(id = R.string.yes)) + } + }, + dismissButton = { + TextButton(onClick = { showDeleteDialog.value = false }) { + Text(text = stringResource(id = R.string.no)) + } + }, + containerColor = MaterialTheme.colorScheme.background, // Background that adapts to light/dark + tonalElevation = 6.dp // Adds a subtle shadow for better UX + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@Composable +fun FarmListHeader( + title: String, + onSearchQueryChanged: (String) -> Unit, + onAddFarmClicked: () -> Unit, + onBackClicked: () -> Unit, + onBackSearchClicked: () -> Unit, + showAdd: Boolean, + showSearch: Boolean, + showRestore: Boolean, + onRestoreClicked: () -> Unit +) { + // State to hold the search query + var searchQuery by remember { mutableStateOf("") } + + // State to determine if the search mode is active + var isSearchVisible by remember { mutableStateOf(false) } + + TopAppBar( + modifier = Modifier + .background(MaterialTheme.colorScheme.primary) + .fillMaxWidth(), + navigationIcon = { + IconButton(onClick = { + if (isSearchVisible) { + // Exit search mode, clear search query + searchQuery = "" + onSearchQueryChanged("") + isSearchVisible = false + } else { + // Navigate back normally + onBackClicked() + } + }) { +// if (!isSearchVisible) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "Back", + tint = MaterialTheme.colorScheme.onPrimary + ) +// } + } + }, + title = { +// if (!isSearchVisible) { + Text( + text = title, + color = MaterialTheme.colorScheme.onPrimary, + fontSize = 22.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) +// } + }, + actions = { + + if (showRestore ){ + IconButton( + onClick = { onRestoreClicked() }, + modifier = Modifier.size(36.dp) + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "Restore", + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onPrimary + ) + } + } + if (showSearch) { + IconButton(onClick = { + isSearchVisible = !isSearchVisible + },modifier = Modifier.size(36.dp)) { +// if (!isSearchVisible) { + Icon( + imageVector = Icons.Default.Search, + contentDescription = "Search", + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onPrimary + ) +// } + } + } + }, + ) + + // Show search field when search mode is active + if (isSearchVisible) { + Box( + modifier = Modifier + .padding(top=54.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center // Center the Row within the Box + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, // Center the contents within the Row + modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = searchQuery, + onValueChange = { + searchQuery = it + onSearchQueryChanged(it) + }, + modifier = Modifier + .fillMaxWidth() // Center with a smaller width + .padding(8.dp) + .clip(RoundedCornerShape(0.dp)), // Add rounded corners + placeholder = { Text(stringResource(R.string.search), color = MaterialTheme.colorScheme.onBackground) }, + leadingIcon = { + IconButton(onClick = { + // Exit search mode and clear search + searchQuery = "" + onSearchQueryChanged("") + isSearchVisible = false + }) { + Icon( + Icons.Default.ArrowBack, + contentDescription = "Back", + tint = MaterialTheme.colorScheme.onSurface + ) + } + }, + trailingIcon = { + if (searchQuery != ""){ + IconButton(onClick = { + // Exit search mode and clear search + searchQuery = "" + onSearchQueryChanged("") + }) { + Icon( + Icons.Default.Clear, + contentDescription = "Clear", + tint = MaterialTheme.colorScheme.onSurface + ) + } + } + }, + singleLine = true, + colors = TextFieldDefaults.colors( + focusedIndicatorColor = MaterialTheme.colorScheme.onPrimary, + unfocusedIndicatorColor = MaterialTheme.colorScheme.onPrimary, + focusedLeadingIconColor = MaterialTheme.colorScheme.onPrimary, + unfocusedLeadingIconColor = MaterialTheme.colorScheme.onPrimary, + focusedTrailingIconColor = MaterialTheme.colorScheme.onPrimary, + unfocusedTrailingIconColor = MaterialTheme.colorScheme.onPrimary, + cursorColor = MaterialTheme.colorScheme.onPrimary, + errorCursorColor = Color.Red + ), + shape = RoundedCornerShape(0.dp) // Set the shape for the field to rounded + ) + + } + } + } +} + + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FarmListHeaderPlots( + title: String, + onAddFarmClicked: () -> Unit, + onBackClicked: () -> Unit, + onBackSearchClicked: () -> Unit, + onExportClicked: () -> Unit, + onShareClicked: () -> Unit, + onImportClicked: () -> Unit, + onSearchQueryChanged: (String) -> Unit, + showAdd: Boolean, + showExport: Boolean, + showShare: Boolean, + showSearch: Boolean, + onRestoreClicked: () -> Unit +) { + val context = LocalContext.current as Activity + + var searchQuery by remember { mutableStateOf("") } + var isSearchVisible by remember { mutableStateOf(false) } + var isImportDisabled by remember { mutableStateOf(false) } + + // Column { + TopAppBar( + title = { +// if (!isSearchVisible) { + Text( + text = title, + fontSize = 22.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) +// } + }, +// navigationIcon = { +// IconButton(onClick = onBackClicked) { +// Icon(Icons.Default.ArrowBack, contentDescription = "Back") +// } +// }, + navigationIcon = { + IconButton(onClick = { + if (isSearchVisible) { + // Exit search mode, clear search query + searchQuery = "" + onSearchQueryChanged("") + isSearchVisible = false + } else { + // Navigate back normally + onBackClicked() + } + }) { +// if ( !isSearchVisible ) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "Back", + tint = MaterialTheme.colorScheme.onPrimary + ) +// } + } + }, + actions = { + Row( + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.horizontalScroll(rememberScrollState()) + ) { +// if ( !isSearchVisible ){ + IconButton( + onClick = { onRestoreClicked() }, + modifier = Modifier.size(36.dp) + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "Restore", + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onPrimary + ) + } +// } + if (showExport +// && !isSearchVisible + ) { + IconButton(onClick = onExportClicked, modifier = Modifier.size(36.dp)) { + Icon( + painter = painterResource(id = R.drawable.save), + contentDescription = "Export", + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } + } + if (showShare +// && !isSearchVisible + ) { + IconButton(onClick = onShareClicked, modifier = Modifier.size(36.dp)) { + Icon( + imageVector = Icons.Default.Share, + contentDescription = "Share", + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onPrimary + ) + } + } +// if ( !isSearchVisible ) { + IconButton( + onClick = { + if (!isImportDisabled) { + onImportClicked() + // isImportDisabled = true + } + }, + modifier = Modifier.size(36.dp), + // enabled = !isImportDisabled + ) { + Icon( + painter = painterResource(id = R.drawable.icons8_import_file_48), + contentDescription = "Import", + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } +// } + if (showAdd) { +// IconButton(onClick = { +// val sharedPref = +// context.getSharedPreferences("FarmCollector", Context.MODE_PRIVATE) +// sharedPref.edit().remove("plot_size").remove("selectedUnit").apply() +// onAddFarmClicked() +// }, modifier = Modifier.size(36.dp)) { +// Icon( +// Icons.Default.Add, +// contentDescription = "Add", +// modifier = Modifier.size(24.dp) +// ) +// } + } + + if (showSearch) { + IconButton(onClick = { + isSearchVisible = !isSearchVisible + },modifier = Modifier.size(36.dp)) { +// if (!isSearchVisible) { + Icon( + imageVector = Icons.Default.Search, + contentDescription = "Search", + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onPrimary + ) +// } + } + } + } + }, + ) + + // Show search field when search mode is active + if (isSearchVisible) { + Box( + modifier = Modifier + .padding(top=54.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center // Center the Row within the Box + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, // Center the contents within the Row + modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = searchQuery, + onValueChange = { + searchQuery = it + onSearchQueryChanged(it) + }, + modifier = Modifier + .fillMaxWidth() // Center with a smaller width + .padding(8.dp) + .clip(RoundedCornerShape(0.dp)), // Add rounded corners + placeholder = { Text(stringResource(R.string.search)) }, + leadingIcon = { + IconButton(onClick = { + // Exit search mode and clear search + searchQuery = "" + onSearchQueryChanged("") + isSearchVisible = false + }) { + Icon( + Icons.Default.ArrowBack, + contentDescription = "Back", + tint = MaterialTheme.colorScheme.onSurface + ) + } + }, + trailingIcon = { + if (searchQuery != ""){ + IconButton(onClick = { + // Exit search mode and clear search + searchQuery = "" + onSearchQueryChanged("") + }) { + Icon( + Icons.Default.Clear, + contentDescription = "Clear", + tint = MaterialTheme.colorScheme.onSurface + ) + } + } + }, + singleLine = true, + colors = TextFieldDefaults.colors( + cursorColor = MaterialTheme.colorScheme.onSurface, + focusedIndicatorColor = MaterialTheme.colorScheme.primary, + unfocusedIndicatorColor = MaterialTheme.colorScheme.onSurfaceVariant, + focusedTextColor = MaterialTheme.colorScheme.onSurface, + errorLeadingIconColor = Color.Red, + errorCursorColor = Color.Red, + errorIndicatorColor = Color.Red + ), + shape = RoundedCornerShape(0.dp) // Set the shape for the field to rounded + ) + + } + } + } + // } +} + +@Composable +fun FarmCard( + farm: Farm, + onCardClick: () -> Unit, + onDeleteClick: () -> Unit, +) { + val isDarkTheme = isSystemInDarkTheme() + val backgroundColor = if (isDarkTheme) Color.Black else Color.White + val textColor = if (isDarkTheme) Color.White else Color.Black + + Column( + modifier = + Modifier + .fillMaxSize() + .padding(top = 8.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + ElevatedCard( + elevation = + CardDefaults.cardElevation( + defaultElevation = 6.dp, + ), + modifier = + Modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxWidth() + .padding(8.dp), + onClick = { + onCardClick() + }, + ) { + Column( + modifier = + Modifier + .background(MaterialTheme.colorScheme.background) + .padding(16.dp), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = farm.farmerName, + style = + MaterialTheme.typography.bodySmall.copy( + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = textColor, + ), + modifier = + Modifier + .weight(1.1f) + .padding(bottom = 4.dp), + ) + Text( + text = "${stringResource(id = R.string.size)}: ${formatInput(farm.size.toString())} ${ + stringResource(id = R.string.ha) + }", + style = MaterialTheme.typography.bodySmall.copy(color = textColor), + modifier = + Modifier + .weight(0.9f) + .padding(bottom = 4.dp), + ) + IconButton( + onClick = { + onDeleteClick() + }, + modifier = + Modifier + .size(24.dp) + .padding(4.dp), + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Delete", + tint = Color.Red, + ) + } + } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = "${stringResource(id = R.string.village)}: ${farm.village}", + style = MaterialTheme.typography.bodySmall.copy(color = textColor), + modifier = Modifier.weight(1f), + ) + Text( + text = "${stringResource(id = R.string.district)}: ${farm.district}", + style = MaterialTheme.typography.bodySmall.copy(color = textColor), + modifier = Modifier.weight(1f), + ) + } + + // Show the label if the farm needs an update + if (farm.needsUpdate) { + Text( + text = stringResource(id = R.string.needs_update), + color = Color.Blue, + fontWeight = FontWeight.Bold, + fontSize = 12.sp, // Adjust font size + modifier = Modifier.padding(top = 4.dp) + ) + } + } + } + } +} + + +fun OutputStream.writeCsv(farms: List) { + val writer = bufferedWriter() + writer.write(""""Farmer Name", "Village", "District"""") + writer.newLine() + farms.forEach { + writer.write("${it.farmerName}, ${it.village}, \"${it.district}\"") + writer.newLine() + } + writer.flush() +} + +// on below line creating a method to write data to txt file. +private fun writeTextData( + file: File, + farms: List, + onDismiss: () -> Unit, + format: String, +) { + var fileOutputStream: FileOutputStream? = null + try { + fileOutputStream = FileOutputStream(file) + + fileOutputStream + .write( + """"Farmer Name", "Village", "District", "Size in Ha", "Cherry harvested this year in Kgs", "latitude", "longitude" , "createdAt", "updatedAt" """ + .toByteArray(), + ) + fileOutputStream.write(10) + farms.forEach { + fileOutputStream.write( + "${it.farmerName}, ${it.village},${it.district},${it.size},${it.purchases},${it.latitude},${it.longitude},${ + Date( + it.createdAt, + ) + }, \"${Date(it.updatedAt)}\"".toByteArray(), + ) + fileOutputStream.write(10) + } + } catch (e: Exception) { + e.printStackTrace() + } finally { + if (fileOutputStream != null) { + try { + fileOutputStream.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + } +} + +@SuppressLint("MissingPermission") +@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) +@Composable +fun UpdateFarmForm( + navController: NavController, + farmId: Long?, + listItems: List, +) { + val floatValue = 123.45f + val item = + listItems.find { it.id == farmId } ?: Farm( +// id = 0, + siteId = 0L, + farmerName = "Default Farmer", + memberId = "", + farmerPhoto = "Default photo", + village = "Default Village", + district = "Default District", + latitude = "Default Village", + longitude = "Default Village", + coordinates = null, + accuracyArray = null, + size = floatValue, + purchases = floatValue, + createdAt = 1L, + updatedAt = 1L, + ) + val context = LocalContext.current as Activity + var farmerName by remember { mutableStateOf(item.farmerName) } + var memberId by remember { mutableStateOf(item.memberId) } + var farmerPhoto by remember { mutableStateOf(item.farmerPhoto) } + var village by remember { mutableStateOf(item.village) } + var district by remember { mutableStateOf(item.district) } +// var size by remember { mutableStateOf(item.size.toString()) } + + val sharedPref = context.getSharedPreferences("FarmCollector", Context.MODE_PRIVATE) + var isValidSize by remember { mutableStateOf(true) } + var size by remember { + mutableStateOf(sharedPref.getString("plot_size", item.size.toString()) ?: item.size.toString()) + } + + val hasNewPolygon: Boolean = sharedPref.getBoolean(KEY_HAS_NEW_POLYGON,false) + + var latitude by remember { mutableStateOf(item.latitude) } + var longitude by remember { mutableStateOf(item.longitude) } + var coordinates by remember { mutableStateOf(item.coordinates) } + + // Flag to track if the polygon was modified + var showKeepPolygonDialog by remember { mutableStateOf(false) } + + + val fusedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) } + val farmViewModel: FarmViewModel = + viewModel( + factory = FarmViewModelFactory(context.applicationContext as Application), + ) + + + val showDialog = remember { mutableStateOf(false) } + val showLocationDialog = remember { mutableStateOf(false) } + val showLocationDialogNew = remember { mutableStateOf(false) } + val showPermissionRequest = remember { mutableStateOf(false) } +// val file = context.createImageFile() +// val uri = +// FileProvider.getUriForFile( +// Objects.requireNonNull(context), +// context.packageName + ".provider", +// file, +// ) + var expanded by remember { mutableStateOf(false) } + val items = listOf("Ha", "Acres", "Sqm", "Timad", "Fichesa", "Manzana", "Tarea") + var selectedUnit by remember { mutableStateOf(items[0]) } + + val scientificNotationPattern = Pattern.compile("([+-]?\\d*\\.?\\d+)[eE][+-]?\\d+") + + LaunchedEffect(Unit) { + if (!isLocationEnabled(context)) { + showLocationDialog.value = true + } + } + + // Define string constants + val titleText = stringResource(id = R.string.enable_location_services) + val messageText = stringResource(id = R.string.location_services_required_message) + val enableButtonText = stringResource(id = R.string.enable) + + // Dialog to prompt user to enable location services + if (showLocationDialog.value) { + AlertDialog( + onDismissRequest = { showLocationDialog.value = false }, + title = { Text(titleText) }, + text = { Text(messageText) }, + confirmButton = { + Button(onClick = { + showLocationDialog.value = false + promptEnableLocation(context) + }) { + Text(enableButtonText) + } + }, + dismissButton = { + Button(onClick = { + showLocationDialog.value = false + Toast.makeText(context, R.string.location_permission_denied_message, Toast.LENGTH_SHORT).show() + }) { + Text(stringResource(id = R.string.cancel)) + } + }, + ) + } + if (navController.currentBackStackEntry!!.savedStateHandle.contains("coordinates")) { + val parcelableCoordinates = navController.currentBackStackEntry!! + .savedStateHandle + .get>("coordinates") + + coordinates = parcelableCoordinates?.map { Pair(it.first, it.second) } + } + + + val fillForm = stringResource(id = R.string.fill_form) + + fun validateForm(): Boolean { + var isValid = true + val textWithNumbersRegex = Regex(".*[a-zA-Z]+.*") // Ensures there is at least one letter + if (farmerName.isBlank() || !farmerName.matches(textWithNumbersRegex)) { + isValid = false + } + + if (village.isBlank() || !village.matches(textWithNumbersRegex)) { + isValid = false + } + + if (district.isBlank() || !district.matches(textWithNumbersRegex)) { + isValid = false + } + + if (size.toFloatOrNull()?.let { it > 0 } != true) { + isValid = false + } + + if (latitude.isBlank() || longitude.isBlank()) { + isValid = false + } + + return isValid + } + + /** + * Updating Farm details + * Before sending to the database + */ + + fun updateFarmInstance() { + + val isValid = validateForm() + if (isValid) { + item.farmerPhoto = "" + item.farmerName = farmerName + item.memberId = memberId + item.latitude = latitude + item.village = village + item.district = district + item.longitude = longitude +// if ((size.toDoubleOrNull()?.let { convertSize(it, selectedUnit).toFloat() } ?: 0f) >= 4) { +// if ((coordinates?.size ?: 0) < 3) { +// Toast +// .makeText( +// context, +// R.string.error_polygon_points, +// Toast.LENGTH_SHORT, +// ).show() +// return +// } +// item.coordinates = coordinates?.plus(coordinates?.first()) as List> +// } else { +// item.coordinates = listOf(Pair(item.longitude.toDoubleOrNull() ?: 0.0, item.latitude.toDoubleOrNull() ?: 0.0)) // Example default value +// } + // Updated condition handling + if ((size.toDoubleOrNull()?.let { convertSize(it, selectedUnit).toFloat() } ?: 0f) >= 4) { + // Check if coordinates are valid for a polygon + if ((coordinates?.size ?: 0) < 3) { + Toast.makeText( + context, + R.string.error_polygon_points, + Toast.LENGTH_SHORT, + ).show() + return + } + + // Show the dialog to ask whether to keep or capture new coordinates + showKeepPolygonDialog = true + + } else { +// // Handle case where size is less than 4 +// if ((coordinates?.size ?: 0) < 3) { +// // Size is less than 4 and not enough points for a polygon +// Toast.makeText( +// context, +// R.string.error_polygon_points, +// Toast.LENGTH_SHORT, +// ).show() +// return +// } else + if ((coordinates?.size ?: 0) >= 3) { + // Size is less than 4 but valid polygon coordinates are present + // Show the dialog to ask whether to keep or capture new coordinates + showKeepPolygonDialog = true + } else { + // Handle the case where size is less than the threshold and only one coordinate is present + item.coordinates = listOf( + Pair( + item.longitude.toDoubleOrNull() ?: 0.0, + item.latitude.toDoubleOrNull() ?: 0.0 + ) + ) // Example default value + } + } + item.size = convertSize(size.toDouble(), selectedUnit).toFloat() + item.purchases = 0.toFloat() + item.updatedAt = Instant.now().millis + updateFarm(farmViewModel, item) + item.needsUpdate = false + val returnIntent = Intent() + context.setResult(Activity.RESULT_OK, returnIntent) + navController.navigate("farmList/$siteID") + } else { + Toast.makeText(context, fillForm, Toast.LENGTH_SHORT).show() + } + } + + // If changes are detected, show dialog to confirm + if (showKeepPolygonDialog) { + KeepPolygonDialog( + onDismiss = { showKeepPolygonDialog = false }, + onKeepExisting = { + // Keep the existing polygon + item.coordinates = coordinates?.plus(coordinates?.first()) as List> + updateFarmInstance() + showKeepPolygonDialog = false // Close dialog + }, + onCaptureNew = { + coordinates = listOf() // Clear coordinates array when starting to capture new polygon + navController.navigate("SetPolygon") + + with(sharedPref.edit()) { + putBoolean(KEY_HAS_NEW_POLYGON, true) + apply() + } + showKeepPolygonDialog = false // Close dialog + } + ) + } + +// Confirm farm update and ask if they wish to capture new polygon + if (showDialog.value) { + AlertDialog( + modifier = Modifier.padding(horizontal = 32.dp), + onDismissRequest = { showDialog.value = false }, + title = { Text(text = stringResource(id = R.string.update_farm)) }, + text = { + Column { + Text(text = stringResource(id = R.string.confirm_update_farm)) + } + }, + confirmButton = { + TextButton(onClick = { + if ((coordinates?.size ?: 0) >= 3) { +// if (!hasNewPolygon) { + showKeepPolygonDialog = true +// } + } + else{ + updateFarmInstance(); + } + }) { + Text(text = stringResource(id = R.string.update_farm)) + } + }, + dismissButton = { + TextButton( + onClick = + { + showDialog.value = false + navController.navigate("setPolygon") + }, + ) { + Text(text = stringResource(id = R.string.set_polygon)) + } + }, + containerColor = MaterialTheme.colorScheme.background, // Background that adapts to light/dark + tonalElevation = 6.dp // Adds a subtle shadow for better UX + ) + } + + + + + + val scrollState = rememberScrollState() + val (focusRequester1) = FocusRequester.createRefs() + val (focusRequester2) = FocusRequester.createRefs() + val (focusRequester3) = FocusRequester.createRefs() + + val isDarkTheme = isSystemInDarkTheme() + val backgroundColor = if (isDarkTheme) Color.Black else Color.White + val inputLabelColor = if (isDarkTheme) Color.LightGray else Color.DarkGray + val inputTextColor = if (isDarkTheme) Color.White else Color.Black + val buttonColor = if (isDarkTheme) Color.Black else Color.White + val inputBorder = if (isDarkTheme) Color.LightGray else Color.DarkGray + + if (showPermissionRequest.value) { + LocationPermissionRequest( + onLocationEnabled = { + showLocationDialog.value = true + }, + onPermissionsGranted = { + showPermissionRequest.value = false + }, + onPermissionsDenied = { + // Handle permissions denied + // Show a message or take appropriate action + }, + showLocationDialogNew = showLocationDialogNew, + hasToShowDialog = showLocationDialogNew.value, + ) + } + + Column( + modifier = + Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) +// .padding(16.dp) + .verticalScroll(state = scrollState), + ) { + FarmListHeader( + title = stringResource(id = R.string.update_farm), + onSearchQueryChanged = {}, + onAddFarmClicked = { /* Handle adding a farm here */ }, + onBackClicked = { navController.popBackStack() }, + onBackSearchClicked = {}, + showAdd = false, + showSearch = false, + showRestore = false, + onRestoreClicked = {} + ) + Spacer(modifier = Modifier.height(16.dp)) + TextField( + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = + KeyboardActions( + onDone = { focusRequester1.requestFocus() }, + ), + value = farmerName, + onValueChange = { farmerName = it }, + label = { Text(stringResource(id = R.string.farm_name), color = inputLabelColor) }, + isError = farmerName.isBlank(), + modifier = + Modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + .onKeyEvent { + if (it.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_ENTER) { + focusRequester1.requestFocus() + true + } + false + }, + ) + TextField( + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = + KeyboardActions( + onDone = { focusRequester1.requestFocus() }, + ), + value = memberId, + onValueChange = { memberId = it }, + label = { Text(stringResource(id = R.string.member_id), color = inputLabelColor) }, + modifier = + Modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + .onKeyEvent { + if (it.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_ENTER) { + focusRequester1.requestFocus() + } + false + }, + ) + TextField( + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = + KeyboardActions( + onDone = { focusRequester2.requestFocus() }, + ), + value = village, + onValueChange = { village = it }, + label = { Text(stringResource(id = R.string.village), color = inputLabelColor) }, + modifier = + Modifier + .focusRequester(focusRequester1) + .fillMaxWidth() + .padding(bottom = 16.dp), + ) + TextField( + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = + KeyboardActions( + onDone = { focusRequester3.requestFocus() }, + ), + value = district, + onValueChange = { district = it }, + label = { Text(stringResource(id = R.string.district), color = inputLabelColor) }, + modifier = + Modifier + .focusRequester(focusRequester2) + .fillMaxWidth() + .padding(bottom = 16.dp), + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + TextField( + singleLine = true, + value = truncateToDecimalPlaces(size,9), + onValueChange = { it -> + val formattedValue = when { + validateSize(it) -> it + scientificNotationPattern.matcher(it).matches() -> { + truncateToDecimalPlaces(formatInput(it), 9) + } + else -> it + } + + // Update the size state with the formatted value + size = formattedValue + isValidSize = validateSize(formattedValue) + + // Save to SharedPreferences + with(sharedPref.edit()) { + putString("plot_size", formattedValue) + apply() + } + }, + keyboardOptions = + KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Number, + ), + label = { Text(stringResource(id = R.string.size_in_hectares) + " (*)", color = inputLabelColor) }, + isError = size.toFloatOrNull() == null || size.toFloat() <= 0, // Validate size + colors = + TextFieldDefaults.colors( + errorLeadingIconColor = Color.Red, + cursorColor = inputTextColor, + errorCursorColor = Color.Red, + focusedIndicatorColor = inputBorder, + unfocusedIndicatorColor = inputBorder, + errorIndicatorColor = Color.Red, + ), + modifier = + Modifier + .focusRequester(focusRequester3) + .weight(1f) + .padding(bottom = 16.dp), + ) + + Spacer(modifier = Modifier.width(16.dp)) + // Size measure + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded + }, + modifier = Modifier.weight(1f), + ) { + TextField( + readOnly = true, + value = selectedUnit, + onValueChange = { }, + label = { Text(stringResource(R.string.unit)) }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon( + expanded = expanded, + ) + }, + colors = ExposedDropdownMenuDefaults.textFieldColors(), + modifier = Modifier.menuAnchor(), + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { + expanded = false + }, + ) { + items.forEach { selectionOption -> + DropdownMenuItem( + { Text(text = selectionOption) }, + onClick = { + selectedUnit = selectionOption + expanded = false + }, + ) + } + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) // Add space between the latitude and longitude input fields + if ((size.toDoubleOrNull()?.let { convertSize(it, selectedUnit).toFloat() } ?: 0f) < 4f) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + TextField( + readOnly = true, + value = latitude, +// onValueChange = { latitude = it }, + onValueChange = { it -> + val formattedValue = when { + validateNumber(it) -> { + truncateToDecimalPlaces(it, 6) // Valid number, truncate to 6 decimal places + } + scientificNotationPattern.matcher(it).matches() -> { + truncateToDecimalPlaces(formatInput(it), 6) // Convert scientific notation and truncate + } + else -> { + // Show a Toast message if the input does not meet the requirements + Toast.makeText( + context, + context.getString(R.string.error_latitude_decimal_places), + Toast.LENGTH_SHORT + ).show() + null // Return null to reject invalid input + } + } + + // Update the latitude state only if the formatted value is valid (not null) + formattedValue?.let { + latitude = it + } + }, + label = { Text(stringResource(id = R.string.latitude), color = inputLabelColor) }, + modifier = + Modifier + .weight(1f) + .padding(bottom = 16.dp), + ) + Spacer(modifier = Modifier.width(16.dp)) // Add space between the latitude and longitude input fields + TextField( + readOnly = true, + value = longitude, +// onValueChange = { longitude = it }, + onValueChange = { it -> + val formattedValue = when { + validateNumber(it) -> { + truncateToDecimalPlaces(it, 6) // Valid number, truncate to 6 decimal places + } + scientificNotationPattern.matcher(it).matches() -> { + truncateToDecimalPlaces(formatInput(it), 6) // Convert scientific notation and truncate + } + else -> { + // Show a Toast message if the input does not meet the requirements + Toast.makeText( + context, + context.getString(R.string.error_longitude_decimal_places), + Toast.LENGTH_SHORT + ).show() + null // Return null to reject invalid input + } + } + + // Update the latitude state only if the formatted value is valid (not null) + formattedValue?.let { + longitude = it + } + }, + label = { Text(stringResource(id = R.string.longitude), color = inputLabelColor) }, + modifier = + Modifier + .weight(1f) + .padding(bottom = 16.dp), + ) + } + } + Button( + onClick = { + showPermissionRequest.value = true + if (!isLocationEnabled(context)) { + showLocationDialog.value = true + } else { + if (isLocationEnabled(context) && context.hasLocationPermission()) { + if (size.toDoubleOrNull()?.let { convertSize(it, selectedUnit).toFloat() }?.let { it < 4f } == true) { + // Simulate collecting latitude and longitude + if (context.hasLocationPermission()) { + val locationRequest = + LocationRequest.create().apply { + priority = LocationRequest.PRIORITY_HIGH_ACCURACY + interval = 10000 // Update interval in milliseconds + fastestInterval = + 5000 // Fastest update interval in milliseconds + } + + fusedLocationClient.requestLocationUpdates( + locationRequest, + object : LocationCallback() { + override fun onLocationResult(locationResult: LocationResult) { + locationResult.lastLocation?.let { lastLocation -> + // Handle the new location + latitude = "${lastLocation.latitude}" + longitude = "${lastLocation.longitude}" + // Log.d("FARM_LOCATION", "loaded success,,,,,,,") + } + } + }, + Looper.getMainLooper(), + ) + } + } else { + if (isLocationEnabled(context)) { + navController.navigate("setPolygon") + } + } + } else { + showPermissionRequest.value = true + showLocationDialog.value = true + } + } + }, + modifier = + Modifier + .align(Alignment.CenterHorizontally) + .fillMaxWidth(0.7f) + .padding(bottom = 5.dp) + .height(50.dp), + enabled = size.toFloatOrNull() != null, + ) { + Text( + // text = if (size.toFloatOrNull() != null && size.toFloat() < 4) stringResource(id = R.string.get_coordinates) else stringResource( + text = + if (size.toDoubleOrNull()?.let { convertSize(it, selectedUnit).toFloat() }?.let { it < 4f } == + true + ) { + stringResource(id = R.string.get_coordinates) + } else { + stringResource( + id = R.string.set_new_polygon, + ) + }, + ) + } + Button( + onClick = { + if (validateForm()) { + showDialog.value = true + } else { + Toast.makeText(context, fillForm, Toast.LENGTH_SHORT).show() + } + }, + modifier = + Modifier + .fillMaxWidth() + .height(50.dp), + ) { + Text(text = stringResource(id = R.string.update_farm)) + } + } +} + +fun updateFarm( + farmViewModel: FarmViewModel, + item: Farm, +) { + farmViewModel.updateFarm(item) +} diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/screens/SetPolygon.kt b/app/src/main/java/org/technoserve/farmcollector/ui/screens/SetPolygon.kt new file mode 100644 index 0000000..6a9f382 --- /dev/null +++ b/app/src/main/java/org/technoserve/farmcollector/ui/screens/SetPolygon.kt @@ -0,0 +1,766 @@ +package org.technoserve.farmcollector.ui.screens + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.location.Location +import android.os.Looper +import android.util.Log +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Done +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import com.google.android.gms.location.LocationCallback +import com.google.android.gms.location.LocationRequest +import com.google.android.gms.location.LocationResult +import com.google.android.gms.location.LocationServices +import com.google.android.gms.tasks.CancellationToken +import com.google.android.gms.tasks.CancellationTokenSource +import com.google.android.gms.tasks.OnTokenCanceledListener +import org.technoserve.farmcollector.R +//import org.technoserve.farmcollector.hasLocationPermission +//import org.technoserve.farmcollector.map.MapScreen +//import org.technoserve.farmcollector.map.MapViewModel +import org.technoserve.farmcollector.ui.composes.AreaDialog +import org.technoserve.farmcollector.ui.composes.ConfirmDialog +import org.technoserve.farmcollector.ui.screens.farms.formatInput +import org.technoserve.farmcollector.ui.screens.farms.isLocationEnabled +import org.technoserve.farmcollector.ui.screens.farms.promptEnableLocation +import org.technoserve.farmcollector.ui.screens.farms.truncateToDecimalPlaces +import org.technoserve.farmcollector.ui.screens.map.MapScreen +import org.technoserve.farmcollector.utils.convertSize +import org.technoserve.farmcollector.utils.hasLocationPermission +import org.technoserve.farmcollector.viewmodels.MapViewModel + +/** + * This screen helps you to capture and visualize farm polygon. + * When capturing, You are able to start, add point, clear map or remove a point on the map + */ + +const val CALCULATED_AREA_OPTION = "CALCULATED_AREA" +const val ENTERED_AREA_OPTION = "ENTERED_AREA" + +@OptIn(ExperimentalLayoutApi::class) +@SuppressLint("MissingPermission") +@Composable +fun SetPolygon( + navController: NavController, + viewModel: MapViewModel, +) { + val context = LocalContext.current as Activity + var coordinates by remember { mutableStateOf(listOf>()) } + var accuracyArray by remember { mutableStateOf(listOf()) } + var isCapturingCoordinates by remember { mutableStateOf(false) } + var hasPointsOnMap by remember { mutableStateOf(false) } + val fusedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) } + var showConfirmDialog = remember { mutableStateOf(false) } + val showClearMapDialog = remember { mutableStateOf(false) } + // Getting farm details such as polygon or single pair of lat and long if shared from farm list + val farmData = navController.previousBackStackEntry?.arguments?.getParcelable("farmData") + + // cast farmData string to Farm object + val farmInfo = farmData?.farm + var accuracy by remember { mutableStateOf("") } + var viewSelectFarm by remember { mutableStateOf(false) } + val sharedPref = context.getSharedPreferences("FarmCollector", Context.MODE_PRIVATE) + + val locationRequest = + LocationRequest.create().apply { + priority = LocationRequest.PRIORITY_HIGH_ACCURACY + interval = 1000 // Update interval in milliseconds + fastestInterval = 500 // Fastest update interval in milliseconds + } + + val showAlertDialog = remember { mutableStateOf(false) } + + val mapViewModel: MapViewModel = viewModel() + // Remember the state for showing the dialog + val showLocationDialog = remember { mutableStateOf(false) } + + + LaunchedEffect(Unit) { + mapViewModel.clearCoordinates() + // mapViewModel.clearPolygon() + + // Get the accuracyArrayData from savedStateHandle + val accuracyArrayData = navController.currentBackStackEntry?.savedStateHandle?.get>("accuracyArray") + + // If the accuracyArrayData exists, clear it + accuracyArrayData?.let { + navController.currentBackStackEntry?.savedStateHandle?.set("accuracyArray", emptyList()) + } + if (!isLocationEnabled(context)) { + showLocationDialog.value = true + } + } + + // Define string constants + val titleText = stringResource(id = R.string.enable_location_services) + val messageText = stringResource(id = R.string.location_services_required_message) + val enableButtonText = stringResource(id = R.string.enable) + + // Dialog to prompt user to enable location services + if (showLocationDialog.value) { + AlertDialog( + onDismissRequest = { showLocationDialog.value = false }, + title = { Text(titleText) }, + text = { Text(messageText) }, + confirmButton = { + Button(onClick = { + showLocationDialog.value = false + promptEnableLocation(context) + }) { + Text(enableButtonText) + } + }, + dismissButton = { + Button(onClick = { + showLocationDialog.value = false + Toast + .makeText( + context, + R.string.location_permission_denied_message, + Toast.LENGTH_SHORT, + ).show() + }) { + Text(stringResource(id = R.string.cancel)) + } + }, + containerColor = MaterialTheme.colorScheme.background, // Background that adapts to light/dark + tonalElevation = 6.dp // Adds a subtle shadow for better UX + ) + } + + if (!isCapturingCoordinates && farmInfo == null) { + fusedLocationClient + .getCurrentLocation( + locationRequest.priority, + object : CancellationToken() { + override fun onCanceledRequested(p0: OnTokenCanceledListener) = CancellationTokenSource().token + + override fun isCancellationRequested() = false + }, + ).addOnSuccessListener { location: Location? -> + // update map camera position + if (location != null) { + accuracy = location.accuracy.toString() + if (viewModel.state.value.clusterItems + .isEmpty() + ) { + viewModel.addCoordinate(location.latitude, location.longitude) + } + } + } + } + + fusedLocationClient.requestLocationUpdates( + locationRequest, + object : LocationCallback() { + override fun onLocationResult(locationResult: LocationResult) { + val location = locationResult.lastLocation ?: return + accuracy = location.accuracy.toString() + } + }, + Looper.getMainLooper(), + ) + + // Display coordinates of a farm on map + if (farmInfo != null && !isCapturingCoordinates && !viewSelectFarm) { + viewModel.clearCoordinates() + // mapViewModel.clearPolygon() + if (farmInfo.coordinates?.isNotEmpty() == true) { + viewModel.addCoordinates(farmInfo.coordinates!!) + } else if (farmInfo.latitude.isNotEmpty() && farmInfo.longitude.isNotEmpty()) { + viewModel.addMarker(Pair(farmInfo.latitude.toDouble(), farmInfo.longitude.toDouble())) + } + + viewSelectFarm = true + } + + val enteredArea = sharedPref.getString("plot_size", "0.0")?.toDoubleOrNull() ?: 0.0 + val selectedUnit = sharedPref.getString("selectedUnit", "Ha")?:"Ha" + val enteredAreaConverted= convertSize(enteredArea,selectedUnit) + val calculatedArea = mapViewModel.calculateArea(coordinates) + var showSaveButton by remember { mutableStateOf(false) } // To track if save button should be shown + + // State to show invalid polygon dialog + val showInvalidPolygonDialog = remember { mutableStateOf(false) } + + // Confirm dialog to finalize the polygon + if (showConfirmDialog.value) { + ConfirmDialog( + title = stringResource(id = R.string.set_polygon), + message = stringResource(id = R.string.confirm_set_polygon), + showConfirmDialog, + onProceedFn = { + if (coordinates.size >= 3) { + + mapViewModel.clearCoordinates() + // mapViewModel.clearPolygon() + mapViewModel.addCoordinates(coordinates) + + if (coordinates.isNotEmpty()) { + + // update map camera position + val coordinate = coordinates.first() + + coordinates = coordinates + coordinate + viewModel.addMarker(coordinate) + + // add camera position + viewModel.addCoordinate( + coordinates.first().first, + coordinates.first().second, + ) + } + + // Show the save button after preview + showSaveButton = true + } else { + showAlertDialog.value = true // Handle if there aren't enough points + } + // Hide the confirmation dialog after clicking yes + showConfirmDialog.value = false + }, + onCancelFn = { + // Set capturing state even if user cancels + isCapturingCoordinates = true + + // Hide the dialog after user clicks "No" + showConfirmDialog.value = false + } + ) + } + + + + + + + + // Alert dialog for insufficient coordinates + if (showAlertDialog.value) { + AlertDialog( + onDismissRequest = { + showAlertDialog.value = false + }, + title = { + Text(text = stringResource(id = R.string.insufficient_coordinates_title)) + }, + text = { + Text(text = stringResource(id = R.string.insufficient_coordinates_message)) + }, + confirmButton = { + Button( + onClick = { + showAlertDialog.value = false + }, + ) { + Text(text = stringResource(id = R.string.ok)) + isCapturingCoordinates= true + showConfirmDialog.value = false + mapViewModel.clearCoordinates() + // mapViewModel.clearPolygon() + } + }, + containerColor = MaterialTheme.colorScheme.background, // Background that adapts to light/dark + tonalElevation = 6.dp // Adds a subtle shadow for better UX + ) + } + + // Display AreaDialog + AreaDialog( + showDialog = mapViewModel.showDialog.collectAsState().value, + onDismiss = { mapViewModel.dismissDialog() }, + onConfirm = { chosenArea -> + val chosenSize = + when (chosenArea) { + CALCULATED_AREA_OPTION -> calculatedArea.toString() + ENTERED_AREA_OPTION -> enteredAreaConverted.toString() + else -> throw IllegalArgumentException("Unknown area option: $chosenArea") + } + val truncatedSize = truncateToDecimalPlaces(formatInput(chosenSize), 9) + sharedPref.edit().putString("plot_size", truncatedSize).apply() + if (sharedPref.contains("selectedUnit")) { + sharedPref.edit().remove("selectedUnit").apply() + } + coordinates = listOf() // Clear coordinates array when starting + mapViewModel.clearCoordinates() + // mapViewModel.clearPolygon() + navController.navigateUp() + }, + calculatedArea = calculatedArea, + enteredArea = enteredAreaConverted + ) + + // Confirm clear map + if (showClearMapDialog.value) { + ConfirmDialog( + stringResource(id = R.string.set_polygon), + stringResource(id = R.string.clear_map), + showClearMapDialog, + fun() { + coordinates = listOf() // Clear coordinates array when starting + accuracy = "" + viewModel.clearCoordinates() // Clear google map + // mapViewModel.clearPolygon() + showClearMapDialog.value = false + }, + onCancelFn = {showClearMapDialog.value = false} + ) + } + + val isDarkTheme = isSystemInDarkTheme() + val backgroundColor = if (isDarkTheme) Color.Black else Color.White + val textColor = if (isDarkTheme) Color.White else Color.Black + + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + ) { + Column( + modifier = + Modifier + .fillMaxWidth() + .fillMaxHeight( + if (viewSelectFarm) { + 0.65f + } else if (accuracy.isNotEmpty()) { + .87f + } else { + .93f + }, + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + // Google map + MapScreen( + state = viewModel.state.value, + setupClusterManager = viewModel::setupClusterManager, + calculateZoneViewCenter = viewModel::calculateZoneLatLngBounds, + onMapTypeChange = viewModel::onMapTypeChange, + ) + } + Column( + modifier = + Modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxWidth() + .fillMaxHeight(), + ) { + if (!viewSelectFarm && accuracy.isNotEmpty()) { + Column( + modifier = + Modifier + .fillMaxWidth() + .height(40.dp) + .padding(horizontal = 14.dp), + ) { + Text( + modifier = Modifier.padding(horizontal = 2.dp), + color = Color.Black, + text = stringResource(id = R.string.accuracy) + ": $accuracy m", + ) + } + } + + FlowRow( + modifier = + Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(bottom = 10.dp), + horizontalArrangement = if (viewSelectFarm) Arrangement.Center else Arrangement.Start, + ) { + // Hiding some buttons depending on page usage. Viewing verse setting farm polygon + if (viewSelectFarm) { + Row { + if (farmInfo != null) { + Column( + modifier = + Modifier + .background(MaterialTheme.colorScheme.background) + .padding(5.dp), + ) { + Text( + text = stringResource(id = R.string.farm_info), + style = + MaterialTheme.typography.bodySmall.copy( + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + ), + modifier = Modifier.padding(5.dp), + ) + Column( + content = { }, + modifier = + Modifier + .width(200.dp) + .background(Color.Black) + .height(2.dp), + ) + Text( + text = "${stringResource(id = R.string.farm_name)}: ${farmInfo.farmerName}", + style = MaterialTheme.typography.bodyMedium.copy(color = textColor), + modifier = Modifier.padding(top = 5.dp), + ) + Text( + text = "${stringResource(id = R.string.member_id)}: ${farmInfo.memberId.ifEmpty { "N/A" }}", + style = MaterialTheme.typography.bodyMedium.copy(color = textColor), + ) + Text( + text = "${stringResource(id = R.string.village)}: ${farmInfo.village}", + style = MaterialTheme.typography.bodyMedium.copy(color = textColor), + ) + Text( + text = "${stringResource(id = R.string.district)}: ${farmInfo.district}", + style = MaterialTheme.typography.bodyMedium.copy(color = textColor), + ) + Text(text = "${stringResource(id = R.string.latitude)}: ${farmInfo.latitude}",style = MaterialTheme.typography.bodyMedium.copy(color = textColor)) + Text(text = "${stringResource(id = R.string.longitude)}: ${farmInfo.longitude}",style = MaterialTheme.typography.bodyMedium.copy(color = textColor)) + Text( + text = "${stringResource(id = R.string.size)}: ${truncateToDecimalPlaces(formatInput(farmInfo.size.toString()),9)} ${ + stringResource( + id = R.string.ha, + ) + }", + style = MaterialTheme.typography.bodyMedium.copy(color = textColor), + ) + } + } + } + Row { + Button( + shape = RoundedCornerShape(10.dp), + modifier = + Modifier + .width(120.dp) + .fillMaxWidth(0.23f), + onClick = { + viewModel.clearCoordinates() + // mapViewModel.clearPolygon() + navController.navigateUp() + }, + ) { + Text(text = stringResource(id = R.string.close)) + } + Button( + shape = RoundedCornerShape(10.dp), + modifier = + Modifier + .width(150.dp) + .fillMaxWidth(0.23f) + .padding(start = 10.dp), + onClick = { + navController.navigate("updateFarm/${farmInfo?.id}") + }, + ) { + Text(text = stringResource(id = R.string.update)) + } + } + } else { + + // "Start" button - visible only when not capturing coordinates and the "Finish" button hasn't been clicked + if (!isCapturingCoordinates && !showConfirmDialog.value && !showSaveButton) { + ElevatedButton( + modifier = Modifier.fillMaxWidth(0.25f).size(width = 80.dp, height = 80.dp), + shape = RoundedCornerShape(0.dp), + colors = ButtonDefaults.buttonColors(Color.White), + onClick = { + if (!isLocationEnabled(context)) { + showLocationDialog.value = true + } else { + // Get the accuracyArrayData from savedStateHandle + val accuracyArrayData = navController.currentBackStackEntry?.savedStateHandle?.get>("accuracyArray") + + // If the accuracyArrayData exists, clear it + accuracyArrayData?.let { + navController.currentBackStackEntry?.savedStateHandle?.set("accuracyArray", emptyList()) + } + coordinates = listOf() // Clear coordinates array when starting + viewModel.clearCoordinates() + // mapViewModel.clearPolygon() + isCapturingCoordinates = true + } + } + ) { + Icon( + imageVector = Icons.Default.PlayArrow, + contentDescription = "Start", + tint = Color.Black, + modifier = Modifier.padding(4.dp) + ) + } + } + + // "Finish" button - visible when capturing coordinates but not yet finished or saved + if (isCapturingCoordinates && !showSaveButton) { + ElevatedButton( + modifier = Modifier.fillMaxWidth(0.25f).size(width = 80.dp, height = 80.dp), + shape = RoundedCornerShape(0.dp), + colors = ButtonDefaults.buttonColors(Color.White), + onClick = { + if (coordinates.isNotEmpty()) { + showConfirmDialog.value = true // Show confirm dialog to finish capturing + isCapturingCoordinates = false // Stop capturing when "Finish" is clicked + } + } + ) { + Icon( + imageVector = Icons.Default.Done, + contentDescription = "Finish", + tint = Color.Black, + modifier = Modifier.padding(4.dp) + ) + } + } + + // "Save" button - visible after finishing the polygon and previewing it, but only when there are at least 3 points + if (showSaveButton && coordinates.size > 3) { + ElevatedButton( + modifier = Modifier.fillMaxWidth(0.25f).size(width = 80.dp, height = 80.dp), + shape = RoundedCornerShape(0.dp), + colors = ButtonDefaults.buttonColors(Color.White), + onClick = { + mapViewModel.addCoordinates(coordinates) + // Show the polygon on the map for review + val parcelableCoordinates = coordinates.map { ParcelablePair(it.first, it.second) } + navController.previousBackStackEntry?.savedStateHandle?.set("coordinates", parcelableCoordinates) + navController.previousBackStackEntry?.savedStateHandle?.set("accuracyArray", accuracyArray) + if(calculatedArea > 0.000000001) { + mapViewModel.showAreaDialog( + calculatedArea.toString(), + enteredAreaConverted.toString() + ) + } + else{ + // Show the dialog for invalid points + showInvalidPolygonDialog.value = true + } + }, + enabled = hasPointsOnMap + ) { + Icon( + painter = painterResource(id = R.drawable.save_polygon), + contentDescription = "Save Polygon", + tint = Color.Black, + modifier = Modifier.padding(4.dp) + ) + } + } + // Invalid Polygon Dialog + InvalidPolygonDialog( + showDialog = showInvalidPolygonDialog, + onDismiss = { showInvalidPolygonDialog.value = false } + ) + + // Logic to handle when a point is deleted + LaunchedEffect(coordinates.size) { + if (coordinates.size < 3 ) { + showSaveButton = false + if (coordinates.size > 1 ) { + isCapturingCoordinates = + true // Show "Finish" button again when points are deleted + } + } + } + + + + + + + ElevatedButton( + modifier = + Modifier + .fillMaxWidth(0.25f).size(width = 80.dp, height = 80.dp), + shape = RoundedCornerShape(0.dp), + colors = ButtonDefaults.buttonColors(Color.White), +// enabled = isCapturingCoordinates, // Enable only when capturing coordinates + onClick = { + if (!isLocationEnabled(context)) { + showLocationDialog.value = true + } else { + if (context.hasLocationPermission() && isCapturingCoordinates) { + fusedLocationClient + .getCurrentLocation( + locationRequest.priority, + object : CancellationToken() { + override fun onCanceledRequested(p0: OnTokenCanceledListener) = + CancellationTokenSource().token + + override fun isCancellationRequested() = false + }, + ).addOnSuccessListener { location: Location? -> + if (location == null) { + Toast + .makeText( + context, + context.getString(R.string.can_not_get_location), + Toast.LENGTH_LONG, + ).show() + } else { + if (location.latitude + .toString() + .split(".")[1] + .length < 6 || + location.longitude + .toString() + .split(".")[1] + .length < 6 + ) { + Toast + .makeText( + context, + context.getString(R.string.can_not_get_location), + Toast.LENGTH_LONG, + ).show() + + return@addOnSuccessListener + } + + // update map camera position + val coordinate = Pair(location.latitude, location.longitude) + accuracy = location.accuracy.toString() + + val accuracyFloat = location.accuracy // accuracy is a Float + + Log.d("Coordinates", "Coordinates : $coordinate") + Log.d("Accuracy", "Accuracy set to : $accuracyFloat") + + + + coordinates = coordinates + coordinate + accuracyArray = accuracyArray + accuracyFloat + + // Log the updated arrays + Log.d("Accuracy Array", "Accuracy Array is set to : $accuracyArray") + + viewModel.addMarker(coordinate) + // add camera position + viewModel.addCoordinate( + location.latitude, + location.longitude, + ) + hasPointsOnMap = coordinates.isNotEmpty() // Enable Drop/Reset buttons + } + } + } + } + }, + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = stringResource(id = R.string.add_point), + tint = Color.Black, + modifier = Modifier.padding(4.dp), + ) + } + ElevatedButton( + modifier = Modifier.fillMaxWidth(0.25f).size(width = 80.dp, height = 80.dp), + colors = ButtonDefaults.buttonColors(Color.White), +// enabled = hasPointsOnMap, // Enable only when there are points to drop + shape = RoundedCornerShape(0.dp), + onClick = { + coordinates = coordinates.dropLast(1) + viewModel.removeLastCoordinate() + }, + ) { + Icon( + painter = painterResource(R.drawable.drop), + contentDescription = stringResource(id = R.string.drop_point), + tint = Color.Black, + modifier = Modifier.padding(4.dp), + ) + } + ElevatedButton( + modifier = + Modifier + .fillMaxWidth(0.25f).size(width = 80.dp, height = 80.dp), + shape = RoundedCornerShape(0.dp), + colors = ButtonDefaults.buttonColors(Color.White), +// enabled = hasPointsOnMap, // Enable only when there are points to reset + onClick = { + showClearMapDialog.value = true + }, + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(id = R.string.reset), + tint = Color.Red, + modifier = Modifier.padding(4.dp), + ) + } + } + } + } + } +} +@Composable +fun InvalidPolygonDialog( + showDialog: MutableState, + onDismiss: () -> Unit +) { + if (showDialog.value) { + AlertDialog( + onDismissRequest = { showDialog.value = false }, + title = { Text(text = stringResource(id = R.string.invalid_polygon_title)) }, + text = { Text(text = stringResource(id = R.string.invalid_polygon_message)) }, + confirmButton = { + TextButton(onClick = { + onDismiss() + }) { + Text(text = stringResource(id = R.string.ok)) + } + }, + containerColor = MaterialTheme.colorScheme.background, + tonalElevation = 6.dp + ) + } +} + + diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/screens/collectionsites/AddSite.kt b/app/src/main/java/org/technoserve/farmcollector/ui/screens/collectionsites/AddSite.kt index 889fb2a..8e31792 100644 --- a/app/src/main/java/org/technoserve/farmcollector/ui/screens/collectionsites/AddSite.kt +++ b/app/src/main/java/org/technoserve/farmcollector/ui/screens/collectionsites/AddSite.kt @@ -12,10 +12,14 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -52,6 +56,7 @@ import androidx.navigation.NavController import org.joda.time.Instant import org.technoserve.farmcollector.R import org.technoserve.farmcollector.database.models.CollectionSite +import org.technoserve.farmcollector.database.models.Commodity import org.technoserve.farmcollector.ui.components.FarmListHeader import org.technoserve.farmcollector.ui.components.SiteForm import org.technoserve.farmcollector.utils.isSystemInDarkTheme @@ -77,7 +82,11 @@ fun AddSite(navController: NavController) { onBackClicked = { navController.popBackStack() }, showSearch = false, showRestore = false, - onRestoreClicked = {} + onRestoreClicked = {}, + isBackupEnabled = false, + showLastSync = false, + lastSyncTime="", + onBackupToggleClicked= {} ) Spacer(modifier = Modifier.height(16.dp)) SiteForm(navController) @@ -92,6 +101,7 @@ fun addSite( email: String, village: String, district: String, + commodity: Commodity ): CollectionSite { val site = CollectionSite( name, @@ -101,7 +111,8 @@ fun addSite( village, district, createdAt = Instant.now().millis, - updatedAt = Instant.now().millis + updatedAt = Instant.now().millis, + commodity = commodity ) farmViewModel.addSite(site) { isAdded -> if (isAdded) { diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/screens/collectionsites/CollectionSiteList.kt b/app/src/main/java/org/technoserve/farmcollector/ui/screens/collectionsites/CollectionSiteList.kt index 54572db..36f577a 100644 --- a/app/src/main/java/org/technoserve/farmcollector/ui/screens/collectionsites/CollectionSiteList.kt +++ b/app/src/main/java/org/technoserve/farmcollector/ui/screens/collectionsites/CollectionSiteList.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons @@ -27,11 +28,13 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableIntStateOf @@ -54,8 +57,12 @@ import androidx.navigation.NavController import androidx.paging.LoadState import androidx.paging.compose.collectAsLazyPagingItems import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.technoserve.farmcollector.R import org.technoserve.farmcollector.database.models.CollectionSite +import org.technoserve.farmcollector.database.models.Commodity +import org.technoserve.farmcollector.database.models.Farm +import org.technoserve.farmcollector.ui.components.BackupConfirmationDialog import org.technoserve.farmcollector.ui.components.CustomPaginationControls import org.technoserve.farmcollector.ui.components.FarmListHeader import org.technoserve.farmcollector.ui.components.RestoreDataAlert @@ -65,9 +72,9 @@ import org.technoserve.farmcollector.ui.components.SkeletonSiteCard import org.technoserve.farmcollector.viewmodels.FarmViewModel import org.technoserve.farmcollector.viewmodels.FarmViewModelFactory import org.technoserve.farmcollector.viewmodels.RestoreStatus -import org.technoserve.farmcollector.viewmodels.UndoDeleteSnackbar import org.technoserve.farmcollector.utils.DeviceIdUtil import org.technoserve.farmcollector.ui.composes.isValidPhoneNumber +import org.technoserve.farmcollector.utils.BackupPreferences import org.technoserve.farmcollector.utils.isSystemInDarkTheme @@ -112,7 +119,7 @@ fun CollectionSiteList(navController: NavController) { var showFinalMessage by remember { mutableStateOf(false) } val isDarkTheme = isSystemInDarkTheme() - val inputLabelColor = if (isDarkTheme) Color.LightGray else Color.DarkGray + val inputLabelColor = if (isDarkTheme) Color.White else Color.Black val inputTextColor = if (isDarkTheme) Color.White else Color.Black val inputBorder = if (isDarkTheme) Color.LightGray else Color.DarkGray @@ -137,6 +144,21 @@ fun CollectionSiteList(navController: NavController) { val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + + // Observe backup state + val isBackupEnabled by BackupPreferences.isBackupEnabled(context).collectAsState(initial = false) + + // Observe last sync time + val lastSyncTime by BackupPreferences.getLastBackupTime(context).collectAsState(initial = "Never") + + // Boolean state to control if Last Sync Time is shown + var showLastSync by remember { mutableStateOf(true) } // Default is true, can be toggled + + // State for Confirmation Dialog + var showDialog by remember { mutableStateOf(false) } + var pendingBackupState by remember { mutableStateOf(isBackupEnabled) } + LaunchedEffect(Unit) { @@ -159,20 +181,14 @@ fun CollectionSiteList(navController: NavController) { showSearch = true, showRestore = true, onRestoreClicked = { -// farmViewModel.restoreData( -// deviceId = deviceId, -// phoneNumber = "", -// email = "", -// farmViewModel = farmViewModel -// ) { success -> -// if (success) { -// finalMessage = context.getString(R.string.data_restored_successfully) -// } else { -// showFinalMessage = true -// showRestorePrompt = true -// } -// } showRestoreAlert = true + }, + isBackupEnabled = isBackupEnabled, // Pass backup state + showLastSync = showLastSync, // Boolean to toggle visibility + lastSyncTime = lastSyncTime, // Pass last sync timestamp + onBackupToggleClicked = { newState -> + pendingBackupState = newState // Store user's choice before confirmation + showDialog = true // Show confirmation dialog } ) }, @@ -219,23 +235,29 @@ fun CollectionSiteList(navController: NavController) { } // Restore Alert Dialog - // Show restore alert dialog RestoreDataAlert( showDialog = showRestoreAlert, onDismiss = { showRestoreAlert = false }, deviceId = deviceId, farmViewModel = farmViewModel ) -// -// // Undo Delete Snackbar -// UndoDeleteSnackbar( -// show = showUndoSnackbar, -// onDismiss = { showUndoSnackbar = false }, -// onUndo = { -// // Implement undo logic here -// showUndoSnackbar = false -// } -// ) + + // Show Confirmation Dialog when toggling backup + if (showDialog) { + BackupConfirmationDialog( + isEnablingBackup = pendingBackupState, + onConfirm = { + coroutineScope.launch { + BackupPreferences.setBackupEnabled(context, pendingBackupState) // Save choice + if (pendingBackupState) { + BackupPreferences.setLastBackupTime(context) // Update sync time + } + } + showDialog = false + }, + onCancel = { showDialog = false } + ) + } when { pagedData.loadState.refresh is LoadState.Loading -> { @@ -535,7 +557,6 @@ fun CollectionSiteList(navController: NavController) { } if (showDeleteDialog.value) { - // SiteDeleteAllDialogPresenter(showDeleteDialog, onProceedFn = { onDelete() }) selectedSite.value?.let { SiteDeleteAllDialogPresenter( @@ -545,7 +566,6 @@ fun CollectionSiteList(navController: NavController) { snackbarHostState = snackbarHostState, onProceedFn = { farmViewModel.deleteListSite(selectedIds) - // Show the undo snackbar }, showUndoSnackbar = showUndoSnackbar diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/screens/farms/AddFarm.kt b/app/src/main/java/org/technoserve/farmcollector/ui/screens/farms/AddFarm.kt index f35bddd..d3edc1b 100644 --- a/app/src/main/java/org/technoserve/farmcollector/ui/screens/farms/AddFarm.kt +++ b/app/src/main/java/org/technoserve/farmcollector/ui/screens/farms/AddFarm.kt @@ -5,13 +5,20 @@ import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.location.LocationManager +import android.net.Uri import android.provider.Settings +import android.util.Log import android.widget.Toast import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme @@ -19,6 +26,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -30,11 +41,11 @@ import com.google.android.gms.maps.model.LatLng import org.joda.time.Instant import org.technoserve.farmcollector.R import org.technoserve.farmcollector.database.models.Farm -import org.technoserve.farmcollector.database.models.ParcelablePair import org.technoserve.farmcollector.viewmodels.FarmViewModel import org.technoserve.farmcollector.ui.components.FarmForm import org.technoserve.farmcollector.ui.components.FarmListHeader +import org.technoserve.farmcollector.viewmodels.MapViewModel import java.math.BigDecimal import java.math.RoundingMode import java.util.UUID @@ -47,18 +58,29 @@ import java.util.UUID * @param siteId the id of the collection site to which the farm belongs * @param coordinatesData the initial coordinates of the farm's location */ + @Composable -fun AddFarm(navController: NavController, siteId: Long) { - var coordinatesData: List>? = null - var accuracyArrayData: List? = null - if (navController.currentBackStackEntry!!.savedStateHandle.contains("coordinates")) { - val parcelableCoordinates = navController.currentBackStackEntry!! - .savedStateHandle - .get>("coordinates") - coordinatesData = parcelableCoordinates?.map { Pair(it.first, it.second) } - accuracyArrayData = - navController.currentBackStackEntry!!.savedStateHandle.get>("accuracyArray") - } +fun AddFarm( + navController: NavController, + siteId: Long, + plotData: Farm?, + mapViewModel: MapViewModel +) { + + Log.d("Farm Data on add farm ", "Farm Data on add farm: $plotData") + // State to hold farm data + var farmData by remember { mutableStateOf(plotData) } + + // Extract coordinates and accuracy array from farmData + val coordinatesData = farmData?.coordinates as List>? + val accuracyArrayData = farmData?.accuracyArray + + // Log the coordinates and accuracy array + Log.d("Coordinates Data", "Coordinates Data: $coordinatesData") + Log.d("Accuracy Array Data", "Accuracy Array Data: $accuracyArrayData") + Log.d("Size", "Size: ${farmData?.size}") + + Column( modifier = Modifier .fillMaxSize() @@ -70,13 +92,18 @@ fun AddFarm(navController: NavController, siteId: Long) { onBackClicked = { navController.popBackStack() }, showSearch = false, showRestore = false, - onRestoreClicked = {} + onRestoreClicked = {}, + isBackupEnabled = false, + showLastSync = false, + lastSyncTime="", + onBackupToggleClicked= {} ) Spacer(modifier = Modifier.height(16.dp)) - FarmForm(navController, siteId, coordinatesData, accuracyArrayData) + FarmForm(navController, siteId, coordinatesData, accuracyArrayData,mapViewModel) } } + // Helper function to truncate a string representation of a number to a specific number of decimal places fun truncateToDecimalPlaces(value: String, decimalPlaces: Int): String { val dotIndex = value.indexOf('.') @@ -87,13 +114,6 @@ fun truncateToDecimalPlaces(value: String, decimalPlaces: Int): String { } } -// Function to read and format stored value -fun readStoredValue(sharedPref: SharedPreferences): String { - val storedValue = sharedPref.getString("plot_size", "") ?: "" - val formattedValue = truncateToDecimalPlaces(storedValue, 9) - return formattedValue -} - fun formatInput(input: String): String { return try { val number = BigDecimal(input) @@ -179,12 +199,12 @@ fun promptEnableLocation(context: Context) { context.startActivity(intent) } - @OptIn(ExperimentalPermissionsApi::class) @Composable fun LocationPermissionRequest( onLocationEnabled: () -> Unit, onPermissionsGranted: () -> Unit, + onPermissionsDenied: () -> Unit, showLocationDialogNew: MutableState, hasToShowDialog: Boolean ) { @@ -196,54 +216,87 @@ fun LocationPermissionRequest( ) ) + LaunchedEffect(Unit) { if (isLocationEnabled(context)) { - if (multiplePermissionsState.allPermissionsGranted) { - onPermissionsGranted() - } else { - multiplePermissionsState.launchMultiplePermissionRequest() + when { + multiplePermissionsState.allPermissionsGranted -> { + onPermissionsGranted() + } + multiplePermissionsState.shouldShowRationale -> { + showLocationDialogNew.value = true // Show rationale dialog + } + else -> { + multiplePermissionsState.launchMultiplePermissionRequest() + } } } else { onLocationEnabled() } } + if (!multiplePermissionsState.allPermissionsGranted && hasToShowDialog) { + AlertDialog( + onDismissRequest = { showLocationDialogNew.value = false }, + title = { Text(stringResource(id = R.string.enable_location)) }, + text = { Text(stringResource(id = R.string.enable_location_msg)) }, + confirmButton = { + Button(onClick = { + multiplePermissionsState.launchMultiplePermissionRequest() + showLocationDialogNew.value = false + }) { + Text(stringResource(id = R.string.yes)) + } + }, + dismissButton = { + Button(onClick = { + showLocationDialogNew.value = false + onPermissionsDenied() + Toast.makeText( + context, + context.getString(R.string.location_permission_denied_message), + Toast.LENGTH_SHORT + ).show() + }) { + Text(stringResource(id = R.string.no)) + } + }, + containerColor = MaterialTheme.colorScheme.background, + tonalElevation = 6.dp + ) + } - if ((!multiplePermissionsState.allPermissionsGranted) && hasToShowDialog) { - Column { - AlertDialog( - onDismissRequest = { showLocationDialogNew.value = false }, - title = { Text(stringResource(id = R.string.enable_location)) }, - text = { Text(stringResource(id = R.string.enable_location_msg)) }, - confirmButton = { - Button(onClick = { - // Perform action to enable location permissions - promptEnableLocation(context) - showLocationDialogNew.value = false - }) { - Text(stringResource(id = R.string.yes)) - } - }, - dismissButton = { - Button(onClick = { - // Show a toast message indicating that the permission was denied - Toast.makeText( - context, - R.string.location_permission_denied_message, - Toast.LENGTH_SHORT - ).show() - showLocationDialogNew.value = false - }) { - Text(stringResource(id = R.string.no)) + // Handle case when permission is permanently denied (user selected "Don't ask again") + if (!multiplePermissionsState.allPermissionsGranted && !multiplePermissionsState.shouldShowRationale) { + AlertDialog( + onDismissRequest = { showLocationDialogNew.value = false }, + title = { Text(stringResource(id = R.string.permission_required)) }, + text = { Text(stringResource(id = R.string.go_to_settings_msg)) }, + confirmButton = { + Button(onClick = { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", context.packageName, null) } - }, - containerColor = MaterialTheme.colorScheme.background, - tonalElevation = 6.dp - ) - } + context.startActivity(intent) + showLocationDialogNew.value = false + }) { + Text(stringResource(id = R.string.open_settings)) + } + }, + dismissButton = { + Button(onClick = { + showLocationDialogNew.value = false + }) { + Text(stringResource(id = R.string.cancel)) + } + }, + containerColor = MaterialTheme.colorScheme.background, + tonalElevation = 6.dp + ) } } + fun List>.toLatLngList(): List { return map { LatLng(it.first, it.second) } } diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/screens/farms/FarmList.kt b/app/src/main/java/org/technoserve/farmcollector/ui/screens/farms/FarmList.kt index 082128d..578e0e8 100644 --- a/app/src/main/java/org/technoserve/farmcollector/ui/screens/farms/FarmList.kt +++ b/app/src/main/java/org/technoserve/farmcollector/ui/screens/farms/FarmList.kt @@ -7,6 +7,9 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build +import android.util.Log +import android.webkit.JavascriptInterface +import android.webkit.WebView import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -23,12 +26,14 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FloatingActionButton @@ -45,6 +50,7 @@ import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateListOf @@ -64,12 +70,16 @@ import androidx.compose.ui.unit.dp import androidx.core.content.FileProvider import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.reflect.TypeToken import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.technoserve.farmcollector.R import org.technoserve.farmcollector.database.models.Farm import org.technoserve.farmcollector.database.models.ParcelableFarmData import org.technoserve.farmcollector.database.models.ParcelablePair +import org.technoserve.farmcollector.ui.components.BackupConfirmationDialog import org.technoserve.farmcollector.viewmodels.FarmViewModel import org.technoserve.farmcollector.viewmodels.FarmViewModelFactory @@ -84,11 +94,15 @@ import org.technoserve.farmcollector.ui.components.FormatSelectionDialog import org.technoserve.farmcollector.ui.components.ImportFileDialog import org.technoserve.farmcollector.ui.components.RestoreDataAlert import org.technoserve.farmcollector.ui.composes.isValidPhoneNumber +import org.technoserve.farmcollector.utils.BackupPreferences import org.technoserve.farmcollector.utils.createFile import org.technoserve.farmcollector.utils.createFileForSharing import org.technoserve.farmcollector.utils.isSystemInDarkTheme +import org.technoserve.farmcollector.viewmodels.MapViewModel +import org.technoserve.farmcollector.viewmodels.MapViewModelFactory import java.io.File +import java.net.URLEncoder import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -131,6 +145,7 @@ fun FarmList( viewModel( factory = FarmViewModelFactory(context.applicationContext as Application), ) + val mapViewModel: MapViewModel = viewModel(factory = MapViewModelFactory())// Use Hilt's hiltViewModel() val selectedIds = remember { mutableStateListOf() } val selectedFarm = remember { mutableStateOf(null) } val showDeleteDialog = remember { mutableStateOf(false) } @@ -142,6 +157,9 @@ fun FarmList( var exportFormat by remember { mutableStateOf("") } var showImportDialog by remember { mutableStateOf(false) } var showConfirmationDialog by remember { mutableStateOf(false) } + var includeFarmerNames by remember { mutableStateOf(true) } // Default: Include names + var showIncludeFarmerNamesDialog by remember { mutableStateOf(false) } + val (searchQuery, setSearchQuery) = remember { mutableStateOf("") } val tabs = @@ -164,11 +182,30 @@ fun FarmList( var showRestoreAlert by remember { mutableStateOf(false) } + // val coroutineScope = rememberCoroutineScope() + + // Observe backup state + val isBackupEnabled by BackupPreferences.isBackupEnabled(context).collectAsState(initial = false) + + // Observe last sync time + val lastSyncTime by BackupPreferences.getLastBackupTime(context).collectAsState(initial = "Never") + + // Boolean state to control if Last Sync Time is shown + var showLastSync by remember { mutableStateOf(true) } // Default is true, can be toggled + + // State for Confirmation Dialog + var showDialog by remember { mutableStateOf(false) } + var pendingBackupState by remember { mutableStateOf(isBackupEnabled) } + + + val isDarkTheme = isSystemInDarkTheme() val inputLabelColor = if (isDarkTheme) Color.LightGray else Color.DarkGray val inputTextColor = if (isDarkTheme) Color.White else Color.Black val inputBorder = if (isDarkTheme) Color.LightGray else Color.DarkGray + // Class-level variable to store the latest plot data + var latestPlotData: String? = null LaunchedEffect(Unit) { deviceId = DeviceIdUtil.getDeviceId(context) @@ -179,13 +216,15 @@ fun FarmList( delay(2000) isLoading.value = false } + // State to store the final list before export + var finalList by remember { mutableStateOf(listItems) } val createDocumentLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { result.data?.data?.let { uri -> if (createFile( - context, uri,listItems, + context, uri,finalList, exportFormat, siteID , cwsListItems @@ -196,23 +235,26 @@ fun FarmList( } } } +// Function to initiate file creation with the selected data +fun initiateFileCreation(selectedList: List) { + finalList = selectedList // Store the filtered list before export + + val mimeType = if (exportFormat == "CSV") "text/csv" else "application/geo+json" + val intent = + Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = mimeType + val getSiteById = cwsListItems.find { it.siteId == siteID } + val siteName = getSiteById?.name ?: "SiteName" + val timestamp = + SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + val filename = + if (exportFormat == "CSV") "farms_${siteName}_$timestamp.csv" else "farms_${siteName}_$timestamp.geojson" + putExtra(Intent.EXTRA_TITLE, filename) + } + createDocumentLauncher.launch(intent) +} - fun initiateFileCreation() { - val mimeType = if (exportFormat == "CSV") "text/csv" else "application/geo+json" - val intent = - Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = mimeType - val getSiteById = cwsListItems.find { it.siteId == siteID } - val siteName = getSiteById?.name ?: "SiteName" - val timestamp = - SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) - val filename = - if (exportFormat == "CSV") "farms_${siteName}_$timestamp.csv" else "farms_${siteName}_$timestamp.geojson" - putExtra(Intent.EXTRA_TITLE, filename) - } - createDocumentLauncher.launch(intent) - } // Function to share the file fun shareFile(file: File) { @@ -252,41 +294,104 @@ fun FarmList( onFormatSelected = { format -> exportFormat = format showFormatDialog = false - when (action) { - Action.Export -> exportFile() - Action.Share -> shareFileAction() - else -> {} - } + showIncludeFarmerNamesDialog = true // Show the next dialog }, ) } + + if (showIncludeFarmerNamesDialog) { + AlertDialog( + onDismissRequest = { showIncludeFarmerNamesDialog = false }, + title = { Text(stringResource(id = R.string.include_farmer_names_title)) }, + text = { Text(stringResource(id = R.string.include_farmer_names_text)) }, + confirmButton = { + Button( + onClick = { + includeFarmerNames = true + showIncludeFarmerNamesDialog = false + showConfirmationDialog = true + } + ) { Text(stringResource(id = R.string.yes)) } + }, + dismissButton = { + Button( + onClick = { + includeFarmerNames = false + showIncludeFarmerNamesDialog = false + showConfirmationDialog = true + } + ) { Text(stringResource(id = R.string.no)) } + } + ) + } + +// if (showConfirmationDialog) { +// CustomizedConfirmationDialog( +// listItems, +// action = action!!, // Ensure action is not null +// onConfirm = { +// when (action) { +// Action.Export -> initiateFileCreation() +// Action.Share -> { +// // file = createFileForSharing() +// val file = createFileForSharing( +// context, +// listItems, +// exportFormat, +// siteID, +// cwsListItems +// ) +// if (file != null) { +// shareFile(file) +// } +// } +// +// else -> {} +// } +// }, +// onDismiss = { showConfirmationDialog = false }, +// ) +// } + + fun List.filterWithoutFarmerNames(): List { + return this.map { dataItem -> + dataItem.copy(farmerName = "") // Exclude farmer names if the user chooses + } + } + + if (showConfirmationDialog) { CustomizedConfirmationDialog( listItems, action = action!!, // Ensure action is not null onConfirm = { + val selectedList = if (!includeFarmerNames) { + listItems.filterWithoutFarmerNames() // Remove farmer names + } else { + listItems // Keep original list + } + when (action) { - Action.Export -> initiateFileCreation() + Action.Export -> initiateFileCreation(selectedList) Action.Share -> { - // file = createFileForSharing() val file = createFileForSharing( context, - listItems, - exportFormat, - siteID, - cwsListItems + selectedList, // Use user-selected data + exportFormat, + siteID, + cwsListItems ) if (file != null) { shareFile(file) } } - else -> {} } }, onDismiss = { showConfirmationDialog = false }, ) } + if (showImportDialog) { ImportFileDialog( siteId, @@ -311,6 +416,21 @@ fun FarmList( } } + + + @JavascriptInterface + fun receivePlotData(plotDataJson: String) { + Log.d("JavaScriptInterface", "Received Plot Data: $plotDataJson") + // Store the received plot data + latestPlotData = plotDataJson + } + + // Function to clear plot data + fun clearPlotData() { + latestPlotData = null + } + + // Function to show data or no data message @Composable fun showDataContent() { @@ -369,11 +489,11 @@ fun FarmList( .weight(1f) .fillMaxWidth(), ) { page -> - // Determine which category to display based on the current tab index val filteredListItems = when (page) { - 1 -> filteredListItemsNeedUpdate // Farms that need update - else -> filteredListItemsNoUpdate // Farms that do not need update + 1 -> filteredListItemsNeedUpdate // Farms that need updates + else -> filteredListItemsNoUpdate // Farms that do not need updates } + if (filteredListItems.isNotEmpty() || searchQuery.isNotEmpty()) { LazyColumn( modifier = Modifier @@ -381,24 +501,28 @@ fun FarmList( .padding(bottom = 90.dp) ) { val pageSize = 5 - val startIndex = maxOf(0, (currentPage - 1) * pageSize) // Ensure startIndex is non-negative - val endIndex = minOf(filteredListItems.size, startIndex + pageSize) // Ensure endIndex is within bounds - - // Safeguard: Ensure indices are within bounds - if (filteredListItems.isNotEmpty()) { - // Show the items for the current page - items(endIndex - startIndex) { index -> - val item = filteredListItems[startIndex + index] + val startIndex = maxOf(0, (currentPage - 1) * pageSize) + val endIndex = minOf(filteredListItems.size, startIndex + pageSize) + + if (startIndex < endIndex) { // Ensure we only proceed if indices are valid + items( + count = filteredListItems.subList(startIndex, endIndex).size, + key = { item -> + val item = filteredListItems[startIndex + item] + item.id + } + ) { item -> + val item = filteredListItems[startIndex + item] FarmCard( farm = item, onCardClick = { navController.currentBackStackEntry?.arguments?.apply { putParcelableArrayList( "coordinates", - item.coordinates?.map { - it.first?.let { it1 -> - it.second?.let { it2 -> - ParcelablePair(it1, it2) + item.coordinates?.mapNotNull { coord -> + coord.first?.let { lat -> + coord.second?.let { lon -> + ParcelablePair(lat, lon) } } }?.let { ArrayList(it) } @@ -408,7 +532,8 @@ fun FarmList( ParcelableFarmData(item, "view") ) } - navController.navigate(route = "setPolygon") + mapViewModel.submitForm() + navController.navigate(route = "setPolygon/${siteId}") }, onDeleteClick = { selectedIds.add(item.id) @@ -416,15 +541,15 @@ fun FarmList( showDeleteDialog.value = true } ) - // Spacer(modifier = Modifier.height(16.dp)) } + // Pagination Controls item { CustomPaginationControls( currentPage = currentPage, totalPages = when (currentCategoryIndex) { - 0 -> totalPagesNoUpdate // Pages for farms that do not need updates - 1 -> totalPagesNeedUpdate // Pages for farms needing updates + 0 -> totalPagesNoUpdate + 1 -> totalPagesNeedUpdate else -> 0 }, onPageChange = { newPage -> @@ -432,9 +557,8 @@ fun FarmList( } ) } - } - - else { + } else { + // Show "No Results" message if the list is empty item { Text( text = stringResource(R.string.no_results_found), @@ -446,7 +570,6 @@ fun FarmList( ) } } - } } else { Spacer(modifier = Modifier.height(8.dp)) @@ -460,6 +583,7 @@ fun FarmList( ) } } + } } else { // Display a message or image indicating no data available @@ -495,21 +619,14 @@ fun FarmList( showShare = listItems.isNotEmpty(), showSearch = listItems.isNotEmpty(), onRestoreClicked = { -// farmViewModel.restoreData( -// deviceId = deviceId, -// phoneNumber = "", -// email = "", -// farmViewModel = farmViewModel -// ) { success -> -// if (success) { -// finalMessage = context.getString(R.string.data_restored_successfully) -// showFinalMessage = true -// } else { -// showFinalMessage = true -// showRestorePrompt = true -// } -// } showRestoreAlert = true + }, + isBackupEnabled = isBackupEnabled, // Pass backup state + showLastSync = showLastSync, // Boolean to toggle visibility + lastSyncTime = lastSyncTime, // Pass last sync timestamp + onBackupToggleClicked = { newState -> + pendingBackupState = newState // Store user's choice before confirmation + showDialog = true // Show confirmation dialog } ) @@ -524,7 +641,16 @@ fun FarmList( val sharedPref = context.getSharedPreferences("FarmCollector", Context.MODE_PRIVATE) sharedPref.edit().remove("plot_size").remove("selectedUnit").apply() - navController.navigate("addFarm/${siteId}") + if (latestPlotData != null) { + val encodedPlotData = Uri.encode(latestPlotData) + // navController.navigate("addFarm/${siteId}/${encodedPlotData}") + navController.navigate("addFarm/$siteId/${URLEncoder.encode(latestPlotData, "UTF-8")}") + + // Clear the plot data after navigation if needed + clearPlotData() + } else { + navController.navigate("addFarm/${siteId}") + } }, containerColor = MaterialTheme.colorScheme.surface, contentColor = MaterialTheme.colorScheme.onSurface, @@ -553,6 +679,24 @@ fun FarmList( farmViewModel = farmViewModel ) + + // Show Confirmation Dialog when toggling backup + if (showDialog) { + BackupConfirmationDialog( + isEnablingBackup = pendingBackupState, + onConfirm = { + coroutineScope.launch { + BackupPreferences.setBackupEnabled(context, pendingBackupState) // Save choice + if (pendingBackupState) { + BackupPreferences.setLastBackupTime(context) // Update sync time + } + } + showDialog = false + }, + onCancel = { showDialog = false } + ) + } + showDataContent() } } diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/screens/farms/UpdateFarmForm.kt b/app/src/main/java/org/technoserve/farmcollector/ui/screens/farms/UpdateFarmForm.kt index 1d8a259..91a4967 100644 --- a/app/src/main/java/org/technoserve/farmcollector/ui/screens/farms/UpdateFarmForm.kt +++ b/app/src/main/java/org/technoserve/farmcollector/ui/screens/farms/UpdateFarmForm.kt @@ -133,6 +133,9 @@ fun UpdateFarmForm( var selectedUnit by remember { mutableStateOf(items[0]) } val scientificNotationPattern = Pattern.compile("([+-]?\\d*\\.?\\d+)[eE][+-]?\\d+") + // Add a state to track permission denial attempts + var permissionDenialCount by remember { mutableStateOf(0) } + LaunchedEffect(Unit) { if (!isLocationEnabled(context)) { showLocationDialog.value = true @@ -281,7 +284,7 @@ fun UpdateFarmForm( onCaptureNew = { coordinates = listOf() - navController.navigate("SetPolygon") + navController.navigate("setPolygon/${item.siteId}") with(sharedPref.edit()) { putBoolean(KEY_HAS_NEW_POLYGON, true) @@ -321,7 +324,7 @@ fun UpdateFarmForm( onClick = { showDialog.value = false - navController.navigate("setPolygon") + navController.navigate("setPolygon/${item.siteId}") }, ) { Text(text = stringResource(id = R.string.set_polygon)) @@ -346,8 +349,21 @@ fun UpdateFarmForm( showLocationDialog.value = true }, onPermissionsGranted = { + // Reset denial count on successful permission grant + permissionDenialCount = 0 showPermissionRequest.value = false }, + onPermissionsDenied = { + // Optional: Additional handling for denied permissions + if (permissionDenialCount > 1) { + // You can add a toast or snackbar explaining why permissions are needed + Toast.makeText( + context, + R.string.location_permission_required_for_this_feature, + Toast.LENGTH_LONG + ).show() + } + }, showLocationDialogNew = showLocationDialogNew, hasToShowDialog = showLocationDialogNew.value, ) @@ -371,7 +387,11 @@ fun UpdateFarmForm( onBackClicked = { navController.popBackStack() }, showSearch = false, showRestore = false, - onRestoreClicked = {} + onRestoreClicked = {}, + isBackupEnabled = false, + showLastSync = false, + lastSyncTime="", + onBackupToggleClicked= {} ) Spacer(modifier = Modifier.height(16.dp)) TextField( diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/screens/home/Home.kt b/app/src/main/java/org/technoserve/farmcollector/ui/screens/home/Home.kt index 5ec6028..6d6237d 100644 --- a/app/src/main/java/org/technoserve/farmcollector/ui/screens/home/Home.kt +++ b/app/src/main/java/org/technoserve/farmcollector/ui/screens/home/Home.kt @@ -13,17 +13,28 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -34,12 +45,16 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController +import com.google.accompanist.systemuicontroller.rememberSystemUiController import org.technoserve.farmcollector.R import org.technoserve.farmcollector.database.models.Language +import org.technoserve.farmcollector.ui.components.BackupPromptDialog +import org.technoserve.farmcollector.ui.screens.settings.LanguageSelector import org.technoserve.farmcollector.ui.theme.Teal import org.technoserve.farmcollector.ui.theme.Turquoise import org.technoserve.farmcollector.ui.theme.White -import org.technoserve.farmcollector.ui.screens.settings.LanguageSelector +import org.technoserve.farmcollector.utils.BackupPreferences +import org.technoserve.farmcollector.utils.isSystemInDarkTheme import org.technoserve.farmcollector.viewmodels.LanguageViewModel import java.util.Locale @@ -62,31 +77,66 @@ fun Home( val currentLanguage by languageViewModel.currentLanguage.collectAsState() val context = LocalContext.current + var showBackupDialog by remember { mutableStateOf(false) } // State to control dialog visibility + // Observe if the user has already made a backup decision + val isBackupDecisionMade by BackupPreferences.isBackupDecisionMade(context) + .collectAsState(initial = false) LaunchedEffect(currentLanguage) { languageViewModel.updateLocale(context = context, Locale(currentLanguage.code)) } + // This will make the status bar visible with a light theme + val systemUiController = rememberSystemUiController() + val useDarkIcons = !isSystemInDarkTheme() + + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + // Adjust sizes based on screen width + val iconSize = if (screenWidth < 450.dp) 24.dp else 24.dp + + DisposableEffect(systemUiController, useDarkIcons) { + systemUiController.setSystemBarsColor( + color = Color.Transparent, + darkIcons = useDarkIcons, + isNavigationBarContrastEnforced = false + ) + systemUiController.isSystemBarsVisible = true + onDispose {} + } + Column( Modifier - .padding(top = 20.dp) - .fillMaxSize(), + .fillMaxSize() + .statusBarsPadding(), verticalArrangement = Arrangement.spacedBy(16.dp, alignment = Alignment.Top), horizontalAlignment = Alignment.CenterHorizontally ) { // Add language selector here and align on the right Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + IconButton( + onClick = { + navController.navigate("userGuideScreen") + } + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = stringResource(id = R.string.settings), + modifier = Modifier.size(iconSize) + ) + } LanguageSelector(viewModel = languageViewModel, languages = languages) } Column( Modifier .fillMaxWidth() - // .fillMaxHeight(0.4f) .weight(0.4f) .padding(top = 30.dp), horizontalAlignment = Alignment.CenterHorizontally @@ -104,20 +154,21 @@ fun Home( Image( painter = painterResource(id = R.drawable.app_icon), contentDescription = null, -// modifier = Modifier -// .width(80.dp) -// .height(80.dp) modifier = Modifier - .width(when (LocalConfiguration.current.screenWidthDp) { - in 0..320 -> 60.dp // Small screens - in 321..600 -> 80.dp // Medium screens - else -> 100.dp // Large screens - }) - .height(when (LocalConfiguration.current.screenWidthDp) { - in 0..320 -> 60.dp - in 321..600 -> 80.dp - else -> 100.dp - }) + .width( + when (LocalConfiguration.current.screenWidthDp) { + in 0..320 -> 60.dp // Small screens + in 321..600 -> 80.dp // Medium screens + else -> 100.dp // Large screens + } + ) + .height( + when (LocalConfiguration.current.screenWidthDp) { + in 0..320 -> 60.dp + in 321..600 -> 80.dp + else -> 100.dp + } + ) .padding(bottom = 10.dp) ) @@ -138,7 +189,6 @@ fun Home( } } - Box( modifier = Modifier .padding(30.dp) @@ -147,40 +197,47 @@ fun Home( shape = RoundedCornerShape(10.dp) ) .clickable { - navController.navigate("siteList") + if (!isBackupDecisionMade) { + showBackupDialog = true // Show the backup prompt if it's the first time + } else { + navController.navigate("siteList") // Directly navigate if the user has already chosen + } } - .padding(16.dp) + .padding(16.dp), + contentAlignment = Alignment.Center ) { Text( text = stringResource(id = R.string.get_started), style = TextStyle( fontWeight = FontWeight.Bold, color = White - ), - modifier = Modifier.align(Alignment.Center) + ) + ) + + // Show Backup Dialog if it's the first time + BackupPromptDialog( + context = context, + navController = navController, + showDialog = showBackupDialog, + onDismiss = { showBackupDialog = false } ) } Spacer(modifier = Modifier.fillMaxHeight(0.2f)) Box( -// modifier = Modifier -// .fillMaxWidth(0.8f) -// .padding(20.dp) modifier = Modifier .fillMaxWidth(0.8f) - .padding(when (LocalConfiguration.current.screenWidthDp) { - in 0..320 -> 12.dp - in 321..600 -> 16.dp - else -> 20.dp - }) + .padding( + when (LocalConfiguration.current.screenWidthDp) { + in 0..320 -> 12.dp + in 321..600 -> 16.dp + else -> 20.dp + } + ) ) { Text( text = stringResource(id = R.string.app_intro), -// style = TextStyle( -// fontWeight = FontWeight.Bold, -// color = MaterialTheme.colorScheme.onBackground -// ), style = TextStyle( fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onBackground, @@ -210,20 +267,21 @@ fun Home( Image( painter = painterResource(id = R.drawable.tns_labs), contentDescription = null, -// modifier = Modifier -// .width(130.dp) -// .height(20.dp) modifier = Modifier - .width(when (LocalConfiguration.current.screenWidthDp) { - in 0..320 -> 100.dp - in 321..600 -> 120.dp - else -> 130.dp - }) - .height(when (LocalConfiguration.current.screenWidthDp) { - in 0..320 -> 15.dp - in 321..600 -> 20.dp - else -> 20.dp - }) + .width( + when (LocalConfiguration.current.screenWidthDp) { + in 0..320 -> 100.dp + in 321..600 -> 120.dp + else -> 130.dp + } + ) + .height( + when (LocalConfiguration.current.screenWidthDp) { + in 0..320 -> 15.dp + in 321..600 -> 20.dp + else -> 20.dp + } + ) ) } diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/screens/map/SetPolygon.kt b/app/src/main/java/org/technoserve/farmcollector/ui/screens/map/SetPolygon.kt index ddbe553..0382d8f 100644 --- a/app/src/main/java/org/technoserve/farmcollector/ui/screens/map/SetPolygon.kt +++ b/app/src/main/java/org/technoserve/farmcollector/ui/screens/map/SetPolygon.kt @@ -380,102 +380,6 @@ fun SetPolygon( horizontalArrangement = if (viewSelectFarm) Arrangement.Center else Arrangement.Start, ) { // Hiding some buttons depending on page usage. Viewing verse setting farm polygon -// if (viewSelectFarm) { -// Row { -// if (farmInfo != null) { -// Column( -// modifier = -// Modifier -// .background(MaterialTheme.colorScheme.background) -// .padding(5.dp), -// ) { -// Text( -// text = stringResource(id = R.string.farm_info), -// style = -// MaterialTheme.typography.bodySmall.copy( -// fontSize = 18.sp, -// fontWeight = FontWeight.Bold, -// ), -// modifier = Modifier.padding(5.dp), -// ) -// Column( -// content = { }, -// modifier = -// Modifier -// .width(200.dp) -// .background(Color.Black) -// .height(2.dp), -// ) -// Text( -// text = "${stringResource(id = R.string.farm_name)}: ${farmInfo.farmerName}", -// style = MaterialTheme.typography.bodyMedium.copy(color = textColor), -// modifier = Modifier.padding(top = 5.dp), -// ) -// Text( -// text = "${stringResource(id = R.string.member_id)}: ${farmInfo.memberId.ifEmpty { "N/A" }}", -// style = MaterialTheme.typography.bodyMedium.copy(color = textColor), -// ) -// Text( -// text = "${stringResource(id = R.string.village)}: ${farmInfo.village}", -// style = MaterialTheme.typography.bodyMedium.copy(color = textColor), -// ) -// Text( -// text = "${stringResource(id = R.string.district)}: ${farmInfo.district}", -// style = MaterialTheme.typography.bodyMedium.copy(color = textColor), -// ) -// Text( -// text = "${stringResource(id = R.string.latitude)}: ${farmInfo.latitude}", -// style = MaterialTheme.typography.bodyMedium.copy(color = textColor) -// ) -// Text( -// text = "${stringResource(id = R.string.longitude)}: ${farmInfo.longitude}", -// style = MaterialTheme.typography.bodyMedium.copy(color = textColor) -// ) -// Text( -// text = "${stringResource(id = R.string.size)}: ${ -// truncateToDecimalPlaces( -// formatInput(farmInfo.size.toString()), -// 9 -// ) -// } ${ -// stringResource( -// id = R.string.ha, -// ) -// }", -// style = MaterialTheme.typography.bodyMedium.copy(color = textColor), -// ) -// } -// } -// } -// Row { -// Button( -// shape = RoundedCornerShape(10.dp), -// modifier = -// Modifier -// .width(120.dp) -// .fillMaxWidth(0.23f), -// onClick = { -// viewModel.clearCoordinates() -// navController.navigateUp() -// }, -// ) { -// Text(text = stringResource(id = R.string.close)) -// } -// Button( -// shape = RoundedCornerShape(10.dp), -// modifier = -// Modifier -// .width(150.dp) -// .fillMaxWidth(0.23f) -// .padding(start = 10.dp), -// onClick = { -// navController.navigate("updateFarm/${farmInfo?.id}") -// }, -// ) { -// Text(text = stringResource(id = R.string.update)) -// } -// } -// } if (viewSelectFarm) { Column( modifier = Modifier diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/screens/map/WebViewPage.kt b/app/src/main/java/org/technoserve/farmcollector/ui/screens/map/WebViewPage.kt new file mode 100644 index 0000000..7780747 --- /dev/null +++ b/app/src/main/java/org/technoserve/farmcollector/ui/screens/map/WebViewPage.kt @@ -0,0 +1,809 @@ +package org.technoserve.farmcollector.ui.screens.map + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.SharedPreferences +import android.graphics.Bitmap +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.net.Uri +import android.os.Build +import android.util.Log +import android.view.ViewGroup +import android.webkit.GeolocationPermissions +import android.webkit.WebChromeClient +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.Toast +import androidx.activity.compose.BackHandler +import androidx.annotation.RequiresApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeContent +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.google.gson.Gson +import org.technoserve.farmcollector.R +import org.technoserve.farmcollector.database.helpers.map.JavaScriptInterface +import org.technoserve.farmcollector.database.helpers.map.LocationHelper +import org.technoserve.farmcollector.database.models.ParcelableFarmData +import org.technoserve.farmcollector.ui.components.InvalidPolygonDialog +import org.technoserve.farmcollector.ui.composes.AreaDialog +import org.technoserve.farmcollector.ui.composes.ConfirmDialog +import org.technoserve.farmcollector.ui.screens.farms.formatInput +import org.technoserve.farmcollector.ui.screens.farms.truncateToDecimalPlaces +import org.technoserve.farmcollector.utils.convertSize +import org.technoserve.farmcollector.utils.isSystemInDarkTheme +import org.technoserve.farmcollector.viewmodels.LanguageViewModel +import org.technoserve.farmcollector.viewmodels.MapViewModel +import java.io.File +import java.net.URLConnection +import java.net.URLEncoder + +/** + * Implementation of caching mechanism for performance + * + */ + + + + +@RequiresApi(Build.VERSION_CODES.M) +@SuppressLint("SetJavaScriptEnabled") +@Composable +fun WebViewPage( + url: String, + onWebViewCreated: (WebView) -> Unit, + navController: NavController, + viewModel: MapViewModel +) { + val context = LocalContext.current + var backEnabled by remember { mutableStateOf(false) } + val webViewState = remember { mutableStateOf(null) } // Store WebView instanc + // var webView: WebView? = null + val sharedPref = context.getSharedPreferences("FarmCollector", Context.MODE_PRIVATE) + + // Observe dialog state + val showDialog by viewModel.showDialog.collectAsState() + + + +// val showClearMapDialog by viewModel.showClearMapDialog.collectAsState() + + val plotData by viewModel.plotData.collectAsState() + Log.d("Data in web view page", "Data in web view page: $plotData") + + // Define cache directory + val cachePath = File(context.cacheDir, "webview-cache") + if (!cachePath.exists()) { + cachePath.mkdirs() + } + + val gson = Gson() + val farmDataJson = gson.toJson(plotData) + if (farmDataJson == null) { + Log.d("JavaScriptInterface", "Farm Data Json is null") + navController.navigate("addFarm/${plotData?.siteId}") + } + val encodedFarmDataJson = + URLEncoder.encode(farmDataJson, "UTF-8") // Encode J SON to avoid special character issues' + + val calculatedArea: Double = plotData?.size?.toDouble() ?: 0.0 + + val enteredArea = sharedPref.getString("plot_size", "0.0")?.toDoubleOrNull() ?: 0.0 + val selectedUnit = sharedPref.getString("selectedUnit", "Ha") ?: "Ha" + val enteredAreaConverted = convertSize(enteredArea, selectedUnit) + + + // Display AreaDialog when triggered + if (showDialog) { + AreaDialog( + showDialog = true, + onDismiss = { viewModel.dismissDialog() }, + onConfirm = { chosenArea -> + val chosenSize = + when (chosenArea) { + CALCULATED_AREA_OPTION -> calculatedArea.toString() + ENTERED_AREA_OPTION -> enteredAreaConverted.toString() + else -> throw IllegalArgumentException("Unknown area option: $chosenArea") + } + val truncatedSize = truncateToDecimalPlaces(formatInput(chosenSize), 9) + sharedPref.edit().putString("plot_size", truncatedSize).apply() + if (sharedPref.contains("selectedUnit")) { + sharedPref.edit().remove("selectedUnit").apply() + } + navController.navigate("addFarm/${plotData?.siteId}?plotDataJson=$encodedFarmDataJson") + viewModel.dismissDialog() // Close the dialog + //navController.navigateUp() + }, + calculatedArea = calculatedArea, + enteredArea = enteredAreaConverted + ) + } + + println("showclearmap: ${viewModel.showClearMapDialog.value}") + + + AndroidView( + factory = { + WebView(it).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + + // Enable JavaScript and required settings + settings.apply { + javaScriptEnabled = true + domStorageEnabled = true + setGeolocationEnabled(true) + builtInZoomControls = true + displayZoomControls = false + allowFileAccess = true + allowContentAccess = true + loadWithOverviewMode = true + useWideViewPort = true + allowFileAccessFromFileURLs = true + + // Cache settings + cacheMode = + WebSettings.LOAD_CACHE_ELSE_NETWORK // Use cached resources when available + databaseEnabled = true + } + + webChromeClient = object : WebChromeClient() { + override fun onGeolocationPermissionsShowPrompt( + origin: String, + callback: GeolocationPermissions.Callback + ) { + callback.invoke(origin, true, false) + } + } + + // Attach JavaScript interface with form data lambdas + addJavascriptInterface( + JavaScriptInterface( + context = context, + navController = navController, + viewModel + ), + "Android" + ) + + webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + } + + override fun shouldInterceptRequest( + view: WebView?, + request: WebResourceRequest? + ): WebResourceResponse? { + val url = request?.url.toString() + val cachedResponse = getCachedResponse(context, url) + if (cachedResponse != null) { + return cachedResponse + } + return super.shouldInterceptRequest(view, request) + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + view?.evaluateJavascript("if(map){map.invalidateSize(true);}", null) + } + } + + // Load URL with fallback to cached content + if (isNetworkAvailable(context)) { + loadUrl(url) + } else { + val cachedFile = File(cachePath, "${Uri.parse(url).host}.mht") + if (cachedFile.exists()) { + loadUrl("file://${cachedFile.absolutePath}") + } else { + loadUrl(url) + } + } + + // Save WebView instance in the remembered state + webViewState.value = this + //webView = this + onWebViewCreated(this) // Pass the WebView instance + } + }, + modifier = Modifier + .fillMaxSize() + .semantics { contentDescription = "Web View" }, + update = { webView -> + webView.evaluateJavascript("if(map){map.invalidateSize(true);}", null) + } + ) + + // Confirm Clear Map Dialog + if (viewModel.showClearMapDialog.value) { + ConfirmDialog( + title = stringResource(id = R.string.set_polygon), + message = stringResource(id = R.string.clear_map), + showDialog = viewModel.showClearMapDialog, + onProceedFn = { + // viewModel.clearMap(webView) + viewModel.clearMap(webViewState.value) // Use stored WebView instance + }, + onCancelFn = { + viewModel.dismissClearDialog() + } + ) + } + + // Confirm dialog for setting the polygon + if (viewModel.showConfirmDialog.value) { + ConfirmDialog( + title = stringResource(id = R.string.set_polygon), + message = stringResource(id = R.string.confirm_set_polygon), + showDialog = viewModel.showConfirmDialog, + onProceedFn = { + //viewModel.finalizePolygon(mapViewModel) + // viewModel.confirmFinishPolygon(webView) + viewModel.confirmFinishPolygon(webViewState.value) // Use stored WebView instance + }, + onCancelFn = { + viewModel.dismissConfirmPolygonDialog() + } + ) + } + + + // Alert dialog for insufficient coordinates + if (viewModel.showAlertDialog.value) { + AlertDialog( + onDismissRequest = { + viewModel.showAlertDialog.value = false + }, + title = { + Text(text = stringResource(id = R.string.insufficient_coordinates_title)) + }, + text = { + Text(text = stringResource(id = R.string.insufficient_coordinates_message)) + }, + confirmButton = { + Button( + onClick = { + viewModel.showAlertDialog.value = false + }, + ) { + Text(text = stringResource(id = R.string.ok)) + viewModel.showConfirmDialog.value = false + viewModel.clearMap(webViewState.value) + } + }, + containerColor = MaterialTheme.colorScheme.background, + tonalElevation = 6.dp + ) + } + + if(viewModel.showInvalidPolygonDialog.value) { + // Invalid Polygon Dialog + InvalidPolygonDialog( + showDialog = viewModel.showInvalidPolygonDialog, + onDismiss = { viewModel.showInvalidPolygonDialog.value = false } + ) + } + + +// BackHandler(enabled = backEnabled) { +// webView?.goBack() +// } + + BackHandler(enabled = backEnabled) { + webViewState.value?.goBack() + } +} + +// Utility function to check network availability +@RequiresApi(Build.VERSION_CODES.M) +private fun isNetworkAvailable(context: Context): Boolean { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val network = connectivityManager.activeNetwork + val capabilities = connectivityManager.getNetworkCapabilities(network) + return capabilities != null && + (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) +} + +// Function to get cached response +private fun getCachedResponse(context: Context, url: String): WebResourceResponse? { + val cache = context.cacheDir + val cachedFile = File(cache, Uri.parse(url).lastPathSegment ?: return null) + + if (cachedFile.exists()) { + try { + val mimeType = URLConnection.guessContentTypeFromName(url) + return WebResourceResponse( + mimeType ?: "text/plain", + "UTF-8", + cachedFile.inputStream() + ) + } catch (e: Exception) { + e.printStackTrace() + } + } + return null +} + + +@SuppressLint("SetJavaScriptEnabled") +@Composable +fun WebViewWithVisualization( + dataJson: String, + farmId: Long, + navController: NavController, + mapViewModel: MapViewModel, + currentLanguage: String +) { + val context = LocalContext.current + var backEnabled by remember { mutableStateOf(false) } + var webView: WebView? = null + + println("Data JSON: $dataJson") + println("Farm ID: $farmId") + println("current Language: $currentLanguage") + + + AndroidView( + factory = { ctx -> + WebView(ctx).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + + // Enable JavaScript and other settings + settings.apply { + javaScriptEnabled = true + domStorageEnabled = true + setGeolocationEnabled(true) + builtInZoomControls = true + displayZoomControls = false + allowFileAccess = true + allowContentAccess = true + loadWithOverviewMode = true + useWideViewPort = true + cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK + databaseEnabled = true + } + + webChromeClient = object : WebChromeClient() { + override fun onGeolocationPermissionsShowPrompt( + origin: String, + callback: GeolocationPermissions.Callback + ) { + callback.invoke(origin, true, false) + } + } + + addJavascriptInterface( + JavaScriptInterface( + context, + navController = navController, + mapViewModel + ), "Android" + ) + + webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + backEnabled = view.canGoBack() + } + + } + + // Load the URL + loadUrl("file:///android_asset/index.html?plotId=${farmId}&lang=${currentLanguage}") + webView = this + } + }, + modifier = Modifier + .fillMaxSize(), +// .padding(WindowInsets.systemBars.asPaddingValues()), // Respect safe areas,, + update = { webView -> + webView.evaluateJavascript( + """ + (function() { + if (typeof Android !== 'undefined') { + const plotJson = Android.getSelectedPlot(${farmId}); + if (typeof visualizeData === 'function') { + visualizeData(plotJson); + } else { + console.error('visualizeData is not defined'); + } + } else { + console.error('Android interface is not available'); + } + })(); + """.trimIndent(), + null + ) + } + ) + + // Handle back navigation + BackHandler(enabled = backEnabled) { + webView?.goBack() + } +} + + +@SuppressLint("DefaultLocale") +@RequiresApi(Build.VERSION_CODES.M) +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun PlotVisualizationApp( + navController: NavController, + viewModel: MapViewModel, siteId: Long, languageViewModel: LanguageViewModel +) { + val farmData = + navController.previousBackStackEntry?.arguments?.getParcelable("farmData") + + val plotData by viewModel.plotData.collectAsState() + + val farmInfo = farmData?.farm ?: plotData + + var viewSelectFarm by remember { mutableStateOf(false) } + val mapViewModel: MapViewModel = viewModel() + var accuracy by remember { mutableStateOf("") } + val context = LocalContext.current as Activity + val locationHelper = LocationHelper(context) + + + // Observe language from ViewModel (Assuming it's an object) + val currentLanguageObject by languageViewModel.currentLanguage.collectAsState() + + // Extract only the `code` field (e.g., "es") + val languageCode = currentLanguageObject.code // Extract only the code part + + // Append language as a query parameter + val url = "file:///android_asset/leaflet_map.html?siteId=$siteId&lang=$languageCode" + + println("URL Language Code: $languageCode") // This should print "es" + + + + LaunchedEffect(Unit) { + mapViewModel.clearCoordinates() + + // Get the accuracyArrayData from savedStateHandle + val accuracyArrayData = + navController.currentBackStackEntry?.savedStateHandle?.get>("accuracyArray") + + // If the accuracyArrayData exists, clear it + accuracyArrayData?.let { + navController.currentBackStackEntry?.savedStateHandle?.set( + "accuracyArray", + emptyList() + ) + } + } + + // First, get initial location if needed + if (farmInfo == null) { + locationHelper.getCurrentLocation { location -> + location?.let { + accuracy = it.accuracy.toString() + if (viewModel.state.value.clusterItems.isEmpty()) { + viewModel.addCoordinate(it.latitude, it.longitude) + } + } ?: run { + Toast.makeText( + context, + context.getString(R.string.can_not_get_location), + Toast.LENGTH_LONG + ).show() + } + } + } + +// Then start continuous location updates + locationHelper.requestLocationUpdates(onLocationUpdate = { location -> + location?.let { + accuracy = it.accuracy.toString() + } ?: run { + Toast.makeText( + context, + context.getString(R.string.location_update_failed), + Toast.LENGTH_SHORT + ).show() + } + } + ) + + // Display coordinates of a farm on map + if ( farmInfo.id != 0L && !viewSelectFarm) { + viewModel.clearCoordinates() + if (farmInfo.coordinates?.isNotEmpty() == true) { + viewModel.addCoordinates(farmInfo.coordinates!!) + } else if (farmInfo.latitude.isNotEmpty() && farmInfo.longitude.isNotEmpty()) { + viewModel.addMarker(Pair(farmInfo.latitude.toDouble(), farmInfo.longitude.toDouble())) + } + viewSelectFarm = true + } + + // This will make the status bar visible with a light theme + val systemUiController = rememberSystemUiController() + val useDarkIcons = !isSystemInDarkTheme() + + DisposableEffect(systemUiController, useDarkIcons) { + systemUiController.setSystemBarsColor( + color = Color.Transparent, + darkIcons = useDarkIcons, + isNavigationBarContrastEnforced = false + ) + systemUiController.isSystemBarsVisible = true + onDispose {} + } + + Column( + modifier = Modifier.fillMaxSize().statusBarsPadding(), + verticalArrangement = Arrangement.Center, + ) { + // Top Section: Map Visualization + Column( + modifier = + Modifier + .fillMaxWidth() + .fillMaxHeight( + when { + viewSelectFarm -> 0.65f + accuracy.isNotEmpty() -> 0.99f + else -> 0.93f + } + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Log.d("INFO Visualization", "${farmInfo.coordinates}") + // Leaflet map + val farmId = farmInfo.id + val farmJson = Gson().toJson(farmInfo) + if (farmId != 0L) { + WebViewWithVisualization( + dataJson = farmJson, + farmId = farmId, + navController = navController, + mapViewModel = viewModel, + currentLanguage = languageCode + ) + } else { + if(siteId != 0L) { + WebViewPage( +// url = "file:///android_asset/leaflet_map.html?siteId=${siteId}", + url = "file:///android_asset/leaflet_map.html?siteId=$siteId&lang=$languageCode", + onWebViewCreated = { webView -> + webView.evaluateJavascript( + "if (typeof visualizeData === 'function') { visualizeData([]); } else { console.error('visualizeData is not defined'); }", + null + ) + }, + navController = navController, + viewModel + ) + } + } + + } + + + // Bottom Section: Farm Details + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxWidth() + .fillMaxHeight() + ) { + FlowRow( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(), + horizontalArrangement = if (viewSelectFarm) Arrangement.Center else Arrangement.Start + ) { + if (viewSelectFarm) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(horizontal = 4.dp) + .verticalScroll(rememberScrollState()) + ) { + farmInfo?.let { + // Farm Information Card + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.background + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + // Farm Info Title + Text( + text = stringResource(id = R.string.farm_info), + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.Bold + ), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + + // Divider + Divider( + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .background( + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f) + ) + ) + + // Farm Details Grid + Column( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp) + ) { + ResponsiveFarmDetailRow( + label = stringResource(id = R.string.farm_name), + value = farmInfo.farmerName + ) + ResponsiveFarmDetailRow( + label = stringResource(id = R.string.member_id), + value = farmInfo.memberId.ifEmpty { "N/A" } + ) + ResponsiveFarmDetailRow( + label = stringResource(id = R.string.district), + value = farmInfo.district + ) + ResponsiveFarmDetailRow( + label = stringResource(id = R.string.village), + value = farmInfo.village + ) + ResponsiveFarmDetailRow( + label = stringResource(id = R.string.longitude), + value = farmInfo.longitude.toDoubleOrNull() + ?.let { String.format("%.6f", it) } ?: "N/A" + ) + ResponsiveFarmDetailRow( + label = stringResource(id = R.string.latitude), + value = farmInfo.latitude.toDoubleOrNull() + ?.let { String.format("%.6f", it) } ?: "N/A" + ) + ResponsiveFarmDetailRow( + label = stringResource(id = R.string.size), + value = "${ + truncateToDecimalPlaces( + formatInput(farmInfo.size.toString()), + 4 + ) + } ${stringResource(id = R.string.ha)}" + ) + + } + } + } + + + // Action Buttons + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Close Button + Button( + onClick = { + viewModel.clearCoordinates() + navController.navigateUp() + }, + modifier = Modifier + .weight(1f) + .heightIn(min = 24.dp), + shape = RoundedCornerShape(10.dp) + ) { + Text( + text = stringResource(id = R.string.close), + style = MaterialTheme.typography.labelLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + // Update Button + Button( + onClick = { + navController.navigate("updateFarm/${farmInfo.id}") + }, + modifier = Modifier + .weight(1f) + .heightIn(min = 24.dp), + shape = RoundedCornerShape(10.dp) + ) { + Text( + text = stringResource(id = R.string.update), + style = MaterialTheme.typography.labelLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + } + } + } + } + + + } +} \ No newline at end of file diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/screens/map/cacheMapInBackground.kt b/app/src/main/java/org/technoserve/farmcollector/ui/screens/map/cacheMapInBackground.kt new file mode 100644 index 0000000..66cc269 --- /dev/null +++ b/app/src/main/java/org/technoserve/farmcollector/ui/screens/map/cacheMapInBackground.kt @@ -0,0 +1,97 @@ +package org.technoserve.farmcollector.ui.screens.map + +import android.Manifest +import android.content.Context +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.location.Location +import android.location.LocationManager +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.Toast +import androidx.annotation.RequiresApi +import androidx.core.app.ActivityCompat +import org.technoserve.farmcollector.R + + +@RequiresApi(Build.VERSION_CODES.M) +fun isConnected(context: Context): Boolean { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val network = connectivityManager.activeNetwork + val capabilities = connectivityManager.getNetworkCapabilities(network) + return capabilities != null && + (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) +} + +fun getUserLocation(context: Context): Location? { + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + return null // Permissions are not granted + } + + return locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) + ?: locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER) +} + +@RequiresApi(Build.VERSION_CODES.M) +fun cacheMapInBackground(context: Context, mapUrl: String) { + val sharedPreferences: SharedPreferences = + context.getSharedPreferences("AppPreferences", Context.MODE_PRIVATE) + val isMapCached = sharedPreferences.getBoolean("MapCached", false) + + if (!isMapCached) { + if (isConnected(context)) { + val location = getUserLocation(context) + if (location != null) { + val latitude = location.latitude + val longitude = location.longitude + + // Modify the map URL to center it on the user's location + val centeredMapUrl = "$mapUrl?center=$latitude,$longitude&zoom=14" + + // Create a WebView for caching + val webView = WebView(context).apply { + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.cacheMode = WebSettings.LOAD_DEFAULT + settings.allowFileAccess = true // Enable access to cached files + + webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + + // Mark map caching as complete + sharedPreferences.edit().putBoolean("MapCached", true).apply() + Toast.makeText( + context, + context.getString(R.string.map_tiles_cached), + Toast.LENGTH_LONG + ).show() + println("Map tiles cached successfully.") + // Destroy the WebView to free resources + destroy() + } + } + + // Load the centered map URL in the WebView + loadUrl(centeredMapUrl) + } + } else { + Toast.makeText(context, context.getString(R.string.unable_to_get_location), Toast.LENGTH_LONG).show() + } + } else { + Toast.makeText(context, context.getString(R.string.no_internet), Toast.LENGTH_LONG).show() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/screens/privacy/PrivacyPolicyScreen.kt b/app/src/main/java/org/technoserve/farmcollector/ui/screens/privacy/PrivacyPolicyScreen.kt new file mode 100644 index 0000000..9e53359 --- /dev/null +++ b/app/src/main/java/org/technoserve/farmcollector/ui/screens/privacy/PrivacyPolicyScreen.kt @@ -0,0 +1,58 @@ +package org.technoserve.farmcollector.ui.screens.privacy + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import org.technoserve.farmcollector.database.helpers.PreferencesManager + + +@Composable +fun PrivacyPolicyScreen( + url: String, + onAgree: () -> Unit // Callback for when the user agrees to the terms +) { + + val context = LocalContext.current + val preferencesManager = remember { PreferencesManager(context) } + + var isAtBottom by remember { mutableStateOf(false) } + var isAgreeEnabled by remember { mutableStateOf(false) } + + Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { + // WebView for Privacy Policy + Box(modifier = Modifier.weight(1f)) { + PrivacyPolicyWebView(url = url, onScrollAtBottom = { + isAtBottom = true + isAgreeEnabled = true + }) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // "Agree" button, enabled when user scrolls to the bottom + Button( + onClick = { + //preferencesManager.hasAgreedToTerms = true + onAgree() + }, + enabled = isAgreeEnabled, + modifier = Modifier.fillMaxWidth() + ) { + Text("Agree") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/screens/privacy/PrivacyPolicyWebView.kt b/app/src/main/java/org/technoserve/farmcollector/ui/screens/privacy/PrivacyPolicyWebView.kt new file mode 100644 index 0000000..efec632 --- /dev/null +++ b/app/src/main/java/org/technoserve/farmcollector/ui/screens/privacy/PrivacyPolicyWebView.kt @@ -0,0 +1,36 @@ +package org.technoserve.farmcollector.ui.screens.privacy + +import android.os.Build +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView + + +@Composable +fun PrivacyPolicyWebView(url: String, onScrollAtBottom: () -> Unit) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + WebView(context).apply { + webViewClient = object : WebViewClient() { + @RequiresApi(Build.VERSION_CODES.M) + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + // Add a scroll listener to detect when the user has reached the bottom + view?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> + if (!view.canScrollVertically(1)) { + // User has scrolled to the bottom of the WebView + onScrollAtBottom() + } + } + } + } + loadUrl(url) + } + } + ) +} diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/screens/settings/BottomBar.kt b/app/src/main/java/org/technoserve/farmcollector/ui/screens/settings/BottomBar.kt index a7aa091..84313ec 100644 --- a/app/src/main/java/org/technoserve/farmcollector/ui/screens/settings/BottomBar.kt +++ b/app/src/main/java/org/technoserve/farmcollector/ui/screens/settings/BottomBar.kt @@ -1,6 +1,5 @@ package org.technoserve.farmcollector.ui.screens.settings -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/screens/settings/Settings.kt b/app/src/main/java/org/technoserve/farmcollector/ui/screens/settings/Settings.kt index d46980c..7f2775f 100644 --- a/app/src/main/java/org/technoserve/farmcollector/ui/screens/settings/Settings.kt +++ b/app/src/main/java/org/technoserve/farmcollector/ui/screens/settings/Settings.kt @@ -7,9 +7,13 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -51,7 +55,7 @@ fun SettingsScreen( Column( modifier = Modifier - .fillMaxSize(), + .fillMaxSize() ) { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/app/src/main/java/org/technoserve/farmcollector/ui/theme/Theme.kt b/app/src/main/java/org/technoserve/farmcollector/ui/theme/Theme.kt index 071ec54..205b6f4 100644 --- a/app/src/main/java/org/technoserve/farmcollector/ui/theme/Theme.kt +++ b/app/src/main/java/org/technoserve/farmcollector/ui/theme/Theme.kt @@ -25,7 +25,7 @@ private val DarkColorScheme = darkColorScheme( error = Red, onPrimary = White, onSecondary = White, - onBackground = IceBlue, + onBackground = White, onSurface = White, onError = White ) @@ -34,7 +34,7 @@ private val LightColorScheme = lightColorScheme( primary = Teal, secondary = Yellow, tertiary = Green, - background = IceBlue, + background = White, surface = Teal, error = Red, onPrimary = Black, diff --git a/app/src/main/java/org/technoserve/farmcollector/utils/BackupPreferences.kt b/app/src/main/java/org/technoserve/farmcollector/utils/BackupPreferences.kt new file mode 100644 index 0000000..98e39b0 --- /dev/null +++ b/app/src/main/java/org/technoserve/farmcollector/utils/BackupPreferences.kt @@ -0,0 +1,72 @@ +package org.technoserve.farmcollector.utils + +import android.content.Context +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.longPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +// Define the DataStore instance for the Context +val Context.dataStore by preferencesDataStore(name = "backup_preferences") + +object BackupPreferences { + private val BACKUP_ENABLED_KEY = booleanPreferencesKey("backup_enabled") + private val BACKUP_DECISION_MADE_KEY = booleanPreferencesKey("backup_decision_made") + private val LAST_SYNC_TIMESTAMP_KEY = longPreferencesKey("last_sync_timestamp") + + // Check if the user has enabled backup + fun isBackupEnabled(context: Context): Flow { + return context.dataStore.data.map { preferences -> + preferences[BACKUP_ENABLED_KEY] ?: false // Default is disabled + } + } + + // Check if the user has made a decision + fun isBackupDecisionMade(context: Context): Flow { + return context.dataStore.data.map { preferences -> + preferences[BACKUP_DECISION_MADE_KEY] ?: false + } + } + + // Save user decision + suspend fun saveBackupChoice(context: Context, isEnabled: Boolean) { + context.dataStore.edit { preferences -> + preferences[BACKUP_ENABLED_KEY] = isEnabled + preferences[BACKUP_DECISION_MADE_KEY] = true + } + } + + // Read the last sync timestamp + fun getLastBackupTime(context: Context): Flow { + return context.dataStore.data.map { preferences -> + val timestamp = preferences[LAST_SYNC_TIMESTAMP_KEY] ?: 0L + if (timestamp > 0) { + SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date(timestamp)) + } else { + "Never backed up" + } + } + } + + // Save backup toggle state + suspend fun setBackupEnabled(context: Context, isEnabled: Boolean) { + context.dataStore.edit { preferences -> + preferences[BACKUP_ENABLED_KEY] = isEnabled + } + } + + // Save the last sync timestamp + suspend fun setLastBackupTime(context: Context) { + context.dataStore.edit { preferences -> + preferences[LAST_SYNC_TIMESTAMP_KEY] = System.currentTimeMillis() + } + } + + + +} diff --git a/app/src/main/java/org/technoserve/farmcollector/utils/FileCreator.kt b/app/src/main/java/org/technoserve/farmcollector/utils/FileCreator.kt index a6f5b4d..733c22b 100644 --- a/app/src/main/java/org/technoserve/farmcollector/utils/FileCreator.kt +++ b/app/src/main/java/org/technoserve/farmcollector/utils/FileCreator.kt @@ -44,7 +44,6 @@ fun createFile( exportFormat: String, siteID : Long, cwsListItems: List - ): Boolean { val getSiteById = cwsListItems.find { it.siteId == siteID } @@ -53,32 +52,57 @@ fun createFile( BufferedWriter(OutputStreamWriter(outputStream)).use { writer -> if (exportFormat == "CSV") { writer.write( - "remote_id,farmer_name,member_id,collection_site,agent_name,farm_village,farm_district,farm_size,latitude,longitude,polygon,accuracyArray,created_at,updated_at\n", + "remote_id,commodity,farmer_name,member_id,collection_site,agent_name,farm_village,farm_district,farm_size,latitude,longitude,polygon,accuracyArray,created_at,updated_at\n", ) listItems.forEach { farm -> val regex = "\\(([^,]+), ([^)]+)\\)".toRegex() val matches = regex.findAll(farm.coordinates.toString()) +// val reversedCoordinates = +// matches +// .map { match -> +// val (lat, lon) = match.destructured +// "[$lon, $lat]" +// }.toList() +// .let { coordinates -> +// if (coordinates.isNotEmpty()) { +// // Always include brackets, even for a single point +// coordinates.joinToString( +// ", ", +// prefix = "[", +// postfix = "]" +// ) +// } else { +// "" +// } +// } val reversedCoordinates = matches .map { match -> val (lat, lon) = match.destructured "[$lon, $lat]" - }.toList() + } + .toList() .let { coordinates -> - if (coordinates.isNotEmpty()) { - // Always include brackets, even for a single point - coordinates.joinToString( - ", ", - prefix = "[", - postfix = "]" - ) + val closedCoordinates = if (coordinates.isNotEmpty()) { + if (coordinates.first() != coordinates.last()) { + coordinates + coordinates.first() // add first again to close the polygon + } else { + coordinates + } + } else { + emptyList() + } + + if (closedCoordinates.isNotEmpty()) { + closedCoordinates.joinToString(", ", prefix = "[", postfix = "]") } else { "" } } + val line = - "${farm.remoteId},\"${ + "${farm.remoteId},\"${getSiteById?.commodity ?: "Unknown Commodity"}\",\"${ farm.farmerName.split(" ").joinToString(" ") }\",${farm.memberId},${getSiteById?.name},\"${getSiteById?.agentName}\",\"${farm.village}\",\"${farm.district}\",${farm.size},${farm.latitude},${farm.longitude},\"${reversedCoordinates}\",\"${farm.accuracyArray}\",${ Date(farm.createdAt) @@ -92,12 +116,31 @@ fun createFile( listItems.forEachIndexed { index, farm -> val regex = "\\(([^,]+), ([^)]+)\\)".toRegex() val matches = regex.findAll(farm.coordinates.toString()) - val geoJsonCoordinates = - matches - .map { match -> - val (lat, lon) = match.destructured - "[$lon, $lat]" - }.joinToString(", ", prefix = "[", postfix = "]") +// val geoJsonCoordinates = +// matches +// .map { match -> +// val (lat, lon) = match.destructured +// "[$lon, $lat]" +// }.joinToString(", ", prefix = "[", postfix = "]") + val geoJsonCoordinates = matches + .map { match -> + val (lat, lon) = match.destructured + "[$lon, $lat]" + } + .toList() + .let { coordinates -> + val closedCoordinates = if ( + coordinates.isNotEmpty() && + coordinates.first() != coordinates.last() + ) { + coordinates + coordinates.first() // Close the polygon + } else { + coordinates + } + + closedCoordinates.joinToString(", ", prefix = "[", postfix = "]") + } + // Ensure latitude and longitude are not null val latitude = farm.latitude.toDoubleOrNull()?.takeIf { it != 0.0 } ?: 0.0 @@ -110,6 +153,7 @@ fun createFile( "type": "Feature", "properties": { "remote_id": "${farm.remoteId}", + "commodity": "${getSiteById?.commodity ?: "Unknown Commodity"}", "farmer_name": "${ farm.farmerName.split(" ").joinToString(" ") }", @@ -128,7 +172,7 @@ fun createFile( }, "geometry": { "type": "${if ((farm.coordinates?.size ?: 0) > 1) "Polygon" else "Point"}", - "coordinates": ${if ((farm.coordinates?.size ?: 0) > 1) "[$geoJsonCoordinates]" else "[$latitude, $longitude]"} + "coordinates": ${if ((farm.coordinates?.size ?: 0) > 1) "[$geoJsonCoordinates]" else "[$longitude, $latitude]"} } } """.trimIndent() @@ -176,28 +220,53 @@ fun createFileForSharing( file.bufferedWriter().use { writer -> if (exportFormat == "CSV") { writer.write( - "remote_id,farmer_name,member_id,collection_site,agent_name,farm_village,farm_district,farm_size,latitude,longitude,polygon,accuracyArray,created_at,updated_at\n", + "remote_id,commodity,farmer_name,member_id,collection_site,agent_name,farm_village,farm_district,farm_size,latitude,longitude,polygon,accuracyArray,created_at,updated_at\n", ) listItems.forEach { farm -> val regex = "\\(([^,]+), ([^)]+)\\)".toRegex() val matches = regex.findAll(farm.coordinates.toString()) +// val reversedCoordinates = +// matches +// .map { match -> +// val (lat, lon) = match.destructured +// "[$lon, $lat]" +// }.toList() +// .let { coordinates -> +// if (coordinates.isNotEmpty()) { +// // Always include brackets, even for a single point +// coordinates.joinToString(", ", prefix = "[", postfix = "]") +// } else { +// "" +// } +// } val reversedCoordinates = matches .map { match -> val (lat, lon) = match.destructured "[$lon, $lat]" - }.toList() + } + .toList() .let { coordinates -> - if (coordinates.isNotEmpty()) { - // Always include brackets, even for a single point - coordinates.joinToString(", ", prefix = "[", postfix = "]") + val closedCoordinates = if (coordinates.isNotEmpty()) { + if (coordinates.first() != coordinates.last()) { + coordinates + coordinates.first() // add first again to close the polygon + } else { + coordinates + } + } else { + emptyList() + } + + if (closedCoordinates.isNotEmpty()) { + closedCoordinates.joinToString(", ", prefix = "[", postfix = "]") } else { "" } } + val line = - "${farm.remoteId},\"${ + "${farm.remoteId},\"${getSiteById?.commodity ?: "Unknown Commodity"}\",\"${ farm.farmerName.split(" ").joinToString(" ") }\",${farm.memberId},\"${getSiteById?.name}\",\"${getSiteById?.agentName}\",\"${farm.village}\",\"${farm.district}\",${farm.size},${farm.latitude},${farm.longitude},\"${reversedCoordinates}\",\"${farm.accuracyArray}\",${ Date( @@ -213,12 +282,31 @@ fun createFileForSharing( listItems.forEachIndexed { index, farm -> val regex = "\\(([^,]+), ([^)]+)\\)".toRegex() val matches = regex.findAll(farm.coordinates.toString()) - val geoJsonCoordinates = - matches - .map { match -> - val (lat, lon) = match.destructured - "[$lon, $lat]" - }.joinToString(", ", prefix = "[", postfix = "]") +// val geoJsonCoordinates = +// matches +// .map { match -> +// val (lat, lon) = match.destructured +// "[$lon, $lat]" +// }.joinToString(", ", prefix = "[", postfix = "]") + val geoJsonCoordinates = matches + .map { match -> + val (lat, lon) = match.destructured + "[$lon, $lat]" + } + .toList() + .let { coordinates -> + val closedCoordinates = if ( + coordinates.isNotEmpty() && + coordinates.first() != coordinates.last() + ) { + coordinates + coordinates.first() // Close the polygon + } else { + coordinates + } + + closedCoordinates.joinToString(", ", prefix = "[", postfix = "]") + } + val latitude = farm.latitude.toDoubleOrNull()?.takeIf { it != 0.0 } ?: 0.0 val longitude = @@ -230,6 +318,7 @@ fun createFileForSharing( "type": "Feature", "properties": { "remote_id": "${farm.remoteId}", + "commodity": "${getSiteById?.commodity ?: "Unknown Commodity"}", "farmer_name":"${ farm.farmerName.split(" ").joinToString(" ") }", @@ -247,7 +336,7 @@ fun createFileForSharing( }, "geometry": { "type": "${if ((farm.coordinates?.size ?: 0) > 1) "Polygon" else "Point"}", - "coordinates": ${if ((farm.coordinates?.size ?: 0) > 1) "[$geoJsonCoordinates]" else "[$latitude, $longitude]"} + "coordinates": ${if ((farm.coordinates?.size ?: 0) > 1) "[$geoJsonCoordinates]" else "[$longitude, $latitude]"} } } """.trimIndent() diff --git a/app/src/main/java/org/technoserve/farmcollector/viewmodels/AppUpdateViewModel.kt b/app/src/main/java/org/technoserve/farmcollector/viewmodels/AppUpdateViewModel.kt index 931d4c6..f0338fc 100644 --- a/app/src/main/java/org/technoserve/farmcollector/viewmodels/AppUpdateViewModel.kt +++ b/app/src/main/java/org/technoserve/farmcollector/viewmodels/AppUpdateViewModel.kt @@ -74,54 +74,3 @@ fun UpdateAlert( ) } } - - - -//@Composable -//fun ExitConfirmationDialog( -// showDialog: Boolean, -// onDismiss: () -> Unit, -// onConfirm: () -> Unit -//) { -// if (showDialog) { -// AlertDialog( -// onDismissRequest = onDismiss, -// title = { Text("Exit App") }, -// text = { Text("Are you sure you want to exit the app?") }, -// confirmButton = { -// Button(onClick = onConfirm) { -// Text("Yes") -// } -// }, -// dismissButton = { -// TextButton(onClick = onDismiss) { -// Text("No") -// } -// } -// ) -// } -//} - -@Composable -fun UndoDeleteSnackbar( - show: Boolean, - onDismiss: () -> Unit, - onUndo: () -> Unit -) { - if (show) { - Snackbar( - action = { - TextButton(onClick = onUndo) { - Text("UNDO") - } - }, - dismissAction = { - IconButton(onClick = onDismiss) { - Icon(Icons.Default.Close, contentDescription = "Dismiss") - } - } - ) { - Text("Item deleted") - } - } -} diff --git a/app/src/main/java/org/technoserve/farmcollector/viewmodels/FarmViewModel.kt b/app/src/main/java/org/technoserve/farmcollector/viewmodels/FarmViewModel.kt index 87e827a..99d36ea 100644 --- a/app/src/main/java/org/technoserve/farmcollector/viewmodels/FarmViewModel.kt +++ b/app/src/main/java/org/technoserve/farmcollector/viewmodels/FarmViewModel.kt @@ -22,6 +22,7 @@ import androidx.paging.PagingConfig import androidx.paging.cachedIn import com.google.gson.Gson import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.joda.time.Instant @@ -835,6 +836,11 @@ class FarmViewModel( withContext(Dispatchers.IO) { repository.getAllSites() } + // GET SITE BY ID + fun getSiteByIdNew(id: Long): Flow { + return repository.getSiteByIdNew(id) + } + private val TAG = "FarmConversion" @@ -957,6 +963,8 @@ class FarmViewModel( return farm } + + /** * Restore data from the server */ diff --git a/app/src/main/java/org/technoserve/farmcollector/viewmodels/MapViewModel.kt b/app/src/main/java/org/technoserve/farmcollector/viewmodels/MapViewModel.kt index ac18b60..bceff1c 100644 --- a/app/src/main/java/org/technoserve/farmcollector/viewmodels/MapViewModel.kt +++ b/app/src/main/java/org/technoserve/farmcollector/viewmodels/MapViewModel.kt @@ -2,12 +2,18 @@ package org.technoserve.farmcollector.viewmodels import android.content.Context +import android.content.SharedPreferences import android.graphics.Color +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.webkit.WebView import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.LatLngBounds @@ -20,6 +26,7 @@ import kotlinx.coroutines.flow.asStateFlow import org.technoserve.farmcollector.database.models.map.MapState import org.technoserve.farmcollector.database.models.map.ZoneClusterItem import org.technoserve.farmcollector.database.helpers.map.ZoneClusterManager +import org.technoserve.farmcollector.database.models.Farm import org.technoserve.farmcollector.utils.GeoCalculator import org.technoserve.farmcollector.utils.map.calculateCameraViewPoints import org.technoserve.farmcollector.utils.map.getCenterOfPolygon @@ -57,6 +64,167 @@ class MapViewModel @Inject constructor() : ViewModel() { private val _showDialog = MutableStateFlow(false) val showDialog: StateFlow = _showDialog.asStateFlow() + private val _showClearMapDialog: MutableState = mutableStateOf(false) + val showClearMapDialog: MutableState = _showClearMapDialog + + + fun showClearDialog() { + _showClearMapDialog.value = true + } + + fun dismissClearDialog() { + _showClearMapDialog.value = false + } + + fun clearMap(webView: WebView ?) { + + if (webView == null) { + Log.e("MapViewModel", "WebView is null! Cannot execute JavaScript.") + return + } + + Log.d("MapViewModel", "Clearing map...") + + Handler(Looper.getMainLooper()).post { + Handler(Looper.getMainLooper()).post { + webView.evaluateJavascript( + """ + (function() { + if (typeof window.clearMapConfirmed === "function") { + window.clearMapConfirmed(); + console.log("clearMapConfirmed() executed successfully"); + return "success"; + } else { + console.error("clearMapConfirmed() is not defined"); + return "error: function not found"; + } + })(); + """ + ) { result -> + Log.d("MapViewModel", "JavaScript execution result: $result") + } + Log.d("MapViewModel", "Clear map request sent to WebView") + } + } + _showClearMapDialog.value = false + } + + + private val _showConfirmDialog = mutableStateOf(false) + val showConfirmDialog: MutableState = _showConfirmDialog + + + // Show Dialogs + fun showConfirmPolygonDialog() { + _showConfirmDialog.value = true + } + + fun dismissConfirmPolygonDialog() { + _showConfirmDialog.value = false + } + + + private val _showInvalidPolygonDialog = mutableStateOf(false) + val showInvalidPolygonDialog: MutableState = _showInvalidPolygonDialog + + fun showInvalidPolygonDialog() { + _showInvalidPolygonDialog.value = true + } + + fun dismissInvalidPolygonDialog() { + _showInvalidPolygonDialog.value = false + } + + fun confirmFinishPolygon(webView: WebView?) { + Log.d("MapViewModel", "confirmFinishPolygon() called") + + if (webView == null) { + Log.e("MapViewModel", "WebView is null! Cannot execute JavaScript.") + return + } + + Handler(Looper.getMainLooper()).post { + Log.d("MapViewModel", "Sending JavaScript execution request...") + + webView.evaluateJavascript( + """ + (function() { + if (typeof window.stopCaptureConfirmed === "function") { + console.log("Calling stopCaptureConfirmed()"); + window.stopCaptureConfirmed(); + return "success"; + } else { + console.error("stopCaptureConfirmed() is not defined"); + return "error: function not found"; + } + })(); + """ + ) { result -> + Log.d("MapViewModel", "JavaScript execution result: $result") + } + + Log.d("MapViewModel", "JavaScript execution request sent to WebView.") + } + + dismissConfirmPolygonDialog() + Log.d("MapViewModel", "confirmFinishPolygon() finished execution") + } + + + private val _showAlertDialog = mutableStateOf(false) + val showAlertDialog: MutableState = _showAlertDialog + + fun showAlertDialog() { + _showAlertDialog.value = true + } + + fun dismissAlertDialog() { + _showAlertDialog.value = false + } + + + + + + + + private val _plotData = MutableStateFlow(Farm()) // colors Ensure non-null default value + val plotData: StateFlow = _plotData.asStateFlow() + + fun updatePlotData( + siteId: Long? = null, // Allow siteId to be updated + coordinates: List>? = null, + latitude: String? = null, + longitude: String? = null, + size: Float? = null, + accuracyArray: List? = null, + farmerName: String? = null, + memberId: String? = null, + farmerPhoto: String? = null, + village: String? = null, + district: String? = null + ) { + _plotData.value = _plotData.value.copy( + siteId = siteId ?: _plotData.value.siteId, // colors Update siteId if provided + coordinates = coordinates ?: _plotData.value.coordinates, + latitude = latitude ?: _plotData.value.latitude, + longitude = longitude ?: _plotData.value.longitude, + size = size ?: _plotData.value.size, + accuracyArray = accuracyArray ?: _plotData.value.accuracyArray, + farmerName = farmerName ?: _plotData.value.farmerName, + memberId = memberId ?: _plotData.value.memberId, + farmerPhoto = farmerPhoto ?: _plotData.value.farmerPhoto, + village = village ?: _plotData.value.village, + district = district ?: _plotData.value.district + ) + } + + fun submitForm() { + // colors Perform form submission logic here (e.g., API call, database update) + + // colors Reset the form after successful submission + _plotData.value = Farm() // Clears all values + } // Method to set coordinates and calculated area @@ -191,4 +359,15 @@ class MapViewModel @Inject constructor() : ViewModel() { state.value = state.value.copy(mapType = mapType) } -} \ No newline at end of file +} + +class MapViewModelFactory @Inject constructor() : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(MapViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return MapViewModel() as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} diff --git a/app/src/main/res/values-am/strings.xml b/app/src/main/res/values-am/strings.xml index b02850e..76a8498 100644 --- a/app/src/main/res/values-am/strings.xml +++ b/app/src/main/res/values-am/strings.xml @@ -142,7 +142,7 @@ ውሂብን እየማቀለሽ ነው… ማቅለሽ_አገልግሎት_ቻነል ማቅለሽ ተጠናቋል - እርሻዎች ከሰርቨር ጋር በትክክል ተመርተዋል። + የእርሻዎች መረጃ በተሳካ ሁኔታ ተቀምጧል። ማቅለሽ ቻነል የማቅለሽ ማስታወቂያዎች ለማቀለሽ ቻነል ማቅለሽ_ቻነል_id @@ -156,7 +156,7 @@ ውሂብ መልሰው አልተቻለም፦ %1$s ማስተካከያው አልተሳካም - የእርሻ መረጃን ከአገልጋይ ጋር ማስተካከል አልተሳካም + የእርሻዎችን መረጃ ማስቀመጥ አልተቻለም። ፖሊጎን አድስ? ያለውን ፖሊጎን መጠቀም ወይም አዲስ መቆጣጠር ትፈልጋለህ? @@ -177,8 +177,48 @@ %1$d አጠቃላይ ግብሮች %1$d የማይሞላ ዳታ አለበት ተጨማሪ ቦታዎችን ማስገባት ላይ ስህተት + ወጣሁ የሚል ለመውጣት ደግሞ ይጫኑ + ፈቃድ ያስፈልጋል + የአንተን የአካባቢ ፈቃድ በቋሚነት ስላስረዳኸው፡፡ እባኮትን ወደ ቅንብሮች ሄደው እንዲሆን ያድርጉ፡፡ + የካርታ ጥልፍ በተሳካ ሁኔታ ተከማቹበታል። + አካባቢ ማግኘት አልተቻለም። የካርታ ማስቀመጥ ተወል. + የኢንተርኔት ግንኙነት የለም። የካርታ ማስቀመጥ ተወል. + የገበያ ስሞችን ይያዙ? + የገበያ ስሞችን በውጭ ላይ መላክ ይፈልጋሉ? + ተመለስ + የቅርብ ጊዜ ማስቀመጫ: + ምትኬ + እንደገና መልስ + አካፍል + እስከ ውስጥ አስገባ + ምትኬን ማብራት? + ምትኬን ማጥፋት?\ + መቀጠል ይፈልጋሉ? + የውሂብ ምትኬን ማንቃት? + ምትኬን ማስቻወት የእርስዎን ውሂብ በደህና በአገልግሎት አጠቃቀም ይሰናከላል እንደ የኢንተርኔት ግንኙነት ቢኖር። ይህ መረጃዎችዎን በመሳሪያዎ ሲጠፋ፣ ሲጎድል፣ ወይም ከላይ ሲያስጀምሩ እንደገና እንድትመልሱት ያረጋግጣል። ለመቀጠል ይፈልጋሉ? + ምትኬን ማጥፋት የእርስዎን ውሂብ በአዲስ ዝማኔ ማስቀመጥ ይቆማል። ቀደም ብሎ የተሰናከለ ውሂብ እንደገና ለማስጀምር በቀላሉ ይገኛል፣ ነገር ግን አዲስ ለውጦች አይቀመጡም። ለመቀጠል ይፈልጋሉ? + + + ምትኬን አንቀላፋ + ምትኬን አጥፋ + ይህን የባህሪ ስርዓት ለመጠቀም የአካባቢ ፍቃድ ያስፈልጋል + + አካባቢን በማግኘት ላይ… + + የተጠቃሚ መመሪያ + + ወደ የተጠቃሚ መመሪያ እንኳን ደህና መጡ! ይህ መመሪያ የመተግበሪያውን ባህላዊ ባለሞያነት እና ስርዓተ ስራዎችን ይገልጻል። ሙሉ መመሪያ ለማውሰድ ከአሳሽ ምንጮቻችን ይውሰዱ። + + የተጠቃሚ መመሪያ አውርድ + የተጠቃሚ መመሪያ በተሳካ ሁኔታ ተወስዷል። + ማውሰድ አልተሳካም: %1$s + ምንም ቦታ አልተመረጠም። + + የእኛን + የግላዊነት መመሪያዎች + ለመቀጠል የግላዊነት መመሪያዎቹን እባክዎን ይቀበሉ። \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 431287c..20af3f9 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -142,7 +142,7 @@ Sincronizando datos… canal_servicio_sync Sincronización Completa - Las granjas se han sincronizado correctamente con el servidor. + Los datos de las granjas se han respaldado correctamente. Canal de Sincronización Canal para notificaciones de sincronización canal_sync_id @@ -156,7 +156,7 @@ No se pudo restaurar los datos: %1$s Sincronización fallida - Error al sincronizar los datos de las granjas con el servidor + Fallo al respaldar los datos de las granjas. ¿Actualizar polígono? ¿Te gustaría mantener el polígono existente o capturar uno nuevo? @@ -178,9 +178,44 @@ %1$d granjas en total %1$d con datos incompletos Error al cargar más sitios + Presiona atrás otra vez para salir + Permiso Requerido + Has denegado permanentemente el permiso de ubicación. Por favor, ve a la configuración para habilitarlo. + Los mosaicos del mapa se almacenaron en caché con éxito. + No se pudo obtener la ubicación. Se omitió el almacenamiento en caché del mapa. + Sin conexión a Internet. Se omitió el almacenamiento en caché del mapa. + ¿Incluir nombres de agricultores? + ¿Quieres incluir los nombres de los agricultores en la exportación? + Atrás + Última sincronización: + Copia de seguridad + Restaurar + Compartir + Importar + ¿Habilitar copia de seguridad? + ¿Deshabilitar copia de seguridad? + ¿Quieres continuar? + ¿Habilitar copia de seguridad de datos? + Habilitar la copia de seguridad almacena de forma segura tus datos en el servidor siempre que haya conexión a Internet. Esto garantiza que puedas restaurarlos si tu dispositivo se pierde, se daña o se restablece. ¿Quieres continuar? + Deshabilitar la copia de seguridad detendrá el guardado automático de tus datos en el servidor cuando haya conexión a Internet. Los datos previamente respaldados seguirán estando disponibles para su restauración, pero los cambios nuevos no se guardarán. ¿Quieres continuar? + Habilitar copia de seguridad + Deshabilitar copia de seguridad + Se requieren permisos de ubicación para esta función + Obteniendo ubicación… + Guía del Usuario + + ¡Bienvenido a la Guía del Usuario! Esta guía ofrece una breve visión general de las características y funcionalidades de la aplicación. Para instrucciones detalladas, descarga la guía completa de nuestros recursos. + + Descargar Guía del Usuario + La Guía del Usuario se descargó correctamente. + Fallo en la descarga: %1$s + No se seleccionó ubicación. + Acepta nuestras + políticas de privacidad de datos + Por favor, acepta la política de privacidad para continuar. \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 8329e34..86bc892 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -142,7 +142,7 @@ Synchronisation des données… canal_service_sync Synchronisation terminée - Les fermes ont été synchronisées avec succès avec le serveur. + Les données des fermes ont été sauvegardées avec succès. Canal de synchronisation Canal pour les notifications de synchronisation canal_sync_id @@ -155,7 +155,7 @@ Restaurer les données Échec de la restauration des données : %1$s Échec de la synchronisation - Échec de la synchronisation des données des fermes avec le serveur + Échec de la sauvegarde des données des fermes. Mettre à jour le polygone? Voulez-vous garder le polygone existant ou en capturer un nouveau? @@ -177,7 +177,46 @@ %1$d avec des données incomplètes Erreur de chargement de sites supplémentaires + Appuyez à nouveau sur retour pour quitter + Autorisation requise + Vous avez définitivement refusé l\'autorisation de localisation.Veuillez aller dans les paramètres pour l\'activer. + Les tuiles de la carte ont été mises en cache avec succès. + Impossible d\'obtenir l\'emplacement. Mise en cache de la carte ignorée. + Pas de connexion Internet. Mise en cache de la carte ignorée. + Inclure les noms des agriculteurs ? + Voulez-vous inclure les noms des agriculteurs dans l\'exportation ? + Retour + Dernière synchronisation : + Sauvegarde + Restaurer + Partager + Importer + Activer la sauvegarde ? + Désactiver la sauvegarde ? + Voulez-vous continuer ? + Activer la sauvegarde des données ? + L\'activation de la sauvegarde stocke en toute sécurité vos données sur le serveur dès qu\'une connexion Internet est disponible. Cela vous permet de les restaurer en cas de perte, de dommage ou de réinitialisation de votre appareil. Voulez-vous continuer ? + La désactivation de la sauvegarde empêchera l\'enregistrement automatique de vos données sur le serveur lorsque vous êtes connecté à Internet. Les données précédemment sauvegardées resteront accessibles pour la restauration, mais les nouvelles modifications ne seront pas enregistrées. Voulez-vous continuer ? + + Activer la sauvegarde + Désactiver la sauvegarde + Les autorisations de localisation sont requises pour cette fonctionnalité + Récupération de la localisation… + + Guide d\'utilisation + + Bienvenue dans le guide d\'utilisation ! Ce guide vous offre un aperçu des fonctionnalités de l\'application. Pour des instructions détaillées, téléchargez le guide complet depuis nos ressources. + + Télécharger le guide d\'utilisation + + Guide d\'utilisation téléchargé avec succès. + Échec du téléchargement : %1$s + Aucun emplacement sélectionné. + + Acceptez notre + politique de confidentialité des données + Veuillez accepter la politique de confidentialité pour continuer. diff --git a/app/src/main/res/values-night/strings.xml b/app/src/main/res/values-night/strings.xml index 2352b56..995f729 100644 --- a/app/src/main/res/values-night/strings.xml +++ b/app/src/main/res/values-night/strings.xml @@ -142,7 +142,7 @@ Syncing data… sync_service_channel Sync Complete - Farms have been successfully synchronized with the server. + Farms data have been backed up successfully. Sync Channel Channel for sync notifications sync_channel_id @@ -155,7 +155,7 @@ Restore Data Failed to restore data: %1$s Sync Failed - Failed to synchronize Farms Data with the server + Failed to backup the Farms data. Update Polygon?" Would you like to keep the existing polygon or capture a new one? @@ -176,6 +176,48 @@ %1$d total farms %1$d with incomplete data Error Loading More Sites + Press back again to exit + + Permission Required + You have permanently denied location permission. Please go to settings to enable it. + Map tiles cached successfully. + Unable to get location. Map caching skipped. + No internet connection. Map caching skipped. + Include Farmer Names? + Do you want to include farmer names in the export? + + Back + Last Synced: + Backup + Restore + Share + Import + Enable Backup? + Disable Backup? + Enabling backup securely stores your data on the server whenever an internet connection is available. This ensures you can restore it if your device is lost, damaged, or reset. Do you want to proceed? + Disabling backup will stop automatically saving your data on the server when an internet connection is available. Any previously backed-up data will remain accessible for restoration, but new changes won\'t be saved. Do you want to proceed?. + Do you want to proceed? + Enable Data Backup? + Enable Backup + Disable Backup + Location permissions are required for this feature + Fetching location… + + User Guide + + Welcome to the User Guide! This guide provides a brief overview of the app\'s features and functionalities. For detailed instructions, download the complete guide from our assets. + + Download User Guide + User Guide downloaded successfully. + Download failed: %1$s + No location selected. + + + Accept our + data privacy policies + Please accept the privacy policy to continue. + + \ No newline at end of file diff --git a/app/src/main/res/values-om/strings.xml b/app/src/main/res/values-om/strings.xml index d342710..1bcf325 100644 --- a/app/src/main/res/values-om/strings.xml +++ b/app/src/main/res/values-om/strings.xml @@ -142,7 +142,7 @@ Daataan walitti fufaa jira… sinkii_tajaajila_kaanaalii Sinkii Xumurameera - Lafotuun milkiidhaan sirriitti seerveratti walfudhateera. + Datalawwan qonnaa milkaa’inaan deeggaramanii jiru. Kaanaalii Sinkii Kaanaalii beeksisa sinkii tajaajilu sinkii_kaanaalii_id @@ -155,7 +155,7 @@ Odeeffannoo Deebi\'aa Odeeffannoo deebi\'uu hin dandeenye: %1$s Sinksii kufaatee - Daataa Fardeen Sinksii Saffisa waliin galchuu hin danda\'amne + Datalawwan qonnaa deeggeruun hin milkoofne. Poligoonii Haaromsuu? Poligoonii jiru qabachuuf moo haaraa qabsiisuuf? @@ -178,6 +178,44 @@ %1$d odeeffannoo hin guutamne waliin Bakkaa dabalataa fe’uuf dogoggora + Dubbii duubatti deebi’aa ba’uu + Hayyama Barbaachisa + Hayyama bakka argachuu idilee dhoorkiteetta. Maaloo gara settings deemi itti dabaluu. + Kaartaan fayyadamaa milkaa’inaan kuufame. + Iddoo argachuu hin dandeenye. Kuufama kaartaa dhiisame. + Toora interneetii hin jiru. Kuufama kaartaa dhiisame. + Maqaa qonnaan bultoota dabaluu? + Maqaa qonnaan bultoota erguuf dabaluuf barbaadaa? + Gadi Deebi’i + Waggaa Dhuma Simimne: + Kuusdeebi’aa + Deebisi + Qooduu + Galeessa + Kuusdeebi’aa bana? + Kuusdeebi’aa cufaa? + Sii deemsaa? + Kuusdeebi’aa bana? + Deebisaa haalaa gochuun, yommuu walqunnaamtii interneetii argattu, odeeffannoo kee sirriitti tajaajilaa irratti kuusa. Kun yoo meeshaan kee badaa, manca’a yookaan haaraa ta’ee, deebisuu akka dandeessu siif mirkaneessa. Itti fufuu barbaaddaa? + Deebisaa haalaa gochuu dhiisuu, yeroo interneetiin jiru, odeeffannoo kee tajaajilaa irratti otuu hin kuufamin hambisa. Odeeffannoon duraan kuufamee ture deebisuu ni danda’ama, garuu jijjiirraan haaraan hin kuufamu. Itti fufuu barbaaddaa? + + Kuusdeebi’aa bana + Kuusdeebi’aa cufaa + Ijaarsa kana fayyadamuuf eeyyama iddoo barbaachisa + Bakka argachaa jira… + + Qajeelfama Fayyadamtootaa + + Baga nagaan dhufte! Qajeelfamni kun muuxannoo gabaabaa amaloota fi tajaajiloota app kanaa ibsa. Qajeelfama guutuu argachuuf, galmee guutuu assets irraa buufadhaa. + + Qajeelfama Fayyadamtootaa Buufadhu + Qajeelfamni Fayyadamtootaa milkaa’inaan buufameera. + Buufachuun hin milkoofne: %1$s + Bakki filatame hin jiru. + + Heera dhuunfaa keenya + fudhachiisaa + Itti fufuu dura heera dhuunfaa fudhachuu qabda. diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml index 324ed7c..512b195 100644 --- a/app/src/main/res/values-sw/strings.xml +++ b/app/src/main/res/values-sw/strings.xml @@ -142,7 +142,7 @@ Inasawazisha data… usawazishaji_huduma_kanali Usawazishaji Umekamilika - Mashamba yamesawazishwa kwa mafanikio na seva. + Data za mashamba zimehifadhiwa nakala kwa mafanikio. Kanali ya Usawazishaji Kanali kwa taarifa za usawazishaji kanali_usawazishaji_id @@ -155,7 +155,7 @@ Rejesha Data Imeshindwa kurejesha data: %1$s Usawazishaji Umeshindwa - Imeshindwa kusawazisha Data za Mashamba na seva + Imeshindwa kuhifadhi nakala za data za mashamba. Sasisha Poligoni? Je, ungependa kuweka poligoni iliyopo au kunasa mpya? @@ -179,8 +179,43 @@ Hitilafu katika kupakia tovuti zaidi + Bonyeza nyuma tena ili utoke + Ruhusa Inahitajika + Umezuia ruhusa ya eneo kabisa. Tafadhali nenda kwenye mipangilio ili kuiwezesha. + Vigae vya ramani vimehifadhiwa kwenye kache kwa mafanikio. + Imeshindikana kupata eneo. Uhifadhi wa ramani umepitwa. + Hakuna muunganisho wa intaneti. Uhifadhi wa ramani umepitwa. + Je, ujumuishwe majina ya wakulima? + Je, unataka kujumuisha majina ya wakulima katika usafirishaji wa data? + Rudi + Mwisho kusawazishwa: + Hifadhi + Rejesha + Shiriki + Ingiza + Washa chelezo? + Zima chelezo? + Je, unataka kuendelea? + Washa chelezo cha data? + Kuwasha chelezo huhifadhi data yako salama kwenye seva kila wakati unapo kuwa na muunganisho wa intaneti. Hii inahakikisha kuwa unaweza kuirejesha ikiwa kifaa chako kitapotea, kuharibika, au kurejeshwa upya. Je, unataka kuendelea? + Kuzima chelezo kutazuia kuhifadhi data yako moja kwa moja kwenye seva wakati kuna muunganisho wa intaneti. Data zilizochelezwa hapo awali zitaendelea kupatikana kwa urejeshaji, lakini mabadiliko mapya hayatahifadhiwa. Je, unataka kuendelea? + Washa chelezo + Zima chelezo + Ruhusa ya mahali inahitajika kwa kipengele hiki + Inapata eneo… + Mwongozo wa Mtumiaji + + Karibu kwenye Mwongozo wa Mtumiaji! Mwongozo huu unatoa muhtasari wa vipengele na utendaji wa programu. Kwa maelekezo kamili, pakua mwongozo kamili kutoka rasilimali zetu. + + Pakua Mwongozo wa Mtumiaji + Mwongozo wa Mtumiaji umefanikiwa kupakuliwa. + Pakua imeshindwa: %1$s + Hakuna eneo lililochaguliwa. + Kubali + sera zetu za faragha ya data + Tafadhali kubali sera ya faragha ili kuendelea. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1927ac8..b756d3e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -141,7 +141,7 @@ Syncing data… sync_service_channel Sync Complete - Farms have been successfully synchronized with the server. + Farms data have been backed up successfully. Sync Channel Channel for sync notifications sync_channel_id @@ -153,7 +153,7 @@ Restore Data Failed to restore data: %1$s Sync Failed - Failed to synchronize Farms Data with the server + Failed to backup the Farms data. Update Polygon?" Would you like to keep the existing polygon or capture a new one? @@ -173,6 +173,47 @@ %1$d total farms %1$d with incomplete data Error Loading More Sites + Press back again to exit + + Permission Required + You have permanently denied location permission. Please go to settings to enable it. + Map tiles cached successfully. + Unable to get location. Map caching skipped. + No internet connection. Map caching skipped. + Include Farmer Names? + Do you want to include farmer names in the export? + + Back + Last Synced: + Backup + Restore + Share + Import + + Enable Backup? + Disable Backup? + Do you want to proceed? + Enable Data Backup? + Enabling backup securely stores your data on the server whenever an internet connection is available. This ensures you can restore it if your device is lost, damaged, or reset. Do you want to proceed? + Disabling backup will stop automatically saving your data on the server when an internet connection is available. Any previously backed-up data will remain accessible for restoration, but new changes won\'t be saved. Do you want to proceed?. + + + Enable Backup + Disable Backup + Location permissions are required for this feature + Fetching location… + + User Guide + + Welcome to the User Guide! This guide provides a brief overview of the app\'s features and functionalities. For detailed instructions, download the complete guide from our assets. + + Download User Guide + User Guide downloaded successfully. + Download failed: %1$s + No location selected. + Accept our + data privacy policies + Please accept the privacy policy to continue. \ No newline at end of file diff --git a/app/src/test/java/org/technoserve/farmcollector/FarmCollectorAppTest.kt b/app/src/test/java/org/technoserve/farmcollector/FarmCollectorAppTest.kt deleted file mode 100644 index 5d8b61c..0000000 --- a/app/src/test/java/org/technoserve/farmcollector/FarmCollectorAppTest.kt +++ /dev/null @@ -1,91 +0,0 @@ -package org.technoserve.farmcollector - -/* -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.work.Constraints -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.NetworkType -import androidx.work.PeriodicWorkRequest -import androidx.work.WorkManager -import org.junit.Assert.* -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.mockito.ArgumentMatchers.any -import org.mockito.ArgumentMatchers.anyString -import org.mockito.Mockito -import org.mockito.Mockito.mockStatic -import java.util.concurrent.TimeUnit - -class FarmCollectorAppTest { - - @get:Rule - val instantTaskExecutorRule = InstantTaskExecutorRule() - - private lateinit var mockWorkManager: WorkManager - private lateinit var app: FarmCollectorApp - - @Before - fun setup() { - mockWorkManager = Mockito.mock(WorkManager::class.java) - mockStatic(WorkManager::class.java).use { mockedStatic -> - mockedStatic.`when` { WorkManager.getInstance(any()) }.thenReturn(mockWorkManager) - } - app = FarmCollectorApp() - } - - @Test - fun `test onCreate initializes WorkManager`() { - // Arrange - val expectedTag = "sync_work_tag" - val expectedPolicy = ExistingPeriodicWorkPolicy.UPDATE - - // Act - app.onCreate() - - // Assert - Mockito.verify(mockWorkManager).enqueueUniquePeriodicWork( - Mockito.eq(expectedTag), - Mockito.eq(expectedPolicy), - Mockito.any() - ) - } - - @Test - fun `test initializeWorkWorker creates PeriodicWorkRequest`() { - // Arrange - val expectedInterval = 2L - val expectedTimeUnit = TimeUnit.HOURS - - // Act - val workRequest = app.initializeWorkManager() - - // Assert - Mockito.verify(mockWorkManager).enqueueUniquePeriodicWork( - anyString(), - Mockito.any(), - Mockito.argThat { request: PeriodicWorkRequest -> - request.workSpec.intervalDuration == TimeUnit.HOURS.toMillis(expectedInterval) && - request.workSpec.constraints.requiredNetworkType == NetworkType.CONNECTED - } - ) - } - - @Test - fun `test initializeWorkWorker throws exception when network not connected`() { - // Arrange - val mockConstraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.NOT_REQUIRED) - .build() - Mockito.`when`(app.initializeWorkManager()).thenThrow(Exception("Required network type is not connected")) - - // Act & Assert - try { - app.initializeWorkManager() - fail("Expected an exception to be thrown") - } catch (exception: Exception) { - assertTrue(exception.message?.contains("Required network type") == true) - } - } - -}*/ \ No newline at end of file diff --git a/app/src/test/java/org/technoserve/farmcollector/MainActivityTest.kt b/app/src/test/java/org/technoserve/farmcollector/MainActivityTest.kt deleted file mode 100644 index 277e950..0000000 --- a/app/src/test/java/org/technoserve/farmcollector/MainActivityTest.kt +++ /dev/null @@ -1,173 +0,0 @@ -package org.technoserve.farmcollector - -/* -import android.Manifest -import android.app.Application -import android.content.SharedPreferences -import androidx.appcompat.app.AppCompatDelegate -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.test.core.app.ActivityScenario -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.After -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -//@RunWith(AndroidJUnit4::class) -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [33]) -class MainActivityKtTest { - - @get:Rule - val composeTestRule = createComposeRule() - - private lateinit var sharedPreferences: SharedPreferences - private lateinit var darkModePref: SharedPreferences - private lateinit var app: Application - - @Before - fun setUp() { - // Intents.init() - app = ApplicationProvider.getApplicationContext() -// sharedPreferences = app.getSharedPreferences("FarmCollector", ComponentActivity.MODE_PRIVATE) -// darkModePref = app.getSharedPreferences("theme_mode", ComponentActivity.MODE_PRIVATE) - } - @After - fun tearDown() { - // Intents.release() - } - - @Test - fun testDarkModeApplied() { - // Set dark mode in SharedPreferences - darkModePref.edit().putBoolean("dark_mode", true).apply() - - val scenario = ActivityScenario.launch(MainActivity::class.java) - - scenario.onActivity { activity -> - assertTrue(AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES) - } - } - - @Test - fun testLightModeApplied() { - // Set light mode in SharedPreferences - darkModePref.edit().putBoolean("dark_mode", false).apply() - - val scenario = ActivityScenario.launch(MainActivity::class.java) - - scenario.onActivity { activity -> - assertTrue(AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_NO) - } - } - - @Test - fun testSharedPreferencesKeysRemoved() { - // Set values in SharedPreferences - sharedPreferences.edit().putString("plot_size", "100").apply() - sharedPreferences.edit().putString("selectedUnit", "meters").apply() - - // Launch the activity - val scenario = ActivityScenario.launch(MainActivity::class.java) - - scenario.onActivity { activity -> - // Check that keys are removed - assertTrue(!sharedPreferences.contains("plot_size")) - assertTrue(!sharedPreferences.contains("selectedUnit")) - } - } - - @Test - fun testPermissionsRequestedOnLaunch() { - val permissions = listOf( - Manifest.permission.ACCESS_COARSE_LOCATION, - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.POST_NOTIFICATIONS, - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE - ) - - val scenario = ActivityScenario.launch(MainActivity::class.java) - - scenario.onActivity { activity -> - permissions.forEach { permission -> - // Check if permission was requested - assertTrue(activity.shouldShowRequestPermissionRationale(permission)) - } - } - } - -// @Test -// fun testUpdateAlertShownWhenUpdateAvailable() = runBlocking { -// // Create a mock AppUpdateViewModel -// val appUpdateViewModel = mock(AppUpdateViewModel::class.java) -// `when`(appUpdateViewModel.updateAvailable).thenReturn(true) -// -// val scenario = ActivityScenario.launch(MainActivity::class.java) -// -// scenario.onActivity { activity -> -// // Verify that the update alert dialog is shown -// assertTrue(activity.isDialogVisible ("update_alert_dialog")) -// } -// } - -// @Test -// fun testExitDialogShownOnBackPress() { -// val scenario = ActivityScenario.launch(MainActivity::class.java) -// -// scenario.onActivity { activity -> -// // Simulate back press -// activity.onBackPressedDispatcher.onBackPressed() -// -// // Verify that the exit confirmation dialog is shown -// assertTrue(activity.isDialogVisible("exit_confirmation_dialog")) -// } -// } - -// @Test -// fun testNavigationToHome() { -// val scenario = ActivityScenario.launch(MainActivity::class.java) -// -// scenario.onActivity { activity -> -// val navController = findNavController(activity, R.id.nav_host_fragment) -// -// // Verify navigation to home screen -// assertThat(navController.currentDestination?.route).isEqualTo(Routes.HOME) -// } -// } - -// @Test -// fun testLocaleUpdatedOnLanguageChange() = runBlocking { -// val languageViewModel = mock(LanguageViewModel::class.java) -// val savedStateHandle = SavedStateHandle().apply { -// set("language", "es") // Simulate Spanish language selection -// } -// -// val scenario = ActivityScenario.launch(MainActivity::class.java) -// -// scenario.onActivity { activity -> -// verify(languageViewModel).updateLocale(app, Locale("es")) -// } -// } - -// @Test -// fun testOpenPlayStoreOnUpdateConfirm() { -// val scenario = ActivityScenario.launch(MainActivity::class.java) -// -// scenario.onActivity { activity -> -// val intent = Intent(Intent.ACTION_VIEW).apply { -// data = Uri.parse("market://details?id=${activity.packageName}") -// } -// -// // Verify that the Play Store intent is correctly created -// assertThat(intent).hasAction(Intent.ACTION_VIEW) -// assertThat(intent.data).isEqualTo(Uri.parse("market://details?id=${activity.packageName}")) -// } -// } -} -*/ \ No newline at end of file diff --git a/app/src/test/java/org/technoserve/farmcollector/MapViewModelUnitTest.kt b/app/src/test/java/org/technoserve/farmcollector/MapViewModelUnitTest.kt deleted file mode 100644 index aa346fb..0000000 --- a/app/src/test/java/org/technoserve/farmcollector/MapViewModelUnitTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package org.technoserve.farmcollector -/* -import dagger.hilt.android.testing.HiltAndroidRule -import junit.framework.TestCase.assertEquals -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.technoserve.farmcollector.viewmodels.MapViewModel - -@RunWith(RobolectricTestRunner::class) -class MapViewModelKtTest { - - - @get:Rule - var hiltRule = HiltAndroidRule(this) - - private lateinit var viewModel: MapViewModel - - @Before - fun setUp() { - hiltRule.inject() // Inject dependencies - viewModel = MapViewModel() // No dependency to mock since the constructor is empty - } - - @Test - fun testAddCoordinate() { - val lat = 12.345678 - val lng = 98.765432 - - viewModel.addCoordinate(lat, lng) - - val clusterItems = viewModel.state.value.clusterItems - assert(clusterItems.isNotEmpty()) - assertEquals("zone-0", clusterItems.last().id) - } -} -*/ \ No newline at end of file diff --git a/app/src/test/java/org/technoserve/farmcollector/database/models/CollectionSiteTest.kt b/app/src/test/java/org/technoserve/farmcollector/database/models/CollectionSiteTest.kt deleted file mode 100644 index 864cc78..0000000 --- a/app/src/test/java/org/technoserve/farmcollector/database/models/CollectionSiteTest.kt +++ /dev/null @@ -1,150 +0,0 @@ -package org.technoserve.farmcollector.database.models -/* -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.room.Room -import androidx.test.core.app.ApplicationProvider -import org.junit.After -import org.junit.Assert.* -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.technoserve.farmcollector.database.TestDatabase -import org.technoserve.farmcollector.database.dao.CollectionSiteDAO -import java.util.concurrent.Executors - -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [33]) -class CollectionSiteTest{ - - @get:Rule - val instantTaskExecutorRule = InstantTaskExecutorRule() // For LiveData - - private lateinit var database: TestDatabase - private lateinit var collectionSiteDao: CollectionSiteDAO - - @Before - fun setUp() { - database = Room.inMemoryDatabaseBuilder( - ApplicationProvider.getApplicationContext(), - TestDatabase::class.java - ) - .setTransactionExecutor(Executors.newSingleThreadExecutor()) - .build() - - collectionSiteDao = database.collectionSiteDAO() - } - - @After - fun tearDown() { - database.close() - } - - @Test - fun `insert and retrieve CollectionSite`() { - val collectionSite = CollectionSite( - name = "Test Site", - agentName = "Agent Smith", - phoneNumber = "1234567890", - email = "agent@example.com", - village = "Test Village", - district = "Test District", - createdAt = System.currentTimeMillis(), - updatedAt = System.currentTimeMillis() - ) - - // Insert the entity - val id = collectionSiteDao.insertSite(collectionSite) - assertTrue(id > 0) // Ensure the ID was auto-generated - - // Retrieve and verify the entity - val retrievedSite = collectionSiteDao.getCollectionSiteById(id) - assertNotNull(retrievedSite) - assertEquals(collectionSite.name, retrievedSite?.name) - assertEquals(collectionSite.agentName, retrievedSite?.agentName) - assertEquals(collectionSite.phoneNumber, retrievedSite?.phoneNumber) - assertEquals(collectionSite.email, retrievedSite?.email) - assertEquals(collectionSite.village, retrievedSite?.village) - assertEquals(collectionSite.district, retrievedSite?.district) - } - - @Test - fun `update CollectionSite`() { - val collectionSite = CollectionSite( - name = "Test Site", - agentName = "Agent Smith", - phoneNumber = "1234567890", - email = "agent@example.com", - village = "Test Village", - district = "Test District", - createdAt = System.currentTimeMillis(), - updatedAt = System.currentTimeMillis() - ) - - // Insert and retrieve ID - val id = collectionSiteDao.insertSite(collectionSite) - assertTrue(id > 0) - - // Update entity - val updatedSite = collectionSite.copy(name = "Updated Site") - collectionSiteDao.updateSite(updatedSite) - - // Retrieve and verify update - val retrievedSite = collectionSiteDao.getCollectionSiteById(id) - assertNotNull(retrievedSite) - assertEquals("Updated Site", retrievedSite?.name) - } - - @Test - fun `delete CollectionSite`() { - val collectionSite = CollectionSite( - name = "Test Site", - agentName = "Agent Smith", - phoneNumber = "1234567890", - email = "agent@example.com", - village = "Test Village", - district = "Test District", - createdAt = System.currentTimeMillis(), - updatedAt = System.currentTimeMillis() - ) - - // Insert and retrieve ID - val id = collectionSiteDao.insertSite(collectionSite) - assertTrue(id > 0) - - // Delete the entity - collectionSiteDao.delete(collectionSite) - - // Verify deletion - val retrievedSite = collectionSiteDao.getCollectionSiteById(id) - assertNull(retrievedSite) - } - - @Test - fun `verify DateConverter works with CollectionSite`() { - val createdAt = System.currentTimeMillis() - val updatedAt = System.currentTimeMillis() - - val collectionSite = CollectionSite( - name = "Test Site", - agentName = "Agent Smith", - phoneNumber = "1234567890", - email = "agent@example.com", - village = "Test Village", - district = "Test District", - createdAt = createdAt, - updatedAt = updatedAt - ) - - // Insert and retrieve ID - val id = collectionSiteDao.insertSite(collectionSite) - val retrievedSite = collectionSiteDao.getCollectionSiteById(id) - - assertNotNull(retrievedSite) - assertEquals(createdAt, retrievedSite?.createdAt) - assertEquals(updatedAt, retrievedSite?.updatedAt) - } -} -*/ \ No newline at end of file diff --git a/app/src/test/java/org/technoserve/farmcollector/repositories/FarmRepositoryTest.kt b/app/src/test/java/org/technoserve/farmcollector/repositories/FarmRepositoryTest.kt index 7852e92..f6add2a 100644 --- a/app/src/test/java/org/technoserve/farmcollector/repositories/FarmRepositoryTest.kt +++ b/app/src/test/java/org/technoserve/farmcollector/repositories/FarmRepositoryTest.kt @@ -4,7 +4,6 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule @@ -18,7 +17,6 @@ import org.mockito.MockitoAnnotations import org.technoserve.farmcollector.database.dao.FarmDAO import org.technoserve.farmcollector.database.models.CollectionSite import org.technoserve.farmcollector.database.models.Farm -import java.util.UUID class FarmRepositoryTest { @@ -35,110 +33,6 @@ class FarmRepositoryTest { farmRepository = FarmRepository(mockFarmDAO) } - -// @Test -// fun `get readAllSites returns all sites`() { -// // Mock DAO to return expected LiveData -// val expectedSites = MutableLiveData>() -// val testSites = listOf( -// CollectionSite( -// name = "Site A", -// agentName = "Agent Name", -// phoneNumber = "12345", -// email = "test@example.com", -// village = "Village Name", -// district = "District Name", -// createdAt = System.currentTimeMillis(), -// updatedAt = System.currentTimeMillis() -// ), -// CollectionSite( -// name = "Site B", -// agentName = "Agent Name", -// phoneNumber = "12345", -// email = "test@example.com", -// village = "Village Name", -// district = "District Name", -// createdAt = System.currentTimeMillis(), -// updatedAt = System.currentTimeMillis() -// ) -// ) -// expectedSites.value = testSites -// `when`(mockFarmDAO.getSites()).thenReturn(expectedSites) -// -// // Call repository and ensure LiveData is not null -// val result = farmRepository.readAllSites -// assertNotNull(result) -// -// // Observe the LiveData and validate the content -// result.observeForever { actualSites -> -// assertEquals(testSites, actualSites) -// } -// -// // Verify DAO method was called -// verify(mockFarmDAO).getSites() -// } - - -// @Test -// fun `get readData returns all farms`() { -// // Mock DAO to return expected LiveData -// val expectedFarms = MutableLiveData>() -// val testFarms = listOf( -// Farm( -// siteId = 1L, -// farmerPhoto = "photo.jpg", -// farmerName = "New Farmer A", -// memberId = "12345", -// village = "Village A", -// district = "District X", -// purchases = 10f, -// size = 100f, -// latitude = "12.34", -// longitude = "56.78", -// coordinates = listOf(Pair(12.34, 56.78)), -// accuracyArray = listOf(5.0f), -// synced = false, -// scheduledForSync = false, -// createdAt = System.currentTimeMillis(), -// updatedAt = System.currentTimeMillis(), -// needsUpdate = true -// ), -// Farm( -// siteId = 2L, -// farmerPhoto = "photo.jpg", -// farmerName = "New Farmer A", -// memberId = "12345", -// village = "Village A", -// district = "District X", -// purchases = 10f, -// size = 100f, -// latitude = "12.34", -// longitude = "56.78", -// coordinates = listOf(Pair(12.34, 56.78)), -// accuracyArray = listOf(5.0f), -// synced = false, -// scheduledForSync = false, -// createdAt = System.currentTimeMillis(), -// updatedAt = System.currentTimeMillis(), -// needsUpdate = true -// ) -// ) -// expectedFarms.value = testFarms -// `when`(mockFarmDAO.getData()).thenReturn(expectedFarms) -// -// // Call repository and ensure LiveData is not null -// val result = farmRepository.readData -// assertNotNull(result) -// -// // Observe the LiveData and validate the content -// result.observeForever { actualFarms -> -// assertEquals(testFarms, actualFarms) -// } -// -// // Verify DAO method was called -// verify(mockFarmDAO).getData() -// } - @Test fun `readAllFarms returns farms for specific site`() { val siteId = 1L @@ -149,85 +43,6 @@ class FarmRepositoryTest { assertEquals(expectedFarms, farms) } -// @Test -// fun `addFarm inserts new farm when no duplicate exists`(): Unit = runBlocking { -// val farm = Farm( -// siteId = 1L, -// farmerPhoto = "photo.jpg", -// farmerName = "Old Farmer", -// memberId = "12345", -// village = "Village A", -// district = "District X", -// purchases = 10f, -// size = 100f, -// latitude = "12.34", -// longitude = "56.78", -// coordinates = listOf(Pair(12.34, 56.78)), -// accuracyArray = listOf(5.0f), -// synced = false, -// scheduledForSync = false, -// createdAt = System.currentTimeMillis(), -// updatedAt = System.currentTimeMillis(), -// needsUpdate = true -// ) -// -// `when`( -// mockFarmDAO.getFarmByDetails( -// UUID.randomUUID(), -// "Old Farmer", -// "Village A", -// "District X" -// ) -// ).thenReturn(null) -// -// farmRepository.addFarm(farm) -// verify(mockFarmDAO).insert(farm) -// } - -// @Test -// fun `addFarm updates farm if duplicate exists and needs update`() = runBlocking { -// // Setup data for existing farm -// val existingFarm = Farm( -// siteId = 1L, -// farmerPhoto = "photo.jpg", -// farmerName = "Old Farmer", -// memberId = "12345", -// village = "Village A", -// district = "District X", -// purchases = 10f, -// size = 100f, -// latitude = "12.34", -// longitude = "56.78", -// coordinates = listOf(Pair(12.34, 56.78)), -// accuracyArray = listOf(5.0f), -// synced = false, -// scheduledForSync = false, -// createdAt = System.currentTimeMillis(), -// updatedAt = System.currentTimeMillis(), -// needsUpdate = true -// ) -// -// val newFarm = existingFarm.copy(farmerName = "New Farmer") -// -// // Mock DAO method to return existing farm -// `when`( -// mockFarmDAO.getFarmByDetails( -// existingFarm.remoteId, -// existingFarm.memberId, -// existingFarm.village, -// existingFarm.district -// ) -// ) -// .thenReturn(existingFarm) -// -// // Perform the add operation -// farmRepository.addFarm(newFarm) -// -// // Verify that the DAO update method was called -// verify(mockFarmDAO).update(newFarm) -// } - - @Test fun `addSite inserts site if not duplicate`(): Unit = runBlocking { val site = CollectionSite( diff --git a/app/src/test/java/org/technoserve/farmcollector/ui/components/CustomPaginationControlsKtTest.kt b/app/src/test/java/org/technoserve/farmcollector/ui/components/CustomPaginationControlsKtTest.kt deleted file mode 100644 index 994e3a8..0000000 --- a/app/src/test/java/org/technoserve/farmcollector/ui/components/CustomPaginationControlsKtTest.kt +++ /dev/null @@ -1,210 +0,0 @@ -package org.technoserve.farmcollector.ui.components - -/* -import android.content.Context -import androidx.compose.ui.test.assertIsNotEnabled -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.lifecycle.LiveData -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.work.Configuration -import androidx.work.Operation -import androidx.work.WorkInfo -import androidx.work.WorkManager -import androidx.work.WorkRequest -import androidx.work.testing.WorkManagerTestInitHelper -import io.mockk.every -import io.mockk.mockk -import org.junit.After -import org.junit.Assert.* -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers -import org.mockito.Mockito.doNothing -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` -import org.mockito.Mockito.* -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.testng.junit.JUnit4TestRunner - - - - -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [33], manifest = Config.NONE) -//@Config( -// sdk = [33], -// manifest = Config.NONE, -// instrumentedPackages = ["androidx.compose.ui.test"] -//) -class CustomPaginationControlsKtTest{ - - @get:Rule - val composeTestRule = createComposeRule() - -// @Before -// fun setUp() { -// val context = ApplicationProvider.getApplicationContext() -// -// // Create the configuration for WorkManager -// val config = Configuration.Builder() -// .setMinimumLoggingLevel(android.util.Log.DEBUG) -// .build() -// -// // Initialize WorkManager for testing -// WorkManagerTestInitHelper.initializeTestWorkManager(context, config) -// } -// -// @After -// fun tearDown() { -// WorkManagerTestInitHelper.getTestDriver()?.let { -// WorkManager.getInstance(ApplicationProvider.getApplicationContext()).cancelAllWork() -// WorkManager.getInstance(ApplicationProvider.getApplicationContext()).pruneWork() -// System.gc() // Force garbage collection to release locked resources -// } -// } - - -// private val mockWorkManager: WorkManager = mock(WorkManager::class.java) -// -// @Before -// fun setUp() { -// // Create a mock Operation -// val mockOperation = mock(Operation::class.java) -// -// // Create a mock LiveData -// val mockStateLiveData = mock(LiveData::class.java) as LiveData -// val mockState = mock(Operation.State::class.java) -// -// // Stub the state to return a LiveData with a mock state -// `when`(mockOperation.state).thenReturn(mockStateLiveData) -// -// // Stub the LiveData to return a specific state -// `when`(mockStateLiveData.value).thenReturn(mockState) -// -// // Mock WorkManager's enqueue method to return the mockOperation -// `when`(mockWorkManager.enqueue(any(WorkRequest::class.java))).thenReturn(mockOperation) -// -// // Mock cancelAllWork to do nothing -// doNothing().`when`(mockWorkManager).cancelAllWork() -// } - - @Before - fun setUp() { - // Set up any necessary configurations - System.setProperty("robolectric.build.model", "device") - System.setProperty("robolectric.enabledSdks", "33") - } - - - - - @Test - fun paginationDisplaysCorrectCurrentAndTotalPages() { - val currentPage = 3 - val totalPages = 5 - - composeTestRule.setContent { - CustomPaginationControls( - currentPage = currentPage, - totalPages = totalPages, - onPageChange = {} - ) - } - - // Verify the displayed text for current and total pages - composeTestRule.onNodeWithText("Page $currentPage of $totalPages").assertExists() - } - - @Test - fun previousButtonIsDisabledOnFirstPage() { - composeTestRule.setContent { - CustomPaginationControls( - currentPage = 1, - totalPages = 5, - onPageChange = {} - ) - } - - // Verify that the "Previous Page" button is disabled - composeTestRule.onNodeWithContentDescription("Previous Page") - .assertIsNotEnabled() - } - - @Test - fun nextButtonIsDisabledOnLastPage() { - composeTestRule.setContent { - CustomPaginationControls( - currentPage = 5, - totalPages = 5, - onPageChange = {} - ) - } - - // Verify that the "Next Page" button is disabled - composeTestRule.onNodeWithContentDescription("Next Page") - .assertIsNotEnabled() - } - - @Test - fun previousButtonTriggersOnPageChangeCorrectly() { - var pageChangedTo = -1 - - composeTestRule.setContent { - CustomPaginationControls( - currentPage = 3, - totalPages = 5, - onPageChange = { page -> pageChangedTo = page } - ) - } - - // Click the "Previous Page" button - composeTestRule.onNodeWithContentDescription("Previous Page") - .performClick() - - // Verify the page changed to the correct value - assert(pageChangedTo == 2) - } - - @Test - fun nextButtonTriggersOnPageChangeCorrectly() { - var pageChangedTo = -1 - - composeTestRule.setContent { - CustomPaginationControls( - currentPage = 3, - totalPages = 5, - onPageChange = { page -> pageChangedTo = page } - ) - } - - // Click the "Next Page" button - composeTestRule.onNodeWithContentDescription("Next Page") - .performClick() - - // Verify the page changed to the correct value - assert(pageChangedTo == 4) - } - - @Test - fun buttonsAreDisabledWhenOnlyOnePage() { - composeTestRule.setContent { - CustomPaginationControls( - currentPage = 1, - totalPages = 1, - onPageChange = {} - ) - } - - // Verify both "Previous Page" and "Next Page" buttons are disabled - composeTestRule.onNodeWithContentDescription("Previous Page").assertIsNotEnabled() - composeTestRule.onNodeWithContentDescription("Next Page").assertIsNotEnabled() - } -} - */ \ No newline at end of file diff --git a/app/src/test/java/org/technoserve/farmcollector/ui/components/CustomizedConfirmationDialogKtTest.kt b/app/src/test/java/org/technoserve/farmcollector/ui/components/CustomizedConfirmationDialogKtTest.kt deleted file mode 100644 index 278dd96..0000000 --- a/app/src/test/java/org/technoserve/farmcollector/ui/components/CustomizedConfirmationDialogKtTest.kt +++ /dev/null @@ -1,186 +0,0 @@ -package org.technoserve.farmcollector.ui.components -/* -import androidx.compose.ui.test.* -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.work.Configuration -import androidx.work.WorkManager -import org.junit.Assert.* -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.RuntimeEnvironment -import org.robolectric.annotation.Config -import org.technoserve.farmcollector.database.models.Farm -import org.technoserve.farmcollector.ui.screens.farms.Action - -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [33]) -class CustomizedConfirmationDialogKtTest { - -// @Before -// fun setUp() { -// val config = Configuration.Builder() -// .setMinimumLoggingLevel(android.util.Log.DEBUG) -// .build() -// WorkManager.initialize(RuntimeEnvironment.application, config) -// } - - - @get:Rule - val composeTestRule = createComposeRule() - - private val sampleFarms = listOf( - - - Farm( - siteId = 1L, - farmerPhoto = "photo.jpg", - farmerName = "John Doe", - memberId = "12345", - village = "Village1", - district = "District1", - purchases = 10f, - size = 1.0f, - latitude = "1.234", - longitude = "2.345", - coordinates = listOf(Pair(12.34, 56.78)), - accuracyArray = listOf(5.0f), - synced = false, - scheduledForSync = false, - createdAt = System.currentTimeMillis(), - updatedAt = System.currentTimeMillis(), - needsUpdate = true - ), - Farm( - siteId = 1L, - farmerPhoto = "photo.jpg", - farmerName = "", - memberId = "12345", - village = "Village1", - district = "District1", - purchases = 10f, - size = 1.0f, - latitude = "0.0", - longitude = "0.0", - coordinates = listOf(Pair(0.0, 0.0)), - accuracyArray = listOf(5.0f), - synced = false, - scheduledForSync = false, - createdAt = System.currentTimeMillis(), - updatedAt = System.currentTimeMillis(), - needsUpdate = true - ), // Incomplete - Farm( - siteId = 1L, - farmerPhoto = "photo.jpg", - farmerName = "Jane Doe", - memberId = "12345", - village = "Village3", - district = "District3", - purchases = 10f, - size = 2.0f, - latitude = "3.456", - longitude = "4.567", - coordinates = listOf(Pair(3.456, 4.567)), - accuracyArray = listOf(1.0f), - synced = false, - scheduledForSync = false, - createdAt = System.currentTimeMillis(), - updatedAt = System.currentTimeMillis(), - needsUpdate = true - ) - ) - -// @Test -// fun customizedConfirmationDialogDisplaysCorrectMessageForExport() { -// composeTestRule.setContent { -// CustomizedConfirmationDialog( -// listItems = sampleFarms, -// action = Action.Export, -// onConfirm = {}, -// onDismiss = {} -// ) -// } -// -// composeTestRule.onNodeWithText("Confirm").assertExists() -// composeTestRule.onNodeWithText("You are about to export 3 farms, including 1 incomplete farm.") -// .assertExists() // Assuming proper string resources -// } - -// @Test -// fun customizedConfirmationDialogDisplaysCorrectMessageForShare() { -// composeTestRule.setContent { -// CustomizedConfirmationDialog( -// listItems = sampleFarms, -// action = Action.Share, -// onConfirm = {}, -// onDismiss = {} -// ) -// } -// -// composeTestRule.onNodeWithText("Confirm").assertExists() -// composeTestRule.onNodeWithText("You are about to share 3 farms, including 1 incomplete farm.") -// .assertExists() // Assuming proper string resources -// } - - @Test - fun customizedConfirmationDialogTriggersOnConfirm() { - var confirmed = false - var dismissed = false - - composeTestRule.setContent { - CustomizedConfirmationDialog( - listItems = sampleFarms, - action = Action.Export, - onConfirm = { confirmed = true }, - onDismiss = { dismissed = true } - ) - } - - // Click the confirm button - composeTestRule.onNodeWithText("Yes").performClick() - - // Verify that onConfirm and onDismiss are triggered - assert(confirmed) - assert(dismissed) - } - - @Test - fun customizedConfirmationDialogTriggersOnDismiss() { - var dismissed = false - - composeTestRule.setContent { - CustomizedConfirmationDialog( - listItems = sampleFarms, - action = Action.Share, - onConfirm = {}, - onDismiss = { dismissed = true } - ) - } - - // Click the dismiss button - composeTestRule.onNodeWithText("No").performClick() - - // Verify that onDismiss is triggered - assert(dismissed) - } - - @Test - fun customizedConfirmationDialogValidatesFarmsCorrectly() { - val incompleteFarms = sampleFarms.filter { - it.farmerName.isEmpty() || - it.district.isEmpty() || - it.village.isEmpty() || - it.latitude == "0.0" || - it.longitude == "0.0" || - it.size == 0.0f || - it.remoteId.toString().isEmpty() - } - - assert(incompleteFarms.size == 1) - } -} - - */ \ No newline at end of file diff --git a/app/src/test/java/org/technoserve/farmcollector/ui/components/FarmCardKtTest.kt b/app/src/test/java/org/technoserve/farmcollector/ui/components/FarmCardKtTest.kt deleted file mode 100644 index cb68d03..0000000 --- a/app/src/test/java/org/technoserve/farmcollector/ui/components/FarmCardKtTest.kt +++ /dev/null @@ -1,188 +0,0 @@ -package org.technoserve.farmcollector.ui.components -/* -import android.content.Context -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.test.core.app.ApplicationProvider -import androidx.work.Configuration -import androidx.work.testing.WorkManagerTestInitHelper -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.technoserve.farmcollector.database.models.Farm - -@RunWith(RobolectricTestRunner::class) -//@Config(sdk = [33]) -@Config(sdk = [33], manifest = Config.NONE) -class FarmCardKtTest{ - @get:Rule - val composeTestRule = createComposeRule() - -// @Before -// fun setUp() { -// val context = ApplicationProvider.getApplicationContext() -// -// // Create the configuration for WorkManager -// val config = Configuration.Builder() -// .setMinimumLoggingLevel(android.util.Log.DEBUG) -// .build() -// -// // Initialize WorkManager for testing -// WorkManagerTestInitHelper.initializeTestWorkManager(context, config) -// } - -// @Test -// fun farmCardDisplaysContentCorrectly() { -// val testFarm = Farm( -// siteId = 1L, -// farmerPhoto = "photo.jpg", -// farmerName = "John Doe", -// memberId = "12345", -// village = "Sample Village", -// district = "Sample District", -// purchases = 10f, -// size = 5.0f, -// latitude = "12.34", -// longitude = "56.78", -// coordinates = listOf(Pair(12.34, 56.78)), -// accuracyArray = listOf(5.0f), -// synced = false, -// scheduledForSync = false, -// createdAt = System.currentTimeMillis(), -// updatedAt = System.currentTimeMillis(), -// needsUpdate = true -// ) -// -// composeTestRule.setContent { -// FarmCard( -// farm = testFarm, -// onCardClick = {}, -// onDeleteClick = {} -// ) -// } -// -// // Verify farmer name -// composeTestRule.onNodeWithText("John Doe").assertExists() -// -// // Verify farm size with formatting -// composeTestRule.onNodeWithText("Size: 5.0 ha").assertExists() -// -// // Verify village and district -// composeTestRule.onNodeWithText("Village: Sample Village").assertExists() -// composeTestRule.onNodeWithText("District: Sample District").assertExists() -// -// // Verify the "needs update" label -// composeTestRule.onNodeWithText("Needs update").assertExists() -// } - - @Test - fun farmCardHandlesCardClick() { - var cardClicked = false - composeTestRule.setContent { - FarmCard( - farm = Farm( - siteId = 1L, - farmerPhoto = "photo.jpg", - farmerName = "John Doe", - memberId = "12345", - village = "Sample Village", - district = "Sample District", - purchases = 10f, - size = 5.0f, - latitude = "12.34", - longitude = "56.78", - coordinates = listOf(Pair(12.34, 56.78)), - accuracyArray = listOf(5.0f), - synced = false, - scheduledForSync = false, - createdAt = System.currentTimeMillis(), - updatedAt = System.currentTimeMillis(), - needsUpdate = false - ), - onCardClick = { cardClicked = true }, - onDeleteClick = {} - ) - } - - // Perform click on the card - composeTestRule.onNodeWithText("John Doe").performClick() - - // Verify the click handler is called - assert(cardClicked) - } - - @Test - fun farmCardHandlesDeleteClick() { - var deleteClicked = false - composeTestRule.setContent { - FarmCard( - farm = Farm( - siteId = 1L, - farmerPhoto = "photo.jpg", - farmerName = "John Doe", - memberId = "12345", - village = "Sample Village", - district = "Sample District", - purchases = 10f, - size = 5.0f, - latitude = "12.34", - longitude = "56.78", - coordinates = listOf(Pair(12.34, 56.78)), - accuracyArray = listOf(5.0f), - synced = false, - scheduledForSync = false, - createdAt = System.currentTimeMillis(), - updatedAt = System.currentTimeMillis(), - needsUpdate = false - ), - onCardClick = {}, - onDeleteClick = { deleteClicked = true } - ) - } - - // Perform click on the delete button - composeTestRule.onNodeWithContentDescription("Delete").performClick() - - // Verify the delete click handler is called - assert(deleteClicked) - } - - @Test - fun farmCardDoesNotShowNeedsUpdateIfFalse() { - composeTestRule.setContent { - FarmCard( - farm = Farm( - siteId = 1L, - farmerPhoto = "photo.jpg", - farmerName = "John Doe", - memberId = "12345", - village = "Sample Village", - district = "Sample District", - purchases = 10f, - size = 5.0f, - latitude = "12.34", - longitude = "56.78", - coordinates = listOf(Pair(12.34, 56.78)), - accuracyArray = listOf(5.0f), - synced = false, - scheduledForSync = false, - createdAt = System.currentTimeMillis(), - updatedAt = System.currentTimeMillis(), - needsUpdate = false - ), - onCardClick = {}, - onDeleteClick = {} - ) - } - - // Verify the "needs update" label does not exist - composeTestRule.onNodeWithText("Needs update").assertDoesNotExist() - } -} - - */ \ No newline at end of file diff --git a/app/src/test/java/org/technoserve/farmcollector/ui/components/FarmListHeaderKtTest.kt b/app/src/test/java/org/technoserve/farmcollector/ui/components/FarmListHeaderKtTest.kt deleted file mode 100644 index 0b60e94..0000000 --- a/app/src/test/java/org/technoserve/farmcollector/ui/components/FarmListHeaderKtTest.kt +++ /dev/null @@ -1,197 +0,0 @@ -package org.technoserve.farmcollector.ui.components - -/* -import android.content.Context -import androidx.compose.ui.test.assertTextEquals -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput -import androidx.test.core.app.ApplicationProvider -import androidx.work.Configuration -import androidx.work.testing.WorkManagerTestInitHelper -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -@RunWith(RobolectricTestRunner::class) -//@Config(sdk = [33]) -@Config(sdk = [33], manifest = Config.NONE) -class FarmListHeaderKtTest { - - @get:Rule - val composeTestRule = createComposeRule() - -// @Before -// fun setUp() { -// val context = ApplicationProvider.getApplicationContext() -// -// // Create the configuration for WorkManager -// val config = Configuration.Builder() -// .setMinimumLoggingLevel(android.util.Log.DEBUG) -// .build() -// -// // Initialize WorkManager for testing -// WorkManagerTestInitHelper.initializeTestWorkManager(context, config) -// } - - @Test - fun farmListHeaderDisplaysTitleCorrectly() { - val title = "Farm List" - - composeTestRule.setContent { - FarmListHeader( - title = title, - onSearchQueryChanged = {}, - onBackClicked = {}, - showSearch = true, - showRestore = true, - onRestoreClicked = {} - ) - } - - // Verify the title is displayed - composeTestRule.onNodeWithText(title).assertExists() - } - -// @Test -// fun searchButtonTogglesSearchField() { -// val title = "Farm List" -// var searchQuery = "" -// -// composeTestRule.setContent { -// FarmListHeader( -// title = title, -// onSearchQueryChanged = { searchQuery = it }, -// onBackClicked = {}, -// showSearch = true, -// showRestore = false, -// onRestoreClicked = {} -// ) -// } -// -// // Initially, the search field should not be visible -// composeTestRule.onNodeWithTag("SearchField").assertDoesNotExist() -// -// // Click on the search button -// composeTestRule.onNodeWithContentDescription("Search").performClick() -// -// // The search field should now be visible -// composeTestRule.onNodeWithTag("SearchField").assertExists() -// -// // Perform a search -// val query = "test query" -// composeTestRule.onNodeWithTag("SearchField").performTextInput(query) -// -// // Verify that the search query was passed correctly -// assert(searchQuery == query) -// -// // Click on the search button again to hide the search field -// composeTestRule.onNodeWithContentDescription("Search").performClick() -// -// // The search field should no longer be visible -// composeTestRule.onNodeWithTag("SearchField").assertDoesNotExist() -// } - -// @Test -// fun searchQueryIsClearedWhenBackClicked() { -// var searchQuery = "some search query" -// -// composeTestRule.setContent { -// FarmListHeader( -// title = "Farm List", -// onSearchQueryChanged = { searchQuery = it }, -// onBackClicked = {}, -// showSearch = true, -// showRestore = false, -// onRestoreClicked = {} -// ) -// } -// -// // Click on the search button to show search field -// composeTestRule.onNodeWithContentDescription("Search").performClick() -// -// // Perform some search input -// composeTestRule.onNodeWithTag("SearchField").performTextInput("new query") -// -// // Verify the search query is updated -// composeTestRule.onNodeWithTag("SearchField").assertTextEquals("new query") -// -// // Click on the back button to clear the query -// composeTestRule.onNodeWithContentDescription("Back").performClick() -// -// // Verify that the search query is cleared -// composeTestRule.onNodeWithTag("SearchField").assertTextEquals("") -// } - - @Test - fun farmListHeaderHandlesBackClickCorrectly() { - var backClicked = false - - composeTestRule.setContent { - FarmListHeader( - title = "Farm List", - onSearchQueryChanged = {}, - onBackClicked = { backClicked = true }, - showSearch = true, - showRestore = false, - onRestoreClicked = {} - ) - } - - // Perform click on the back button - composeTestRule.onNodeWithContentDescription("Back").performClick() - - // Verify that the back click handler is called - assert(backClicked) - } - - @Test - fun restoreButtonVisibility() { - var restoreClicked = false - - composeTestRule.setContent { - FarmListHeader( - title = "Farm List", - onSearchQueryChanged = {}, - onBackClicked = {}, - showSearch = false, - showRestore = true, - onRestoreClicked = { restoreClicked = true } - ) - } - - // Verify the restore button is visible - composeTestRule.onNodeWithContentDescription("Restore").assertExists() - - // Perform click on the restore button - composeTestRule.onNodeWithContentDescription("Restore").performClick() - - // Verify that the restore click handler is called - assert(restoreClicked) - } - - @Test - fun restoreButtonNotVisibleWhenShowRestoreFalse() { - composeTestRule.setContent { - FarmListHeader( - title = "Farm List", - onSearchQueryChanged = {}, - onBackClicked = {}, - showSearch = false, - showRestore = false, - onRestoreClicked = {} - ) - } - - // Verify the restore button is NOT visible - composeTestRule.onNodeWithContentDescription("Restore").assertDoesNotExist() - } -} - - */ \ No newline at end of file diff --git a/app/src/test/java/org/technoserve/farmcollector/ui/components/FarmListHeaderPlotsKtTest.kt b/app/src/test/java/org/technoserve/farmcollector/ui/components/FarmListHeaderPlotsKtTest.kt deleted file mode 100644 index 416dda4..0000000 --- a/app/src/test/java/org/technoserve/farmcollector/ui/components/FarmListHeaderPlotsKtTest.kt +++ /dev/null @@ -1,269 +0,0 @@ -package org.technoserve.farmcollector.ui.components - -/* -import android.content.Context -import androidx.compose.ui.test.assertTextEquals -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput -import androidx.test.core.app.ApplicationProvider -import androidx.work.Configuration -import androidx.work.testing.WorkManagerTestInitHelper -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -@RunWith(RobolectricTestRunner::class) -//@Config(sdk = [33]) -@Config(sdk = [33], manifest = Config.NONE) -class FarmListHeaderPlotsKtTest{ - @get:Rule - val composeTestRule = createComposeRule() - -// @Before -// fun setUp() { -// val context = ApplicationProvider.getApplicationContext() -// -// // Create the configuration for WorkManager -// val config = Configuration.Builder() -// .setMinimumLoggingLevel(android.util.Log.DEBUG) -// .build() -// -// // Initialize WorkManager for testing -// WorkManagerTestInitHelper.initializeTestWorkManager(context, config) -// } - - @Test - fun farmListHeaderPlotsDisplaysTitleCorrectly() { - val title = "Farm Plots" - - composeTestRule.setContent { - FarmListHeaderPlots( - title = title, - onBackClicked = {}, - onExportClicked = {}, - onShareClicked = {}, - onImportClicked = {}, - onSearchQueryChanged = {}, - showExport = true, - showShare = true, - showSearch = true, - onRestoreClicked = {} - ) - } - - // Verify the title is displayed - composeTestRule.onNodeWithText(title).assertExists() - } - -// @Test -// fun searchButtonTogglesSearchField() { -// val title = "Farm Plots" -// var searchQuery = "" -// -// composeTestRule.setContent { -// FarmListHeaderPlots( -// title = title, -// onBackClicked = {}, -// onExportClicked = {}, -// onShareClicked = {}, -// onImportClicked = {}, -// onSearchQueryChanged = { searchQuery = it }, -// showExport = false, -// showShare = false, -// showSearch = true, -// onRestoreClicked = {} -// ) -// } -// -// // Initially, the search field should not be visible -// composeTestRule.onNodeWithTag("SearchField").assertDoesNotExist() -// -// // Click on the search button -// composeTestRule.onNodeWithContentDescription("Search").performClick() -// -// // The search field should now be visible -// composeTestRule.onNodeWithTag("SearchField").assertExists() -// -// // Perform a search -// val query = "plot search" -// composeTestRule.onNodeWithTag("SearchField").performTextInput(query) -// -// // Verify that the search query was passed correctly -// assert(searchQuery == query) -// -// // Click on the search button again to hide the search field -// composeTestRule.onNodeWithContentDescription("Search").performClick() -// -// // The search field should no longer be visible -// composeTestRule.onNodeWithTag("SearchField").assertDoesNotExist() -// } - -// @Test -// fun searchQueryIsClearedWhenBackClicked() { -// var searchQuery = "some search query" -// -// composeTestRule.setContent { -// FarmListHeaderPlots( -// title = "Farm Plots", -// onBackClicked = {}, -// onExportClicked = {}, -// onShareClicked = {}, -// onImportClicked = {}, -// onSearchQueryChanged = { searchQuery = it }, -// showExport = false, -// showShare = false, -// showSearch = true, -// onRestoreClicked = {} -// ) -// } -// -// // Click on the search button to show search field -// composeTestRule.onNodeWithContentDescription("Search").performClick() -// -// // Perform some search input -// composeTestRule.onNodeWithTag("SearchField").performTextInput("new plot query") -// -// // Verify the search query is updated -// composeTestRule.onNodeWithTag("SearchField").assertTextEquals("new plot query") -// -// // Click on the back button to clear the query -// composeTestRule.onNodeWithContentDescription("Back").performClick() -// -// // Verify that the search query is cleared -// composeTestRule.onNodeWithTag("SearchField").assertTextEquals("") -// } - - @Test - fun farmListHeaderPlotsHandlesBackClickCorrectly() { - var backClicked = false - - composeTestRule.setContent { - FarmListHeaderPlots( - title = "Farm Plots", - onBackClicked = { backClicked = true }, - onExportClicked = {}, - onShareClicked = {}, - onImportClicked = {}, - onSearchQueryChanged = {}, - showExport = false, - showShare = false, - showSearch = true, - onRestoreClicked = {} - ) - } - - // Perform click on the back button - composeTestRule.onNodeWithContentDescription("Back").performClick() - - // Verify that the back click handler is called - assert(backClicked) - } - - @Test - fun restoreButtonVisibility() { - var restoreClicked = false - - composeTestRule.setContent { - FarmListHeaderPlots( - title = "Farm Plots", - onBackClicked = {}, - onExportClicked = {}, - onShareClicked = {}, - onImportClicked = {}, - onSearchQueryChanged = {}, - showExport = false, - showShare = false, - showSearch = false, - onRestoreClicked = { restoreClicked = true } - ) - } - - // Verify the restore button is visible - composeTestRule.onNodeWithContentDescription("Restore").assertExists() - - // Perform click on the restore button - composeTestRule.onNodeWithContentDescription("Restore").performClick() - - // Verify that the restore click handler is called - assert(restoreClicked) - } - -// @Test -// fun restoreButtonNotVisibleWhenShowRestoreFalse() { -// composeTestRule.setContent { -// FarmListHeaderPlots( -// title = "Farm Plots", -// onBackClicked = {}, -// onExportClicked = {}, -// onShareClicked = {}, -// onImportClicked = {}, -// onSearchQueryChanged = {}, -// showExport = false, -// showShare = false, -// showSearch = false, -// onRestoreClicked = {} -// ) -// } -// -// // Verify the restore button is NOT visible -// composeTestRule.onNodeWithContentDescription("Restore").assertDoesNotExist() -// } - - @Test - fun exportButtonVisibility() { - var exportClicked = false - - composeTestRule.setContent { - FarmListHeaderPlots( - title = "Farm Plots", - onBackClicked = {}, - onExportClicked = { exportClicked = true }, - onShareClicked = {}, - onImportClicked = {}, - onSearchQueryChanged = {}, - showExport = true, - showShare = false, - showSearch = false, - onRestoreClicked = {} - ) - } - - // Verify the export button is visible - composeTestRule.onNodeWithContentDescription("Export").assertExists() - - // Perform click on the export button - composeTestRule.onNodeWithContentDescription("Export").performClick() - - // Verify that the export click handler is called - assert(exportClicked) - } - - @Test - fun exportButtonNotVisibleWhenShowExportFalse() { - composeTestRule.setContent { - FarmListHeaderPlots( - title = "Farm Plots", - onBackClicked = {}, - onExportClicked = {}, - onShareClicked = {}, - onImportClicked = {}, - onSearchQueryChanged = {}, - showExport = false, - showShare = false, - showSearch = false, - onRestoreClicked = {} - ) - } - - // Verify the export button is NOT visible - composeTestRule.onNodeWithContentDescription("Export").assertDoesNotExist() - } -} - */ \ No newline at end of file diff --git a/app/src/test/java/org/technoserve/farmcollector/ui/components/FormatSelectionDialogKtTest.kt b/app/src/test/java/org/technoserve/farmcollector/ui/components/FormatSelectionDialogKtTest.kt deleted file mode 100644 index 9084f58..0000000 --- a/app/src/test/java/org/technoserve/farmcollector/ui/components/FormatSelectionDialogKtTest.kt +++ /dev/null @@ -1,156 +0,0 @@ -package org.technoserve.farmcollector.ui.components - -/* -import android.content.Context -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.test.core.app.ApplicationProvider -import androidx.work.Configuration -import androidx.work.WorkManager -import androidx.work.testing.WorkManagerTestInitHelper -import org.junit.Assert.* -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -@RunWith(RobolectricTestRunner::class) -//@Config(sdk = [33]) -@Config(sdk = [33], manifest = Config.NONE) -class FormatSelectionDialogKtTest{ - @get:Rule - val composeTestRule = createComposeRule() - -// @Before -// fun setUp() { -// val context = ApplicationProvider.getApplicationContext() -// -// // Create the configuration for WorkManager -// val config = Configuration.Builder() -// .setMinimumLoggingLevel(android.util.Log.DEBUG) -// .build() -// -// // Initialize WorkManager for testing -// WorkManagerTestInitHelper.initializeTestWorkManager(context, config) -// } - - @Test - fun formatSelectionDialogDisplaysCorrectOptions() { - // Set up the dialog - composeTestRule.setContent { - FormatSelectionDialog( - onDismiss = {}, - onFormatSelected = {} - ) - } - - // Verify the dialog title - composeTestRule.onNodeWithText("Select File Format").assertExists() - - // Verify the format options are displayed - composeTestRule.onNodeWithText("CSV").assertExists() - composeTestRule.onNodeWithText("GeoJSON").assertExists() - } - -// @Test -// fun formatSelectionDialogSelectsCSV() { -// var selectedFormat = "" -// -// // Set up the dialog with a callback for format selection -// composeTestRule.setContent { -// FormatSelectionDialog( -// onDismiss = {}, -// onFormatSelected = { format -> selectedFormat = format } -// ) -// } -// -// // Select the CSV radio button -// composeTestRule.onNodeWithText("CSV").performClick() -// -// // Verify that the selected format is CSV -// assert(selectedFormat == "CSV") -// } - -// @Test -// fun formatSelectionDialogSelectsGeoJSON() { -// var selectedFormat = "" -// -// // Set up the dialog with a callback for format selection -// composeTestRule.setContent { -// FormatSelectionDialog( -// onDismiss = {}, -// onFormatSelected = { format -> selectedFormat = format } -// ) -// } -// -// // Select the GeoJSON radio button -// composeTestRule.onNodeWithText("GeoJSON").performClick() -// -// // Verify that the selected format is GeoJSON -// assert(selectedFormat == "GeoJSON") -// } - - @Test - fun formatSelectionDialogConfirmButtonCallsOnFormatSelected() { - var selectedFormat = "" - var dismissed = false - - // Set up the dialog with onDismiss and onFormatSelected callbacks - composeTestRule.setContent { - FormatSelectionDialog( - onDismiss = { dismissed = true }, - onFormatSelected = { format -> selectedFormat = format } - ) - } - - // Select the CSV format - composeTestRule.onNodeWithText("CSV").performClick() - - // Click the confirm button - composeTestRule.onNodeWithText("Confirm").performClick() - - // Verify that the format is selected and onDismiss is called - assert(selectedFormat == "CSV") - assert(dismissed) - } - - @Test - fun formatSelectionDialogDismissButtonCallsOnDismiss() { - var dismissed = false - - // Set up the dialog with the onDismiss callback - composeTestRule.setContent { - FormatSelectionDialog( - onDismiss = { dismissed = true }, - onFormatSelected = {} - ) - } - - // Click the dismiss button - composeTestRule.onNodeWithText("Cancel").performClick() - - // Verify that the dismiss callback is called - assert(dismissed) - } - -// @Test -// fun formatSelectionDialogStartsWithCSVSelectedByDefault() { -// var selectedFormat = "" -// -// // Set up the dialog with a callback for format selection -// composeTestRule.setContent { -// FormatSelectionDialog( -// onDismiss = {}, -// onFormatSelected = { format -> selectedFormat = format } -// ) -// } -// -// // Verify that CSV is selected by default -// assert(selectedFormat == "CSV") -// } -} - - */ \ No newline at end of file diff --git a/app/src/test/java/org/technoserve/farmcollector/ui/components/InvalidPolygonDialogKtTest.kt b/app/src/test/java/org/technoserve/farmcollector/ui/components/InvalidPolygonDialogKtTest.kt deleted file mode 100644 index 426549c..0000000 --- a/app/src/test/java/org/technoserve/farmcollector/ui/components/InvalidPolygonDialogKtTest.kt +++ /dev/null @@ -1,136 +0,0 @@ -package org.technoserve.farmcollector.ui.components - -import android.content.Context -import androidx.compose.ui.test.* -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.runtime.mutableStateOf -import androidx.test.core.app.ApplicationProvider -import androidx.work.Configuration -import androidx.work.testing.WorkManagerTestInitHelper -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -/* -@RunWith(RobolectricTestRunner::class) -//@Config(sdk = [33]) -@Config(sdk = [33], manifest = Config.NONE) -class InvalidPolygonDialogKtTest { - - @get:Rule - val composeTestRule = createComposeRule() - - @Before -// fun setUp() { -// val context = ApplicationProvider.getApplicationContext() -// -// // Create the configuration for WorkManager -// val config = Configuration.Builder() -// .setMinimumLoggingLevel(android.util.Log.DEBUG) -// .build() -// -// // Initialize WorkManager for testing -// WorkManagerTestInitHelper.initializeTestWorkManager(context, config) -// } - -// @Test -// fun invalidPolygonDialogDisplaysCorrectly() { -// // Create a mutable state to control the visibility of the dialog -// val showDialog = mutableStateOf(true) -// -// // Set up the dialog -// composeTestRule.setContent { -// InvalidPolygonDialog( -// showDialog = showDialog, -// onDismiss = {} -// ) -// } -// -// // Verify that the dialog title and message are displayed -// composeTestRule.onNodeWithText("Invalid Polygon").assertExists() // Assuming string resource is "invalid_polygon_title" -// composeTestRule.onNodeWithText("The polygon is invalid. Please check the coordinates.").assertExists() // Assuming string resource is "invalid_polygon_message" -// } - - @Test - fun invalidPolygonDialogDismissesWhenConfirmButtonClicked() { - var dismissed = false - val showDialog = mutableStateOf(true) - - // Set up the dialog with the dismiss callback - composeTestRule.setContent { - InvalidPolygonDialog( - showDialog = showDialog, - onDismiss = { dismissed = true } - ) - } - - // Verify the dialog is visible initially - composeTestRule.onNodeWithText("Invalid Polygon").assertExists() - - // Click the confirm button - composeTestRule.onNodeWithText("OK").performClick() - - // Verify that the dismiss callback is called - assert(dismissed) - } - - @Test - fun invalidPolygonDialogDoesNotShowWhenShowDialogIsFalse() { - val showDialog = mutableStateOf(false) - - // Set up the dialog - composeTestRule.setContent { - InvalidPolygonDialog( - showDialog = showDialog, - onDismiss = {} - ) - } - - // Verify that the dialog is not shown - composeTestRule.onNodeWithText("Invalid Polygon").assertDoesNotExist() - } - -// @Test -// fun invalidPolygonDialogDismissesWhenDialogIsDismissed() { -// val showDialog = mutableStateOf(true) -// var dismissed = false -// -// // Set up the dialog with the dismiss callback -// composeTestRule.setContent { -// InvalidPolygonDialog( -// showDialog = showDialog, -// onDismiss = { dismissed = true } -// ) -// } -// -// // Click outside the dialog to dismiss it -// composeTestRule.onNodeWithTag("DialogBackground").performClick() -// -// // Verify that the dialog is dismissed when the background is clicked -// assert(dismissed) -// } - - @Test - fun invalidPolygonDialogRetainsStateWhenShowDialogIsTrue() { - val showDialog = mutableStateOf(true) - - // Set up the dialog - composeTestRule.setContent { - InvalidPolygonDialog( - showDialog = showDialog, - onDismiss = {} - ) - } - - // Verify the dialog is displayed initially - composeTestRule.onNodeWithText("Invalid Polygon").assertExists() - - // Now set showDialog to false and verify the dialog is no longer visible - showDialog.value = false - composeTestRule.onNodeWithText("Invalid Polygon").assertDoesNotExist() - } -} - - */ diff --git a/app/src/test/java/org/technoserve/farmcollector/ui/components/KeepPolygonDialogKtTest.kt b/app/src/test/java/org/technoserve/farmcollector/ui/components/KeepPolygonDialogKtTest.kt deleted file mode 100644 index 0cfcb44..0000000 --- a/app/src/test/java/org/technoserve/farmcollector/ui/components/KeepPolygonDialogKtTest.kt +++ /dev/null @@ -1,81 +0,0 @@ -package org.technoserve.farmcollector.ui.components - -/* -import android.content.Context -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.test.core.app.ApplicationProvider -import androidx.work.Configuration -import androidx.work.testing.WorkManagerTestInitHelper -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -@RunWith(RobolectricTestRunner::class) -//@Config(sdk = [33]) -@Config(sdk = [33], manifest = Config.NONE) -class KeepPolygonDialogKtTest { - - @get:Rule - val composeTestRule = createComposeRule() - - @Before - fun setUp() { - val context = ApplicationProvider.getApplicationContext() - - // Create the configuration for WorkManager - val config = Configuration.Builder() - .setMinimumLoggingLevel(android.util.Log.DEBUG) - .build() - - // Initialize WorkManager for testing - WorkManagerTestInitHelper.initializeTestWorkManager(context, config) - } - -// @Test -// fun testKeepPolygonDialogDisplaysCorrectly() { -// composeTestRule.setContent { -// KeepPolygonDialog( -// onDismiss = {}, -// onKeepExisting = {}, -// onCaptureNew = {} -// ) -// } -// -// // Verify the dialog title and text are displayed -// composeTestRule.onNodeWithText("Update Polygon").assertExists() -// composeTestRule.onNodeWithText("Keep existing polygon or capture new").assertExists() -// -// // Verify the buttons are displayed -// composeTestRule.onNodeWithText("Keep Existing").assertExists() -// composeTestRule.onNodeWithText("Capture New").assertExists() -// } - - @Test - fun testKeepPolygonDialogButtonActions() { - var keepExistingClicked = false - var captureNewClicked = false - - composeTestRule.setContent { - KeepPolygonDialog( - onDismiss = {}, - onKeepExisting = { keepExistingClicked = true }, - onCaptureNew = { captureNewClicked = true } - ) - } - - // Click the "Keep Existing" button - composeTestRule.onNodeWithText("Keep Existing").performClick() - assert(keepExistingClicked) - - // Click the "Capture New" button - composeTestRule.onNodeWithText("Capture New").performClick() - assert(captureNewClicked) - } -} - - */ diff --git a/app/src/test/java/org/technoserve/farmcollector/ui/components/RestoreDataAlertKtTest.kt b/app/src/test/java/org/technoserve/farmcollector/ui/components/RestoreDataAlertKtTest.kt deleted file mode 100644 index 1b6281e..0000000 --- a/app/src/test/java/org/technoserve/farmcollector/ui/components/RestoreDataAlertKtTest.kt +++ /dev/null @@ -1,194 +0,0 @@ -package org.technoserve.farmcollector.ui.components - -import android.content.Context -import org.junit.Assert.* - - - - -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.test.* -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.test.core.app.ApplicationProvider -import androidx.work.Configuration -import androidx.work.testing.WorkManagerTestInitHelper -import dagger.hilt.android.testing.HiltAndroidTest -import io.mockk.every -import io.mockk.invoke -import io.mockk.mockk -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.kotlin.* -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.technoserve.farmcollector.viewmodels.FarmViewModel -import org.mockito.Mockito.mock - -/* -@HiltAndroidTest -@RunWith(RobolectricTestRunner::class) -//@Config(sdk = [33]) -@Config(sdk = [33], manifest = Config.NONE) -class RestoreDataAlertKtTest{ - - @get:Rule - val composeTestRule = createComposeRule() - - @Before - fun setUp() { - val context = ApplicationProvider.getApplicationContext() - - // Create the configuration for WorkManager - val config = Configuration.Builder() - .setMinimumLoggingLevel(android.util.Log.DEBUG) - .build() - - // Initialize WorkManager for testing - WorkManagerTestInitHelper.initializeTestWorkManager(context, config) - } - - @Test - fun restoreDataAlertDisplaysContentCorrectly() { - composeTestRule.setContent { - RestoreDataAlert( - showDialog = true, - onDismiss = {}, - deviceId = "sampleDeviceId", - farmViewModel = FarmViewModel(ApplicationProvider.getApplicationContext()) - ) - } - - // Assert dialog title and description - composeTestRule.onNodeWithText("Data Restoration").assertExists() - composeTestRule.onNodeWithText( - "During restoration, you will recover some of the previously deleted records. Do you want to continue?" - ).assertExists() - - // Assert buttons exist - composeTestRule.onNodeWithText("Continue").assertExists() - composeTestRule.onNodeWithText("Cancel").assertExists() - } - -// @Test -// fun restoreDataAlertCallsRestoreDataOnContinueClick() { -// val mockFarmViewModel = mockk(relaxed = true) -// var isDismissed = false -// -// composeTestRule.setContent { -// RestoreDataAlert( -// showDialog = true, -// onDismiss = { isDismissed = true }, -// deviceId = "sampleDeviceId", -// farmViewModel = mockFarmViewModel -// ) -// } -// -// // Click the "Continue" button -// composeTestRule.onNodeWithText("Continue").performClick() -// -// // Verify `restoreData` is called -// verify { -// mockFarmViewModel.restoreData( -// deviceId = "sampleDeviceId", -// phoneNumber = "", -// email = "", -// farmViewModel = mockFarmViewModel, -// onCompletion = any() -// ) -// } -// -// // Assert dialog is dismissed -// assert(isDismissed) -// } - - @Test - fun restoreDataAlertCallsOnDismissOnCancelClick() { - var isDismissed = false - - composeTestRule.setContent { - RestoreDataAlert( - showDialog = true, - onDismiss = { isDismissed = true }, - deviceId = "sampleDeviceId", - farmViewModel = FarmViewModel(ApplicationProvider.getApplicationContext()) - ) - } - - // Click the "Cancel" button - composeTestRule.onNodeWithText("Cancel").performClick() - - // Assert dialog is dismissed - assert(isDismissed) - } - - @Test - fun restoreDataAlertShowsSuccessMessageWhenRestoreSucceeds() { - val mockFarmViewModel = mockk(relaxed = true) - var isDismissed = false - - every { - mockFarmViewModel.restoreData( - any(), - any(), - any(), - any(), - captureLambda() - ) - } answers { - lambda<(Boolean) -> Unit>().invoke(true) - } - - composeTestRule.setContent { - RestoreDataAlert( - showDialog = true, - onDismiss = { isDismissed = true }, - deviceId = "sampleDeviceId", - farmViewModel = mockFarmViewModel - ) - } - - // Click the "Continue" button - composeTestRule.onNodeWithText("Continue").performClick() - - // Assert dialog is dismissed and success message logic executes - assert(isDismissed) - } - -// @Test -// fun restoreDataAlertShowsFailureMessageWhenRestoreFails() { -// val mockFarmViewModel = mockk(relaxed = true) -// var isDismissed = false -// -// every { -// mockFarmViewModel.restoreData( -// any(), -// any(), -// any(), -// any(), -// captureLambda() -// ) -// } answers { -// lambda<(Boolean) -> Unit>().invoke(false) -// } -// -// composeTestRule.setContent { -// RestoreDataAlert( -// showDialog = true, -// onDismiss = { isDismissed = true }, -// deviceId = "sampleDeviceId", -// farmViewModel = mockFarmViewModel -// ) -// } -// -// // Click the "Continue" button -// composeTestRule.onNodeWithText("Continue").performClick() -// -// // Assert failure message logic executes -// composeTestRule.onNodeWithText("Data restoration failed").assertExists() -// assert(isDismissed) -// } -} - - */ \ No newline at end of file diff --git a/app/src/test/java/org/technoserve/farmcollector/ui/components/SiteCardKtTest.kt b/app/src/test/java/org/technoserve/farmcollector/ui/components/SiteCardKtTest.kt deleted file mode 100644 index 0217ab6..0000000 --- a/app/src/test/java/org/technoserve/farmcollector/ui/components/SiteCardKtTest.kt +++ /dev/null @@ -1,199 +0,0 @@ -package org.technoserve.farmcollector.ui.components -/* -import android.content.Context -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.test.core.app.ApplicationProvider -import androidx.work.Configuration -import androidx.work.testing.WorkManagerTestInitHelper -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.mock -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.technoserve.farmcollector.database.models.CollectionSite - -@RunWith(RobolectricTestRunner::class) -//@Config(sdk = [33]) -@Config(sdk = [33], manifest = Config.NONE) -class SiteCardKtTest{ - @get:Rule - val composeTestRule = createComposeRule() - -// @Before -// fun setUp() { -// val context = ApplicationProvider.getApplicationContext() -// -// // Create the configuration for WorkManager -// val config = Configuration.Builder() -// .setMinimumLoggingLevel(android.util.Log.DEBUG) -// .build() -// -// // Initialize WorkManager for testing -// WorkManagerTestInitHelper.initializeTestWorkManager(context, config) -// } - -// @Test -// fun siteCardDisplaysContentCorrectly() { -// val testSite = CollectionSite( -// name = "Sample Site", -// agentName = "John Agent", -// phoneNumber = "", -// email = "john@example.com", -// village = "Sample Village", -// district = "Sample District", -// createdAt = System.currentTimeMillis(), -// updatedAt = System.currentTimeMillis() -// ) -// val totalFarms = 10 -// val farmsWithIncompleteData = 2 -// -// composeTestRule.setContent { -// SiteCard( -// site = testSite, -// onCardClick = {}, -// totalFarms = totalFarms, -// farmsWithIncompleteData = farmsWithIncompleteData, -// onDeleteClick = {}, -// farmViewModel = mock() -// ) -// } -// -// // Verify site name, agent name, and phone number -// composeTestRule.onNodeWithText("Sample Site").assertExists() -// composeTestRule.onNodeWithText("Agent name: John Agent").assertExists() -// composeTestRule.onNodeWithText("Phone number: 1234567890").assertExists() -// -// // Verify total farms and incomplete farms -// composeTestRule.onNodeWithText("Total farms: 10").assertExists() -// composeTestRule.onNodeWithText("Farms with incomplete data: 2").assertExists() -// } - -// @Test -// fun siteCardHandlesCardClick() { -// var cardClicked = false -// -// composeTestRule.setContent { -// SiteCard( -// site = CollectionSite( -// name = "Sample Site", -// agentName = "John Agent", -// phoneNumber = "", -// email = "john@example.com", -// village = "Sample Village", -// district = "Sample District", -// createdAt = System.currentTimeMillis(), -// updatedAt = System.currentTimeMillis() -// ), -// onCardClick = { cardClicked = true }, -// totalFarms = 10, -// farmsWithIncompleteData = 2, -// onDeleteClick = {}, -// farmViewModel = mock() -// ) -// } -// -// // Perform click on the card -// composeTestRule.onNodeWithText("Sample Site").performClick() -// -// // Verify the click handler is called -// assert(cardClicked) -// } - -// @Test -// fun siteCardHandlesDeleteClick() { -// var deleteClicked = false -// -// composeTestRule.setContent { -// SiteCard( -// site = CollectionSite( -// name = "Sample Site", -// agentName = "John Agent", -// phoneNumber = "", -// email = "john@example.com", -// village = "Sample Village", -// district = "Sample District", -// createdAt = System.currentTimeMillis(), -// updatedAt = System.currentTimeMillis() -// ), -// onCardClick = {}, -// totalFarms = 10, -// farmsWithIncompleteData = 2, -// onDeleteClick = { deleteClicked = true }, -// farmViewModel = mock() -// ) -// } -// -// // Perform click on the delete button -// composeTestRule.onNodeWithContentDescription("Delete").performClick() -// -// // Verify the delete click handler is called -// assert(deleteClicked) -// } - -// @Test -// fun siteCardOpensUpdateDialogOnEditClick() { -// val showDialog = mutableStateOf(false) -// -// composeTestRule.setContent { -// SiteCard( -// site = CollectionSite( -// name = "Sample Site", -// agentName = "John Agent", -// phoneNumber = "", -// email = "john@example.com", -// village = "Sample Village", -// district = "Sample District", -// createdAt = System.currentTimeMillis(), -// updatedAt = System.currentTimeMillis() -// ), -// onCardClick = {}, -// totalFarms = 10, -// farmsWithIncompleteData = 2, -// onDeleteClick = {}, -// farmViewModel = mock() -// ) -// } -// -// // Perform click on the edit button -// composeTestRule.onNodeWithContentDescription("Update").performClick() -// -// // Assert the dialog is now displayed -// composeTestRule.onNodeWithText("Update Collection Dialog").assertExists() // Adjust this to match the dialog's content -// } - -// @Test -// fun siteCardHidesPhoneNumberIfEmpty() { -// val testSite = CollectionSite( -// name = "Sample Site", -// agentName = "John Agent", -// phoneNumber = "", -// email = "john@example.com", -// village = "Sample Village", -// district = "Sample District", -// createdAt = System.currentTimeMillis(), -// updatedAt = System.currentTimeMillis() -// ) -// -// composeTestRule.setContent { -// SiteCard( -// site = testSite, -// onCardClick = {}, -// totalFarms = 10, -// farmsWithIncompleteData = 2, -// onDeleteClick = {}, -// farmViewModel = mock() -// ) -// } -// -// // Verify the phone number does not exist -// composeTestRule.onNodeWithText("Phone number: 1234567890").assertDoesNotExist() -// } -} - - */ \ No newline at end of file diff --git a/app/src/test/java/org/technoserve/farmcollector/viewmodels/FarmViewModelTest.kt b/app/src/test/java/org/technoserve/farmcollector/viewmodels/FarmViewModelTest.kt deleted file mode 100644 index e985120..0000000 --- a/app/src/test/java/org/technoserve/farmcollector/viewmodels/FarmViewModelTest.kt +++ /dev/null @@ -1,176 +0,0 @@ -package org.technoserve.farmcollector.viewmodels - -import android.app.Application -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.MutableLiveData -import kotlinx.coroutines.runBlocking -import org.junit.Assert.* -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.* -import org.mockito.MockitoAnnotations -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.technoserve.farmcollector.database.models.Farm -import org.technoserve.farmcollector.repositories.FarmRepository - -/* -//@RunWith(RobolectricTestRunner::class) -//@Config(sdk = [33]) -class FarmViewModelTest { - - @get:Rule - val instantTaskExecutorRule = InstantTaskExecutorRule() // Ensures LiveData updates occur synchronously - - @Mock - private lateinit var mockFarmRepository: FarmRepository - - private lateinit var farmViewModel: FarmViewModel - private lateinit var liveDataFarms: MutableLiveData> - - @Mock - private lateinit var mockApplication: Application - - @Before - fun setUp() { - MockitoAnnotations.openMocks(this) - - // Initialize LiveData - liveDataFarms = MutableLiveData() - `when`(mockFarmRepository.readAllFarms(anyLong())).thenReturn(liveDataFarms) - - - // Initialize ViewModel - farmViewModel = FarmViewModel(mockApplication).apply { - farmRepository = mockFarmRepository // Inject mock repository - } - } - - @Test - fun `addFarm adds farm if not duplicate`() = runBlocking { - // Given - val newFarm = createTestFarm() - val siteId = newFarm.siteId - liveDataFarms.value = emptyList() // Initial state - - // When - `when`(mockFarmRepository.isFarmDuplicateBoolean(newFarm)).thenReturn(false) - farmViewModel.addFarm(newFarm, siteId) - - // Simulate repository adding farm - liveDataFarms.value = listOf(newFarm) - - // Then - verify(mockFarmRepository).addFarm(newFarm) - val updatedFarms = farmViewModel.farms.value - assertNotNull(updatedFarms) - assertTrue(updatedFarms!!.contains(newFarm)) - } - - @Test - fun `addFarm returns error if duplicate farm exists`() = runBlocking { - // Given - val duplicateFarm = createTestFarm() - val siteId = duplicateFarm.siteId - liveDataFarms.value = listOf(duplicateFarm) // Simulate duplicate - - // When - `when`(mockFarmRepository.isFarmDuplicateBoolean(duplicateFarm)).thenReturn(true) - - farmViewModel.addFarm(duplicateFarm, siteId) - - // Then - verify(mockFarmRepository, never()).addFarm(duplicateFarm) - val updatedFarms = farmViewModel.farms.value - assertNotNull(updatedFarms) - assertFalse(updatedFarms!!.contains(duplicateFarm)) // Farm was not added - } - - @Test - fun `updateFarm updates farm successfully`() = runBlocking { - // Given - val existingFarm = createTestFarm() - val updatedFarm = existingFarm.copy(farmerName = "Updated Farmer") - liveDataFarms.value = listOf(existingFarm) - - // When - `when`(mockFarmRepository.updateFarm(updatedFarm)).thenReturn(Unit) - farmViewModel.updateFarm(updatedFarm) - - // Simulate repository updating farm - liveDataFarms.value = listOf(updatedFarm) - - // Then - verify(mockFarmRepository).updateFarm(updatedFarm) - val updatedFarms = farmViewModel.farms.value - assertNotNull(updatedFarms) - assertTrue(updatedFarms!!.contains(updatedFarm)) - } - - @Test - fun `deleteFarmById deletes farm successfully`() = runBlocking { - // Given - val farmToDelete = createTestFarm() - liveDataFarms.value = listOf(farmToDelete) - - // When - `when`(mockFarmRepository.deleteFarmById(farmToDelete)).thenReturn(Unit) - farmViewModel.deleteFarmById(farmToDelete) - - // Simulate repository deleting farm - liveDataFarms.value = emptyList() - - // Then - verify(mockFarmRepository).deleteFarmById(farmToDelete) - val updatedFarms = farmViewModel.farms.value - assertNotNull(updatedFarms) - assertTrue(updatedFarms!!.isEmpty()) // Farm was deleted - } - - @Test - fun `addFarm updates LiveData`() = runBlocking { - // Given - val newFarm = createTestFarm() - val siteId = newFarm.siteId - liveDataFarms.value = emptyList() // Initial state - - // When - `when`(mockFarmRepository.isFarmDuplicateBoolean(newFarm)).thenReturn(false) - farmViewModel.addFarm(newFarm, siteId) - - // Simulate repository adding farm - liveDataFarms.value = listOf(newFarm) - - // Then - val updatedFarms = farmViewModel.farms.value - assertNotNull(updatedFarms) - assertTrue(updatedFarms!!.contains(newFarm)) - } - - private fun createTestFarm(): Farm { - return Farm( - siteId = 1L, - farmerPhoto = "photo.jpg", - farmerName = "New Farmer", - memberId = "12345", - village = "Village A", - district = "District X", - purchases = 10f, - size = 100f, - latitude = "12.34", - longitude = "56.78", - coordinates = listOf(Pair(12.34, 56.78)), - accuracyArray = listOf(5.0f), - synced = false, - scheduledForSync = false, - createdAt = System.currentTimeMillis(), - updatedAt = System.currentTimeMillis(), - needsUpdate = true - ) - } -} - - */ diff --git a/app/src/test/java/org/technoserve/farmcollector/viewmodels/LanguageViewModelTest.kt b/app/src/test/java/org/technoserve/farmcollector/viewmodels/LanguageViewModelTest.kt deleted file mode 100644 index 5a28719..0000000 --- a/app/src/test/java/org/technoserve/farmcollector/viewmodels/LanguageViewModelTest.kt +++ /dev/null @@ -1,271 +0,0 @@ -package org.technoserve.farmcollector.viewmodels - -import android.content.Context -import android.content.SharedPreferences -import android.content.res.Configuration -import android.content.res.Resources -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.test.core.app.ApplicationProvider -import androidx.work.testing.WorkManagerTestInitHelper -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito -import org.mockito.Mockito.mock -import org.mockito.kotlin.whenever -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.technoserve.farmcollector.database.models.Language -import java.util.Locale - -/* -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [33]) -class LanguageViewModelTest { - @get:Rule - val instantTaskExecutorRule = InstantTaskExecutorRule() - - private lateinit var languageViewModel: LanguageViewModel - private lateinit var context: Context - - private lateinit var sharedPreferences: SharedPreferences - private lateinit var resources: Resources - -// @Mock -// lateinit var sharedPreferences: SharedPreferences - - @Mock - lateinit var editor: SharedPreferences.Editor - - - @Before - fun setUp() { - // Mocking Context and SharedPreferences - context = mock() - sharedPreferences = mock() - resources = mock() - editor = mock() // Initialize editor mock - - // Mocking shared preferences return value - whenever( - sharedPreferences.getString( - "preferred_language", - Locale.getDefault().language - ) - ).thenReturn("en") - whenever(context.getSharedPreferences("settings", Context.MODE_PRIVATE)).thenReturn( - sharedPreferences - ) - whenever(context.resources).thenReturn(resources) - - // Initializing the ViewModel - languageViewModel = LanguageViewModel(ApplicationProvider.getApplicationContext()) - - val context = ApplicationProvider.getApplicationContext() - - // Create the configuration for WorkManager - val config = androidx.work.Configuration.Builder() - .setMinimumLoggingLevel(android.util.Log.DEBUG) - .build() - - // Initialize WorkManager for testing - WorkManagerTestInitHelper.initializeTestWorkManager(context, config) - - } - - - - - - @Test - fun testGetDefaultLanguage() { - // Given that the preferred language is "en" - val defaultLanguage = languageViewModel.getDefaultLanguage() - - // Then it should return the "en" language - assertEquals("en", defaultLanguage.code) - } - -// @Test -// fun testSavePreferredLanguage() { -// // Given a new language to save -// val newLanguage = Language("fr", "French") -// -// // When saving the new language -// languageViewModel.savePreferredLanguage(newLanguage) -// -// // Then SharedPreferences should store the new language -// Mockito.verify(editor).putString("preferred_language", "fr") -// Mockito.verify(editor).apply() -// -// // Additionally, verify the current language LiveData is updated -// assertEquals("fr", languageViewModel.currentLanguage.value?.code) -// } - -// @Test -// fun testSelectLanguage() { -// // Given a language "fr" and a mock context -// val newLanguage = Language("fr", "French") -// -// // When selecting the language -// languageViewModel.selectLanguage(newLanguage, ApplicationProvider.getApplicationContext()) -// -// // Then currentLanguage should be updated -// assertEquals("fr", languageViewModel.currentLanguage.value?.code) -// -// // And SharedPreferences should be updated with the new language -// Mockito.verify(editor).putString("preferred_language", "fr") -// Mockito.verify(editor).apply() -// } - - - @Test - fun testUpdateLocale() { - // Given a new locale "fr" and mock configuration - val locale = Locale("fr") - val mockContext = Mockito.mock(Context::class.java) - val resources = Mockito.mock(Resources::class.java) - val config = Configuration() - - // Mock resources and configuration - Mockito.`when`(mockContext.resources).thenReturn(resources) - Mockito.`when`(resources.configuration).thenReturn(config) - - // When updating the locale - languageViewModel.updateLocale(mockContext, locale) - - // Then verify the locale and layout direction were updated - assertEquals(locale, config.locales[0]) - assertEquals(locale.language, config.locale.language) - } - - @Test - fun testGetLocalizedLanguages() { - // Given a mock context with language strings - val context = ApplicationProvider.getApplicationContext() - val languages = languageViewModel.getLocalizedLanguages(context) - - // Then the languages list should contain the predefined languages - assertEquals(6, languages.size) - assertTrue(languages.any { it.code == "en" }) - assertTrue(languages.any { it.code == "fr" }) - } -} - - -// -//@RunWith(RobolectricTestRunner::class) -//@Config(sdk = [33]) -//class LanguageViewModelTest { -// -// @get:Rule -// val instantTaskExecutorRule = InstantTaskExecutorRule() -// -// private lateinit var languageViewModel: LanguageViewModel -// -// @Mock -// lateinit var sharedPreferences: SharedPreferences -// -// @Mock -// lateinit var editor: SharedPreferences.Editor -// -// @Before -// fun setUp() { -// // Initialize Mockito mocks -// MockitoAnnotations.openMocks(this) -// -// // Mock SharedPreferences and its Editor -// Mockito.`when`(sharedPreferences.edit()).thenReturn(editor) -// Mockito.`when`(editor.putString(Mockito.anyString(), Mockito.anyString())).thenReturn(editor) -// -// // Mock SharedPreferences behavior for getString -// Mockito.`when`( -// sharedPreferences.getString( -// Mockito.eq("preferred_language"), -// Mockito.anyString() -// ) -// ).thenReturn("en") -// -// // Use Robolectric's Application context -// val application = ApplicationProvider.getApplicationContext() -// -// // Initialize the ViewModel -// languageViewModel = LanguageViewModel(application) -// } -// -// @Test -// fun testGetDefaultLanguage() { -// // Given a default language in SharedPreferences is "en" -// val defaultLanguage = languageViewModel.getDefaultLanguage() -// -// // Then the default language should match -// assertEquals("en", defaultLanguage.code) -// } -// -// @Test -// fun testSavePreferredLanguage() { -// // Given a new language to save -// val newLanguage = Language("fr", "French") -// -// // When saving the new language -// languageViewModel.savePreferredLanguage(newLanguage) -// -// // Then verify interactions with SharedPreferences -// Mockito.verify(editor).putString("preferred_language", newLanguage.code) -// Mockito.verify(editor).apply() -// } -// -// @Test -// fun testSelectLanguage() { -// // Given a new language "fr" -// val newLanguage = Language("fr", "French") -// -// // When selecting the language -// languageViewModel.selectLanguage(newLanguage, ApplicationProvider.getApplicationContext()) -// // Then verify the current language LiveData is updated -// assertEquals("fr", languageViewModel.currentLanguage.value?.code) -// -// // Verify SharedPreferences is updated -// Mockito.verify(editor).putString("preferred_language", "fr") -// Mockito.verify(editor).apply() -// } -// -// @Test -// fun testUpdateLocale() { -// // Given a new locale "fr" and mock configuration -// val locale = Locale("fr") -// val mockContext = Mockito.mock(Context::class.java) -// val resources = Mockito.mock(Resources::class.java) -// val config = Configuration() -// -// // Mock resources and configuration -// Mockito.`when`(mockContext.resources).thenReturn(resources) -// Mockito.`when`(resources.configuration).thenReturn(config) -// -// // When updating the locale -// languageViewModel.updateLocale(mockContext, locale) -// -// // Then verify the locale and layout direction were updated -// assertEquals(locale, config.locales[0]) -// assertEquals(locale.language, config.locale.language) -// } -// -// @Test -// fun testGetLocalizedLanguages() { -// // Given predefined languages in the ViewModel -// val localizedLanguages = languageViewModel.getLocalizedLanguages( -// ApplicationProvider.getApplicationContext() -// ) -// -// // Then verify the list contains expected values -// assertEquals(6, localizedLanguages.size) -// assertTrue(localizedLanguages.any { it.code == "en" }) -// assertTrue(localizedLanguages.any { it.code == "fr" }) -// } -//} - - */ \ No newline at end of file