diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 416d3c1c..952263f1 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -23,8 +23,6 @@
-
-
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index fb7f4a8a..b589d56e 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 6b6f226f..406683f6 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -12,6 +12,7 @@
+
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 00000000..2b8a50fc
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cf8d26a5..1274c79c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,17 +1,127 @@
# Changelog
+## [2.6.0](https://github.com/PnX-SI/gn_mobile_occtax/releases/tag/2.6.0) (2023-05-08, release)
+
+### đ NouveautĂ©s
+
+* Support Android 13 (API 33).
+* Support des fonds Geoportail (https://github.com/PnX-SI/gn_mobile_maps/issues/8).
+ Le module ["maps"](https://github.com/PnX-SI/gn_mobile_maps/tree/develop/maps) supporte
+ officiellement les fonds suivants :
+ * [Geoportail WMTS](https://www.geoportail.gouv.fr)
+ * [OpenTopoMap](https://www.opentopomap.org)
+ * [OpenStreetMap](https://www.openstreetmap.org)
+ * [Wikimedia Maps](https://maps.wikimedia.org)
+* Gestion automatique des attributions sur les fonds en ligne (https://github.com/PnX-SI/gn_mobile_occtax/issues/191).
+ L'attribution est définie automatiquement selon la nature de la source si aucune n'a été précisée
+ dans la configuration. L'attribution n'est valable que pour les fonds en ligne.
+* Petites améliorations sur la documentation, notamment sur la gestion, la configuration et
+ l'ordonnancement des couches coté module ["maps"](https://github.com/PnX-SI/gn_mobile_maps/tree/develop/maps)
+ (https://github.com/PnX-SI/gn_mobile_occtax/issues/192)
+* La synchronisation périodique de l'ensemble des données issues de GeoNature est configuré par
+ défaut à 7 jours. Cette configuration n'est active uniquement que si la synchronisation périodique
+ n'est pas configurée (les paramÚtres `sync_periodicity_data` et `sync_periodicity_data_essential`
+ ne sont pas renseignés).
+* La synchronisation des données est maintenant décoléré de la synchronisation des relevés (https://github.com/PnX-SI/gn_mobile_occtax/issues/133).
+* La synchronisation des relevés se fait à la demande de l'utilisateur (https://github.com/PnX-SI/gn_mobile_occtax/issues/137).
+* La synchronisation des données s'exécute mantenant dans un contexte transactionnel afin de
+ toujours garantir une cohérence des données présentes localement.
+* Refonte de l'Ă©cran d'accueil pour mettre en valeur les relevĂ©s en cours ou prĂȘt Ă ĂȘtre
+ synchronisés. La partie paramétrage et synchronisation des données sont déportées dans le menu
+ latéral.
+
+### đ Corrections
+
+* Meilleur support de la taille des textes de l'interface selon la densité et la configuration
+ d'affichage du terminal (https://github.com/PnX-SI/gn_mobile_occtax/issues/217).
+
+### â ïž Notes de version
+
+* Code de version : 3200
+* Depuis sa version 2.12.0, GeoNature permet de gérer le contenu de la table
+ `gn_commons.t_mobile_apps` directement dans le backoffice du module "Admin" de GeoNature (https://github.com/PnX-SI/gn_mobile_occtax/issues/214)
+* Dans cette mĂȘme version, les mĂ©dias (incluant le dossier `mobile/` comprenant les fichiers APK et
+ le fichier `settings.json` d'Occtax-mobile) ont été déplacés du dossier `~/geonature/backend/static/`
+ Ă `~/geonature/backend/media/` (https://github.com/PnX-SI/gn_mobile_occtax/issues/214)
+
+## [2.6.0-rc2](https://github.com/PnX-SI/gn_mobile_occtax/releases/tag/2.6.0-rc2) (2023-04-29, pre-release)
+
+### đ NouveautĂ©s
+
+* La synchronisation périodique de l'ensemble des données issues de GeoNature est configuré par
+ défaut à 7 jours. Cette configuration n'est active uniquement que si la synchronisation périodique
+ n'est pas configurée (les paramÚtres `sync_periodicity_data` et `sync_periodicity_data_essential`
+ ne sont pas renseignés).
+
+### đ Corrections
+
+* Meilleur support de la taille des textes de l'interface selon la densité et la configuration
+ d'affichage du terminal (https://github.com/PnX-SI/gn_mobile_occtax/issues/217).
+* Le bouton "Envoyer les relevés" présenté sous forme de bouton icÎne dans la barre de menu en page
+ d'accueil est affiché sous forme de texte simple "Envoyer" et non plus sous forme d'icÎne pour
+ plus de clarté.
+* Petites améliorations sur la documentation, notamment sur la gestion, la configuration et
+ l'ordonnancement des couches coté module ["maps"](https://github.com/PnX-SI/gn_mobile_maps/tree/develop/maps)
+ (https://github.com/PnX-SI/gn_mobile_occtax/issues/192).
+
+### â ïž Notes de version
+
+* Code de version : 3191
+
+## [2.6.0-rc1](https://github.com/PnX-SI/gn_mobile_occtax/releases/tag/2.6.0-rc1) (2023-04-19, pre-release)
+
+### đ NouveautĂ©s
+
+* Support Android 13 (API 33).
+* Support des fonds Geoportail (https://github.com/PnX-SI/gn_mobile_maps/issues/8).
+ Le module ["maps"](https://github.com/PnX-SI/gn_mobile_maps/tree/develop/maps) supporte
+ officiellement les fonds suivants :
+ * [Geoportail WMTS](https://www.geoportail.gouv.fr)
+ * [OpenTopoMap](https://www.opentopomap.org)
+ * [OpenStreetMap](https://www.openstreetmap.org)
+ * [Wikimedia Maps](https://maps.wikimedia.org)
+* Gestion automatique des attributions sur les fonds en ligne (https://github.com/PnX-SI/gn_mobile_occtax/issues/191).
+ L'attribution est définie automatiquement selon la nature de la source si aucune n'a été précisée
+ dans la configuration. L'attribution n'est valable que pour les fonds en ligne.
+* Petites améliorations sur la documentation, notamment sur la gestion des couches coté module
+ ["maps"](https://github.com/PnX-SI/gn_mobile_maps/tree/develop/maps) (https://github.com/PnX-SI/gn_mobile_occtax/issues/192).
+
+### đ Corrections
+
+* Mise à jour de la liste des relevés et de leurs statuts pendant la synchronisation.
+
+### â ïž Notes de version
+
+* Code de version : 3187
+
+## [2.6.0-rc0](https://github.com/PnX-SI/gn_mobile_occtax/releases/tag/2.6.0-rc0) (2023-03-25, pre-release)
+
+### đ NouveautĂ©s
+
+* La synchronisation des données est maintenant décoléré de la synchronisation des relevés (https://github.com/PnX-SI/gn_mobile_occtax/issues/133).
+* La synchronisation des relevés se fait à la demande de l'utilisateur (https://github.com/PnX-SI/gn_mobile_occtax/issues/137).
+* La synchronisation des données s'exécute mantenant dans un contexte transactionnel afin de
+ toujours garantir une cohérence des données présentes localement.
+* Refonte de l'Ă©cran d'accueil pour mettre en valeur les relevĂ©s en cours ou prĂȘt Ă ĂȘtre
+ synchronisés. La partie paramétrage et synchronisation des données sont déportées dans le menu
+ latéral.
+
+### â ïž Notes de version
+
+* Code de version : 3181
+
## [2.5.0](https://github.com/PnX-SI/gn_mobile_occtax/releases/tag/2.5.0) (2023-03-21, release)
### đ NouveautĂ©s
* Gestion des médias sur la partie dénombrement (https://github.com/PnX-SI/gn_mobile_occtax/issues/84)
-* Refonte de la synchronisation des relevés en consommant les nouvelles APIs du module "Occtax".
+* Refonte de la synchronisation des relevés en consommant les APIs v2 du module "Occtax".
* Refonte de la gestion des relevés.
* Accélérer la saisie en permettant de mémoriser les derniÚres nomenclatures saisies sur la partie
dénombrement (https://github.com/PnX-SI/gn_mobile_occtax/issues/169).
* PossibilitĂ© de reprendre en Ă©dition un relevĂ© terminĂ© prĂȘt Ă ĂȘtre synchronisĂ© (https://github.com/PnX-SI/gn_mobile_occtax/issues/78).
-### đ Corrections
+### đ Corrections
* Valeur par défaut des champs "Min"et "Max" dans la partie dénombrement (https://github.com/PnX-SI/gn_mobile_occtax/issues/209, https://github.com/PnX-SI/gn_mobile_occtax/issues/210)
* Quelques petits ajustements sur la documentation de l'installation, notamment sur la récupération
@@ -20,13 +130,14 @@
### â ïž Notes de version
* Code de version : 3170
+* NĂ©cessite la version 2.10 (ou plus) de GeoNature.
* Suite à la refonte sur la partie gestion des relevés, le paramétrage de la nomenclature en
configuration avancée a évolué aussi (cf. [README.md](https://github.com/PnX-SI/gn_mobile_occtax#nomenclature-settings)),
notamment sur le nommage des attributs et du respect de la casse (Par exemple `MIN` devient `count_min`).
## [2.4.1-rc4](https://github.com/PnX-SI/gn_mobile_occtax/releases/tag/2.4.1-rc4) (2023-02-21, pre-release)
-### đ Corrections
+### đ Corrections
* Gestion des médias sur la partie dénombrement (https://github.com/PnX-SI/gn_mobile_occtax/issues/84)
* Accélérer la saisie en permettant de mémoriser les derniÚres nomenclatures saisies sur la partie
diff --git a/README.md b/README.md
index eaba8929..11815412 100644
--- a/README.md
+++ b/README.md
@@ -50,19 +50,36 @@ Example:
"base_path": "Offline_maps",
"layers": [
{
- "source": "plan.mbtiles",
- "label": "IGN plan"
+ "label": "IGN: plan v2",
+ "source": "https://wxs.ign.fr/essentiels/geoportail/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&STYLE=normal&TILEMATRIXSET=PM&FORMAT=image/png&LAYER=GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2"
},
{
- "source": "ortho.mbtiles",
- "label": "IGN ortho"
+ "label": "IGN: ortho",
+ "source": "https://wxs.ign.fr/ortho/geoportail/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&STYLE=normal&TILEMATRIXSET=PM&FORMAT=image/jpeg&LAYER=ORTHOIMAGERY.ORTHOPHOTOS"
},
{
"label": "OpenStreetMap",
- "source": "https://a.tile.openstreetmap.org/",
- "properties": {
- "attribution": "© OSM contributors"
- }
+ "source": [
+ "https://a.tile.openstreetmap.org",
+ "https://b.tile.openstreetmap.org",
+ "https://c.tile.openstreetmap.org"
+ ]
+ },
+ {
+ "label": "OpenTopoMap",
+ "source": [
+ "https://a.tile.opentopomap.org",
+ "https://b.tile.opentopomap.org",
+ "https://c.tile.opentopomap.org"
+ ]
+ },
+ {
+ "label": "IGN plan",
+ "source": "plan.mbtiles"
+ },
+ {
+ "label": "IGN ortho",
+ "source": "ortho.mbtiles"
},
{
"label": "Mailles 5x5",
@@ -252,10 +269,11 @@ Each property may be a simple string representing the nomenclature attribute to
## Upgrade git sub modules
-Do **NOT** modify directly any git sub modules (e.g. `commons`, `mountpoint`, `viewpager` and `maps`).
-Any changes should be made from each underlying git repository:
+Do **NOT** modify directly any git sub modules (e.g. `commons`, `compat`, `mountpoint`, `viewpager`
+and `maps`). Any changes should be made from each underlying git repository:
- `commons`: [gn_mobile_core](https://github.com/PnX-SI/gn_mobile_core) git repository
+- `compat`: [gn_mobile_core](https://github.com/PnX-SI/gn_mobile_core) git repository
- `datasync`: [gn_mobile_core](https://github.com/PnX-SI/gn_mobile_core) git repository
- `mountpoint`: [gn_mobile_core](https://github.com/PnX-SI/gn_mobile_core) git repository
- `viewpager`: [gn_mobile_core](https://github.com/PnX-SI/gn_mobile_core) git repository
diff --git a/build.gradle b/build.gradle
index 2bb3477f..cea15c3d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,10 +1,12 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = '1.6.21'
+ ext.klint_version = '11.3.1'
+ ext.kotlin_version = '1.8.0'
ext.androidx_hilt_version = '1.0.0'
- ext.hilt_version = '2.41'
+ ext.hilt_version = '2.44'
ext.retrofit_version = '2.9.0'
+ ext.room_version = '2.5.1'
ext.tinylog_version = '2.4.1'
repositories {
@@ -16,13 +18,17 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:7.1.3'
+ classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
- classpath "org.jlleitschuh.gradle:ktlint-gradle:9.4.1"
}
}
+plugins {
+ id 'com.google.dagger.hilt.android' version "$hilt_version" apply false
+ id 'org.jlleitschuh.gradle.ktlint' version "$klint_version"
+ id 'org.jlleitschuh.gradle.ktlint-idea' version "$klint_version"
+}
+
allprojects {
repositories {
google()
diff --git a/compat b/compat
new file mode 120000
index 00000000..d45d8c08
--- /dev/null
+++ b/compat
@@ -0,0 +1 @@
+gn_mobile_core/compat
\ No newline at end of file
diff --git a/docs/installation-fr.adoc b/docs/installation-fr.adoc
index 9548848c..4c6e9416 100644
--- a/docs/installation-fr.adoc
+++ b/docs/installation-fr.adoc
@@ -44,17 +44,17 @@ Il est nécessaire de gérer les fichiers de configuration, l'installation et la
L'application *_Occtax-mobile_* se chargera alors de récupérer automatiquement sur le serveur GeoNature la derniÚre version du fichier de configuration des applications et détectera les éventuelles mises à jour disponibles pour l'application.
-Pour cela, chargez l'APK de l'application *_Occtax-mobile_* ainsi que son fichier de configuration `settings.json` dans le dossier `$HOME/geonature/backend/static/mobile/occtax` du serveur GeoNature.
+Pour cela, chargez l'APK de l'application *_Occtax-mobile_* ainsi que son fichier de configuration `settings.json` dans le dossier `$HOME/geonature/backend/media/mobile/occtax` du serveur GeoNature.
Dans les commandes ci-dessous, remplacez `x.y.y` par le numéro (tag) de la version (release) utilisée.
* *_Occtax-mobile_* : https://github.com/PnX-SI/gn_mobile_occtax/releases
-Sur votre serveur GeoNature, créer le sous-répertoire de l'application mobile *_Occtax-mobile_*.
+Sur votre serveur GeoNature, créez le sous-répertoire de l'application mobile *_Occtax-mobile_*.
[source,shell]
----
-cd $HOME/geonature/backend/static/mobile
+cd ~/geonature/backend/media/mobile
mkdir occtax
----
@@ -68,7 +68,7 @@ wget https://github.com/PnX-SI/gn_mobile_occtax/releases/download/x.y.z/occtax-y
Créer le fichier de settings `settings.json` en suivant https://github.com/PnX-SI/gn_mobile_occtax#settings[la documentation] en fonction de la configuration de votre serveur GeoNature.
-Renseigner ensuite la table `gn_commons.t_mobile_apps` de la base de données.
+Renseigner ensuite la table `gn_commons.t_mobile_apps` directement dans la base de données ou depuis le backoffice du module "Admin" de GeoNature.
Pour trouver la valeur Ă renseigner dans le champs `version_code`, celui-ci est mentionnĂ© dans les releases, ou reportez-vous au fichier suivant en sĂ©lectionnant le tag de version oĂč la branche que vous utilisez :
@@ -80,14 +80,14 @@ Exemple de contenu de la table `gn_commons.t_mobile_apps` :
[source,csv]
----
-1;"OCCTAX";"static/mobile/occtax/occtax-2.0.0-pne-debug.apk";"";"fr.geonature.occtax2";"2555"
+1;"OCCTAX";"occtax/occtax-2.0.0-pne-debug.apk";"";"fr.geonature.occtax2";"2555"
----
Le rĂ©sultat peut ĂȘtre testĂ© en interrogeant directement la route `<URL_GEONATURE>/api/gn_commons/t_mobile_apps` qui est celle utilisĂ©e par l'application *_Occtax-mobile_* pour faire les mises Ă jour.
Installez ensuite l'application *_Occtax-mobile_* sur le terminal mobile.
-Récupérer le fichier APK de la version souhaitée dans la fichiers de la release (assets)
+Récupérer le fichier APK de la version souhaitée dans les fichiers de la release (assets)
Lancez l'application *_Occtax-mobile_* et déclarez l'URL de GeoNature et de TaxHub dans sa configuration (paramÚtres).
diff --git a/gn_mobile_core b/gn_mobile_core
index bcde27d7..2d920c99 160000
--- a/gn_mobile_core
+++ b/gn_mobile_core
@@ -1 +1 @@
-Subproject commit bcde27d7d4ccbe69470678f42c88d75f1d05df75
+Subproject commit 2d920c9902eef44089271419fa95b8b08cc40a38
diff --git a/gn_mobile_maps b/gn_mobile_maps
index b67e4b3d..50a73697 160000
--- a/gn_mobile_maps
+++ b/gn_mobile_maps
@@ -1 +1 @@
-Subproject commit b67e4b3dbac61878225aec98bda23ec7330a033f
+Subproject commit 50a736972d6012ad4eabb356b1214b2168e0ecf4
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 69d91e98..84ebdd7c 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
diff --git a/occtax/build.gradle b/occtax/build.gradle
index cb6c7e96..e311afbf 100644
--- a/occtax/build.gradle
+++ b/occtax/build.gradle
@@ -7,7 +7,7 @@ plugins {
}
android {
- compileSdkVersion 31
+ compileSdkVersion 33
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
@@ -21,9 +21,9 @@ android {
defaultConfig {
applicationId "fr.geonature.occtax2"
minSdkVersion 26
- targetSdkVersion 31
- versionCode 3170
- versionName "2.5.0"
+ targetSdkVersion 33
+ versionCode 3200
+ versionName "2.6.0"
buildConfigField "String", "BUILD_DATE", "\"" + new Date().getTime() + "\""
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
archivesBaseName = project.name + "-" + versionName
@@ -76,10 +76,12 @@ dependencies {
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
implementation 'androidx.cardview:cardview:1.0.0'
- implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
implementation 'androidx.preference:preference-ktx:1.2.0'
- implementation 'androidx.recyclerview:recyclerview:1.2.1'
- implementation 'com.google.android.material:material:1.5.0'
+ implementation 'androidx.recyclerview:recyclerview:1.3.0'
+ implementation 'com.google.android.material:material:1.8.0'
+ implementation 'androidx.work:work-runtime-ktx:2.8.1'
implementation "io.github.l4digital:fastscroll:2.0.1"
// Logging
@@ -87,12 +89,14 @@ dependencies {
runtimeOnly "org.tinylog:tinylog-impl:$tinylog_version"
// Testing dependencies
- testImplementation 'androidx.arch.core:core-testing:2.1.0'
- testImplementation 'androidx.test.ext:junit-ktx:1.1.3'
+ testImplementation 'androidx.arch.core:core-testing:2.2.0'
+ testImplementation 'androidx.test.ext:junit-ktx:1.1.5'
+ testImplementation 'androidx.work:work-testing:2.8.1'
testImplementation("com.squareup.okhttp3:mockwebserver:4.10.0")
- testImplementation 'io.mockk:mockk:1.12.3'
- testImplementation 'io.mockk:mockk-agent-jvm:1.12.3'
+ testImplementation 'io.mockk:mockk:1.13.4'
+ testImplementation 'io.mockk:mockk-agent-jvm:1.13.4'
testImplementation 'junit:junit:4.13.2'
- testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1'
- testImplementation 'org.robolectric:robolectric:4.8.1'
+ testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
+ testImplementation 'org.robolectric:robolectric:4.9.2'
+ testImplementation "org.tinylog:slf4j-tinylog:$tinylog_version"
}
diff --git a/occtax/src/main/AndroidManifest.xml b/occtax/src/main/AndroidManifest.xml
index bba5d78a..073c7c88 100644
--- a/occtax/src/main/AndroidManifest.xml
+++ b/occtax/src/main/AndroidManifest.xml
@@ -20,7 +20,8 @@
+ android:launchMode="singleTask"
+ android:theme="@style/AppTheme.NoActionBar">
@@ -68,7 +69,6 @@
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
-
= android.os.Build.VERSION_CODES.O) {
- val channel = NotificationChannel(
- CHANNEL_CHECK_INPUTS_TO_SYNCHRONIZE,
- getText(R.string.channel_name_check_inputs_to_synchronize),
- NotificationManager.IMPORTANCE_LOW
- ).apply {
- description = getString(R.string.channel_description_check_inputs_to_synchronize)
- setShowBadge(true)
- }
-
- // register this channel with the system
- notificationManager.createNotificationChannel(channel)
-
- return channel
+ private fun configureCheckInputsToSynchronizeChannel(notificationManager: NotificationManagerCompat): NotificationChannel {
+ val channel = NotificationChannel(
+ CHANNEL_CHECK_INPUTS_TO_SYNCHRONIZE,
+ getText(R.string.channel_name_check_inputs_to_synchronize),
+ NotificationManager.IMPORTANCE_LOW
+ ).apply {
+ description = getString(R.string.channel_description_check_inputs_to_synchronize)
+ setShowBadge(true)
}
- return null
- }
-
- private fun configureSynchronizeDataChannel(notificationManager: NotificationManagerCompat): NotificationChannel? {
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- val channel = NotificationChannel(
- CHANNEL_DATA_SYNCHRONIZATION,
- getText(R.string.channel_name_data_synchronization),
- NotificationManager.IMPORTANCE_LOW
- ).apply {
- description = getString(R.string.channel_description_data_synchronization)
- }
+ // register this channel with the system
+ notificationManager.createNotificationChannel(channel)
- // register this channel with the system
- notificationManager.createNotificationChannel(channel)
+ return channel
+ }
- return channel
+ private fun configureSynchronizeDataChannel(notificationManager: NotificationManagerCompat): NotificationChannel {
+ val channel = NotificationChannel(
+ CHANNEL_DATA_SYNCHRONIZATION,
+ getText(R.string.channel_name_data_synchronization),
+ NotificationManager.IMPORTANCE_LOW
+ ).apply {
+ description = getString(R.string.channel_description_data_synchronization)
}
- return null
+ // register this channel with the system
+ notificationManager.createNotificationChannel(channel)
+
+ return channel
}
private class TinylogUncaughtExceptionHandler : Thread.UncaughtExceptionHandler {
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/presentation/EditableNomenclatureTypeAdapter.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/presentation/EditableNomenclatureTypeAdapter.kt
index 4ddbdfe9..1f19fb4a 100644
--- a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/presentation/EditableNomenclatureTypeAdapter.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/presentation/EditableNomenclatureTypeAdapter.kt
@@ -1,5 +1,6 @@
package fr.geonature.occtax.features.nomenclature.presentation
+import android.annotation.SuppressLint
import android.text.Editable
import android.text.InputType
import android.text.TextWatcher
@@ -272,6 +273,7 @@ class EditableNomenclatureTypeAdapter(private val listener: OnEditableNomenclatu
/**
* Build the default label for given editable nomenclature type as fallback.
*/
+ @SuppressLint("DiscouragedApi")
fun getNomenclatureTypeLabel(mnemonic: String): String {
return itemView.resources.getIdentifier(
"nomenclature_${mnemonic.lowercase()}",
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/record/ObservationRecordModule.kt b/occtax/src/main/java/fr/geonature/occtax/features/record/ObservationRecordModule.kt
index e4824ebc..423f19c8 100644
--- a/occtax/src/main/java/fr/geonature/occtax/features/record/ObservationRecordModule.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/features/record/ObservationRecordModule.kt
@@ -12,7 +12,6 @@ import fr.geonature.commons.features.taxon.data.ITaxonLocalDataSource
import fr.geonature.commons.settings.IAppSettingsManager
import fr.geonature.datasync.api.IGeoNatureAPIClient
import fr.geonature.datasync.auth.IAuthManager
-import fr.geonature.datasync.packageinfo.ISynchronizeObservationRecordRepository
import fr.geonature.occtax.api.IOcctaxAPIClient
import fr.geonature.occtax.features.record.data.IMediaRecordLocalDataSource
import fr.geonature.occtax.features.record.data.IObservationRecordLocalDataSource
@@ -22,6 +21,7 @@ import fr.geonature.occtax.features.record.data.ObservationRecordLocalDataSource
import fr.geonature.occtax.features.record.data.ObservationRecordRemoteDataSourceImpl
import fr.geonature.occtax.features.record.repository.IMediaRecordRepository
import fr.geonature.occtax.features.record.repository.IObservationRecordRepository
+import fr.geonature.occtax.features.record.repository.ISynchronizeObservationRecordRepository
import fr.geonature.occtax.features.record.repository.MediaRecordRepositoryImpl
import fr.geonature.occtax.features.record.repository.ObservationRecordRepositoryImpl
import fr.geonature.occtax.features.record.repository.SynchronizeObservationRecordRepositoryImpl
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/record/data/ObservationRecordLocalDataSourceImpl.kt b/occtax/src/main/java/fr/geonature/occtax/features/record/data/ObservationRecordLocalDataSourceImpl.kt
index 437c4432..38dcc998 100644
--- a/occtax/src/main/java/fr/geonature/occtax/features/record/data/ObservationRecordLocalDataSourceImpl.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/features/record/data/ObservationRecordLocalDataSourceImpl.kt
@@ -40,7 +40,7 @@ class ObservationRecordLocalDataSourceImpl(
ObservationRecordJsonWriter()
override suspend fun readAll(): List {
- val exportedInput = FileUtils
+ val exportedObservationRecords = FileUtils
.getInputsFolder(context)
.walkTopDown()
.asFlow()
@@ -66,16 +66,17 @@ class ObservationRecordLocalDataSourceImpl(
return withContext(dispatcher) {
(
- exportedInput + preferenceManager.all
+ preferenceManager.all
.filterKeys { it.startsWith("${KEY_PREFERENCE_INPUT}_") }
.values
- .mapNotNull { if (it is String && it.isNotBlank()) runCatching { observationRecordJsonReader.read(it) }.getOrNull() else null }
- ).sortedBy { it.internalId }
+ .mapNotNull { if (it is String && it.isNotBlank()) runCatching { observationRecordJsonReader.read(it) }.getOrNull() else null } + exportedObservationRecords
+ ).distinctBy { it.internalId }
+ .sortedByDescending { it.dates.start }
}
}
override suspend fun read(id: Long): ObservationRecord = withContext(dispatcher) {
- val inputAsJson = preferenceManager.getString(
+ val observationRecordAsJson = preferenceManager.getString(
buildInputPreferenceKey(id),
null
)
@@ -88,11 +89,11 @@ class ObservationRecordLocalDataSourceImpl(
.takeIf { it.exists() }
?.readText()
- if (inputAsJson.isNullOrBlank()) {
+ if (observationRecordAsJson.isNullOrBlank()) {
throw ObservationRecordException.NotFoundException(id)
}
- runCatching { observationRecordJsonReader.read(inputAsJson) }
+ runCatching { observationRecordJsonReader.read(observationRecordAsJson) }
.onFailure { throw ObservationRecordException.ReadException(id) }
.getOrThrow()
}
@@ -162,10 +163,10 @@ class ObservationRecordLocalDataSourceImpl(
}
override suspend fun export(id: Long, settings: AppSettings?): ObservationRecord {
- val inputToExport = read(id)
+ val observationRecordToExport = read(id)
return export(
- inputToExport,
+ observationRecordToExport,
settings
)
}
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/record/domain/ObservationRecord.kt b/occtax/src/main/java/fr/geonature/occtax/features/record/domain/ObservationRecord.kt
index 125eb814..72825be2 100644
--- a/occtax/src/main/java/fr/geonature/occtax/features/record/domain/ObservationRecord.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/features/record/domain/ObservationRecord.kt
@@ -83,7 +83,10 @@ data class ObservationRecord(
enum class Status {
DRAFT,
- TO_SYNC
+ TO_SYNC,
+ SYNC_IN_PROGRESS,
+ SYNC_ERROR,
+ SYNC_SUCCESSFUL
}
}
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/record/domain/SynchronizationStatus.kt b/occtax/src/main/java/fr/geonature/occtax/features/record/domain/SynchronizationStatus.kt
new file mode 100644
index 00000000..4e58e7a9
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/record/domain/SynchronizationStatus.kt
@@ -0,0 +1,25 @@
+package fr.geonature.occtax.features.record.domain
+
+import androidx.work.WorkInfo
+
+/**
+ * Describes the current status of [ObservationRecord] synchronization.
+ *
+ * @author S. Grimault
+ */
+sealed class SynchronizationStatus(open val state: WorkInfo.State ) {
+
+ /**
+ * The current worker status.
+ */
+ data class WorkerStatus(override val state: WorkInfo.State) : SynchronizationStatus(state)
+
+ /**
+ * The current [ObservationRecord] status.
+ */
+ data class ObservationRecordStatus(
+ override val state: WorkInfo.State,
+ val internalId: Long,
+ val status: ObservationRecord.Status
+ ) : SynchronizationStatus(state)
+}
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/record/presentation/ObservationRecordViewModel.kt b/occtax/src/main/java/fr/geonature/occtax/features/record/presentation/ObservationRecordViewModel.kt
index f1cd31ff..9dd39187 100644
--- a/occtax/src/main/java/fr/geonature/occtax/features/record/presentation/ObservationRecordViewModel.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/features/record/presentation/ObservationRecordViewModel.kt
@@ -1,19 +1,30 @@
package fr.geonature.occtax.features.record.presentation
+import android.app.Application
import androidx.lifecycle.LiveData
+import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.liveData
+import androidx.lifecycle.map
+import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
import dagger.hilt.android.lifecycle.HiltViewModel
import fr.geonature.commons.interactor.BaseResultUseCase
-import fr.geonature.commons.lifecycle.BaseViewModel
+import fr.geonature.commons.lifecycle.BaseAndroidViewModel
import fr.geonature.occtax.features.record.domain.ObservationRecord
+import fr.geonature.occtax.features.record.domain.SynchronizationStatus
import fr.geonature.occtax.features.record.usecase.DeleteObservationRecordUseCase
import fr.geonature.occtax.features.record.usecase.EditObservationRecordUseCase
import fr.geonature.occtax.features.record.usecase.ExportObservationRecordUseCase
import fr.geonature.occtax.features.record.usecase.GetAllObservationRecordsUseCase
import fr.geonature.occtax.features.record.usecase.SaveObservationRecordUseCase
+import fr.geonature.occtax.features.record.worker.SynchronizeObservationRecordsWorker
import fr.geonature.occtax.settings.AppSettings
+import kotlinx.coroutines.delay
import org.tinylog.kotlin.Logger
+import java.util.UUID
import javax.inject.Inject
/**
@@ -23,19 +34,123 @@ import javax.inject.Inject
*/
@HiltViewModel
class ObservationRecordViewModel @Inject constructor(
+ application: Application,
private val getAllObservationRecordsUseCase: GetAllObservationRecordsUseCase,
private val saveObservationRecordUseCase: SaveObservationRecordUseCase,
private val editObservationRecordUseCase: EditObservationRecordUseCase,
private val deleteObservationRecordUseCase: DeleteObservationRecordUseCase,
private val exportObservationRecordUseCase: ExportObservationRecordUseCase
-) : BaseViewModel() {
+) : BaseAndroidViewModel(application) {
- private val _observationRecords = MutableLiveData>()
- val observationRecords: LiveData> = _observationRecords
+ private val workManager: WorkManager = WorkManager.getInstance(getApplication())
+ private val _observationRecords = MutableLiveData>()
private val _observationRecord = MutableLiveData()
+
+ /**
+ * The current [ObservationRecord] being edited.
+ */
val observationRecord: LiveData = _observationRecord
+ private var currentSyncWorkerId: UUID? = null
+ set(value) {
+ field = value
+ _isSyncRunning.postValue(field != null)
+ }
+
+ private val _isSyncRunning: MutableLiveData = MutableLiveData(false)
+ val isSyncRunning: LiveData = _isSyncRunning
+
+ private val _observeSynchronizationStatus: LiveData =
+ workManager.getWorkInfosByTagLiveData(
+ SynchronizeObservationRecordsWorker.OBSERVATION_RECORDS_SYNC_WORKER_TAG
+ )
+ .map { workInfoList ->
+ if (workInfoList == null || workInfoList.isEmpty()) {
+ currentSyncWorkerId = null
+ return@map null
+ }
+
+ val workInfo = workInfoList.firstOrNull { it.id == currentSyncWorkerId }
+ ?: workInfoList.firstOrNull { it.state == WorkInfo.State.RUNNING }
+
+ // no work info is running: abort
+ if (workInfo == null) {
+ currentSyncWorkerId = null
+ return@map null
+ }
+
+ // this is a new work info: set the current worker
+ if (workInfo.id != currentSyncWorkerId) {
+ currentSyncWorkerId = workInfo.id
+ }
+
+ workInfoList.firstOrNull()
+ ?.let {
+ SynchronizeObservationRecordsWorker.toSynchronizationStatus(it)
+ }
+ ?.also {
+ if (it.state.isFinished) currentSyncWorkerId = null
+ }
+ ?: return@map null
+ }
+
+ /**
+ * All [ObservationRecord]s loaded.
+ */
+ val observationRecords: LiveData> =
+ MediatorLiveData>().apply {
+ addSource(_observationRecords) {
+ value = it.map { observationRecord ->
+ if (observationRecord.status == ObservationRecord.Status.DRAFT) return@map observationRecord
+
+ (value
+ ?: emptyList()).firstOrNull { existingObservationRecord ->
+ existingObservationRecord.internalId == observationRecord.internalId
+ }
+ ?.let { existingObservationRecord ->
+ observationRecord.copy(status = existingObservationRecord.status.takeIf { status ->
+ status.ordinal > observationRecord.status.ordinal
+ }
+ ?: observationRecord.status)
+ }
+ ?: observationRecord
+ }
+ }
+ addSource(_observeSynchronizationStatus.switchMap { synchronizationStatus ->
+ liveData {
+ if (synchronizationStatus == null) return@liveData
+ if (synchronizationStatus !is SynchronizationStatus.ObservationRecordStatus) return@liveData
+
+ emit(synchronizationStatus)
+
+ if (synchronizationStatus.status == ObservationRecord.Status.SYNC_SUCCESSFUL) {
+ delay(500)
+ value =
+ (value ?: emptyList()).filter {
+ it.internalId != synchronizationStatus.internalId
+ }
+ }
+ }
+ }) { synchronizationStatus ->
+ value = (value ?: emptyList()).map { observationRecord ->
+ if (synchronizationStatus.internalId == observationRecord.internalId) {
+ observationRecord.copy(status = synchronizationStatus.status)
+ } else observationRecord
+ }
+ }
+ }
+
+ /**
+ * Whether some [ObservationRecord]s are ready to synchronize according to their current status.
+ */
+ val hasObservationRecordsReadyToSynchronize =
+ observationRecords.map { observationRecords ->
+ observationRecords.any {
+ it.status == ObservationRecord.Status.TO_SYNC
+ }
+ }
+
/**
* Gets all [ObservationRecord]s.
*
@@ -159,4 +274,13 @@ class ObservationRecordViewModel @Inject constructor(
)
}
}
+
+ /**
+ * Synchronizes all eligible [ObservationRecord]s (i.e. with a valid status [ObservationRecord.Status.TO_SYNC]).
+ */
+ fun synchronizeObservationRecords() {
+ currentSyncWorkerId = SynchronizeObservationRecordsWorker.enqueueUniqueWork(
+ getApplication()
+ )
+ }
}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/record/repository/ISynchronizeObservationRecordRepository.kt b/occtax/src/main/java/fr/geonature/occtax/features/record/repository/ISynchronizeObservationRecordRepository.kt
new file mode 100644
index 00000000..c7da8041
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/record/repository/ISynchronizeObservationRecordRepository.kt
@@ -0,0 +1,19 @@
+package fr.geonature.occtax.features.record.repository
+
+import fr.geonature.occtax.features.record.domain.ObservationRecord
+import fr.geonature.occtax.features.record.error.ObservationRecordException
+
+/**
+ * Synchronize observation record.
+ *
+ * @author S. Grimault
+ */
+interface ISynchronizeObservationRecordRepository {
+
+ /**
+ * Performs synchronization of given [ObservationRecord].
+ * Returns [ObservationRecordException.InvalidStatusException] if this [ObservationRecord] has a
+ * wrong status.
+ */
+ suspend fun synchronize(observationRecord: ObservationRecord): Result
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/record/repository/SynchronizeObservationRecordRepositoryImpl.kt b/occtax/src/main/java/fr/geonature/occtax/features/record/repository/SynchronizeObservationRecordRepositoryImpl.kt
index e6b94bc9..329bafd3 100644
--- a/occtax/src/main/java/fr/geonature/occtax/features/record/repository/SynchronizeObservationRecordRepositoryImpl.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/features/record/repository/SynchronizeObservationRecordRepositoryImpl.kt
@@ -9,7 +9,6 @@ import fr.geonature.datasync.api.IGeoNatureAPIClient
import fr.geonature.datasync.api.model.AuthUser
import fr.geonature.datasync.auth.IAuthManager
import fr.geonature.datasync.auth.error.AuthException
-import fr.geonature.datasync.packageinfo.ISynchronizeObservationRecordRepository
import fr.geonature.datasync.settings.error.DataSyncSettingsNotFoundException
import fr.geonature.occtax.R
import fr.geonature.occtax.features.record.data.IMediaRecordLocalDataSource
@@ -42,8 +41,10 @@ class SynchronizeObservationRecordRepositoryImpl(
private val mediaRecordLocalDataSource: IMediaRecordLocalDataSource
) : ISynchronizeObservationRecordRepository {
- override suspend fun invoke(recordId: Long): Result {
- Logger.info { "synchronize observation record '$recordId'..." }
+ override suspend fun synchronize(observationRecord: ObservationRecord): Result {
+ Logger.info {
+ "synchronize observation record '${observationRecord.internalId}'..."
+ }
val authUser = authManager.getAuthLogin()?.user
?: return Result.failure(AuthException.NotConnectedException)
@@ -56,13 +57,10 @@ class SynchronizeObservationRecordRepositoryImpl(
observationRecordRemoteDataSource.setBaseUrl(dataSyncSettings.geoNatureServerUrl)
- val observationRecord =
- runCatching { observationRecordLocalDataSource.read(recordId) }.getOrElse {
- return Result.failure(it)
- }
-
if (observationRecord.status != ObservationRecord.Status.TO_SYNC) {
- return Result.failure(ObservationRecordException.InvalidStatusException(observationRecord.internalId))
+ return Result.failure(
+ ObservationRecordException.InvalidStatusException(observationRecord.internalId)
+ )
}
val nomenclatureForImage = getNomenclatureForImage()
@@ -72,7 +70,9 @@ class SynchronizeObservationRecordRepositoryImpl(
geoNatureAPIClient.getIdTableLocation()
.await()
}.onFailure {
- Logger.warn { "failed to fetch ID table location" }
+ Logger.warn {
+ "failed to fetch ID table location"
+ }
}
.getOrNull()
@@ -81,13 +81,21 @@ class SynchronizeObservationRecordRepositoryImpl(
observationRecord,
appSettings
)
- }.onFailure { Logger.error { "failed to synchronize observation record '$recordId'" } }
+ }.onFailure {
+ Logger.error {
+ "failed to synchronize observation record '${observationRecord.internalId}'"
+ }
+ }
.getOrElse {
return Result.failure(it)
}
- Logger.info { "observation record created from GeoNature: '${observationRecordSent.id}'" }
- Logger.info { "synchronize ${observationRecordSent.taxa.taxa.size} taxa from observation record '$recordId'..." }
+ Logger.info {
+ "observation record created from GeoNature: '${observationRecordSent.id}'"
+ }
+ Logger.info {
+ "synchronize ${observationRecordSent.taxa.taxa.size} taxa from observation record '${observationRecord.internalId}'..."
+ }
if (nomenclatureForImage != null && idTableLocation != null) {
observationRecord.taxa.taxa.forEach {
@@ -106,28 +114,38 @@ class SynchronizeObservationRecordRepositoryImpl(
appSettings
)
}.onFailure {
- Logger.error { "failed to synchronize all taxa from observation record '$recordId'" }
- Logger.info { "deleting observation record '${observationRecordSent.id}' from GeoNature..." }
+ Logger.error {
+ "failed to synchronize all taxa from observation record '${observationRecord.internalId}'"
+ }
+ Logger.info {
+ "deleting observation record '${observationRecordSent.id}' from GeoNature..."
+ }
deleteAllSynchronizedMediaFiles(observationRecordSent)
runCatching {
observationRecordRemoteDataSource.deleteObservationRecord(observationRecordSent)
}.onFailure {
- Logger.warn { "failed to delete observation record '${observationRecordSent.id}' from GeoNature" }
+ Logger.warn {
+ "failed to delete observation record '${observationRecordSent.id}' from GeoNature"
+ }
}
}
.getOrElse {
return Result.failure(it)
}
- Logger.info { "observation record '$recordId' successfully synchronized" }
+ Logger.info {
+ "observation record '${observationRecord.internalId}' successfully synchronized"
+ }
- runCatching { observationRecordLocalDataSource.delete(recordId) }.onFailure {
- Logger.warn { "failed to delete a fully synchronized observation record '$recordId'" }
+ runCatching { observationRecordLocalDataSource.delete(observationRecord.internalId) }.onFailure {
+ Logger.warn {
+ "failed to delete a fully synchronized observation record '${observationRecord.internalId}'"
+ }
}
- return Result.success(Unit)
+ return Result.success(observationRecordSent)
}
private suspend fun getNomenclatureForImage(): Nomenclature? {
@@ -136,7 +154,10 @@ class SynchronizeObservationRecordRepositoryImpl(
}.getOrNull()
if (nomenclatures == null) {
- Logger.warn { "'TYPE_MEDIA' nomenclature type not found" }
+ Logger.warn {
+ "'TYPE_MEDIA' nomenclature type not found"
+ }
+
return null
}
@@ -146,7 +167,10 @@ class SynchronizeObservationRecordRepositoryImpl(
}
if (nomenclatureForImage == null) {
- Logger.warn { "no nomenclature found matching media type 'image/*'" }
+ Logger.warn {
+ "no nomenclature found matching media type 'image/*'"
+ }
+
return null
}
@@ -196,10 +220,14 @@ class SynchronizeObservationRecordRepositoryImpl(
)
.await()
}.onSuccess {
- Logger.info { "media file '${file.absolutePath}' successfully synchronized" }
+ Logger.info {
+ "media file '${file.absolutePath}' successfully synchronized"
+ }
}
.onFailure {
- Logger.warn(it) { "failed to send media file '${file.absolutePath}'..." }
+ Logger.warn(it) {
+ "failed to send media file '${file.absolutePath}'..."
+ }
}
.getOrNull()
}
@@ -207,7 +235,9 @@ class SynchronizeObservationRecordRepositoryImpl(
}
private suspend fun deleteAllSynchronizedMediaFiles(observationRecord: ObservationRecord) {
- Logger.info { "deleting already uploaded media filesâŠ" }
+ Logger.info {
+ "deleting already uploaded media files..."
+ }
observationRecord.taxa.taxa.forEach { taxonRecord ->
taxonRecord.counting.counting.forEach { countingRecord ->
@@ -216,7 +246,9 @@ class SynchronizeObservationRecordRepositoryImpl(
geoNatureAPIClient.deleteMediaFile(media.id)
.awaitResponse()
}.onFailure {
- Logger.warn(it) { "failed to delete media file ${media.id}..." }
+ Logger.warn(it) {
+ "failed to delete media file ${media.id}..."
+ }
}
}
}
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/record/worker/SynchronizeObservationRecordsWorker.kt b/occtax/src/main/java/fr/geonature/occtax/features/record/worker/SynchronizeObservationRecordsWorker.kt
new file mode 100644
index 00000000..51cf2d1f
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/record/worker/SynchronizeObservationRecordsWorker.kt
@@ -0,0 +1,204 @@
+package fr.geonature.occtax.features.record.worker
+
+import android.content.Context
+import androidx.hilt.work.HiltWorker
+import androidx.work.Constraints
+import androidx.work.CoroutineWorker
+import androidx.work.Data
+import androidx.work.ExistingWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import androidx.work.await
+import androidx.work.workDataOf
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import fr.geonature.occtax.features.record.domain.ObservationRecord
+import fr.geonature.occtax.features.record.domain.SynchronizationStatus
+import fr.geonature.occtax.features.record.repository.IObservationRecordRepository
+import fr.geonature.occtax.features.record.repository.ISynchronizeObservationRecordRepository
+import kotlinx.coroutines.delay
+import org.tinylog.Logger
+import java.util.Date
+import java.util.UUID
+
+/**
+ * Dedicated worker to synchronize all [ObservationRecord]s with valid status.
+ *
+ * @author S. Grimault
+ */
+@HiltWorker
+class SynchronizeObservationRecordsWorker @AssistedInject constructor(
+ @Assisted appContext: Context,
+ @Assisted workerParams: WorkerParameters,
+ private val observationRecordRepository: IObservationRecordRepository,
+ private val synchronizeObservationRecordRepository: ISynchronizeObservationRecordRepository
+) : CoroutineWorker(
+ appContext,
+ workerParams
+) {
+ private val workManager = WorkManager.getInstance(applicationContext)
+
+ override suspend fun doWork(): Result {
+ val startTime = Date()
+
+ val alreadyRunning = workManager
+ .getWorkInfosByTag(OBSERVATION_RECORDS_SYNC_WORKER_TAG)
+ .await()
+ .any { it.id != id && it.state == WorkInfo.State.RUNNING }
+
+ if (alreadyRunning) {
+ Logger.warn { "already running: abort" }
+
+ return Result.retry()
+ }
+
+ val observationRecordsToSynchronize = observationRecordRepository.readAll()
+ .getOrDefault(emptyList())
+ .filter { it.status == ObservationRecord.Status.TO_SYNC }
+
+ if (observationRecordsToSynchronize.isEmpty()) {
+ Logger.info { "no observation records to synchronize" }
+
+ return Result.success(workData(state = WorkInfo.State.SUCCEEDED))
+ }
+
+ val observationRecordsSynchronized = mutableListOf()
+
+ observationRecordsToSynchronize.forEach { observationRecordToSync ->
+ setProgress(
+ workData(
+ WorkInfo.State.RUNNING,
+ observationRecordToSync.internalId,
+ ObservationRecord.Status.SYNC_IN_PROGRESS
+ )
+ )
+
+ delay(500)
+
+ synchronizeObservationRecordRepository.synchronize(observationRecordToSync)
+ .fold(
+ onSuccess = {
+ setProgress(
+ workData(
+ state = WorkInfo.State.RUNNING,
+ recordInternalId = observationRecordToSync.internalId,
+ recordStatus = ObservationRecord.Status.SYNC_SUCCESSFUL
+ )
+ )
+ observationRecordsSynchronized.add(it)
+ },
+ onFailure = {
+ setProgress(
+ workData(
+ state = WorkInfo.State.RUNNING,
+ recordInternalId = observationRecordToSync.internalId,
+ recordStatus = ObservationRecord.Status.SYNC_ERROR
+ )
+ )
+ }
+ )
+
+ delay(500)
+ }
+
+ Logger.info {
+ "observation records synchronization ${if (observationRecordsSynchronized.size == observationRecordsToSynchronize.size) "successfully finished" else "finished with errors"} in ${Date().time - startTime.time}ms"
+ }
+
+ return if (observationRecordsSynchronized.size == observationRecordsToSynchronize.size) {
+ Result.success(
+ workData(state = WorkInfo.State.SUCCEEDED)
+ )
+ } else {
+ Result.failure(
+ workData(state = WorkInfo.State.FAILED)
+ )
+ }
+ }
+
+ private fun workData(
+ state: WorkInfo.State,
+ recordInternalId: Long? = null,
+ recordStatus: ObservationRecord.Status? = null
+ ): Data {
+ return workDataOf(
+ KEY_WORKER_STATUS to state.ordinal,
+ KEY_OBSERVATION_RECORD_INTERNAL_ID to recordInternalId,
+ KEY_OBSERVATION_RECORD_STATUS to recordStatus?.ordinal
+ )
+ }
+
+ companion object {
+
+ private const val KEY_WORKER_STATUS = "key_worker_status"
+ private const val KEY_OBSERVATION_RECORD_INTERNAL_ID = "key_observation_record_internal_id"
+ private const val KEY_OBSERVATION_RECORD_STATUS = "key_observation_record_status"
+
+ private const val OBSERVATION_RECORDS_SYNC_WORKER = "observation_records_sync_worker"
+ const val OBSERVATION_RECORDS_SYNC_WORKER_TAG = "observation_records_sync_worker_tag"
+
+ /**
+ * Convenience method for enqueuing unique work to this worker.
+ */
+ fun enqueueUniqueWork(context: Context): UUID {
+ val workRequest = OneTimeWorkRequest
+ .Builder(SynchronizeObservationRecordsWorker::class.java)
+ .addTag(OBSERVATION_RECORDS_SYNC_WORKER_TAG)
+ .setConstraints(
+ Constraints
+ .Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+ )
+ .build()
+
+ return workRequest.id.also {
+ WorkManager.getInstance(context)
+ .enqueueUniqueWork(
+ OBSERVATION_RECORDS_SYNC_WORKER,
+ ExistingWorkPolicy.KEEP,
+ workRequest
+ )
+ }
+ }
+
+ fun toSynchronizationStatus(workInfo: WorkInfo): SynchronizationStatus? {
+ val validWorkInfo = workInfo.progress.getInt(
+ KEY_WORKER_STATUS,
+ -1
+ )
+ .takeIf { it >= 0 }
+ ?.let { workInfo.progress } ?: workInfo.outputData.getInt(
+ KEY_WORKER_STATUS,
+ -1
+ )
+ .takeIf { it >= 0 }
+ ?.let { workInfo.outputData } ?: return null
+
+ val workerStatus = WorkInfo.State.values()[validWorkInfo.getInt(
+ KEY_WORKER_STATUS,
+ 0
+ )]
+ val observationRecordInternalId = validWorkInfo.getLong(
+ KEY_OBSERVATION_RECORD_INTERNAL_ID,
+ 0
+ )
+ .takeIf { it > 0 }
+ val observationRecordStatus = validWorkInfo.getInt(
+ KEY_OBSERVATION_RECORD_STATUS,
+ -1
+ )
+ .takeIf { it >= 0 }
+ ?.let { ObservationRecord.Status.values()[it] }
+
+ return if (observationRecordInternalId != null && observationRecordStatus != null) SynchronizationStatus.ObservationRecordStatus(
+ state = workerStatus,
+ internalId = observationRecordInternalId,
+ status = observationRecordStatus
+ ) else SynchronizationStatus.WorkerStatus(state = workerStatus)
+ }
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/settings/AppSettings.kt b/occtax/src/main/java/fr/geonature/occtax/settings/AppSettings.kt
index f7d4c42e..bc9a1371 100644
--- a/occtax/src/main/java/fr/geonature/occtax/settings/AppSettings.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/settings/AppSettings.kt
@@ -1,16 +1,16 @@
package fr.geonature.occtax.settings
-import android.os.Parcel
-import android.os.Parcelable
import fr.geonature.commons.settings.IAppSettings
import fr.geonature.datasync.settings.DataSyncSettings
import fr.geonature.maps.settings.MapSettings
+import kotlinx.parcelize.Parcelize
/**
* Global internal settings.
*
* @author S. Grimault
*/
+@Parcelize
data class AppSettings(
var areaObservationDuration: Int = DEFAULT_AREA_OBSERVATION_DURATION,
var inputSettings: InputSettings = InputSettings(dateSettings = InputDateSettings.DEFAULT),
@@ -19,44 +19,6 @@ data class AppSettings(
var nomenclatureSettings: NomenclatureSettings? = null
) : IAppSettings {
- private constructor(source: Parcel) : this(
- source.readInt(),
- source.readParcelable(InputSettings::class.java.classLoader)
- ?: InputSettings(dateSettings = InputDateSettings.DEFAULT),
- source.readParcelable(DataSyncSettings::class.java.classLoader) as DataSyncSettings?,
- source.readParcelable(MapSettings::class.java.classLoader) as MapSettings?,
- source.readParcelable(NomenclatureSettings::class.java.classLoader) as NomenclatureSettings?
- )
-
- override fun describeContents(): Int {
- return 0
- }
-
- override fun writeToParcel(
- dest: Parcel?,
- flags: Int
- ) {
- dest?.also {
- it.writeInt(areaObservationDuration)
- it.writeParcelable(
- inputSettings,
- 0
- )
- it.writeParcelable(
- dataSyncSettings,
- 0
- )
- it.writeParcelable(
- mapSettings,
- 0
- )
- it.writeParcelable(
- nomenclatureSettings,
- 0
- )
- }
- }
-
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is AppSettings) return false
@@ -82,16 +44,5 @@ data class AppSettings(
companion object {
const val DEFAULT_AREA_OBSERVATION_DURATION = 365
-
- @JvmField
- val CREATOR: Parcelable.Creator = object : Parcelable.Creator {
- override fun createFromParcel(parcel: Parcel): AppSettings {
- return AppSettings(parcel)
- }
-
- override fun newArray(size: Int): Array {
- return arrayOfNulls(size)
- }
- }
}
}
diff --git a/occtax/src/main/java/fr/geonature/occtax/settings/InputDateSettings.kt b/occtax/src/main/java/fr/geonature/occtax/settings/InputDateSettings.kt
index 4eaa33dd..8599813b 100644
--- a/occtax/src/main/java/fr/geonature/occtax/settings/InputDateSettings.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/settings/InputDateSettings.kt
@@ -1,56 +1,22 @@
package fr.geonature.occtax.settings
-import android.os.Parcel
import android.os.Parcelable
import fr.geonature.occtax.features.record.domain.ObservationRecord
+import kotlinx.parcelize.Parcelize
/**
* [ObservationRecord] date settings.
*
* @author S. Grimault
*/
+@Parcelize
data class InputDateSettings(
val startDateSettings: DateSettings? = null,
val endDateSettings: DateSettings? = null
) : Parcelable {
- private constructor(parcel: Parcel) : this(
- parcel.readString().let { dateSettingsAsString ->
- DateSettings
- .values()
- .firstOrNull { it.name == dateSettingsAsString }
- },
- parcel.readString().let { dateSettingsAsString ->
- DateSettings
- .values()
- .firstOrNull { it.name == dateSettingsAsString }
- }
- )
-
- override fun describeContents(): Int {
- return 0
- }
-
- override fun writeToParcel(dest: Parcel?, flags: Int) {
- dest?.also {
- it.writeString(startDateSettings?.name)
- it.writeString(endDateSettings?.name)
- }
- }
companion object {
val DEFAULT = InputDateSettings(startDateSettings = DateSettings.DATE)
-
- @JvmField
- val CREATOR: Parcelable.Creator =
- object : Parcelable.Creator {
- override fun createFromParcel(parcel: Parcel): InputDateSettings {
- return InputDateSettings(parcel)
- }
-
- override fun newArray(size: Int): Array {
- return arrayOfNulls(size)
- }
- }
}
enum class DateSettings {
diff --git a/occtax/src/main/java/fr/geonature/occtax/settings/InputSettings.kt b/occtax/src/main/java/fr/geonature/occtax/settings/InputSettings.kt
index 487842be..de790065 100644
--- a/occtax/src/main/java/fr/geonature/occtax/settings/InputSettings.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/settings/InputSettings.kt
@@ -1,43 +1,13 @@
package fr.geonature.occtax.settings
-import android.os.Parcel
import android.os.Parcelable
import fr.geonature.occtax.features.record.domain.ObservationRecord
+import kotlinx.parcelize.Parcelize
/**
* [ObservationRecord] settings.
*
* @author S. Grimault
*/
-data class InputSettings(val dateSettings: InputDateSettings) : Parcelable {
- private constructor(source: Parcel) : this(
- source.readParcelable(InputDateSettings::class.java.classLoader) as InputDateSettings?
- ?: InputDateSettings.DEFAULT
- )
-
- override fun describeContents(): Int {
- return 0
- }
-
- override fun writeToParcel(
- dest: Parcel?,
- flags: Int
- ) {
- dest?.also {
- it.writeParcelable(
- dateSettings,
- 0
- )
- }
- }
-
- companion object CREATOR : Parcelable.Creator {
- override fun createFromParcel(parcel: Parcel): InputSettings {
- return InputSettings(parcel)
- }
-
- override fun newArray(size: Int): Array {
- return arrayOfNulls(size)
- }
- }
-}
+@Parcelize
+data class InputSettings(val dateSettings: InputDateSettings) : Parcelable
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/settings/NomenclatureSettings.kt b/occtax/src/main/java/fr/geonature/occtax/settings/NomenclatureSettings.kt
index 8bae16f2..b0ef06a0 100644
--- a/occtax/src/main/java/fr/geonature/occtax/settings/NomenclatureSettings.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/settings/NomenclatureSettings.kt
@@ -1,60 +1,16 @@
package fr.geonature.occtax.settings
-import android.os.Parcel
import android.os.Parcelable
-import androidx.core.os.ParcelCompat
-import androidx.core.os.ParcelCompat.readBoolean
+import kotlinx.parcelize.Parcelize
/**
* Nomenclature settings.
*
* @author S. Grimault
*/
+@Parcelize
data class NomenclatureSettings(
val saveDefaultValues: Boolean = false,
val information: List,
val counting: List
-) : Parcelable {
- private constructor(source: Parcel) : this(
- readBoolean(source),
- mutableListOf(),
- mutableListOf()
- ) {
- source.readTypedList(
- information,
- PropertySettings.CREATOR
- )
- source.readTypedList(
- counting,
- PropertySettings.CREATOR
- )
- }
-
- override fun describeContents(): Int {
- return 0
- }
-
- override fun writeToParcel(
- dest: Parcel?,
- flags: Int
- ) {
- dest?.also {
- ParcelCompat.writeBoolean(
- it,
- saveDefaultValues
- )
- it.writeTypedList(information)
- it.writeTypedList(counting)
- }
- }
-
- companion object CREATOR : Parcelable.Creator {
- override fun createFromParcel(parcel: Parcel): NomenclatureSettings {
- return NomenclatureSettings(parcel)
- }
-
- override fun newArray(size: Int): Array {
- return arrayOfNulls(size)
- }
- }
-}
\ No newline at end of file
+) : Parcelable
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/settings/PropertySettings.kt b/occtax/src/main/java/fr/geonature/occtax/settings/PropertySettings.kt
index 10ee56ed..bf260846 100644
--- a/occtax/src/main/java/fr/geonature/occtax/settings/PropertySettings.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/settings/PropertySettings.kt
@@ -1,47 +1,16 @@
package fr.geonature.occtax.settings
-import android.os.Parcel
import android.os.Parcelable
-import androidx.core.os.ParcelCompat
+import kotlinx.parcelize.Parcelize
/**
* Property settings.
*
- * @author [S. Grimault](mailto:sebastien.grimault@gmail.com)
+ * @author S. Grimault
*/
+@Parcelize
data class PropertySettings(
val key: String,
val visible: Boolean,
val default: Boolean
-): Parcelable {
- private constructor(source: Parcel) : this(
- source.readString()!!,
- ParcelCompat.readBoolean(source),
- ParcelCompat.readBoolean(source)
- )
-
- override fun describeContents(): Int {
- return 0
- }
-
- override fun writeToParcel(
- dest: Parcel?,
- flags: Int
- ) {
- dest?.also {
- it.writeString(key)
- ParcelCompat.writeBoolean(it, visible)
- ParcelCompat.writeBoolean(it, default)
- }
- }
-
- companion object CREATOR : Parcelable.Creator {
- override fun createFromParcel(parcel: Parcel): PropertySettings {
- return PropertySettings(parcel)
- }
-
- override fun newArray(size: Int): Array {
- return arrayOfNulls(size)
- }
- }
-}
\ No newline at end of file
+) : Parcelable
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetListActivity.kt b/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetListActivity.kt
index c921d025..17587b04 100644
--- a/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetListActivity.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetListActivity.kt
@@ -7,6 +7,7 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import fr.geonature.commons.data.entity.Dataset
+import fr.geonature.compat.content.getParcelableExtraCompat
import fr.geonature.occtax.R
/**
@@ -32,7 +33,7 @@ class DatasetListActivity : AppCompatActivity(),
supportFragmentManager.beginTransaction()
.replace(
R.id.container,
- DatasetListFragment.newInstance(intent.getParcelableExtra(EXTRA_SELECTED_DATASET))
+ DatasetListFragment.newInstance(intent.getParcelableExtraCompat(EXTRA_SELECTED_DATASET))
)
.commit()
}
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetListFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetListFragment.kt
index 736f50de..0d79a0c1 100644
--- a/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetListFragment.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetListFragment.kt
@@ -23,6 +23,7 @@ import fr.geonature.commons.data.ContentProviderAuthority
import fr.geonature.commons.data.GeoNatureModuleName
import fr.geonature.commons.data.entity.Dataset
import fr.geonature.commons.data.helper.ProviderHelper.buildUri
+import fr.geonature.compat.os.getParcelableCompat
import fr.geonature.occtax.R
import fr.geonature.occtax.R.layout.fast_scroll_recycler_view
import javax.inject.Inject
@@ -141,7 +142,7 @@ class DatasetListFragment : Fragment() {
view.smoothScrollToPosition(position)
}
})
- adapter?.setSelectedDataset(arguments?.getParcelable(ARG_SELECTED_DATASET))
+ adapter?.setSelectedDataset(arguments?.getParcelableCompat(ARG_SELECTED_DATASET))
.also { updateActionMode(adapter?.getSelectedDataset()) }
with(view) {
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/home/AppSyncView.kt b/occtax/src/main/java/fr/geonature/occtax/ui/home/AppSyncView.kt
deleted file mode 100644
index da0c764f..00000000
--- a/occtax/src/main/java/fr/geonature/occtax/ui/home/AppSyncView.kt
+++ /dev/null
@@ -1,213 +0,0 @@
-package fr.geonature.occtax.ui.home
-
-import android.content.Context
-import android.text.format.DateFormat
-import android.util.AttributeSet
-import android.util.Pair
-import android.view.View
-import android.view.animation.AlphaAnimation
-import android.view.animation.Animation
-import android.widget.TextView
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.core.content.res.ResourcesCompat
-import androidx.work.WorkInfo
-import fr.geonature.commons.data.entity.AppSync
-import fr.geonature.datasync.packageinfo.PackageInfo
-import fr.geonature.datasync.sync.DataSyncStatus
-import fr.geonature.occtax.R
-import fr.geonature.occtax.ui.shared.view.ListItemActionView
-import java.text.NumberFormat
-
-/**
- * Custom [View] about [AppSync].
- *
- * @author S. Grimault
- */
-class AppSyncView : ConstraintLayout {
-
- private lateinit var iconStatus: TextView
- private lateinit var listItemActionView: ListItemActionView
- private var listener: OnAppSyncViewListener? = null
-
- private val stateAnimation = AlphaAnimation(
- 0.0f,
- 1.0f
- ).apply {
- duration = 250
- startOffset = 10
- repeatMode = Animation.REVERSE
- repeatCount = Animation.INFINITE
- }
-
- constructor(context: Context) : super(context) {
- init()
- }
-
- constructor(
- context: Context,
- attrs: AttributeSet
- ) : super(
- context,
- attrs
- ) {
- init()
- }
-
- constructor(
- context: Context,
- attrs: AttributeSet,
- defStyleAttr: Int
- ) : super(
- context,
- attrs,
- defStyleAttr
- ) {
- init()
- }
-
- fun setListener(listener: OnAppSyncViewListener) {
- this.listener = listener
- }
-
- fun enableActionButton(enabled: Boolean = true) {
- listItemActionView.enableActionButton(enabled)
- }
-
- fun setDataSyncStatus(dataSyncStatus: DataSyncStatus?) {
- when (dataSyncStatus?.state ?: WorkInfo.State.ENQUEUED) {
- WorkInfo.State.RUNNING -> {
- iconStatus.setTextColor(
- ResourcesCompat.getColor(
- resources,
- R.color.datasync_status_pending,
- context?.theme
- )
- )
-
- if (iconStatus.animation?.hasStarted() != true) {
- iconStatus.startAnimation(stateAnimation)
- }
- }
- WorkInfo.State.FAILED -> {
- iconStatus.setTextColor(
- ResourcesCompat.getColor(
- resources,
- R.color.datasync_status_ko,
- context?.theme
- )
- )
- iconStatus.clearAnimation()
- }
- WorkInfo.State.SUCCEEDED -> {
- iconStatus.setTextColor(
- ResourcesCompat.getColor(
- resources,
- R.color.datasync_status_ok,
- context?.theme
- )
- )
- iconStatus.clearAnimation()
- }
- else -> {
- iconStatus.setTextColor(
- ResourcesCompat.getColor(
- resources,
- R.color.datasync_status_unknown,
- context?.theme
- )
- )
- iconStatus.clearAnimation()
- }
- }
-
- listItemActionView.set(
- Pair.create(
- context.getString(R.string.sync_data),
- dataSyncStatus?.syncMessage
- ),
- 0
- )
- }
-
- fun setPackageInfo(packageInfo: PackageInfo) {
- val numberOfInputsToSynchronize = packageInfo.inputsStatus?.inputs ?: return
-
- listItemActionView.set(
- Pair.create(
- context.getString(R.string.sync_inputs_not_synchronized),
- NumberFormat.getInstance().format(numberOfInputsToSynchronize)
- ),
- 2
- )
- }
-
- fun setAppSync(appSync: AppSync?) {
- if (appSync == null) {
- return
- }
-
- listItemActionView.set(
- Pair.create(
- context.getString(R.string.sync_last_synchronization),
- if (appSync.lastSync == null) context.getString(R.string.sync_last_synchronization_never)
- else DateFormat.format(
- context.getString(R.string.sync_last_synchronization_date),
- appSync.lastSync!!
- ).toString()
- ),
- 1
- )
- listItemActionView.set(
- Pair.create(
- context.getString(R.string.sync_inputs_not_synchronized),
- NumberFormat.getInstance().format(appSync.inputsToSynchronize)
- ),
- 2
- )
- }
-
- private fun init() {
- View.inflate(
- context,
- R.layout.view_app_sync,
- this
- )
-
- iconStatus = findViewById(android.R.id.icon)
- listItemActionView = findViewById(R.id.list_item).also {
- it.setListener(object : ListItemActionView.OnListItemActionViewListener {
- override fun onAction() {
- listener?.onAction()
- }
- })
-
- it.setItems(
- listOf(
- Pair.create(
- context.getString(R.string.sync_data),
- null
- ),
- Pair.create(
- context.getString(R.string.sync_last_synchronization),
- context.getString(R.string.sync_last_synchronization_never)
- ),
- Pair.create(
- context.getString(R.string.sync_inputs_not_synchronized),
- NumberFormat.getInstance().format(0)
- )
- )
- )
- }
- }
-
- /**
- * Callback used by [AppSyncView].
- */
- interface OnAppSyncViewListener {
-
- /**
- * Called when the 'synchronize' button has been clicked.
- */
- fun onAction()
- }
-}
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/home/DrawerMenuEntryView.kt b/occtax/src/main/java/fr/geonature/occtax/ui/home/DrawerMenuEntryView.kt
new file mode 100644
index 00000000..1054835b
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/home/DrawerMenuEntryView.kt
@@ -0,0 +1,136 @@
+package fr.geonature.occtax.ui.home
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextSwitcher
+import android.widget.TextView
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.constraintlayout.widget.ConstraintLayout
+import fr.geonature.occtax.R
+
+/**
+ * Generic [View] about drawer menu entry.
+ *
+ * @author S. Grimault
+ */
+class DrawerMenuEntryView : ConstraintLayout {
+
+ lateinit var icon: ImageView
+ private set
+
+ private lateinit var textView1: TextView
+ private lateinit var textView2: TextSwitcher
+
+ constructor(context: Context) : super(context) {
+ init(
+ null,
+ 0
+ )
+ }
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet
+ ) : super(
+ context,
+ attrs
+ ) {
+ init(
+ attrs,
+ 0
+ )
+ }
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet,
+ defStyleAttr: Int
+ ) : super(
+ context,
+ attrs,
+ defStyleAttr
+ ) {
+ init(
+ attrs,
+ defStyleAttr
+ )
+ }
+
+ fun setIcon(@DrawableRes drawableResourceId: Int) {
+ icon.setImageResource(drawableResourceId)
+ }
+
+ fun setText1(@StringRes textResourceId: Int) {
+ setText1(if (textResourceId == 0) null else context.getString(textResourceId))
+ }
+
+ fun setText1(title: String?) {
+ textView1.text = title
+ }
+
+ fun setText2(@StringRes textResourceId: Int) {
+ setText2(if (textResourceId == 0) null else context.getString(textResourceId))
+ }
+
+ fun setText2(text: String?) {
+ textView2.setText(text)
+ textView2.visibility = if (text.isNullOrBlank()) GONE else VISIBLE
+ }
+
+ private fun init(
+ attrs: AttributeSet?,
+ defStyle: Int
+ ) {
+ View.inflate(
+ context,
+ R.layout.view_drawer_menu_entry,
+ this
+ )
+
+ icon = findViewById(android.R.id.icon)
+ textView1 = findViewById(android.R.id.text1)
+ textView2 = findViewById(android.R.id.text2)
+
+ // load attributes
+ val ta = context.obtainStyledAttributes(
+ attrs,
+ R.styleable.DrawerMenuEntryView,
+ defStyle,
+ 0
+ )
+
+ setIcon(
+ ta.getResourceId(
+ R.styleable.DrawerMenuEntryView_icon,
+ 0
+ )
+ )
+
+ ta.getString(R.styleable.DrawerMenuEntryView_text1)
+ ?.also {
+ setText1(it)
+ }
+ setText1(
+ ta.getResourceId(
+ R.styleable.DrawerMenuEntryView_text1,
+ 0
+ )
+ )
+
+ ta.getString(R.styleable.DrawerMenuEntryView_text2)
+ ?.also {
+ setText2(it)
+ }
+ setText2(
+ ta.getResourceId(
+ R.styleable.DrawerMenuEntryView_text2,
+ 0
+ )
+ )
+
+ ta.recycle()
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeActivity.kt b/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeActivity.kt
index 67bedbf5..fa00d971 100644
--- a/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeActivity.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeActivity.kt
@@ -1,53 +1,38 @@
package fr.geonature.occtax.ui.home
-import android.annotation.SuppressLint
import android.content.Intent
-import android.database.Cursor
import android.os.Bundle
-import android.os.VibrationEffect
-import android.os.Vibrator
-import android.view.Menu
-import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
-import android.view.animation.AnimationUtils
+import android.view.animation.Animation
+import android.view.animation.LinearInterpolator
+import android.view.animation.RotateAnimation
+import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
+import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
-import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
-import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
-import androidx.core.os.bundleOf
-import androidx.core.view.children
-import androidx.loader.app.LoaderManager
-import androidx.loader.content.CursorLoader
-import androidx.loader.content.Loader
-import androidx.recyclerview.widget.DividerItemDecoration
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
+import androidx.drawerlayout.widget.DrawerLayout
import androidx.work.WorkInfo
-import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.progressindicator.CircularProgressIndicator
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
-import fr.geonature.commons.data.ContentProviderAuthority
-import fr.geonature.commons.data.entity.AppSync
-import fr.geonature.commons.data.helper.ProviderHelper.buildUri
import fr.geonature.commons.error.Failure
import fr.geonature.commons.lifecycle.observe
import fr.geonature.commons.lifecycle.observeOnce
import fr.geonature.commons.lifecycle.observeUntil
import fr.geonature.commons.lifecycle.onFailure
-import fr.geonature.commons.ui.adapter.AbstractListItemRecyclerViewAdapter
import fr.geonature.commons.util.ThemeUtils.getErrorColor
import fr.geonature.datasync.api.IGeoNatureAPIClient
+import fr.geonature.datasync.api.model.AuthLogin
import fr.geonature.datasync.auth.AuthLoginViewModel
import fr.geonature.datasync.features.settings.presentation.ConfigureServerSettingsActivity
import fr.geonature.datasync.features.settings.presentation.ConfigureServerSettingsViewModel
@@ -65,13 +50,14 @@ import fr.geonature.occtax.BuildConfig
import fr.geonature.occtax.MainApplication
import fr.geonature.occtax.R
import fr.geonature.occtax.features.record.domain.ObservationRecord
-import fr.geonature.occtax.features.record.presentation.ObservationRecordViewModel
import fr.geonature.occtax.settings.AppSettings
import fr.geonature.occtax.settings.AppSettingsViewModel
import fr.geonature.occtax.ui.input.InputPagerFragmentActivity
import fr.geonature.occtax.ui.settings.PreferencesActivity
import org.tinylog.Logger
import java.io.File
+import java.text.DateFormat
+import java.util.Date
import javax.inject.Inject
/**
@@ -80,7 +66,8 @@ import javax.inject.Inject
* @author S. Grimault
*/
@AndroidEntryPoint
-class HomeActivity : AppCompatActivity() {
+class HomeActivity : AppCompatActivity(),
+ LastObservationRecordsFragment.OnLastObservationRecordsFragmentListener {
private val authLoginViewModel: AuthLoginViewModel by viewModels()
private val appSettingsViewModel: AppSettingsViewModel by viewModels()
@@ -88,228 +75,120 @@ class HomeActivity : AppCompatActivity() {
private val dataSyncViewModel: DataSyncViewModel by viewModels()
private val configureServerSettingsViewModel: ConfigureServerSettingsViewModel by viewModels()
private val updateSettingsViewModel: UpdateSettingsViewModel by viewModels()
- private val observationRecordViewModel: ObservationRecordViewModel by viewModels()
@Inject
lateinit var geoNatureAPIClient: IGeoNatureAPIClient
- @ContentProviderAuthority
- @Inject
- lateinit var authority: String
-
+ private var loginLastnameTextView: TextView? = null
+ private var loginFirstnameTextView: TextView? = null
+ private var loginButton: Button? = null
+ private var navMenuDataSync: DrawerMenuEntryView? = null
+ private var navMenuLogout: DrawerMenuEntryView? = null
private var homeContent: CoordinatorLayout? = null
- private var appSyncView: AppSyncView? = null
- private var inputToolbar: Toolbar? = null
- private var inputRecyclerView: RecyclerView? = null
- private var inputEmptyTextView: TextView? = null
- private var fab: ExtendedFloatingActionButton? = null
-
- private lateinit var adapter: InputRecyclerViewAdapter
private var progressSnackbar: Pair? = null
private var appSettings: AppSettings? = null
- private var isLoggedIn: Boolean = false
+ private var isLoggedIn: AuthLogin? = null
private lateinit var startSyncResultLauncher: ActivityResultLauncher
- private val loaderCallbacks = object : LoaderManager.LoaderCallbacks {
- override fun onCreateLoader(
- id: Int,
- args: Bundle?
- ): Loader {
- return when (id) {
- LOADER_APP_SYNC -> CursorLoader(
- this@HomeActivity,
- buildUri(
- authority,
- AppSync.TABLE_NAME,
- args?.getString(AppSync.COLUMN_ID)
- ?: ""
- ),
- null,
- null,
- null,
- null
- )
- else -> throw IllegalArgumentException()
- }
- }
-
- override fun onLoadFinished(
- loader: Loader,
- data: Cursor?
- ) {
-
- if (data == null) {
- Logger.warn { "failed to load data from '${(loader as CursorLoader).uri}'" }
-
- return
- }
-
- when (loader.id) {
- LOADER_APP_SYNC -> {
- if (data.moveToFirst()) {
- appSyncView?.setAppSync(AppSync.fromCursor(data))
- }
- }
- }
- }
-
- override fun onLoaderReset(loader: Loader) {
- // nothing to do...
- }
- }
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
- homeContent = findViewById(R.id.homeContent)
- appSyncView = findViewById(R.id.appSyncView)
-
- inputToolbar = findViewById(R.id.toolbar_inputs).apply {
- inflateMenu(R.menu.status)
- // all statuses checked by default
- menu.children.forEach { it.isChecked = true }
- setOnMenuItemClickListener { menuItem ->
- when (menuItem.itemId) {
- R.id.menu_status_draft -> {
- menuItem.isChecked = !menuItem.isChecked
-
- if (menu.children.all { !it.isChecked }) {
- menu.children.forEach { it.isChecked = true }
- }
-
- loadObservationRecords()
+ // setting a custom ActionBar
+ val toolbar: Toolbar = findViewById(R.id.toolbar)
+ setSupportActionBar(toolbar)
- true
- }
- R.id.menu_status_to_sync -> {
- menuItem.isChecked = !menuItem.isChecked
-
- if (menu.children.all { !it.isChecked }) {
- menu.children.forEach { it.isChecked = true }
- }
-
- loadObservationRecords()
+ supportActionBar?.apply {
+ // showing the burger button on the ActionBar
+ setDisplayHomeAsUpEnabled(true)
+ subtitle = getString(R.string.home_last_inputs)
+ }
- true
+ val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
+ val toggle =
+ ActionBarDrawerToggle(
+ this,
+ drawerLayout,
+ toolbar,
+ R.string.home_drawer_open,
+ R.string.home_drawer_close
+ )
+ drawerLayout.addDrawerListener(toggle)
+ toggle.syncState()
+
+ findViewById(R.id.nav_header)?.also {
+ loginLastnameTextView = it.findViewById(android.R.id.text1)
+ loginFirstnameTextView = it.findViewById(android.R.id.text2)
+ loginButton = it.findViewById