diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 719eb24f..d0e26546 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -27,6 +27,10 @@
+
+
+
+
@@ -149,6 +153,7 @@
+
diff --git a/artwork/ic_sync.svg b/artwork/ic_sync.svg
new file mode 100644
index 00000000..071a1a18
--- /dev/null
+++ b/artwork/ic_sync.svg
@@ -0,0 +1,28 @@
+
+
diff --git a/commons/build.gradle b/commons/build.gradle
index 297de215..a9ac0a8e 100644
--- a/commons/build.gradle
+++ b/commons/build.gradle
@@ -2,7 +2,7 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
-version = "0.6.2"
+version = "0.6.3"
android {
compileSdkVersion 28
diff --git a/commons/src/main/java/fr/geonature/commons/util/DeviceUtils.kt b/commons/src/main/java/fr/geonature/commons/util/DeviceUtils.kt
index 8c14deb8..d80dd589 100644
--- a/commons/src/main/java/fr/geonature/commons/util/DeviceUtils.kt
+++ b/commons/src/main/java/fr/geonature/commons/util/DeviceUtils.kt
@@ -15,4 +15,7 @@ object DeviceUtils {
val isPostLollipop: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
+
+ val isPostOreo: Boolean
+ get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
}
diff --git a/sync/README.md b/sync/README.md
index adcd84d9..780a7716 100644
--- a/sync/README.md
+++ b/sync/README.md
@@ -5,7 +5,7 @@
Synchronize local database through GeoNature API:
-- Users (e.g. Observers)
+- Users (i.e. Observers)
- Taxa (with "color" by areas and taxonomy)
- Dataset
- Nomenclature
@@ -18,19 +18,25 @@ Example:
```json
{
- "application_id": 3,
- "users_menu_id": 1,
- "taxref_list_id": 100
+ "geonature_url": "http://demo.geonature/geonature",
+ "taxhub_url": "http://demo.geonature/taxhub",
+ "uh_application_id": 3,
+ "observers_list_id": 1,
+ "taxa_list_id": 100
}
```
### Parameters description
-| Parameter | UI | Description |
-| ---------------- | ------- | -------------------------------- |
-| `application_id` | ☐ | GeoNature application ID |
-| `users_menu_id` | ☐ | GeoNature selected users menu ID |
-| `taxref_list_id` | ☐ | GeoNature selected taxa list ID |
+| Parameter | UI | Description | Default value |
+| ------------------- | ------- | ------------------------------------------------------ | ------------- |
+| `geonature_url` | ☑ | GeoNature URL | |
+| `taxhub_url` | ☑ | TaxHub URL | |
+| `uh_application_id` | ☐ | GeoNature application ID | |
+| `observers_list_id` | ☐ | GeoNature selected users menu ID | |
+| `taxa_list_id` | ☐ | GeoNature selected taxa list ID | |
+| `page_size` | ☐ | Default page size while fetching paginated values | 1000 |
+| `page_max_retry` | ☐ | Max attempt to fetch data according to given page size | 20 |
## Content Provider
diff --git a/sync/build.gradle b/sync/build.gradle
index 8e84656e..850c192f 100644
--- a/sync/build.gradle
+++ b/sync/build.gradle
@@ -3,7 +3,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: "kotlin-kapt"
-version = "0.2.5"
+version = "0.2.6"
android {
compileSdkVersion 28
@@ -13,6 +13,10 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_1_8.toString()
+ }
+
defaultConfig {
applicationId "fr.geonature.sync"
minSdkVersion 21
@@ -61,11 +65,11 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
- implementation 'androidx.core:core-ktx:1.3.0-alpha01'
+ implementation 'androidx.core:core-ktx:1.3.0-alpha02'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
- implementation 'androidx.work:work-runtime:2.3.2'
- implementation 'androidx.work:work-runtime-ktx:2.3.2'
+ implementation 'androidx.work:work-runtime:2.3.3'
+ implementation 'androidx.work:work-runtime-ktx:2.3.3'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'com.google.android.material:material:1.2.0-alpha05'
diff --git a/sync/src/debug/res/drawable/ic_launcher_background.xml b/sync/src/debug/res/drawable/ic_launcher_background.xml
index efd3d6ef..e241a3de 100644
--- a/sync/src/debug/res/drawable/ic_launcher_background.xml
+++ b/sync/src/debug/res/drawable/ic_launcher_background.xml
@@ -15,7 +15,7 @@
android:fillColor="#000000"
android:pathData="M55,26V25h53v1z" />
(
+ 15,
+ TimeUnit.MINUTES
+ ).addTag(CheckInputsToSynchronizeWorker.CHECK_INPUTS_TO_SYNC_WORKER_TAG)
+ .build()
+
+ workManager.enqueueUniquePeriodicWork(
+ CheckInputsToSynchronizeWorker.CHECK_INPUTS_TO_SYNC_WORKER,
+ ExistingPeriodicWorkPolicy.REPLACE,
+ request
+ )
+ }
+
+ private fun configureSyncChannel(notificationManager: NotificationManagerCompat): NotificationChannel? {
+ if (DeviceUtils.isPostOreo) {
+ val channel = NotificationChannel(
+ SYNC_CHANNEL_ID,
+ getText(R.string.sync_channel_name),
+ NotificationManager.IMPORTANCE_LOW
+ ).apply {
+ description = getString(R.string.sync_channel_description)
+ setShowBadge(true)
+ }
+
+ // register this channel with the system
+ notificationManager.createNotificationChannel(channel)
+
+ return channel
+ }
+
+ return null
}
companion object {
private val TAG = MainApplication::class.java.name
+
+ const val SYNC_CHANNEL_ID = "sync_channel"
}
}
diff --git a/sync/src/main/java/fr/geonature/sync/api/GeoNatureAPIClient.kt b/sync/src/main/java/fr/geonature/sync/api/GeoNatureAPIClient.kt
index 06c56c53..b07c1688 100644
--- a/sync/src/main/java/fr/geonature/sync/api/GeoNatureAPIClient.kt
+++ b/sync/src/main/java/fr/geonature/sync/api/GeoNatureAPIClient.kt
@@ -87,14 +87,24 @@ class GeoNatureAPIClient private constructor(
geoNatureService = Retrofit.Builder()
.baseUrl("$geoNatureBaseUrl/")
.client(client)
- .addConverterFactory(GsonConverterFactory.create(GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create()))
+ .addConverterFactory(
+ GsonConverterFactory.create(
+ GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss")
+ .create()
+ )
+ )
.build()
.create(GeoNatureService::class.java)
taxHubService = Retrofit.Builder()
.baseUrl("$taxHubBaseUrl/")
.client(client)
- .addConverterFactory(GsonConverterFactory.create(GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create()))
+ .addConverterFactory(
+ GsonConverterFactory.create(
+ GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss")
+ .create()
+ )
+ )
.build()
.create(TaxHubService::class.java)
}
@@ -128,12 +138,19 @@ class GeoNatureAPIClient private constructor(
return taxHubService.getTaxonomyRanks()
}
- fun getTaxref(listId: Int): Call> {
- return taxHubService.getTaxref(listId)
+ fun getTaxref(listId: Int, limit: Int? = null, offset: Int? = null): Call> {
+ return taxHubService.getTaxref(
+ listId,
+ limit,
+ offset
+ )
}
- fun getTaxrefAreas(): Call> {
- return geoNatureService.getTaxrefAreas()
+ fun getTaxrefAreas(limit: Int? = null, offset: Int? = null): Call> {
+ return geoNatureService.getTaxrefAreas(
+ limit,
+ offset
+ )
}
fun getNomenclatures(): Call> {
diff --git a/sync/src/main/java/fr/geonature/sync/api/GeoNatureService.kt b/sync/src/main/java/fr/geonature/sync/api/GeoNatureService.kt
index 1230e96d..f48fd16e 100644
--- a/sync/src/main/java/fr/geonature/sync/api/GeoNatureService.kt
+++ b/sync/src/main/java/fr/geonature/sync/api/GeoNatureService.kt
@@ -13,6 +13,7 @@ import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
+import retrofit2.http.Query
/**
* GeoNature API interface definition.
@@ -45,7 +46,10 @@ interface GeoNatureService {
): Call>
@GET("api/synthese/color_taxon")
- fun getTaxrefAreas(): Call>
+ fun getTaxrefAreas(
+ @Query("limit") limit: Int? = null,
+ @Query("offset") offset: Int? = null
+ ): Call>
@GET("api/nomenclatures/nomenclatures/taxonomy")
fun getNomenclatures(): Call>
diff --git a/sync/src/main/java/fr/geonature/sync/api/TaxHubService.kt b/sync/src/main/java/fr/geonature/sync/api/TaxHubService.kt
index fba66736..f0a0469e 100644
--- a/sync/src/main/java/fr/geonature/sync/api/TaxHubService.kt
+++ b/sync/src/main/java/fr/geonature/sync/api/TaxHubService.kt
@@ -5,6 +5,7 @@ import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Path
+import retrofit2.http.Query
/**
* TaxHub API interface definition.
@@ -16,10 +17,10 @@ interface TaxHubService {
@GET("api/taxref/regnewithgroupe2")
fun getTaxonomyRanks(): Call
- // TODO: fetch all taxa
- @GET("api/taxref/allnamebylist/{id}?limit=10000")
+ @GET("api/taxref/allnamebylist/{id}")
fun getTaxref(
- @Path("id")
- listId: Int
+ @Path("id") listId: Int,
+ @Query("limit") limit: Int? = null,
+ @Query("offset") offset: Int? = null
): Call>
}
\ No newline at end of file
diff --git a/sync/src/main/java/fr/geonature/sync/settings/AppSettings.kt b/sync/src/main/java/fr/geonature/sync/settings/AppSettings.kt
index 0fffc443..1019cef8 100644
--- a/sync/src/main/java/fr/geonature/sync/settings/AppSettings.kt
+++ b/sync/src/main/java/fr/geonature/sync/settings/AppSettings.kt
@@ -10,12 +10,24 @@ import fr.geonature.commons.settings.IAppSettings
* @author [S. Grimault](mailto:sebastien.grimault@gmail.com)
*/
data class AppSettings(
+ var geoNatureServerUrl: String? = null,
+ var taxHubServerUrl: String? = null,
var applicationId: Int = 0,
- var usersMenuId: Int = 0,
- var taxrefListId: Int = 0
+ var usersListId: Int = 0,
+ var taxrefListId: Int = 0,
+ var pageSize: Int = DEFAULT_PAGE_SIZE,
+ var pageMaxRetry: Int = DEFAULT_PAGE_MAX_RETRY
) : IAppSettings {
- private constructor(source: Parcel) : this(source.readInt())
+ private constructor(source: Parcel) : this(
+ source.readString(),
+ source.readString(),
+ source.readInt(),
+ source.readInt(),
+ source.readInt(),
+ source.readInt(),
+ source.readInt()
+ )
override fun describeContents(): Int {
return 0
@@ -26,19 +38,29 @@ data class AppSettings(
flags: Int
) {
dest?.also {
+ it.writeString(geoNatureServerUrl)
+ it.writeString(taxHubServerUrl)
it.writeInt(applicationId)
- it.writeInt(usersMenuId)
+ it.writeInt(usersListId)
it.writeInt(taxrefListId)
+ it.writeInt(pageSize)
+ it.writeInt(pageMaxRetry)
}
}
- companion object CREATOR : Parcelable.Creator {
- override fun createFromParcel(parcel: Parcel): AppSettings {
- return AppSettings(parcel)
- }
+ companion object {
+ const val DEFAULT_PAGE_SIZE = 1000
+ const val DEFAULT_PAGE_MAX_RETRY = 20
+
+ @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)
+ override fun newArray(size: Int): Array {
+ return arrayOfNulls(size)
+ }
}
}
}
diff --git a/sync/src/main/java/fr/geonature/sync/settings/io/OnAppSettingsJsonReaderListenerImpl.kt b/sync/src/main/java/fr/geonature/sync/settings/io/OnAppSettingsJsonReaderListenerImpl.kt
index 985bf30c..878c3717 100644
--- a/sync/src/main/java/fr/geonature/sync/settings/io/OnAppSettingsJsonReaderListenerImpl.kt
+++ b/sync/src/main/java/fr/geonature/sync/settings/io/OnAppSettingsJsonReaderListenerImpl.kt
@@ -21,9 +21,14 @@ class OnAppSettingsJsonReaderListenerImpl :
appSettings: AppSettings
) {
when (keyName) {
- "application_id" -> appSettings.applicationId = reader.nextInt()
- "users_menu_id" -> appSettings.usersMenuId = reader.nextInt()
- "taxref_list_id" -> appSettings.taxrefListId = reader.nextInt()
+ "geonature_url" -> appSettings.geoNatureServerUrl = reader.nextString()
+ "taxhub_url" -> appSettings.taxHubServerUrl = reader.nextString()
+ "uh_application_id" -> appSettings.applicationId = reader.nextInt()
+ "observers_list_id" -> appSettings.usersListId = reader.nextInt()
+ "taxa_list_id" -> appSettings.taxrefListId = reader.nextInt()
+ "page_size" -> appSettings.pageSize = reader.nextInt()
+ "page_max_retry" -> appSettings.pageMaxRetry = reader.nextInt()
+ else -> reader.skipValue()
}
}
}
diff --git a/sync/src/main/java/fr/geonature/sync/sync/CheckInputsToSynchronizeWorker.kt b/sync/src/main/java/fr/geonature/sync/sync/CheckInputsToSynchronizeWorker.kt
new file mode 100644
index 00000000..ac11da16
--- /dev/null
+++ b/sync/src/main/java/fr/geonature/sync/sync/CheckInputsToSynchronizeWorker.kt
@@ -0,0 +1,91 @@
+package fr.geonature.sync.sync
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import fr.geonature.sync.MainApplication
+import fr.geonature.sync.R
+import fr.geonature.sync.ui.home.HomeActivity
+
+/**
+ * Checks Inputs to synchronize.
+ *
+ * @author [S. Grimault](mailto:sebastien.grimault@gmail.com)
+ */
+class CheckInputsToSynchronizeWorker(
+ appContext: Context,
+ workerParams: WorkerParameters
+) : CoroutineWorker(
+ appContext,
+ workerParams
+) {
+ private val packageInfoManager = PackageInfoManager.getInstance(applicationContext)
+
+ override suspend fun doWork(): Result {
+ val availablePackageInfos = packageInfoManager.getInstalledApplications()
+ val availableInputs =
+ availablePackageInfos.map { packageInfo -> packageInfoManager.getInputsToSynchronize(packageInfo) }
+ .flatten()
+
+ val inputsToSynchronize = availableInputs.size
+
+ Log.i(
+ TAG,
+ "available inputs to synchronize: $inputsToSynchronize"
+ )
+
+ with(NotificationManagerCompat.from(applicationContext)) {
+ cancel(SYNC_NOTIFICATION_ID)
+
+ if (inputsToSynchronize > 0) {
+ notify(
+ SYNC_NOTIFICATION_ID,
+ NotificationCompat.Builder(
+ applicationContext,
+ MainApplication.SYNC_CHANNEL_ID
+ )
+ .setContentTitle(applicationContext.getText(R.string.notification_inputs_to_synchronize_title))
+ .setContentText(
+ applicationContext.resources.getQuantityString(
+ R.plurals.notification_inputs_to_synchronize_description,
+ inputsToSynchronize,
+ inputsToSynchronize
+ )
+ )
+ .setContentIntent(
+ PendingIntent.getActivity(
+ applicationContext,
+ 0,
+ Intent(
+ applicationContext,
+ HomeActivity::class.java
+ ).apply {
+ flags =
+ Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ },
+ 0
+ )
+ )
+ .setSmallIcon(R.drawable.ic_sync)
+ .setNumber(inputsToSynchronize)
+ .build()
+ )
+ }
+ }
+
+ return Result.success()
+ }
+
+ companion object {
+ private val TAG = CheckInputsToSynchronizeWorker::class.java.name
+
+ const val CHECK_INPUTS_TO_SYNC_WORKER = "check_inputs_to_sync_worker"
+ const val CHECK_INPUTS_TO_SYNC_WORKER_TAG = "check_inputs_to_sync_worker_tag"
+ const val SYNC_NOTIFICATION_ID = 2
+ }
+}
\ No newline at end of file
diff --git a/sync/src/main/java/fr/geonature/sync/sync/DataSyncManager.kt b/sync/src/main/java/fr/geonature/sync/sync/DataSyncManager.kt
index f4104211..17fbb913 100644
--- a/sync/src/main/java/fr/geonature/sync/sync/DataSyncManager.kt
+++ b/sync/src/main/java/fr/geonature/sync/sync/DataSyncManager.kt
@@ -17,8 +17,6 @@ class DataSyncManager private constructor(applicationContext: Context) {
private val _lastSynchronizedDate: MutableLiveData = MutableLiveData()
val lastSynchronizedDate: LiveData = _lastSynchronizedDate
- val syncMessage: MutableLiveData = MutableLiveData()
- val serverStatus: MutableLiveData = MutableLiveData()
fun updateLastSynchronizedDate() {
_lastSynchronizedDate.postValue(appSyncDao.updateLastSynchronizedDate())
diff --git a/sync/src/main/java/fr/geonature/sync/sync/DataSyncStatus.kt b/sync/src/main/java/fr/geonature/sync/sync/DataSyncStatus.kt
new file mode 100644
index 00000000..f14548b5
--- /dev/null
+++ b/sync/src/main/java/fr/geonature/sync/sync/DataSyncStatus.kt
@@ -0,0 +1,14 @@
+package fr.geonature.sync.sync
+
+import androidx.work.WorkInfo
+
+/**
+ * Describes a data synchronization status message.
+ *
+ * @author [S. Grimault](mailto:sebastien.grimault@gmail.com)
+ */
+data class DataSyncStatus(
+ val state: WorkInfo.State,
+ val syncMessage: String?,
+ val serverStatus: ServerStatus = ServerStatus.OK
+)
\ No newline at end of file
diff --git a/sync/src/main/java/fr/geonature/sync/sync/DataSyncViewModel.kt b/sync/src/main/java/fr/geonature/sync/sync/DataSyncViewModel.kt
index d38030de..fd074c79 100644
--- a/sync/src/main/java/fr/geonature/sync/sync/DataSyncViewModel.kt
+++ b/sync/src/main/java/fr/geonature/sync/sync/DataSyncViewModel.kt
@@ -3,7 +3,7 @@ package fr.geonature.sync.sync
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
-import androidx.lifecycle.Transformations
+import androidx.lifecycle.Transformations.map
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.work.Constraints
@@ -11,7 +11,6 @@ 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 fr.geonature.sync.settings.AppSettings
import java.util.Date
@@ -29,40 +28,71 @@ class DataSyncViewModel(application: Application) : AndroidViewModel(application
it.getLastSynchronizedDate()
}
- val syncOutputStatus: LiveData> =
- workManager.getWorkInfosByTagLiveData(DataSyncWorker.DATA_SYNC_WORKER_TAG)
- val lastSynchronizedDate: LiveData = dataSyncManager.lastSynchronizedDate
- val syncMessage: LiveData = dataSyncManager.syncMessage
- val serverStatus: LiveData =
- Transformations.map(dataSyncManager.serverStatus) { serverStatus ->
- if (serverStatus == null) return@map serverStatus
+ val dataSyncStatus: LiveData =
+ map(workManager.getWorkInfosByTagLiveData(DataSyncWorker.DATA_SYNC_WORKER_TAG)) { workInfos ->
+ val workInfo = workInfos.firstOrNull() ?: return@map null
+
+ val serverStatus = ServerStatus.values()[workInfo.progress.getInt(
+ DataSyncWorker.KEY_SERVER_STATUS,
+ workInfo.outputData.getInt(
+ DataSyncWorker.KEY_SERVER_STATUS,
+ ServerStatus.OK.ordinal
+ )
+ )]
- when (serverStatus) {
- ServerStatus.FORBIDDEN, ServerStatus.INTERNAL_SERVER_ERROR -> cancelTasks()
+ if (arrayOf(
+ ServerStatus.FORBIDDEN,
+ ServerStatus.INTERNAL_SERVER_ERROR
+ ).contains(serverStatus)
+ ) {
+ cancelTasks()
}
- serverStatus
+ DataSyncStatus(
+ workInfo.state,
+ workInfo.progress.getString(DataSyncWorker.KEY_SYNC_MESSAGE)
+ ?: workInfo.outputData.getString(DataSyncWorker.KEY_SYNC_MESSAGE),
+ serverStatus
+ )
}
+ val lastSynchronizedDate: LiveData = dataSyncManager.lastSynchronizedDate
fun startSync(appSettings: AppSettings) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
+ val dataSyncWorkRequest = OneTimeWorkRequest.Builder(DataSyncWorker::class.java)
+ .addTag(DataSyncWorker.DATA_SYNC_WORKER_TAG)
+ .setInputData(
+ Data.Builder()
+ .putInt(
+ DataSyncWorker.INPUT_USERS_MENU_ID,
+ appSettings.usersListId
+ )
+ .putInt(
+ DataSyncWorker.INPUT_TAXREF_LIST_ID,
+ appSettings.taxrefListId
+ )
+ .putInt(
+ DataSyncWorker.INPUT_PAGE_SIZE,
+ appSettings.pageSize
+ )
+ .putInt(
+ DataSyncWorker.INPUT_PAGE_MAX_RETRY,
+ appSettings.pageMaxRetry
+ )
+ .build()
+ )
+ .setConstraints(
+ constraints
+ )
+ .build()
+
val continuation = workManager.beginUniqueWork(
DataSyncWorker.DATA_SYNC_WORKER,
ExistingWorkPolicy.KEEP,
- OneTimeWorkRequest.Builder(DataSyncWorker::class.java).addTag(DataSyncWorker.DATA_SYNC_WORKER_TAG).setInputData(
- Data.Builder().putInt(
- DataSyncWorker.INPUT_USERS_MENU_ID,
- appSettings.usersMenuId
- ).putInt(
- DataSyncWorker.INPUT_TAXREF_LIST_ID,
- appSettings.taxrefListId
- ).build()
- ).setConstraints(
- constraints
- ).build()
+ dataSyncWorkRequest
)
// start the work
diff --git a/sync/src/main/java/fr/geonature/sync/sync/DataSyncWorker.kt b/sync/src/main/java/fr/geonature/sync/sync/DataSyncWorker.kt
index 21fc9387..ef564475 100644
--- a/sync/src/main/java/fr/geonature/sync/sync/DataSyncWorker.kt
+++ b/sync/src/main/java/fr/geonature/sync/sync/DataSyncWorker.kt
@@ -3,8 +3,10 @@ package fr.geonature.sync.sync
import android.content.Context
import android.text.TextUtils
import android.util.Log
-import androidx.work.Worker
+import androidx.work.CoroutineWorker
+import androidx.work.Data
import androidx.work.WorkerParameters
+import androidx.work.workDataOf
import fr.geonature.commons.data.DefaultNomenclature
import fr.geonature.commons.data.InputObserver
import fr.geonature.commons.data.Nomenclature
@@ -16,10 +18,13 @@ import fr.geonature.commons.data.Taxonomy
import fr.geonature.sync.R
import fr.geonature.sync.api.GeoNatureAPIClient
import fr.geonature.sync.data.LocalDatabase
+import fr.geonature.sync.settings.AppSettings
import fr.geonature.sync.sync.io.DatasetJsonReader
import fr.geonature.sync.sync.io.TaxonomyJsonReader
import org.json.JSONObject
import retrofit2.Response
+import retrofit2.awaitResponse
+import java.io.BufferedReader
import java.util.Date
/**
@@ -28,32 +33,26 @@ import java.util.Date
* @author [S. Grimault](mailto:sebastien.grimault@gmail.com)
*/
class DataSyncWorker(
- appContext: Context,
- workerParams: WorkerParameters
-) : Worker(
+ appContext: Context, workerParams: WorkerParameters
+) : CoroutineWorker(
appContext,
workerParams
) {
-
private val dataSyncManager = DataSyncManager.getInstance(applicationContext)
- override fun doWork(): Result {
+ override suspend fun doWork(): Result {
val startTime = Date()
val geoNatureAPIClient = GeoNatureAPIClient.instance(applicationContext)
-
- if (geoNatureAPIClient == null) {
- dataSyncManager.syncMessage.postValue(applicationContext.getString(R.string.sync_error_server_url_configuration))
- return Result.failure()
- }
-
- dataSyncManager.syncMessage.postValue(applicationContext.getString(R.string.sync_start_synchronization))
+ ?: return Result.failure(workData(applicationContext.getString(R.string.sync_error_server_url_configuration)))
Log.i(
TAG,
"starting local data synchronization from '${geoNatureAPIClient.geoNatureBaseUrl}'..."
)
+ setProgress(workData(applicationContext.getString(R.string.sync_start_synchronization)))
+
val syncDatasetResult = syncDataset(geoNatureAPIClient)
if (syncDatasetResult is Result.Failure) {
@@ -98,6 +97,14 @@ class DataSyncWorker(
inputData.getInt(
INPUT_TAXREF_LIST_ID,
0
+ ),
+ inputData.getInt(
+ INPUT_PAGE_SIZE,
+ AppSettings.DEFAULT_PAGE_SIZE
+ ),
+ inputData.getInt(
+ INPUT_PAGE_MAX_RETRY,
+ AppSettings.DEFAULT_PAGE_MAX_RETRY
)
)
@@ -119,16 +126,16 @@ class DataSyncWorker(
if (syncNomenclatureResult is Result.Success) {
dataSyncManager.updateLastSynchronizedDate()
- dataSyncManager.syncMessage.postValue(applicationContext.getString(R.string.sync_data_succeeded))
+ return Result.success(workData(applicationContext.getString(R.string.sync_data_succeeded)))
}
return syncInputObserversResult
}
- private fun syncDataset(geoNatureServiceClient: GeoNatureAPIClient): Result {
+ private suspend fun syncDataset(geoNatureServiceClient: GeoNatureAPIClient): Result {
return try {
val response = geoNatureServiceClient.getMetaDatasets()
- .execute()
+ .awaitResponse()
checkResponse(response).run {
if (this is Result.Failure) {
@@ -136,14 +143,11 @@ class DataSyncWorker(
}
}
- val jsonString = response.body()?.string() ?: return Result.failure()
- val dataset = DatasetJsonReader().read(jsonString)
-
- dataSyncManager.syncMessage.postValue(
- applicationContext.getString(
- R.string.sync_data_dataset,
- dataset.size
- )
+ val inputStream = response.body()
+ ?.byteStream() ?: return Result.failure()
+ val dataset = DatasetJsonReader().read(
+ inputStream.bufferedReader()
+ .use(BufferedReader::readText)
)
Log.i(
@@ -151,25 +155,35 @@ class DataSyncWorker(
"dataset to update: ${dataset.size}"
)
+ setProgress(
+ workData(
+ applicationContext.getString(
+ R.string.sync_data_dataset,
+ dataset.size
+ )
+ )
+ )
+
LocalDatabase.getInstance(applicationContext)
.datasetDao()
.insert(*dataset.toTypedArray())
Result.success()
} catch (e: Exception) {
- dataSyncManager.syncMessage.postValue(applicationContext.getString(R.string.sync_error_server_error))
-
- Result.failure()
+ Result.failure(
+ workData(
+ applicationContext.getString(R.string.sync_error_server_error)
+ )
+ )
}
}
- private fun syncInputObservers(
- geoNatureServiceClient: GeoNatureAPIClient,
- menuId: Int
+ private suspend fun syncInputObservers(
+ geoNatureServiceClient: GeoNatureAPIClient, menuId: Int
): Result {
return try {
val response = geoNatureServiceClient.getUsers(menuId)
- .execute()
+ .awaitResponse()
checkResponse(response).run {
if (this is Result.Failure) {
@@ -179,42 +193,46 @@ class DataSyncWorker(
val users = response.body() ?: return Result.failure()
val inputObservers = users.map {
- InputObserver(
- it.id,
- it.lastname,
- it.firstname
- )
- }
+ InputObserver(
+ it.id,
+ it.lastname,
+ it.firstname
+ )
+ }
.toTypedArray()
- dataSyncManager.syncMessage.postValue(
- applicationContext.getString(
- R.string.sync_data_observers,
- users.size
- )
- )
-
Log.i(
TAG,
"users to update: ${users.size}"
)
+ setProgress(
+ workData(
+ applicationContext.getString(
+ R.string.sync_data_observers,
+ users.size
+ )
+ )
+ )
+
LocalDatabase.getInstance(applicationContext)
.inputObserverDao()
.insert(*inputObservers)
Result.success()
} catch (e: Exception) {
- dataSyncManager.syncMessage.postValue(applicationContext.getString(R.string.sync_error_server_error))
-
- Result.failure()
+ Result.failure(
+ workData(
+ applicationContext.getString(R.string.sync_error_server_error)
+ )
+ )
}
}
- private fun syncTaxonomyRanks(geoNatureServiceClient: GeoNatureAPIClient): Result {
+ private suspend fun syncTaxonomyRanks(geoNatureServiceClient: GeoNatureAPIClient): Result {
return try {
val taxonomyRanksResponse = geoNatureServiceClient.getTaxonomyRanks()
- .execute()
+ .awaitResponse()
checkResponse(taxonomyRanksResponse).run {
if (this is Result.Failure) {
@@ -222,8 +240,12 @@ class DataSyncWorker(
}
}
- val jsonString = taxonomyRanksResponse.body()?.string() ?: return Result.failure()
- val taxonomy = TaxonomyJsonReader().read(jsonString)
+ val inputStream = taxonomyRanksResponse.body()
+ ?.byteStream() ?: return Result.failure()
+ val taxonomy = TaxonomyJsonReader().read(
+ inputStream.bufferedReader()
+ .use(BufferedReader::readText)
+ )
Log.i(
TAG,
@@ -236,99 +258,159 @@ class DataSyncWorker(
Result.success()
} catch (e: Exception) {
- dataSyncManager.syncMessage.postValue(applicationContext.getString(R.string.sync_error_server_error))
-
- Result.failure()
+ Result.failure(
+ workData(
+ applicationContext.getString(R.string.sync_error_server_error)
+ )
+ )
}
}
- private fun syncTaxa(geoNatureServiceClient: GeoNatureAPIClient, listId: Int): Result {
+ private suspend fun syncTaxa(
+ geoNatureServiceClient: GeoNatureAPIClient,
+ listId: Int,
+ pageSize: Int,
+ pageMaxRetry: Int
+ ): Result {
return try {
- val taxrefResponse = geoNatureServiceClient.getTaxref(listId)
- .execute()
+ var hasNext: Boolean
+ var offset = 0
- checkResponse(taxrefResponse).run {
- if (this is Result.Failure) {
- return this
+ val validTaxaIds = mutableSetOf()
+
+ // fetch all taxa from paginated list
+ do {
+ val taxrefResponse = geoNatureServiceClient.getTaxref(
+ listId,
+ pageSize,
+ offset
+ )
+ .awaitResponse()
+
+ if (checkResponse(taxrefResponse) is Result.Failure) {
+ hasNext = false
+ continue
}
- }
- val taxrefAreasResponse = geoNatureServiceClient.getTaxrefAreas()
- .execute()
+ val taxref = taxrefResponse.body()
- checkResponse(taxrefAreasResponse).run {
- if (this is Result.Failure) {
- return this
+ if (taxref == null || taxref.isEmpty()) {
+ hasNext = false
+ continue
}
- }
- val taxref = taxrefResponse.body() ?: return Result.failure()
- val taxrefAreas = taxrefAreasResponse.body() ?: return Result.failure()
-
- val taxa = taxref.map {
- Taxon(
- it.id,
- it.name,
- Taxonomy(
- it.kingdom,
- it.group
- ),
- it.description
+ val taxa = taxref.asSequence()
+ .map {
+ Taxon(
+ it.id,
+ it.name,
+ Taxonomy(
+ it.kingdom,
+ it.group
+ ),
+ it.description
+ )
+ }
+ .onEach {
+ validTaxaIds.add(it.id)
+ }
+ .toList()
+ .toTypedArray()
+
+ LocalDatabase.getInstance(applicationContext)
+ .taxonDao()
+ .insert(*taxa)
+
+ Log.i(
+ TAG,
+ "taxa to update: ${offset + taxa.size}"
)
- }
- .toTypedArray()
- dataSyncManager.syncMessage.postValue(
- applicationContext.getString(
- R.string.sync_data_taxa,
- taxa.size
+ setProgress(
+ workData(
+ applicationContext.getString(
+ R.string.sync_data_taxa,
+ (offset + taxa.size)
+ )
+ )
)
- )
- Log.i(
- TAG,
- "taxa to update: ${taxa.size}"
- )
+ if (taxa.size == pageSize) {
+ offset += pageSize
+ hasNext = offset / pageSize < pageMaxRetry
+ } else {
+ hasNext = false
+ }
+ } while (hasNext)
- LocalDatabase.getInstance(applicationContext)
- .taxonDao()
- .insert(*taxa)
+ offset = 0
- val taxonAreas = taxrefAreas.asSequence()
- .filter { taxrefArea -> taxa.any { it.id == taxrefArea.taxrefId } }
- .map {
- TaxonArea(
- it.taxrefId,
- it.areaId,
- it.color,
- it.numberOfObservers,
- it.lastUpdatedAt
+ // fetch all taxa metadata from paginated list
+ do {
+ val taxrefAreasResponse = geoNatureServiceClient.getTaxrefAreas(
+ pageSize,
+ offset
)
+ .awaitResponse()
+
+ if (checkResponse(taxrefAreasResponse) is Result.Failure) {
+ hasNext = false
+ continue
}
- .toList()
- .toTypedArray()
- Log.i(
- TAG,
- "taxa with areas to update: ${taxonAreas.size}"
- )
+ val taxrefAreas = taxrefAreasResponse.body()
- LocalDatabase.getInstance(applicationContext)
- .taxonAreaDao()
- .insert(*taxonAreas)
+ if (taxrefAreas == null || taxrefAreas.isEmpty()) {
+ hasNext = false
+ continue
+ }
+
+ val taxonAreas = taxrefAreas.asSequence()
+ .filter { taxrefArea -> validTaxaIds.any { it == taxrefArea.taxrefId } }
+ .map {
+ TaxonArea(
+ it.taxrefId,
+ it.areaId,
+ it.color,
+ it.numberOfObservers,
+ it.lastUpdatedAt
+ )
+ }
+ .toList()
+ .toTypedArray()
+
+ LocalDatabase.getInstance(applicationContext)
+ .taxonAreaDao()
+ .insert(*taxonAreas)
+
+ Log.i(
+ TAG,
+ "taxa with areas to update: ${offset + taxonAreas.size}"
+ )
+
+ offset += pageSize
+ hasNext = offset / pageSize < pageMaxRetry
+ } while (hasNext)
Result.success()
} catch (e: Exception) {
- dataSyncManager.syncMessage.postValue(applicationContext.getString(R.string.sync_error_server_error))
+ Log.w(
+ TAG,
+ e
+ )
- Result.failure()
+ Result.failure(
+ workData(
+ applicationContext.getString(R.string.sync_error_server_error)
+ )
+ )
}
}
- private fun syncNomenclature(geoNatureServiceClient: GeoNatureAPIClient): Result {
+ private suspend fun syncNomenclature(geoNatureServiceClient: GeoNatureAPIClient): Result {
return try {
val nomenclatureResponse = geoNatureServiceClient.getNomenclatures()
- .execute()
+ .awaitResponse()
checkResponse(nomenclatureResponse).run {
if (this is Result.Failure) {
@@ -342,12 +424,12 @@ class DataSyncWorker(
.filter { it.nomenclatures.isNotEmpty() }
val nomenclatureTypesToUpdate = validNomenclatureTypesToUpdate.map {
- NomenclatureType(
- it.id,
- it.mnemonic,
- it.defaultLabel
- )
- }
+ NomenclatureType(
+ it.id,
+ it.mnemonic,
+ it.defaultLabel
+ )
+ }
.toList()
.toTypedArray()
@@ -355,26 +437,29 @@ class DataSyncWorker(
TAG,
"nomenclature types to update: ${nomenclatureTypesToUpdate.size}"
)
- dataSyncManager.syncMessage.postValue(
- applicationContext.getString(
- R.string.sync_data_nomenclature_type,
- nomenclatureTypesToUpdate.size
+
+ setProgress(
+ workData(
+ applicationContext.getString(
+ R.string.sync_data_nomenclature_type,
+ nomenclatureTypesToUpdate.size
+ )
)
)
val nomenclaturesToUpdate = validNomenclatureTypesToUpdate.map { nomenclatureType ->
- nomenclatureType.nomenclatures.asSequence()
- .filter { it.id > 0 }
- .map {
- Nomenclature(
- it.id,
- it.code,
- if (TextUtils.isEmpty(it.hierarchy)) nomenclatureType.id.toString() else it.hierarchy!!,
- it.defaultLabel,
- nomenclatureType.id
- )
- }
- }
+ nomenclatureType.nomenclatures.asSequence()
+ .filter { it.id > 0 }
+ .map {
+ Nomenclature(
+ it.id,
+ it.code,
+ if (TextUtils.isEmpty(it.hierarchy)) nomenclatureType.id.toString() else it.hierarchy!!,
+ it.defaultLabel,
+ nomenclatureType.id
+ )
+ }
+ }
.flatMap { it.asSequence() }
.toList()
.toTypedArray()
@@ -413,17 +498,20 @@ class DataSyncWorker(
TAG,
"nomenclature to update: ${nomenclaturesToUpdate.size}"
)
- dataSyncManager.syncMessage.postValue(
- applicationContext.getString(
- R.string.sync_data_nomenclature,
- nomenclaturesToUpdate.size
+
+ setProgress(
+ workData(
+ applicationContext.getString(
+ R.string.sync_data_nomenclature,
+ nomenclaturesToUpdate.size
+ )
)
)
// TODO: fetch available GeoNature modules
val defaultNomenclatureResponse =
geoNatureServiceClient.getDefaultNomenclaturesValues("occtax")
- .execute()
+ .awaitResponse()
checkResponse(defaultNomenclatureResponse).run {
if (this is Result.Failure) {
@@ -431,8 +519,12 @@ class DataSyncWorker(
}
}
- val jsonString = defaultNomenclatureResponse.body()?.string() ?: return Result.failure()
- val defaultNomenclatureAsJson = JSONObject(jsonString)
+ val inputStream = defaultNomenclatureResponse.body()
+ ?.byteStream() ?: return Result.failure()
+ val defaultNomenclatureAsJson = JSONObject(
+ inputStream.bufferedReader()
+ .use(BufferedReader::readText)
+ )
val defaultNomenclaturesToUpdate = defaultNomenclatureAsJson.keys()
.asSequence()
.filter { mnemonic ->
@@ -451,10 +543,13 @@ class DataSyncWorker(
TAG,
"nomenclature default values to update: ${defaultNomenclaturesToUpdate.size}"
)
- dataSyncManager.syncMessage.postValue(
- applicationContext.getString(
- R.string.sync_data_nomenclature_default,
- defaultNomenclaturesToUpdate.size
+
+ setProgress(
+ workData(
+ applicationContext.getString(
+ R.string.sync_data_nomenclature_default,
+ defaultNomenclaturesToUpdate.size
+ )
)
)
@@ -472,39 +567,57 @@ class DataSyncWorker(
Result.success()
} catch (e: Exception) {
- dataSyncManager.syncMessage.postValue(applicationContext.getString(R.string.sync_error_server_error))
-
- Result.failure()
+ Result.failure(
+ workData(
+ applicationContext.getString(R.string.sync_error_server_error)
+ )
+ )
}
}
private fun checkResponse(response: Response<*>): Result {
// not connected
if (response.code() == 403) {
- dataSyncManager.serverStatus.postValue(ServerStatus.FORBIDDEN)
- dataSyncManager.syncMessage.postValue(applicationContext.getString(R.string.sync_error_server_not_connected))
-
- return Result.failure()
+ return Result.failure(
+ workData(
+ applicationContext.getString(R.string.sync_error_server_not_connected),
+ ServerStatus.FORBIDDEN
+ )
+ )
}
if (!response.isSuccessful) {
- dataSyncManager.serverStatus.postValue(ServerStatus.INTERNAL_SERVER_ERROR)
- dataSyncManager.syncMessage.postValue(applicationContext.getString(R.string.sync_error_server_error))
-
- return Result.failure()
+ return Result.failure(
+ workData(
+ applicationContext.getString(R.string.sync_error_server_error),
+ ServerStatus.INTERNAL_SERVER_ERROR
+ )
+ )
}
return Result.success()
}
+ private fun workData(syncMessage: String, serverStatus: ServerStatus = ServerStatus.OK): Data {
+ return workDataOf(
+ KEY_SYNC_MESSAGE to syncMessage,
+ KEY_SERVER_STATUS to serverStatus.ordinal
+ )
+ }
+
companion object {
private val TAG = DataSyncWorker::class.java.name
- // The name of the synchronisation work
+ const val KEY_SYNC_MESSAGE = "KEY_SYNC_MESSAGE"
+ const val KEY_SERVER_STATUS = "KEY_SERVER_STATUS"
+
+ // The name of the synchronization work
const val DATA_SYNC_WORKER = "data_sync_worker"
const val DATA_SYNC_WORKER_TAG = "data_sync_worker_tag"
const val INPUT_USERS_MENU_ID = "usersMenuId"
const val INPUT_TAXREF_LIST_ID = "taxrefListId"
+ const val INPUT_PAGE_SIZE = "pageSize"
+ const val INPUT_PAGE_MAX_RETRY = "pageMaxRetry"
}
}
diff --git a/sync/src/main/java/fr/geonature/sync/sync/InputsSyncWorker.kt b/sync/src/main/java/fr/geonature/sync/sync/InputsSyncWorker.kt
index 5b444086..6f92fd50 100644
--- a/sync/src/main/java/fr/geonature/sync/sync/InputsSyncWorker.kt
+++ b/sync/src/main/java/fr/geonature/sync/sync/InputsSyncWorker.kt
@@ -2,16 +2,16 @@ package fr.geonature.sync.sync
import android.content.Context
import android.util.Log
+import androidx.core.app.NotificationManagerCompat
import androidx.work.CoroutineWorker
+import androidx.work.Data
import androidx.work.WorkInfo
import androidx.work.WorkerParameters
-import fr.geonature.commons.util.getInputsFolder
-import fr.geonature.mountpoint.util.FileUtils
+import androidx.work.workDataOf
import fr.geonature.sync.api.GeoNatureAPIClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
-import org.json.JSONObject
import java.io.File
/**
@@ -26,7 +26,6 @@ class InputsSyncWorker(
appContext,
workerParams
) {
-
private val packageInfoManager = PackageInfoManager.getInstance(applicationContext)
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
@@ -37,8 +36,7 @@ class InputsSyncWorker(
}
val packageInfo =
- packageInfoManager.packageInfos.value?.firstOrNull { it.packageName == packageName }
- ?: return@withContext Result.failure()
+ packageInfoManager.getPackageInfo(packageName) ?: return@withContext Result.failure()
val geoNatureAPIClient = GeoNatureAPIClient.instance(applicationContext)
?: return@withContext Result.failure()
@@ -48,20 +46,25 @@ class InputsSyncWorker(
"starting inputs synchronization for '$packageName'..."
)
- packageInfoManager.updatePackageInfo(
- packageInfo.packageName,
- WorkInfo.State.RUNNING,
- emptyList()
+ NotificationManagerCompat.from(applicationContext)
+ .cancelAll()
+
+ setProgress(
+ workData(
+ packageInfo.packageName,
+ WorkInfo.State.RUNNING
+ )
)
- val inputsToSynchronize = getInputsToSynchronize(packageInfo)
+ val inputsToSynchronize = packageInfoManager.getInputsToSynchronize(packageInfo)
val inputsSynchronized = mutableListOf()
if (inputsToSynchronize.isEmpty()) {
- packageInfoManager.updatePackageInfo(
- packageInfo.packageName,
- WorkInfo.State.CANCELLED,
- emptyList()
+ setProgress(
+ workData(
+ packageInfo.packageName,
+ WorkInfo.State.CANCELLED
+ )
)
Log.i(
@@ -72,25 +75,29 @@ class InputsSyncWorker(
return@withContext Result.success()
}
- packageInfoManager.updatePackageInfo(
- packageInfo.packageName,
- WorkInfo.State.RUNNING,
- inputsToSynchronize
+ setProgress(
+ workData(
+ packageInfo.packageName,
+ WorkInfo.State.RUNNING,
+ inputsToSynchronize.size
+ )
)
inputsToSynchronize.forEach { syncInput ->
try {
val response = geoNatureAPIClient.sendInput(
- syncInput.module,
- syncInput.payload
- )
+ syncInput.module,
+ syncInput.payload
+ )
.execute()
if (!response.isSuccessful) {
- packageInfoManager.updatePackageInfo(
- packageInfo.packageName,
- WorkInfo.State.FAILED,
- inputsToSynchronize
+ setProgress(
+ workData(
+ packageInfo.packageName,
+ WorkInfo.State.FAILED,
+ inputsToSynchronize.size
+ )
)
delay(1000)
@@ -100,68 +107,77 @@ class InputsSyncWorker(
deleteSynchronizedInput(syncInput).takeIf { deleted -> deleted }
?.also {
inputsSynchronized.add(syncInput)
- packageInfoManager.updatePackageInfo(packageInfo.packageName,
- WorkInfo.State.RUNNING,
- inputsToSynchronize.filter { filtered ->
- !inputsSynchronized.contains(
- filtered
- )
- })
+ setProgress(
+ workData(
+ packageInfo.packageName,
+ WorkInfo.State.RUNNING,
+ inputsToSynchronize.filter { filtered ->
+ !inputsSynchronized.contains(
+ filtered
+ )
+ }.size
+ )
+ )
}
} catch (e: Exception) {
- packageInfoManager.updatePackageInfo(
- packageInfo.packageName,
- WorkInfo.State.FAILED,
- inputsToSynchronize
+ setProgress(
+ workData(
+ packageInfo.packageName,
+ WorkInfo.State.FAILED,
+ inputsToSynchronize.size
+ )
)
delay(1000)
}
}
- packageInfoManager.updatePackageInfo(packageInfo.packageName,
- if (inputsSynchronized.size == inputsToSynchronize.size) WorkInfo.State.SUCCEEDED else WorkInfo.State.FAILED,
- inputsToSynchronize.filter { filtered -> !inputsSynchronized.contains(filtered) })
-
Log.i(
TAG,
"inputs synchronization ${if (inputsSynchronized.size == inputsToSynchronize.size) "successfully finished" else "finished with errors"} for '$packageName'"
)
- Result.success()
- }
-
- private suspend fun getInputsToSynchronize(packageInfo: PackageInfo): List =
- withContext(Dispatchers.IO) {
- FileUtils.getInputsFolder(
- applicationContext,
- packageInfo.packageName
+ if (inputsSynchronized.size == inputsToSynchronize.size) {
+ Result.success(
+ workData(
+ packageInfo.packageName,
+ WorkInfo.State.SUCCEEDED,
+ inputsToSynchronize.filter { filtered -> !inputsSynchronized.contains(filtered) }.size
+ )
+ )
+ } else {
+ Result.failure(
+ workData(
+ packageInfo.packageName,
+ WorkInfo.State.FAILED,
+ inputsToSynchronize.filter { filtered -> !inputsSynchronized.contains(filtered) }.size
+ )
)
- .walkTopDown()
- .filter { f -> f.isFile && f.extension == "json" && f.canRead() }
- .map {
- val rawString = it.readText()
- val toJson = JSONObject(rawString)
- SyncInput(
- packageInfo,
- it.absolutePath,
- toJson.getString("module"),
- toJson
- )
- }
- .toList()
}
+ }
private suspend fun deleteSynchronizedInput(syncInput: SyncInput): Boolean =
withContext(Dispatchers.IO) {
- File(syncInput.filePath).takeIf { it.exists() && it.isFile && it.parentFile.canWrite() }?.delete()?.also {
- getInputsToSynchronize(syncInput.packageInfo)
- } ?: false
+ File(syncInput.filePath).takeIf { it.exists() && it.isFile && it.parentFile.canWrite() }
+ ?.delete()
+ ?.also {
+ packageInfoManager.getInputsToSynchronize(syncInput.packageInfo)
+ } ?: false
}
+ private fun workData(packageName: String, state: WorkInfo.State, inputs: Int = 0): Data {
+ return workDataOf(
+ KEY_PACKAGE_NAME to packageName,
+ KEY_PACKAGE_STATUS to state.ordinal,
+ KEY_PACKAGE_INPUTS to inputs
+ )
+ }
+
companion object {
private val TAG = InputsSyncWorker::class.java.name
const val KEY_PACKAGE_NAME = "KEY_PACKAGE_NAME"
+ const val KEY_PACKAGE_STATUS = "KEY_PACKAGE_STATUS"
+ const val KEY_PACKAGE_INPUTS = "KEY_PACKAGE_INPUTS"
const val INPUT_SYNC_WORKER_TAG = "inputs_sync_worker_tag"
val workName: (packageName: String) -> String = { "inputs_sync_worker:$it" }
diff --git a/sync/src/main/java/fr/geonature/sync/sync/PackageInfo.kt b/sync/src/main/java/fr/geonature/sync/sync/PackageInfo.kt
index 670f028c..f4ae59a1 100644
--- a/sync/src/main/java/fr/geonature/sync/sync/PackageInfo.kt
+++ b/sync/src/main/java/fr/geonature/sync/sync/PackageInfo.kt
@@ -16,6 +16,6 @@ data class PackageInfo(
val icon: Drawable,
val launchIntent: Intent?
) {
- val inputs: MutableList = mutableListOf()
+ var inputs: Int = 0
var state: WorkInfo.State = WorkInfo.State.ENQUEUED
}
diff --git a/sync/src/main/java/fr/geonature/sync/sync/PackageInfoManager.kt b/sync/src/main/java/fr/geonature/sync/sync/PackageInfoManager.kt
index 019a7d16..7a18690e 100644
--- a/sync/src/main/java/fr/geonature/sync/sync/PackageInfoManager.kt
+++ b/sync/src/main/java/fr/geonature/sync/sync/PackageInfoManager.kt
@@ -2,11 +2,11 @@ package fr.geonature.sync.sync
import android.content.Context
import android.content.pm.PackageManager
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import androidx.work.WorkInfo
+import fr.geonature.commons.util.getInputsFolder
+import fr.geonature.mountpoint.util.FileUtils
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
+import org.json.JSONObject
/**
* [PackageInfo] manager.
@@ -26,8 +26,6 @@ class PackageInfoManager private constructor(private val applicationContext: Con
.sharedUserId
private val availablePackageInfos = mutableMapOf()
- private val _packageInfos: MutableLiveData> = MutableLiveData()
- val packageInfos: LiveData> = _packageInfos
/**
* Finds all compatible installed applications.
@@ -42,7 +40,8 @@ class PackageInfoManager private constructor(private val applicationContext: Con
.map {
PackageInfo(
it.packageName,
- pm.getApplicationLabel(it).toString(),
+ pm.getApplicationLabel(it)
+ .toString(),
pm.getPackageInfo(
it.packageName,
PackageManager.GET_META_DATA
@@ -53,36 +52,42 @@ class PackageInfoManager private constructor(private val applicationContext: Con
}
.onEach { availablePackageInfos[it.packageName] = it }
.toList()
- .also {
- _packageInfos.postValue(it)
- }
}
/**
- * Updates given [PackageInfo] status.
+ * Gets related info from package name.
*/
- fun updatePackageInfo(
- packageName: String,
- state: WorkInfo.State,
- syncInputs: List
- ): PackageInfo? {
- val packageInfoToUpdate = availablePackageInfos[packageName]?.copy()?.apply {
- syncInputs.let {
- inputs.also {
- it.clear()
- it.addAll(syncInputs)
- }
- }
-
- this.state = state
- } ?: return null
+ suspend fun getPackageInfo(packageName: String): PackageInfo? = withContext(IO) {
+ val packageInfo = availablePackageInfos[packageName]
- availablePackageInfos[packageName] = packageInfoToUpdate
- _packageInfos.postValue(availablePackageInfos.values.toList())
+ if (packageInfo == null) {
+ getInstalledApplications()
+ }
- return packageInfoToUpdate
+ availablePackageInfos[packageName]
}
+ suspend fun getInputsToSynchronize(packageInfo: PackageInfo): List =
+ withContext(IO) {
+ FileUtils.getInputsFolder(
+ applicationContext,
+ packageInfo.packageName
+ )
+ .walkTopDown()
+ .filter { f -> f.isFile && f.extension == "json" && f.canRead() }
+ .map {
+ val rawString = it.readText()
+ val toJson = JSONObject(rawString)
+ SyncInput(
+ packageInfo,
+ it.absolutePath,
+ toJson.getString("module"),
+ toJson
+ )
+ }
+ .toList()
+ }
+
companion object {
@Volatile
diff --git a/sync/src/main/java/fr/geonature/sync/sync/PackageInfoViewModel.kt b/sync/src/main/java/fr/geonature/sync/sync/PackageInfoViewModel.kt
index 9e07b25a..1b23c0e5 100644
--- a/sync/src/main/java/fr/geonature/sync/sync/PackageInfoViewModel.kt
+++ b/sync/src/main/java/fr/geonature/sync/sync/PackageInfoViewModel.kt
@@ -3,6 +3,9 @@ package fr.geonature.sync.sync
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Transformations.map
+import androidx.lifecycle.Transformations.switchMap
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
@@ -11,6 +14,7 @@ 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 kotlinx.coroutines.launch
@@ -25,16 +29,45 @@ class PackageInfoViewModel(application: Application) : AndroidViewModel(applicat
private val packageInfoManager: PackageInfoManager =
PackageInfoManager.getInstance(getApplication())
- val packageInfos: LiveData> = packageInfoManager.packageInfos
+ private val availablePackageInfos: MutableLiveData> = MutableLiveData()
+ val packageInfos: LiveData> =
+ switchMap(availablePackageInfos) { packageInfos ->
+ map(workManager.getWorkInfosByTagLiveData(InputsSyncWorker.INPUT_SYNC_WORKER_TAG)) { workInfos ->
+ packageInfos.asSequence()
+ .map { packageInfo ->
+ packageInfo.copy()
+ .apply {
+ val workInfo =
+ workInfos.firstOrNull { workInfo -> workInfo.progress.getString(InputsSyncWorker.KEY_PACKAGE_NAME) == packageName }
+ ?: workInfos.firstOrNull { workInfo -> workInfo.outputData.getString(InputsSyncWorker.KEY_PACKAGE_NAME) == packageName }
+
+ if (workInfo != null) {
+ state = WorkInfo.State.values()[workInfo.progress.getInt(
+ InputsSyncWorker.KEY_PACKAGE_STATUS,
+ workInfo.outputData.getInt(
+ InputsSyncWorker.KEY_PACKAGE_STATUS,
+ WorkInfo.State.ENQUEUED.ordinal
+ )
+ )]
+ inputs = workInfo.progress.getInt(
+ InputsSyncWorker.KEY_PACKAGE_INPUTS,
+ 0
+ )
+ }
+ }
+ }
+ .toList()
+ }
+ }
/**
* Gets all compatible installed applications.
*/
fun getInstalledApplications() {
viewModelScope.launch {
- packageInfoManager.getInstalledApplications().asSequence().forEach {
- startSyncInputs(it)
- }
+ val packageInfos = packageInfoManager.getInstalledApplications()
+ availablePackageInfos.postValue(packageInfos)
+ packageInfos.forEach { startSyncInputs(it) }
}
}
@@ -43,18 +76,23 @@ class PackageInfoViewModel(application: Application) : AndroidViewModel(applicat
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
+ val inputsSyncWorkerRequest = OneTimeWorkRequest.Builder(InputsSyncWorker::class.java)
+ .addTag(InputsSyncWorker.INPUT_SYNC_WORKER_TAG)
+ .setConstraints(constraints)
+ .setInputData(
+ Data.Builder()
+ .putString(
+ InputsSyncWorker.KEY_PACKAGE_NAME,
+ packageInfo.packageName
+ )
+ .build()
+ )
+ .build()
+
val continuation = workManager.beginUniqueWork(
InputsSyncWorker.workName(packageInfo.packageName),
ExistingWorkPolicy.KEEP,
- OneTimeWorkRequest.Builder(InputsSyncWorker::class.java)
- .addTag(InputsSyncWorker.INPUT_SYNC_WORKER_TAG)
- .setConstraints(constraints)
- .setInputData(
- Data.Builder()
- .putString(InputsSyncWorker.KEY_PACKAGE_NAME, packageInfo.packageName)
- .build()
- )
- .build()
+ inputsSyncWorkerRequest
)
// start the work
diff --git a/sync/src/main/java/fr/geonature/sync/sync/ServerStatus.kt b/sync/src/main/java/fr/geonature/sync/sync/ServerStatus.kt
index 0387f7e2..5198a068 100644
--- a/sync/src/main/java/fr/geonature/sync/sync/ServerStatus.kt
+++ b/sync/src/main/java/fr/geonature/sync/sync/ServerStatus.kt
@@ -6,5 +6,7 @@ package fr.geonature.sync.sync
* @author [S. Grimault](mailto:sebastien.grimault@gmail.com)
*/
enum class ServerStatus(val httpStatus: Int) {
- FORBIDDEN(403), INTERNAL_SERVER_ERROR(500)
+ OK(200),
+ FORBIDDEN(403),
+ INTERNAL_SERVER_ERROR(500)
}
diff --git a/sync/src/main/java/fr/geonature/sync/ui/home/HomeActivity.kt b/sync/src/main/java/fr/geonature/sync/ui/home/HomeActivity.kt
index 9a3c32af..cc066625 100644
--- a/sync/src/main/java/fr/geonature/sync/ui/home/HomeActivity.kt
+++ b/sync/src/main/java/fr/geonature/sync/ui/home/HomeActivity.kt
@@ -1,5 +1,6 @@
package fr.geonature.sync.ui.home
+import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.os.Bundle
@@ -7,14 +8,18 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.animation.AnimationUtils.loadAnimation
+import android.widget.ProgressBar
+import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.menu.MenuBuilder
+import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
+import androidx.work.WorkInfo
import com.google.android.material.snackbar.Snackbar
import fr.geonature.commons.ui.adapter.AbstractListItemRecyclerViewAdapter
import fr.geonature.sync.R
@@ -24,11 +29,15 @@ import fr.geonature.sync.settings.AppSettingsViewModel
import fr.geonature.sync.sync.DataSyncViewModel
import fr.geonature.sync.sync.PackageInfo
import fr.geonature.sync.sync.PackageInfoViewModel
-import fr.geonature.sync.sync.ServerStatus
+import fr.geonature.sync.sync.ServerStatus.FORBIDDEN
+import fr.geonature.sync.sync.ServerStatus.INTERNAL_SERVER_ERROR
import fr.geonature.sync.ui.login.LoginActivity
import fr.geonature.sync.ui.settings.PreferencesActivity
+import fr.geonature.sync.util.SettingsUtils.getGeoNatureServerUrl
+import fr.geonature.sync.util.SettingsUtils.getTaxHubServerUrl
+import fr.geonature.sync.util.SettingsUtils.setGeoNatureServerUrl
+import fr.geonature.sync.util.SettingsUtils.setTaxHubServerUrl
import fr.geonature.sync.util.observeOnce
-import kotlinx.android.synthetic.main.activity_home.*
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
@@ -45,6 +54,13 @@ class HomeActivity : AppCompatActivity() {
private lateinit var dataSyncViewModel: DataSyncViewModel
private lateinit var packageInfoViewModel: PackageInfoViewModel
private lateinit var adapter: PackageInfoRecyclerViewAdapter
+
+ private var homeContent: ConstraintLayout? = null
+ private var emptyTextView: TextView? = null
+ private var recyclerView: RecyclerView? = null
+ private var progressBar: ProgressBar? = null
+ private var dataSyncView: DataSyncView? = null
+
private var appSettings: AppSettings? = null
private var isLoggedIn: Boolean = false
@@ -53,6 +69,12 @@ class HomeActivity : AppCompatActivity() {
setContentView(R.layout.activity_home)
+ homeContent = findViewById(R.id.homeContent)
+ emptyTextView = findViewById(R.id.emptyTextView)
+ recyclerView = findViewById(R.id.recyclerView)
+ progressBar = findViewById(android.R.id.progress)
+ dataSyncView = findViewById(R.id.dataSyncView)
+
authLoginViewModel = configureAuthLoginViewModel()
dataSyncViewModel = configureDataSyncViewModel()
packageInfoViewModel = configurePackageInfoViewModel()
@@ -73,31 +95,31 @@ class HomeActivity : AppCompatActivity() {
}
override fun showEmptyTextView(show: Boolean) {
- if (emptyTextView.visibility == View.VISIBLE == show) {
+ if (emptyTextView?.visibility == View.VISIBLE == show) {
return
}
if (show) {
- emptyTextView.startAnimation(
+ emptyTextView?.startAnimation(
loadAnimation(
this@HomeActivity,
android.R.anim.fade_in
)
)
- emptyTextView.visibility = View.VISIBLE
+ emptyTextView?.visibility = View.VISIBLE
} else {
- emptyTextView.startAnimation(
+ emptyTextView?.startAnimation(
loadAnimation(
this@HomeActivity,
android.R.anim.fade_out
)
)
- emptyTextView.visibility = View.GONE
+ emptyTextView?.visibility = View.GONE
}
}
})
- with(appRecyclerView as RecyclerView) {
+ with(recyclerView as RecyclerView) {
layoutManager = LinearLayoutManager(context)
adapter = this@HomeActivity.adapter
@@ -123,6 +145,7 @@ class HomeActivity : AppCompatActivity() {
}
}
+ @SuppressLint("RestrictedApi")
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(
R.menu.settings,
@@ -137,10 +160,17 @@ class HomeActivity : AppCompatActivity() {
}
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
- menu?.findItem(R.id.menu_login)
- ?.isVisible = !isLoggedIn
- menu?.findItem(R.id.menu_logout)
- ?.isVisible = isLoggedIn
+ menu?.run {
+ findItem(R.id.menu_settings)?.isEnabled = appSettings != null
+ findItem(R.id.menu_login)?.also {
+ it.isEnabled = appSettings != null
+ it.isVisible = !isLoggedIn
+ }
+ findItem(R.id.menu_logout)?.also {
+ it.isEnabled = appSettings != null
+ it.isVisible = isLoggedIn
+ }
+ }
return super.onPrepareOptionsMenu(menu)
}
@@ -160,10 +190,10 @@ class HomeActivity : AppCompatActivity() {
.observe(this,
Observer {
Toast.makeText(
- this,
- R.string.toast_logout_success,
- Toast.LENGTH_SHORT
- )
+ this,
+ R.string.toast_logout_success,
+ Toast.LENGTH_SHORT
+ )
.show()
})
true
@@ -188,71 +218,68 @@ class HomeActivity : AppCompatActivity() {
private fun configureDataSyncViewModel(): DataSyncViewModel {
return ViewModelProvider(this,
DataSyncViewModel.Factory { DataSyncViewModel(application) }).get(DataSyncViewModel::class.java)
- .also { vm ->
- vm.syncOutputStatus.takeIf { !it.hasActiveObservers() }
- ?.observe(this,
- Observer {
- if (it == null || it.isEmpty()) {
- return@Observer
- }
-
- val workInfo = it[0]
- dataSyncView.setState(workInfo.state)
- })
- vm.lastSynchronizedDate.takeIf { !it.hasActiveObservers() }
- ?.observe(this,
- Observer {
- dataSyncView.setLastSynchronizedDate(it)
- })
- vm.syncMessage.takeIf { !it.hasActiveObservers() }
- ?.observe(this,
- Observer {
- dataSyncView.setMessage(it)
- })
- vm.serverStatus.takeIf { !it.hasActiveObservers() }
- ?.observe(this,
- Observer {
- if (it == null) return@Observer
-
- when (it) {
- ServerStatus.INTERNAL_SERVER_ERROR -> packageInfoViewModel.cancelTasks()
- ServerStatus.FORBIDDEN -> {
- packageInfoViewModel.cancelTasks()
-
- Toast.makeText(
- this,
- R.string.toast_not_connected,
- Toast.LENGTH_SHORT
- )
- .show()
-
- if (appSettings != null) {
- startActivityForResult(
- LoginActivity.newIntent(this),
- 0
- )
- }
- }
- }
- })
- }
}
private fun configurePackageInfoViewModel(): PackageInfoViewModel {
return ViewModelProvider(this,
PackageInfoViewModel.Factory { PackageInfoViewModel(application) }).get(
- PackageInfoViewModel::class.java
- )
+ PackageInfoViewModel::class.java
+ )
.also { vm ->
vm.packageInfos.observe(this@HomeActivity,
Observer {
- progress.visibility = View.GONE
+ progressBar?.visibility = View.GONE
adapter.setItems(it)
})
}
}
+ private fun observeDataSyncStatus(dataSyncViewModel: DataSyncViewModel) {
+ dataSyncViewModel.dataSyncStatus.takeUnless { it.hasActiveObservers() }
+ ?.observe(this,
+ Observer {
+ if (it == null) {
+ return@Observer
+ }
+
+ dataSyncView?.setState(if (it.syncMessage.isNullOrBlank()) WorkInfo.State.ENQUEUED else it.state)
+
+ if (!it.syncMessage.isNullOrBlank()) {
+ dataSyncView?.setMessage(it.syncMessage)
+ }
+
+ @Suppress("NON_EXHAUSTIVE_WHEN")
+ when (it.serverStatus) {
+ INTERNAL_SERVER_ERROR -> packageInfoViewModel.cancelTasks()
+ FORBIDDEN -> {
+ packageInfoViewModel.cancelTasks()
+
+ Toast.makeText(
+ this,
+ R.string.toast_not_connected,
+ Toast.LENGTH_SHORT
+ )
+ .show()
+
+ if (appSettings != null) {
+ startActivityForResult(
+ LoginActivity.newIntent(this),
+ 0
+ )
+ }
+ }
+ }
+ })
+ dataSyncViewModel.lastSynchronizedDate.takeUnless { it.hasActiveObservers() }
+ ?.observe(this,
+ Observer {
+ dataSyncView?.setLastSynchronizedDate(it)
+ })
+ }
+
private fun loadAppSettings() {
+ progressBar?.visibility = View.VISIBLE
+
ViewModelProvider(this,
fr.geonature.commons.settings.AppSettingsViewModel.Factory {
AppSettingsViewModel(
@@ -263,17 +290,19 @@ class HomeActivity : AppCompatActivity() {
vm.getAppSettings()
.observeOnce(this) {
if (it == null) {
- Snackbar.make(
- homeContent,
+ makeSnackbar(
getString(
R.string.snackbar_settings_not_found,
vm.getAppSettingsFilename()
- ),
- Snackbar.LENGTH_LONG
- )
- .show()
+ )
+ )?.show()
+
+ progressBar?.visibility = View.GONE
+ adapter.clear()
} else {
appSettings = it
+ mergeAppSettingsWithSharedPreferences(it)
+ invalidateOptionsMenu()
startSync()
}
@@ -286,11 +315,41 @@ class HomeActivity : AppCompatActivity() {
GlobalScope.launch(Main) {
delay(250)
+ observeDataSyncStatus(dataSyncViewModel)
dataSyncViewModel.startSync(appSettings)
- progress.visibility = View.VISIBLE
delay(500)
packageInfoViewModel.getInstalledApplications()
}
}
+
+ private fun makeSnackbar(text: CharSequence): Snackbar? {
+ val view = homeContent ?: return null
+
+ return Snackbar.make(
+ view,
+ text,
+ Snackbar.LENGTH_LONG
+ )
+ }
+
+ private fun mergeAppSettingsWithSharedPreferences(appSettings: AppSettings) {
+ val geoNatureServerUrl = appSettings.geoNatureServerUrl
+
+ if (!geoNatureServerUrl.isNullOrBlank() && getGeoNatureServerUrl(this).isNullOrBlank()) {
+ setGeoNatureServerUrl(
+ this,
+ geoNatureServerUrl
+ )
+ }
+
+ val taxHubServerUrl = appSettings.taxHubServerUrl
+
+ if (!taxHubServerUrl.isNullOrBlank() && getTaxHubServerUrl(this).isNullOrBlank()) {
+ setTaxHubServerUrl(
+ this,
+ taxHubServerUrl
+ )
+ }
+ }
}
diff --git a/sync/src/main/java/fr/geonature/sync/ui/home/PackageInfoRecyclerViewAdapter.kt b/sync/src/main/java/fr/geonature/sync/ui/home/PackageInfoRecyclerViewAdapter.kt
index 911b1851..ad7059e6 100644
--- a/sync/src/main/java/fr/geonature/sync/ui/home/PackageInfoRecyclerViewAdapter.kt
+++ b/sync/src/main/java/fr/geonature/sync/ui/home/PackageInfoRecyclerViewAdapter.kt
@@ -79,8 +79,8 @@ class PackageInfoRecyclerViewAdapter(listener: OnListItemRecyclerViewAdapterList
)
text2.text = itemView.resources.getQuantityString(
R.plurals.home_app_inputs,
- item.inputs.size,
- item.inputs.size
+ item.inputs,
+ item.inputs
)
setState(item.state)
}
diff --git a/sync/src/main/java/fr/geonature/sync/util/SettingsUtils.kt b/sync/src/main/java/fr/geonature/sync/util/SettingsUtils.kt
index 5dea64a4..4dbe95d9 100644
--- a/sync/src/main/java/fr/geonature/sync/util/SettingsUtils.kt
+++ b/sync/src/main/java/fr/geonature/sync/util/SettingsUtils.kt
@@ -35,6 +35,16 @@ object SettingsUtils {
)
}
+ fun setGeoNatureServerUrl(context: Context, geoNatureServerUrl: String) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putString(
+ context.getString(R.string.preference_category_server_geonature_url_key),
+ geoNatureServerUrl
+ )
+ .apply()
+ }
+
/**
* Gets the current TaxHub server url to use.
*
@@ -50,6 +60,16 @@ object SettingsUtils {
)
}
+ fun setTaxHubServerUrl(context: Context, taxHubServerUrl: String) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putString(
+ context.getString(R.string.preference_category_server_taxhub_url_key),
+ taxHubServerUrl
+ )
+ .apply()
+ }
+
fun updatePreferences(preferenceScreen: PreferenceScreen) {
val context = preferenceScreen.context
val onPreferenceChangeListener =
@@ -88,7 +108,8 @@ object SettingsUtils {
R.string.app_version,
BuildConfig.VERSION_NAME,
BuildConfig.VERSION_CODE,
- DateFormat.getDateTimeInstance().format(Date(BuildConfig.BUILD_DATE.toLong()))
+ DateFormat.getDateTimeInstance()
+ .format(Date(BuildConfig.BUILD_DATE.toLong()))
)
}
}
diff --git a/sync/src/main/res/drawable/ic_action_login.xml b/sync/src/main/res/drawable/ic_action_login.xml
index 00fe546f..eb30e9b0 100644
--- a/sync/src/main/res/drawable/ic_action_login.xml
+++ b/sync/src/main/res/drawable/ic_action_login.xml
@@ -1,6 +1,9 @@
-
+ android:pathData="m19.432 12.98v-1.96l2.11-1.65c0.19-0.15 0.24-0.42 0.12-0.64l-2-3.46c-0.12-0.22-0.39-0.3-0.61-0.22l-2.49 1-1.69-0.98-0.38-2.65c-0.03-0.24-0.24-0.42-0.49-0.42h-4c-0.25 0-0.46 0.18-0.49 0.42l-0.38 2.65-1.69 0.98-2.49-1c-0.23-0.09-0.49 0-0.61 0.22l-2 3.46c-0.13 0.22-0.07 0.49 0.12 0.64l2.11 1.65v1.96l-2.11 1.65c-0.19 0.15-0.24 0.42-0.12 0.64l2 3.46c0.12 0.22 0.39 0.3 0.61 0.22l2.49-1 1.69 0.98 0.38 2.65c0.03 0.24 0.24 0.42 0.49 0.42h4c0.25 0 0.46-0.18 0.49-0.42l0.38-2.65c0.61-0.25 1.17-0.59 1.69-0.98l2.49 1c0.23 0.09 0.49 0 0.61-0.22l2-3.46c0.12-0.22 0.07-0.49-0.12-0.64zm-7.43 2.52c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z" />
diff --git a/sync/src/main/res/drawable/ic_settings.xml b/sync/src/main/res/drawable/ic_settings.xml
new file mode 100644
index 00000000..a4973b3e
--- /dev/null
+++ b/sync/src/main/res/drawable/ic_settings.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/sync/src/main/res/drawable/ic_sync.xml b/sync/src/main/res/drawable/ic_sync.xml
new file mode 100644
index 00000000..6f9347cb
--- /dev/null
+++ b/sync/src/main/res/drawable/ic_sync.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/sync/src/main/res/layout/activity_home.xml b/sync/src/main/res/layout/activity_home.xml
index 83f06d03..285f9008 100644
--- a/sync/src/main/res/layout/activity_home.xml
+++ b/sync/src/main/res/layout/activity_home.xml
@@ -49,7 +49,7 @@
app:layout_constraintTop_toBottomOf="@+id/textViewInstalledApps">
diff --git a/sync/src/main/res/layout/view_sync_data.xml b/sync/src/main/res/layout/view_sync_data.xml
index a8322a4e..d84740b0 100644
--- a/sync/src/main/res/layout/view_sync_data.xml
+++ b/sync/src/main/res/layout/view_sync_data.xml
@@ -25,6 +25,7 @@
android:text="⬤"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textStyle="bold"
+ android:textColor="@color/status_unknown"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="HardcodedText" />
diff --git a/sync/src/main/res/mipmap-hdpi/ic_launcher.png b/sync/src/main/res/mipmap-hdpi/ic_launcher.png
index 101c249f..3df88493 100644
Binary files a/sync/src/main/res/mipmap-hdpi/ic_launcher.png and b/sync/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/sync/src/main/res/mipmap-hdpi/ic_launcher_round.png b/sync/src/main/res/mipmap-hdpi/ic_launcher_round.png
index 1a979c6c..4be06222 100644
Binary files a/sync/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/sync/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/sync/src/main/res/mipmap-mdpi/ic_launcher.png b/sync/src/main/res/mipmap-mdpi/ic_launcher.png
index 2a23a669..8112d9cc 100644
Binary files a/sync/src/main/res/mipmap-mdpi/ic_launcher.png and b/sync/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/sync/src/main/res/mipmap-mdpi/ic_launcher_round.png b/sync/src/main/res/mipmap-mdpi/ic_launcher_round.png
index 6d59f78b..1108bc28 100644
Binary files a/sync/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/sync/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/sync/src/main/res/mipmap-xhdpi/ic_launcher.png b/sync/src/main/res/mipmap-xhdpi/ic_launcher.png
index a80991b5..0a4684d7 100644
Binary files a/sync/src/main/res/mipmap-xhdpi/ic_launcher.png and b/sync/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/sync/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/sync/src/main/res/mipmap-xhdpi/ic_launcher_round.png
index 7b609ddb..09b71e4c 100644
Binary files a/sync/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/sync/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/sync/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sync/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 74c83060..7a623b99 100644
Binary files a/sync/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/sync/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/sync/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/sync/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
index 62aa6730..1f65c042 100644
Binary files a/sync/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/sync/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/sync/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/sync/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index f795f529..23b56360 100644
Binary files a/sync/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/sync/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/sync/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/sync/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
index ef9f147c..8c00b1c6 100644
Binary files a/sync/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/sync/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/sync/src/main/res/values-fr/strings.xml b/sync/src/main/res/values-fr/strings.xml
index f9694d35..a19e5f27 100644
--- a/sync/src/main/res/values-fr/strings.xml
+++ b/sync/src/main/res/values-fr/strings.xml
@@ -1,5 +1,5 @@
-
+
Paramètres
Authentification
@@ -24,6 +24,14 @@
Synchronisation des valeurs par défaut de la nomenclature : %1$d
Done.
+ Nouveaux relevés
+ Relevés à synchroniser
+ Relevés à synchroniser
+
+ - Un relevé est prêt à être synchronisé
+ - %d relevés sont prêts à être synchronisés
+
+
Applications
Aucune application compatible trouvée
%1$s (%2$s)
diff --git a/sync/src/main/res/values/colors.xml b/sync/src/main/res/values/colors.xml
index 4755756b..4cbe33f8 100644
--- a/sync/src/main/res/values/colors.xml
+++ b/sync/src/main/res/values/colors.xml
@@ -1,18 +1,18 @@
- #001489
- #00005b
- #ef3340
+ #00388C
+ #002762
+ #DA4148
#40000000
@android:color/white
#80ffffff
- #4caf50
- #f00
- #ff9800
+ #64DD17
+ #D50000
+ #FF6D00
#ccc
diff --git a/sync/src/main/res/values/strings.xml b/sync/src/main/res/values/strings.xml
index 60c50af1..4a0aad6f 100644
--- a/sync/src/main/res/values/strings.xml
+++ b/sync/src/main/res/values/strings.xml
@@ -27,6 +27,14 @@
Synchronize nomenclature default values: %1$d
Done.
+ New inputs
+ Inputs to synchronize
+ Inputs to synchronize
+
+ - One input is ready to synchronize
+ - %d inputs are ready to synchronize
+
+
Available apps
No compatible app installed
%1$s (%2$s)
diff --git a/sync/src/pne/res/mipmap-hdpi/ic_launcher.png b/sync/src/pne/res/mipmap-hdpi/ic_launcher.png
index 476b4d8a..271c1ef3 100644
Binary files a/sync/src/pne/res/mipmap-hdpi/ic_launcher.png and b/sync/src/pne/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/sync/src/pne/res/mipmap-hdpi/ic_launcher_round.png b/sync/src/pne/res/mipmap-hdpi/ic_launcher_round.png
index 7cb5784f..e347bde7 100644
Binary files a/sync/src/pne/res/mipmap-hdpi/ic_launcher_round.png and b/sync/src/pne/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/sync/src/pne/res/mipmap-mdpi/ic_launcher.png b/sync/src/pne/res/mipmap-mdpi/ic_launcher.png
index d1e35291..e6728371 100644
Binary files a/sync/src/pne/res/mipmap-mdpi/ic_launcher.png and b/sync/src/pne/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/sync/src/pne/res/mipmap-mdpi/ic_launcher_round.png b/sync/src/pne/res/mipmap-mdpi/ic_launcher_round.png
index 77d49055..f725f336 100644
Binary files a/sync/src/pne/res/mipmap-mdpi/ic_launcher_round.png and b/sync/src/pne/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/sync/src/pne/res/mipmap-xhdpi/ic_launcher.png b/sync/src/pne/res/mipmap-xhdpi/ic_launcher.png
index 297b48af..39873e7a 100644
Binary files a/sync/src/pne/res/mipmap-xhdpi/ic_launcher.png and b/sync/src/pne/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/sync/src/pne/res/mipmap-xhdpi/ic_launcher_round.png b/sync/src/pne/res/mipmap-xhdpi/ic_launcher_round.png
index 9ad5f5d6..c0210077 100644
Binary files a/sync/src/pne/res/mipmap-xhdpi/ic_launcher_round.png and b/sync/src/pne/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/sync/src/pne/res/mipmap-xxhdpi/ic_launcher.png b/sync/src/pne/res/mipmap-xxhdpi/ic_launcher.png
index 0ff383c4..236ec1d5 100644
Binary files a/sync/src/pne/res/mipmap-xxhdpi/ic_launcher.png and b/sync/src/pne/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/sync/src/pne/res/mipmap-xxhdpi/ic_launcher_round.png b/sync/src/pne/res/mipmap-xxhdpi/ic_launcher_round.png
index 3aa3d8c8..e214aad0 100644
Binary files a/sync/src/pne/res/mipmap-xxhdpi/ic_launcher_round.png and b/sync/src/pne/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/sync/src/pne/res/mipmap-xxxhdpi/ic_launcher.png b/sync/src/pne/res/mipmap-xxxhdpi/ic_launcher.png
index 3b14da59..06e21bdd 100644
Binary files a/sync/src/pne/res/mipmap-xxxhdpi/ic_launcher.png and b/sync/src/pne/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/sync/src/pne/res/mipmap-xxxhdpi/ic_launcher_round.png b/sync/src/pne/res/mipmap-xxxhdpi/ic_launcher_round.png
index b4bc8faa..7056c962 100644
Binary files a/sync/src/pne/res/mipmap-xxxhdpi/ic_launcher_round.png and b/sync/src/pne/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/sync/src/pne/res/values/colors.xml b/sync/src/pne/res/values/colors.xml
index 47d8ecf5..0b5c72c8 100644
--- a/sync/src/pne/res/values/colors.xml
+++ b/sync/src/pne/res/values/colors.xml
@@ -1,8 +1,8 @@
- #981d97
- #660068
- #00a3e0
+ #81197F
+ #5a1158
+ #00A3DE
diff --git a/sync/src/pneDebug/res/mipmap-hdpi/ic_launcher.png b/sync/src/pneDebug/res/mipmap-hdpi/ic_launcher.png
index 0a1b1ce7..5b10b0b4 100644
Binary files a/sync/src/pneDebug/res/mipmap-hdpi/ic_launcher.png and b/sync/src/pneDebug/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/sync/src/pneDebug/res/mipmap-hdpi/ic_launcher_round.png b/sync/src/pneDebug/res/mipmap-hdpi/ic_launcher_round.png
index 6763f03c..802b9e5c 100644
Binary files a/sync/src/pneDebug/res/mipmap-hdpi/ic_launcher_round.png and b/sync/src/pneDebug/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/sync/src/pneDebug/res/mipmap-mdpi/ic_launcher.png b/sync/src/pneDebug/res/mipmap-mdpi/ic_launcher.png
index e2a5ffd7..e7bd4412 100644
Binary files a/sync/src/pneDebug/res/mipmap-mdpi/ic_launcher.png and b/sync/src/pneDebug/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/sync/src/pneDebug/res/mipmap-mdpi/ic_launcher_round.png b/sync/src/pneDebug/res/mipmap-mdpi/ic_launcher_round.png
index 59edb9a1..b7707834 100644
Binary files a/sync/src/pneDebug/res/mipmap-mdpi/ic_launcher_round.png and b/sync/src/pneDebug/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/sync/src/pneDebug/res/mipmap-xhdpi/ic_launcher.png b/sync/src/pneDebug/res/mipmap-xhdpi/ic_launcher.png
index c6356262..f14f8c26 100644
Binary files a/sync/src/pneDebug/res/mipmap-xhdpi/ic_launcher.png and b/sync/src/pneDebug/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/sync/src/pneDebug/res/mipmap-xhdpi/ic_launcher_round.png b/sync/src/pneDebug/res/mipmap-xhdpi/ic_launcher_round.png
index 00969cf1..1734eea7 100644
Binary files a/sync/src/pneDebug/res/mipmap-xhdpi/ic_launcher_round.png and b/sync/src/pneDebug/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/sync/src/pneDebug/res/mipmap-xxhdpi/ic_launcher.png b/sync/src/pneDebug/res/mipmap-xxhdpi/ic_launcher.png
index 6b8eedb1..79561249 100644
Binary files a/sync/src/pneDebug/res/mipmap-xxhdpi/ic_launcher.png and b/sync/src/pneDebug/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/sync/src/pneDebug/res/mipmap-xxhdpi/ic_launcher_round.png b/sync/src/pneDebug/res/mipmap-xxhdpi/ic_launcher_round.png
index 7b88fcee..85cfe968 100644
Binary files a/sync/src/pneDebug/res/mipmap-xxhdpi/ic_launcher_round.png and b/sync/src/pneDebug/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/sync/src/pneDebug/res/mipmap-xxxhdpi/ic_launcher.png b/sync/src/pneDebug/res/mipmap-xxxhdpi/ic_launcher.png
index 1d7e66f4..ef8a4653 100644
Binary files a/sync/src/pneDebug/res/mipmap-xxxhdpi/ic_launcher.png and b/sync/src/pneDebug/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/sync/src/pneDebug/res/mipmap-xxxhdpi/ic_launcher_round.png b/sync/src/pneDebug/res/mipmap-xxxhdpi/ic_launcher_round.png
index 1b2d902c..c6bfbf49 100644
Binary files a/sync/src/pneDebug/res/mipmap-xxxhdpi/ic_launcher_round.png and b/sync/src/pneDebug/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/sync/src/pnv/res/mipmap-hdpi/ic_launcher.png b/sync/src/pnv/res/mipmap-hdpi/ic_launcher.png
index 101c249f..adf32119 100644
Binary files a/sync/src/pnv/res/mipmap-hdpi/ic_launcher.png and b/sync/src/pnv/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/sync/src/pnv/res/mipmap-hdpi/ic_launcher_round.png b/sync/src/pnv/res/mipmap-hdpi/ic_launcher_round.png
index 1a979c6c..e57215b6 100644
Binary files a/sync/src/pnv/res/mipmap-hdpi/ic_launcher_round.png and b/sync/src/pnv/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/sync/src/pnv/res/mipmap-mdpi/ic_launcher.png b/sync/src/pnv/res/mipmap-mdpi/ic_launcher.png
index 2a23a669..0526e760 100644
Binary files a/sync/src/pnv/res/mipmap-mdpi/ic_launcher.png and b/sync/src/pnv/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/sync/src/pnv/res/mipmap-mdpi/ic_launcher_round.png b/sync/src/pnv/res/mipmap-mdpi/ic_launcher_round.png
index 6d59f78b..2c884b61 100644
Binary files a/sync/src/pnv/res/mipmap-mdpi/ic_launcher_round.png and b/sync/src/pnv/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/sync/src/pnv/res/mipmap-xhdpi/ic_launcher.png b/sync/src/pnv/res/mipmap-xhdpi/ic_launcher.png
index a80991b5..a9800368 100644
Binary files a/sync/src/pnv/res/mipmap-xhdpi/ic_launcher.png and b/sync/src/pnv/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/sync/src/pnv/res/mipmap-xhdpi/ic_launcher_round.png b/sync/src/pnv/res/mipmap-xhdpi/ic_launcher_round.png
index 7b609ddb..df23feeb 100644
Binary files a/sync/src/pnv/res/mipmap-xhdpi/ic_launcher_round.png and b/sync/src/pnv/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/sync/src/pnv/res/mipmap-xxhdpi/ic_launcher.png b/sync/src/pnv/res/mipmap-xxhdpi/ic_launcher.png
index 74c83060..8a0cfe60 100644
Binary files a/sync/src/pnv/res/mipmap-xxhdpi/ic_launcher.png and b/sync/src/pnv/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/sync/src/pnv/res/mipmap-xxhdpi/ic_launcher_round.png b/sync/src/pnv/res/mipmap-xxhdpi/ic_launcher_round.png
index 62aa6730..3fe58447 100644
Binary files a/sync/src/pnv/res/mipmap-xxhdpi/ic_launcher_round.png and b/sync/src/pnv/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/sync/src/pnv/res/mipmap-xxxhdpi/ic_launcher.png b/sync/src/pnv/res/mipmap-xxxhdpi/ic_launcher.png
index f795f529..170c197c 100644
Binary files a/sync/src/pnv/res/mipmap-xxxhdpi/ic_launcher.png and b/sync/src/pnv/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/sync/src/pnv/res/mipmap-xxxhdpi/ic_launcher_round.png b/sync/src/pnv/res/mipmap-xxxhdpi/ic_launcher_round.png
index ef9f147c..985516e0 100644
Binary files a/sync/src/pnv/res/mipmap-xxxhdpi/ic_launcher_round.png and b/sync/src/pnv/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/sync/src/pnv/res/values/colors.xml b/sync/src/pnv/res/values/colors.xml
index 8a804fd1..37829993 100644
--- a/sync/src/pnv/res/values/colors.xml
+++ b/sync/src/pnv/res/values/colors.xml
@@ -1,8 +1,8 @@
- #001489
- #00005b
- #a1561c
+ #004494
+ #002f67
+ #78B74A
diff --git a/sync/src/pnvDebug/res/mipmap-hdpi/ic_launcher.png b/sync/src/pnvDebug/res/mipmap-hdpi/ic_launcher.png
index 81aba636..1495aa79 100644
Binary files a/sync/src/pnvDebug/res/mipmap-hdpi/ic_launcher.png and b/sync/src/pnvDebug/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/sync/src/pnvDebug/res/mipmap-hdpi/ic_launcher_round.png b/sync/src/pnvDebug/res/mipmap-hdpi/ic_launcher_round.png
index 4e5d52c4..dd139e66 100644
Binary files a/sync/src/pnvDebug/res/mipmap-hdpi/ic_launcher_round.png and b/sync/src/pnvDebug/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/sync/src/pnvDebug/res/mipmap-mdpi/ic_launcher.png b/sync/src/pnvDebug/res/mipmap-mdpi/ic_launcher.png
index 972a9239..c3b6859c 100644
Binary files a/sync/src/pnvDebug/res/mipmap-mdpi/ic_launcher.png and b/sync/src/pnvDebug/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/sync/src/pnvDebug/res/mipmap-mdpi/ic_launcher_round.png b/sync/src/pnvDebug/res/mipmap-mdpi/ic_launcher_round.png
index 21342445..f8b44482 100644
Binary files a/sync/src/pnvDebug/res/mipmap-mdpi/ic_launcher_round.png and b/sync/src/pnvDebug/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/sync/src/pnvDebug/res/mipmap-xhdpi/ic_launcher.png b/sync/src/pnvDebug/res/mipmap-xhdpi/ic_launcher.png
index 199b164f..e14a19b9 100644
Binary files a/sync/src/pnvDebug/res/mipmap-xhdpi/ic_launcher.png and b/sync/src/pnvDebug/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/sync/src/pnvDebug/res/mipmap-xhdpi/ic_launcher_round.png b/sync/src/pnvDebug/res/mipmap-xhdpi/ic_launcher_round.png
index 22e4228e..0cb86aa5 100644
Binary files a/sync/src/pnvDebug/res/mipmap-xhdpi/ic_launcher_round.png and b/sync/src/pnvDebug/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/sync/src/pnvDebug/res/mipmap-xxhdpi/ic_launcher.png b/sync/src/pnvDebug/res/mipmap-xxhdpi/ic_launcher.png
index 32aa33e8..d6ef0ea7 100644
Binary files a/sync/src/pnvDebug/res/mipmap-xxhdpi/ic_launcher.png and b/sync/src/pnvDebug/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/sync/src/pnvDebug/res/mipmap-xxhdpi/ic_launcher_round.png b/sync/src/pnvDebug/res/mipmap-xxhdpi/ic_launcher_round.png
index b7c84bbd..81d165fe 100644
Binary files a/sync/src/pnvDebug/res/mipmap-xxhdpi/ic_launcher_round.png and b/sync/src/pnvDebug/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/sync/src/pnvDebug/res/mipmap-xxxhdpi/ic_launcher.png b/sync/src/pnvDebug/res/mipmap-xxxhdpi/ic_launcher.png
index 0c76c372..865df181 100644
Binary files a/sync/src/pnvDebug/res/mipmap-xxxhdpi/ic_launcher.png and b/sync/src/pnvDebug/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/sync/src/pnvDebug/res/mipmap-xxxhdpi/ic_launcher_round.png b/sync/src/pnvDebug/res/mipmap-xxxhdpi/ic_launcher_round.png
index fe15fdfe..63a05c30 100644
Binary files a/sync/src/pnvDebug/res/mipmap-xxxhdpi/ic_launcher_round.png and b/sync/src/pnvDebug/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/sync/src/test/java/fr/geonature/sync/auth/AuthManagerTest.kt b/sync/src/test/java/fr/geonature/sync/auth/AuthManagerTest.kt
index 37902460..a59df417 100644
--- a/sync/src/test/java/fr/geonature/sync/auth/AuthManagerTest.kt
+++ b/sync/src/test/java/fr/geonature/sync/auth/AuthManagerTest.kt
@@ -4,7 +4,6 @@ import android.app.Application
import androidx.test.core.app.ApplicationProvider
import fr.geonature.sync.api.model.AuthLogin
import fr.geonature.sync.api.model.AuthUser
-import java.util.Calendar
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
@@ -14,6 +13,8 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import java.util.Calendar
/**
* Unit tests about [AuthManager].
@@ -21,6 +22,7 @@ import org.robolectric.RobolectricTestRunner
* @author [S. Grimault](mailto:sebastien.grimault@gmail.com)
*/
@RunWith(RobolectricTestRunner::class)
+@Config(application = Application::class)
class AuthManagerTest {
private lateinit var authManager: AuthManager
diff --git a/sync/src/test/java/fr/geonature/sync/auth/io/AuthLoginJsonReaderTest.kt b/sync/src/test/java/fr/geonature/sync/auth/io/AuthLoginJsonReaderTest.kt
index cab4c04e..2bfad6d5 100644
--- a/sync/src/test/java/fr/geonature/sync/auth/io/AuthLoginJsonReaderTest.kt
+++ b/sync/src/test/java/fr/geonature/sync/auth/io/AuthLoginJsonReaderTest.kt
@@ -1,5 +1,6 @@
package fr.geonature.sync.auth.io
+import android.app.Application
import fr.geonature.commons.util.IsoDateUtils.toDate
import fr.geonature.sync.FixtureHelper.getFixture
import fr.geonature.sync.api.model.AuthLogin
@@ -10,6 +11,7 @@ import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
/**
* Unit tests about [AuthLoginJsonReader].
@@ -17,6 +19,7 @@ import org.robolectric.RobolectricTestRunner
* @author [S. Grimault](mailto:sebastien.grimault@gmail.com)
*/
@RunWith(RobolectricTestRunner::class)
+@Config(application = Application::class)
class AuthLoginJsonReaderTest {
private var authLoginJsonReader = AuthLoginJsonReader()
diff --git a/sync/src/test/java/fr/geonature/sync/auth/io/AuthLoginJsonWriterTest.kt b/sync/src/test/java/fr/geonature/sync/auth/io/AuthLoginJsonWriterTest.kt
index b26d665e..0ccce61b 100644
--- a/sync/src/test/java/fr/geonature/sync/auth/io/AuthLoginJsonWriterTest.kt
+++ b/sync/src/test/java/fr/geonature/sync/auth/io/AuthLoginJsonWriterTest.kt
@@ -1,5 +1,6 @@
package fr.geonature.sync.auth.io
+import android.app.Application
import fr.geonature.commons.util.IsoDateUtils.toDate
import fr.geonature.sync.FixtureHelper.getFixture
import fr.geonature.sync.api.model.AuthLogin
@@ -9,6 +10,7 @@ import org.junit.Assert.assertNotNull
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
/**
* Unit tests about [AuthLoginJsonWriter].
@@ -16,6 +18,7 @@ import org.robolectric.RobolectricTestRunner
* @author [S. Grimault](mailto:sebastien.grimault@gmail.com)
*/
@RunWith(RobolectricTestRunner::class)
+@Config(application = Application::class)
class AuthLoginJsonWriterTest {
private val authLoginJsonWriter = AuthLoginJsonWriter()
diff --git a/sync/src/test/java/fr/geonature/sync/settings/AppSettingsTest.kt b/sync/src/test/java/fr/geonature/sync/settings/AppSettingsTest.kt
new file mode 100644
index 00000000..e9aa5f89
--- /dev/null
+++ b/sync/src/test/java/fr/geonature/sync/settings/AppSettingsTest.kt
@@ -0,0 +1,46 @@
+package fr.geonature.sync.settings
+
+import android.app.Application
+import android.os.Parcel
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+/**
+ * Unit tests about [AppSettings].
+ *
+ * @author [S. Grimault](mailto:sebastien.grimault@gmail.com)
+ */
+@RunWith(RobolectricTestRunner::class)
+@Config(application = Application::class)
+class AppSettingsTest {
+ @Test
+ fun testParcelable() {
+ // given an AppSettings instance
+ val appSettings = AppSettings(
+ "http://demo.geonature/geonature",
+ "http://demo.geonature/taxhub",
+ 3,
+ 1,
+ 100
+ )
+
+ // when we obtain a Parcel object to write the AppSettings instance to it
+ val parcel = Parcel.obtain()
+ appSettings.writeToParcel(
+ parcel,
+ 0
+ )
+
+ // reset the parcel for reading
+ parcel.setDataPosition(0)
+
+ // then
+ assertEquals(
+ appSettings,
+ AppSettings.CREATOR.createFromParcel(parcel)
+ )
+ }
+}
\ No newline at end of file
diff --git a/sync/src/test/java/fr/geonature/sync/settings/io/OnAppSettingsJsonReaderListenerImplTest.kt b/sync/src/test/java/fr/geonature/sync/settings/io/OnAppSettingsJsonReaderListenerImplTest.kt
index feca3ba7..33457797 100644
--- a/sync/src/test/java/fr/geonature/sync/settings/io/OnAppSettingsJsonReaderListenerImplTest.kt
+++ b/sync/src/test/java/fr/geonature/sync/settings/io/OnAppSettingsJsonReaderListenerImplTest.kt
@@ -1,5 +1,6 @@
package fr.geonature.sync.settings.io
+import android.app.Application
import fr.geonature.commons.settings.io.AppSettingsJsonReader
import fr.geonature.sync.FixtureHelper.getFixture
import fr.geonature.sync.settings.AppSettings
@@ -10,6 +11,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
/**
* Unit tests about [AppSettingsJsonReader].
@@ -17,6 +19,7 @@ import org.robolectric.RobolectricTestRunner
* @author [S. Grimault](mailto:sebastien.grimault@gmail.com)
*/
@RunWith(RobolectricTestRunner::class)
+@Config(application = Application::class)
class OnAppSettingsJsonReaderListenerImplTest {
lateinit var appSettingsJsonReader: AppSettingsJsonReader
@@ -38,9 +41,13 @@ class OnAppSettingsJsonReaderListenerImplTest {
assertNotNull(appSettings)
assertEquals(
AppSettings(
+ "http://demo.geonature/geonature",
+ "http://demo.geonature/taxhub",
3,
1,
- 100
+ 100,
+ 100,
+ 5
),
appSettings
)
@@ -63,11 +70,7 @@ class OnAppSettingsJsonReaderListenerImplTest {
// then
assertNotNull(appSettings)
assertEquals(
- AppSettings(
- 0,
- 0,
- 0
- ),
+ AppSettings(),
appSettings
)
}
diff --git a/sync/src/test/java/fr/geonature/sync/sync/io/DatasetJsonReaderTest.kt b/sync/src/test/java/fr/geonature/sync/sync/io/DatasetJsonReaderTest.kt
index e82c2e38..6e2f4e28 100644
--- a/sync/src/test/java/fr/geonature/sync/sync/io/DatasetJsonReaderTest.kt
+++ b/sync/src/test/java/fr/geonature/sync/sync/io/DatasetJsonReaderTest.kt
@@ -1,5 +1,6 @@
package fr.geonature.sync.sync.io
+import android.app.Application
import fr.geonature.commons.data.Dataset
import fr.geonature.sync.FixtureHelper.getFixture
import org.junit.Assert.assertArrayEquals
@@ -9,6 +10,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
/**
* Unit tests about [DatasetJsonReader].
@@ -16,6 +18,7 @@ import org.robolectric.RobolectricTestRunner
* @author [S. Grimault](mailto:sebastien.grimault@gmail.com)
*/
@RunWith(RobolectricTestRunner::class)
+@Config(application = Application::class)
class DatasetJsonReaderTest {
private lateinit var datasetJsonReader: DatasetJsonReader
diff --git a/sync/src/test/resources/fixtures/settings_sync.json b/sync/src/test/resources/fixtures/settings_sync.json
index f7645195..d32b83ac 100644
--- a/sync/src/test/resources/fixtures/settings_sync.json
+++ b/sync/src/test/resources/fixtures/settings_sync.json
@@ -1,5 +1,9 @@
{
- "application_id": 3,
- "users_menu_id": 1,
- "taxref_list_id": 100
+ "geonature_url": "http://demo.geonature/geonature",
+ "taxhub_url": "http://demo.geonature/taxhub",
+ "uh_application_id": 3,
+ "observers_list_id": 1,
+ "taxa_list_id": 100,
+ "page_size": 100,
+ "page_max_retry": 5
}
\ No newline at end of file
diff --git a/sync/version.properties b/sync/version.properties
index 6d2cac01..21755d4b 100644
--- a/sync/version.properties
+++ b/sync/version.properties
@@ -1,2 +1,2 @@
-#Sun Mar 01 17:59:25 CET 2020
-VERSION_CODE=2100
+#Sat Mar 28 16:08:14 CET 2020
+VERSION_CODE=2200