diff --git a/.gitignore b/.gitignore index f4f4b3cb..ab834a97 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ /build /captures /docs/.asciidoctor +/docs/images/uml /docs/vendor /docs/*.html .externalNativeBuild diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 00000000..61a9130c --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 3b84fa5b..f4f4cccb 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -18,6 +18,7 @@ diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 00000000..a5f05cd8 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index db29518c..a623051b 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -5,5 +5,5 @@ - + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 1bac1d4c..8cc327f6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.72' + ext.kotlin_version = '1.4.21' repositories { google() @@ -12,9 +12,9 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.0.1' + classpath 'com.android.tools.build:gradle:4.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "org.jlleitschuh.gradle:ktlint-gradle:9.1.1" + classpath "org.jlleitschuh.gradle:ktlint-gradle:9.4.1" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/commons/build.gradle b/commons/build.gradle index fdb016ac..94c12d7b 100644 --- a/commons/build.gradle +++ b/commons/build.gradle @@ -1,8 +1,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' -version = "0.7.6" +version = "0.8.0" android { compileSdkVersion 29 @@ -43,21 +42,23 @@ dependencies { api project(':mountpoint') - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-alpha03" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1" + + implementation 'androidx.appcompat:appcompat:1.3.0-beta01' + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-rc01" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" - implementation 'androidx.preference:preference:1.1.1' - implementation 'com.google.android.material:material:1.1.0' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.4" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0" - api 'androidx.room:room-runtime:2.2.5' - - testImplementation 'androidx.test:core:1.2.0' + implementation 'androidx.preference:preference-ktx:1.1.1' + implementation 'com.google.android.material:material:1.3.0-rc01' + + api 'androidx.room:room-runtime:2.2.6' + testImplementation 'androidx.arch.core:core-testing:2.1.0' - testImplementation 'junit:junit:4.13' + testImplementation 'androidx.test:core:1.3.0' + testImplementation 'junit:junit:4.13.1' testImplementation 'org.mockito:mockito-core:3.0.0' testImplementation 'org.robolectric:robolectric:4.3.1' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + androidTestImplementation 'androidx.test:runner:1.3.0' } \ No newline at end of file diff --git a/commons/src/main/java/fr/geonature/commons/data/DefaultNomenclature.kt b/commons/src/main/java/fr/geonature/commons/data/DefaultNomenclature.kt index e7a6353d..7182e152 100644 --- a/commons/src/main/java/fr/geonature/commons/data/DefaultNomenclature.kt +++ b/commons/src/main/java/fr/geonature/commons/data/DefaultNomenclature.kt @@ -18,12 +18,14 @@ import fr.geonature.commons.data.helper.get @Entity( tableName = DefaultNomenclature.TABLE_NAME, primaryKeys = [DefaultNomenclature.COLUMN_MODULE, DefaultNomenclature.COLUMN_NOMENCLATURE_ID], - foreignKeys = [ForeignKey( - entity = Nomenclature::class, - parentColumns = [Nomenclature.COLUMN_ID], - childColumns = [DefaultNomenclature.COLUMN_NOMENCLATURE_ID], - onDelete = ForeignKey.CASCADE - )] + foreignKeys = [ + ForeignKey( + entity = Nomenclature::class, + parentColumns = [Nomenclature.COLUMN_ID], + childColumns = [DefaultNomenclature.COLUMN_NOMENCLATURE_ID], + onDelete = ForeignKey.CASCADE + ) + ] ) open class DefaultNomenclature : Parcelable { diff --git a/commons/src/main/java/fr/geonature/commons/data/NomenclatureTaxonomy.kt b/commons/src/main/java/fr/geonature/commons/data/NomenclatureTaxonomy.kt index 8ae5a410..61770948 100644 --- a/commons/src/main/java/fr/geonature/commons/data/NomenclatureTaxonomy.kt +++ b/commons/src/main/java/fr/geonature/commons/data/NomenclatureTaxonomy.kt @@ -19,17 +19,19 @@ import fr.geonature.commons.data.helper.get tableName = NomenclatureTaxonomy.TABLE_NAME, primaryKeys = [NomenclatureTaxonomy.COLUMN_NOMENCLATURE_ID, Taxonomy.COLUMN_KINGDOM, Taxonomy.COLUMN_GROUP], indices = [Index(value = [Taxonomy.COLUMN_KINGDOM, Taxonomy.COLUMN_GROUP])], - foreignKeys = [ForeignKey( - entity = Nomenclature::class, - parentColumns = [Nomenclature.COLUMN_ID], - childColumns = [NomenclatureTaxonomy.COLUMN_NOMENCLATURE_ID], - onDelete = ForeignKey.CASCADE - ), ForeignKey( - entity = Taxonomy::class, - parentColumns = [Taxonomy.COLUMN_KINGDOM, Taxonomy.COLUMN_GROUP], - childColumns = [Taxonomy.COLUMN_KINGDOM, Taxonomy.COLUMN_GROUP], - onDelete = ForeignKey.CASCADE - )] + foreignKeys = [ + ForeignKey( + entity = Nomenclature::class, + parentColumns = [Nomenclature.COLUMN_ID], + childColumns = [NomenclatureTaxonomy.COLUMN_NOMENCLATURE_ID], + onDelete = ForeignKey.CASCADE + ), ForeignKey( + entity = Taxonomy::class, + parentColumns = [Taxonomy.COLUMN_KINGDOM, Taxonomy.COLUMN_GROUP], + childColumns = [Taxonomy.COLUMN_KINGDOM, Taxonomy.COLUMN_GROUP], + onDelete = ForeignKey.CASCADE + ) + ] ) class NomenclatureTaxonomy( @ColumnInfo(name = COLUMN_NOMENCLATURE_ID) diff --git a/commons/src/main/java/fr/geonature/commons/data/NomenclatureType.kt b/commons/src/main/java/fr/geonature/commons/data/NomenclatureType.kt index 7b7a3596..9151e062 100644 --- a/commons/src/main/java/fr/geonature/commons/data/NomenclatureType.kt +++ b/commons/src/main/java/fr/geonature/commons/data/NomenclatureType.kt @@ -19,10 +19,12 @@ import fr.geonature.commons.data.helper.get */ @Entity( tableName = NomenclatureType.TABLE_NAME, - indices = [Index( - value = [NomenclatureType.COLUMN_MNEMONIC], - unique = true - )] + indices = [ + Index( + value = [NomenclatureType.COLUMN_MNEMONIC], + unique = true + ) + ] ) data class NomenclatureType( diff --git a/commons/src/main/java/fr/geonature/commons/data/Taxon.kt b/commons/src/main/java/fr/geonature/commons/data/Taxon.kt index 56a2d6aa..00a59cd3 100644 --- a/commons/src/main/java/fr/geonature/commons/data/Taxon.kt +++ b/commons/src/main/java/fr/geonature/commons/data/Taxon.kt @@ -157,5 +157,5 @@ class Taxon : AbstractTaxon { /** * Order by query builder. */ - class OrderBy: AbstractTaxon.OrderBy(TABLE_NAME) + class OrderBy : AbstractTaxon.OrderBy(TABLE_NAME) } diff --git a/commons/src/main/java/fr/geonature/commons/data/TaxonArea.kt b/commons/src/main/java/fr/geonature/commons/data/TaxonArea.kt index f228b252..3bfbb623 100644 --- a/commons/src/main/java/fr/geonature/commons/data/TaxonArea.kt +++ b/commons/src/main/java/fr/geonature/commons/data/TaxonArea.kt @@ -21,12 +21,14 @@ import java.util.Date @Entity( tableName = TaxonArea.TABLE_NAME, primaryKeys = [TaxonArea.COLUMN_TAXON_ID, TaxonArea.COLUMN_AREA_ID], - foreignKeys = [ForeignKey( - entity = Taxon::class, - parentColumns = [AbstractTaxon.COLUMN_ID], - childColumns = [TaxonArea.COLUMN_TAXON_ID], - onDelete = ForeignKey.CASCADE - )] + foreignKeys = [ + ForeignKey( + entity = Taxon::class, + parentColumns = [AbstractTaxon.COLUMN_ID], + childColumns = [TaxonArea.COLUMN_TAXON_ID], + onDelete = ForeignKey.CASCADE + ) + ] ) @TypeConverters(Converters::class) data class TaxonArea( diff --git a/commons/src/main/java/fr/geonature/commons/data/TaxonWithArea.kt b/commons/src/main/java/fr/geonature/commons/data/TaxonWithArea.kt index c99fc94a..f980e464 100644 --- a/commons/src/main/java/fr/geonature/commons/data/TaxonWithArea.kt +++ b/commons/src/main/java/fr/geonature/commons/data/TaxonWithArea.kt @@ -141,7 +141,7 @@ class TaxonWithArea : AbstractTaxon { TaxonArea.COLUMN_COLOR, TaxonArea.TABLE_NAME )} IN (${color.filter { it != "none" } - .joinToString(", ") { "'${it}'" }})${color.find { it == "none" } + .joinToString(", ") { "'$it'" }})${color.find { it == "none" } ?.let { " OR (${getColumnAlias( TaxonArea.COLUMN_COLOR, @@ -159,5 +159,5 @@ class TaxonWithArea : AbstractTaxon { /** * Order by query builder. */ - class OrderBy: AbstractTaxon.OrderBy(Taxon.TABLE_NAME) + class OrderBy : AbstractTaxon.OrderBy(Taxon.TABLE_NAME) } diff --git a/commons/src/main/java/fr/geonature/commons/data/Taxonomy.kt b/commons/src/main/java/fr/geonature/commons/data/Taxonomy.kt index 2c4292af..96a2e989 100644 --- a/commons/src/main/java/fr/geonature/commons/data/Taxonomy.kt +++ b/commons/src/main/java/fr/geonature/commons/data/Taxonomy.kt @@ -23,6 +23,7 @@ class Taxonomy : Parcelable { @ColumnInfo(name = COLUMN_KINGDOM) var kingdom: String + @ColumnInfo(name = COLUMN_GROUP) var group: String diff --git a/commons/src/main/java/fr/geonature/commons/data/helper/EntityHelper.kt b/commons/src/main/java/fr/geonature/commons/data/helper/EntityHelper.kt index fcdbd5e2..68020487 100644 --- a/commons/src/main/java/fr/geonature/commons/data/helper/EntityHelper.kt +++ b/commons/src/main/java/fr/geonature/commons/data/helper/EntityHelper.kt @@ -14,7 +14,9 @@ object EntityHelper { columnName: String, tableAlias: String? = null ): Pair { - return Pair("${tableAlias.orEmpty()}${if (tableAlias.isNullOrBlank()) "" else "."}\"$columnName\"", - "${tableAlias.orEmpty()}${if (tableAlias.isNullOrBlank()) "" else "_"}$columnName") + return Pair( + "${tableAlias.orEmpty()}${if (tableAlias.isNullOrBlank()) "" else "."}\"$columnName\"", + "${tableAlias.orEmpty()}${if (tableAlias.isNullOrBlank()) "" else "_"}$columnName" + ) } } diff --git a/commons/src/main/java/fr/geonature/commons/data/helper/Provider.kt b/commons/src/main/java/fr/geonature/commons/data/helper/Provider.kt index 6d7e449c..b35f1cd8 100644 --- a/commons/src/main/java/fr/geonature/commons/data/helper/Provider.kt +++ b/commons/src/main/java/fr/geonature/commons/data/helper/Provider.kt @@ -34,7 +34,8 @@ object Provider { val baseUri = Uri.parse("content://$AUTHORITY/$resource") return if (path.isEmpty()) baseUri - else withAppendedPath(baseUri, + else withAppendedPath( + baseUri, path.asSequence().filter { it.isNotBlank() }.joinToString("/") ) } diff --git a/commons/src/main/java/fr/geonature/commons/data/helper/SQLiteSelectQueryBuilder.kt b/commons/src/main/java/fr/geonature/commons/data/helper/SQLiteSelectQueryBuilder.kt index ffc3dbd7..2355fb91 100644 --- a/commons/src/main/java/fr/geonature/commons/data/helper/SQLiteSelectQueryBuilder.kt +++ b/commons/src/main/java/fr/geonature/commons/data/helper/SQLiteSelectQueryBuilder.kt @@ -323,7 +323,8 @@ class SQLiteSelectQueryBuilder private constructor(private val tables: MutableSe this.orderBy.joinToString(", ") .let { if (it.isEmpty()) it else "ORDER BY $it" } - val sql = """ + val sql = + """ |SELECT ${if (this.columns.isNotEmpty() && this.distinct) "DISTINCT " else ""}$selectedColumns |FROM $tables |$joinClauses @@ -333,11 +334,11 @@ class SQLiteSelectQueryBuilder private constructor(private val tables: MutableSe |$orderByClauses |${this.limit} """.trimMargin() - .trim() - .replace( - "\n{2,}".toRegex(RegexOption.MULTILINE), - "\n" - ) + .trim() + .replace( + "\n{2,}".toRegex(RegexOption.MULTILINE), + "\n" + ) Log.d( TAG, diff --git a/commons/src/main/java/fr/geonature/commons/input/InputManager.kt b/commons/src/main/java/fr/geonature/commons/input/InputManager.kt index 1cf5065a..80739c6b 100644 --- a/commons/src/main/java/fr/geonature/commons/input/InputManager.kt +++ b/commons/src/main/java/fr/geonature/commons/input/InputManager.kt @@ -46,7 +46,7 @@ class InputManager private constructor( */ suspend fun readInputs(): List = withContext(IO) { preferenceManager.all.filterKeys { it.startsWith("${KEY_PREFERENCE_INPUT}_") } - .values.mapNotNull { if (it is String && !it.isBlank()) inputJsonReader.read(it) else null } + .values.mapNotNull { if (it is String && it.isNotBlank()) inputJsonReader.read(it) else null } .sortedBy { it.id } .also { inputs.postValue(it) } } @@ -174,14 +174,14 @@ class InputManager private constructor( * @return `true` if the given [AbstractInput] has been successfully exported, `false` otherwise */ suspend fun exportInput(input: I): Boolean { - val inputExportFile = withContext(IO) { - val inputExportFile = getInputExportFile(input) + val inputExportFile = getInputExportFile(input) + + @Suppress("BlockingMethodInNonBlockingContext") + withContext(IO) { inputJsonWriter.write( FileWriter(inputExportFile), input ) - - return@withContext inputExportFile } Log.i( @@ -205,11 +205,11 @@ class InputManager private constructor( } @Throws(IOException::class) - private fun getInputExportFile(input: AbstractInput): File { + private suspend fun getInputExportFile(input: AbstractInput): File = withContext(IO) { val inputDir = FileUtils.getInputsFolder(application) inputDir.mkdirs() - return File( + return@withContext File( inputDir, "input_${input.module}_${input.id}.json" ) diff --git a/commons/src/main/java/fr/geonature/commons/input/InputViewModel.kt b/commons/src/main/java/fr/geonature/commons/input/InputViewModel.kt index 40947bb4..a681f473 100644 --- a/commons/src/main/java/fr/geonature/commons/input/InputViewModel.kt +++ b/commons/src/main/java/fr/geonature/commons/input/InputViewModel.kt @@ -23,9 +23,11 @@ open class InputViewModel( inputJsonWriterListener: InputJsonWriter.OnInputJsonWriterListener ) : AndroidViewModel(application) { - private val inputManager = InputManager.getInstance(application, - inputJsonReaderListener, - inputJsonWriterListener) + private val inputManager = InputManager.getInstance( + application, + inputJsonReaderListener, + inputJsonWriterListener + ) private var deletedInputToRestore: I? = null @@ -45,7 +47,7 @@ open class InputViewModel( * * @param id The [AbstractInput] ID to read. If omitted, read the current saved [AbstractInput]. */ - fun readInput(id: Long? = null): LiveData { + open fun readInput(id: Long? = null): LiveData { viewModelScope.launch { inputManager.readInput(id) } @@ -123,7 +125,8 @@ open class InputViewModel( * * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) */ - class Factory, I : AbstractInput>(val creator: () -> T) : ViewModelProvider.Factory { + class Factory, I : AbstractInput>(val creator: () -> T) : + ViewModelProvider.Factory { override fun create(modelClass: Class): T { @Suppress("UNCHECKED_CAST") return creator() as T } diff --git a/commons/src/main/java/fr/geonature/commons/settings/AppSettingsManager.kt b/commons/src/main/java/fr/geonature/commons/settings/AppSettingsManager.kt index b731f60a..32a63b00 100644 --- a/commons/src/main/java/fr/geonature/commons/settings/AppSettingsManager.kt +++ b/commons/src/main/java/fr/geonature/commons/settings/AppSettingsManager.kt @@ -51,7 +51,7 @@ class AppSettingsManager private constructor( * * @return [IAppSettings] or `null` if not found */ - suspend fun loadAppSettings(): AS? = withContext(IO) { + suspend fun loadAppSettings(): AS? { val settingsJsonFile = getAppSettingsAsFile() Log.i( @@ -65,25 +65,28 @@ class AppSettingsManager private constructor( "'${settingsJsonFile.absolutePath}' not found" ) - return@withContext null + return null } - return@withContext try { - val appSettings = appSettingsJsonReader.read(FileReader(settingsJsonFile)) + @Suppress("BlockingMethodInNonBlockingContext") + return withContext(IO) { + try { + val appSettings = appSettingsJsonReader.read(FileReader(settingsJsonFile)) - Log.i( - TAG, - "Settings loaded" - ) + Log.i( + TAG, + "Settings loaded" + ) - appSettings - } catch (e: IOException) { - Log.w( - TAG, - "Failed to load '${settingsJsonFile.name}'" - ) + appSettings + } catch (e: IOException) { + Log.w( + TAG, + "Failed to load '${settingsJsonFile.name}'" + ) - null + null + } } } diff --git a/commons/src/main/java/fr/geonature/commons/ui/adapter/AbstractListItemRecyclerViewAdapter.kt b/commons/src/main/java/fr/geonature/commons/ui/adapter/AbstractListItemRecyclerViewAdapter.kt index aa8438c6..c5c208d2 100644 --- a/commons/src/main/java/fr/geonature/commons/ui/adapter/AbstractListItemRecyclerViewAdapter.kt +++ b/commons/src/main/java/fr/geonature/commons/ui/adapter/AbstractListItemRecyclerViewAdapter.kt @@ -19,49 +19,51 @@ abstract class AbstractListItemRecyclerViewAdapter(private val listener: OnLi val items: List = _items init { - this.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { - override fun onChanged() { - super.onChanged() + this.registerAdapterDataObserver( + object : RecyclerView.AdapterDataObserver() { + override fun onChanged() { + super.onChanged() - listener?.showEmptyTextView(itemCount == 0) - } + listener?.showEmptyTextView(itemCount == 0) + } - override fun onItemRangeChanged( - positionStart: Int, - itemCount: Int - ) { - super.onItemRangeChanged( - positionStart, - itemCount - ) + override fun onItemRangeChanged( + positionStart: Int, + itemCount: Int + ) { + super.onItemRangeChanged( + positionStart, + itemCount + ) - listener?.showEmptyTextView(itemCount == 0) - } + listener?.showEmptyTextView(itemCount == 0) + } - override fun onItemRangeInserted( - positionStart: Int, - itemCount: Int - ) { - super.onItemRangeInserted( - positionStart, - itemCount - ) + override fun onItemRangeInserted( + positionStart: Int, + itemCount: Int + ) { + super.onItemRangeInserted( + positionStart, + itemCount + ) - listener?.showEmptyTextView(false) - } + listener?.showEmptyTextView(false) + } - override fun onItemRangeRemoved( - positionStart: Int, - itemCount: Int - ) { - super.onItemRangeRemoved( - positionStart, - itemCount - ) + override fun onItemRangeRemoved( + positionStart: Int, + itemCount: Int + ) { + super.onItemRangeRemoved( + positionStart, + itemCount + ) - listener?.showEmptyTextView(itemCount == 0) + listener?.showEmptyTextView(itemCount == 0) + } } - }) + ) } override fun onCreateViewHolder( @@ -123,39 +125,41 @@ abstract class AbstractListItemRecyclerViewAdapter(private val listener: OnLi return } - val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() { - override fun getOldListSize(): Int { - return this@AbstractListItemRecyclerViewAdapter._items.size - } + val diffResult = DiffUtil.calculateDiff( + object : DiffUtil.Callback() { + override fun getOldListSize(): Int { + return this@AbstractListItemRecyclerViewAdapter._items.size + } - override fun getNewListSize(): Int { - return newItems.size - } + override fun getNewListSize(): Int { + return newItems.size + } - override fun areItemsTheSame( - oldItemPosition: Int, - newItemPosition: Int - ): Boolean { - return this@AbstractListItemRecyclerViewAdapter.areItemsTheSame( - this@AbstractListItemRecyclerViewAdapter._items, - newItems, - oldItemPosition, - newItemPosition - ) - } + override fun areItemsTheSame( + oldItemPosition: Int, + newItemPosition: Int + ): Boolean { + return this@AbstractListItemRecyclerViewAdapter.areItemsTheSame( + this@AbstractListItemRecyclerViewAdapter._items, + newItems, + oldItemPosition, + newItemPosition + ) + } - override fun areContentsTheSame( - oldItemPosition: Int, - newItemPosition: Int - ): Boolean { - return this@AbstractListItemRecyclerViewAdapter.areContentsTheSame( - this@AbstractListItemRecyclerViewAdapter._items, - newItems, - oldItemPosition, - newItemPosition - ) + override fun areContentsTheSame( + oldItemPosition: Int, + newItemPosition: Int + ): Boolean { + return this@AbstractListItemRecyclerViewAdapter.areContentsTheSame( + this@AbstractListItemRecyclerViewAdapter._items, + newItems, + oldItemPosition, + newItemPosition + ) + } } - }) + ) this._items.clear() this._items.addAll(newItems) diff --git a/commons/src/main/java/fr/geonature/commons/ui/adapter/AbstractStickyRecyclerViewAdapter.kt b/commons/src/main/java/fr/geonature/commons/ui/adapter/AbstractStickyRecyclerViewAdapter.kt index 02e0fc0c..b1e9cc2f 100644 --- a/commons/src/main/java/fr/geonature/commons/ui/adapter/AbstractStickyRecyclerViewAdapter.kt +++ b/commons/src/main/java/fr/geonature/commons/ui/adapter/AbstractStickyRecyclerViewAdapter.kt @@ -35,4 +35,4 @@ abstract class AbstractStickyRecyclerViewAdapter { * @see [onBindHeaderViewHolder] */ fun onCreateHeaderViewHolder(parent: ViewGroup): SVH -} \ No newline at end of file +} diff --git a/commons/src/main/java/fr/geonature/commons/ui/adapter/StickyHeaderItemDecorator.kt b/commons/src/main/java/fr/geonature/commons/ui/adapter/StickyHeaderItemDecorator.kt index 3005bcf0..60f0ce30 100644 --- a/commons/src/main/java/fr/geonature/commons/ui/adapter/StickyHeaderItemDecorator.kt +++ b/commons/src/main/java/fr/geonature/commons/ui/adapter/StickyHeaderItemDecorator.kt @@ -85,10 +85,7 @@ class StickyHeaderItemDecorator( return } - if (preOverlappedPosition != overlappedHeaderPosition && shouldMoveHeader( - viewOverlappedByHeader - ) - ) { + if (preOverlappedPosition != overlappedHeaderPosition && shouldMoveHeader(viewOverlappedByHeader)) { updateStickyHeader(topChildPosition) moveHeader( c, @@ -174,55 +171,57 @@ class StickyHeaderItemDecorator( private fun fixLayoutSize(recyclerView: RecyclerView) { val currentStickyHolder = currentStickyHolder ?: return - recyclerView.addOnLayoutChangeListener(object : View.OnLayoutChangeListener { - override fun onLayoutChange( - v: View?, - left: Int, - top: Int, - right: Int, - bottom: Int, - oldLeft: Int, - oldTop: Int, - oldRight: Int, - oldBottom: Int - ) { - recyclerView.removeOnLayoutChangeListener(this) - - // Specs for parent (RecyclerView) - val widthSpec = View.MeasureSpec.makeMeasureSpec( - recyclerView.width, - View.MeasureSpec.EXACTLY - ) - - val heightSpec = View.MeasureSpec.makeMeasureSpec( - recyclerView.height, - View.MeasureSpec.UNSPECIFIED - ) - - // Specs for children (headers) - val childWidthSpec = ViewGroup.getChildMeasureSpec( - widthSpec, - recyclerView.paddingLeft + recyclerView.paddingRight, - currentStickyHolder.itemView.layoutParams.width - ) - - val childHeightSpec = ViewGroup.getChildMeasureSpec( - heightSpec, - recyclerView.paddingTop + recyclerView.paddingBottom, - currentStickyHolder.itemView.layoutParams.height - ) - - currentStickyHolder.itemView.measure( - childWidthSpec, - childHeightSpec - ) - currentStickyHolder.itemView.layout( - 0, - 0, - currentStickyHolder.itemView.measuredWidth, - currentStickyHolder.itemView.measuredHeight - ) + recyclerView.addOnLayoutChangeListener( + object : View.OnLayoutChangeListener { + override fun onLayoutChange( + v: View?, + left: Int, + top: Int, + right: Int, + bottom: Int, + oldLeft: Int, + oldTop: Int, + oldRight: Int, + oldBottom: Int + ) { + recyclerView.removeOnLayoutChangeListener(this) + +// Specs for parent (RecyclerView) + val widthSpec = View.MeasureSpec.makeMeasureSpec( + recyclerView.width, + View.MeasureSpec.EXACTLY + ) + + val heightSpec = View.MeasureSpec.makeMeasureSpec( + recyclerView.height, + View.MeasureSpec.UNSPECIFIED + ) + +// Specs for children (headers) + val childWidthSpec = ViewGroup.getChildMeasureSpec( + widthSpec, + recyclerView.paddingLeft + recyclerView.paddingRight, + currentStickyHolder.itemView.layoutParams.width + ) + + val childHeightSpec = ViewGroup.getChildMeasureSpec( + heightSpec, + recyclerView.paddingTop + recyclerView.paddingBottom, + currentStickyHolder.itemView.layoutParams.height + ) + + currentStickyHolder.itemView.measure( + childWidthSpec, + childHeightSpec + ) + currentStickyHolder.itemView.layout( + 0, + 0, + currentStickyHolder.itemView.measuredWidth, + currentStickyHolder.itemView.measuredHeight + ) + } } - }) + ) } -} \ No newline at end of file +} diff --git a/commons/src/main/java/fr/geonature/commons/util/EditTextHelper.kt b/commons/src/main/java/fr/geonature/commons/util/EditTextHelper.kt index 6a76ea59..c2936c2f 100644 --- a/commons/src/main/java/fr/geonature/commons/util/EditTextHelper.kt +++ b/commons/src/main/java/fr/geonature/commons/util/EditTextHelper.kt @@ -14,25 +14,27 @@ import android.widget.EditText * Extension function to simplify setting an afterTextChanged action to EditText components. */ fun EditText.afterTextChanged(afterTextChanged: (Editable?) -> Unit) { - this.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(editable: Editable?) { - afterTextChanged.invoke(editable) - } + this.addTextChangedListener( + object : TextWatcher { + override fun afterTextChanged(editable: Editable?) { + afterTextChanged.invoke(editable) + } - override fun beforeTextChanged( - s: CharSequence, - start: Int, - count: Int, - after: Int - ) { - } + override fun beforeTextChanged( + s: CharSequence, + start: Int, + count: Int, + after: Int + ) { + } - override fun onTextChanged( - s: CharSequence, - start: Int, - before: Int, - count: Int - ) { + override fun onTextChanged( + s: CharSequence, + start: Int, + before: Int, + count: Int + ) { + } } - }) + ) } diff --git a/commons/src/main/java/fr/geonature/commons/util/FileUtilsHelper.kt b/commons/src/main/java/fr/geonature/commons/util/FileUtilsHelper.kt index 41fe9263..e65d3c9c 100644 --- a/commons/src/main/java/fr/geonature/commons/util/FileUtilsHelper.kt +++ b/commons/src/main/java/fr/geonature/commons/util/FileUtilsHelper.kt @@ -56,4 +56,4 @@ fun FileUtils.getDatabaseFolder( ), "databases" ) -} \ No newline at end of file +} diff --git a/commons/src/main/java/fr/geonature/commons/util/LiveDataHelper.kt b/commons/src/main/java/fr/geonature/commons/util/LiveDataHelper.kt index 2beb56f4..70121b18 100644 --- a/commons/src/main/java/fr/geonature/commons/util/LiveDataHelper.kt +++ b/commons/src/main/java/fr/geonature/commons/util/LiveDataHelper.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Observer /** - * Helpers for [LiveData] utilities. + * Helpers for LiveData utilities. * * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) */ @@ -22,7 +22,8 @@ fun LiveData.observeOnce(owner: LifecycleOwner, observer: (T?) -> Unit) { removeObserver(this) observer(value) } - }) + } + ) } /** @@ -44,5 +45,6 @@ fun LiveData.observeUntil( removeObserver(this) } } - }) -} \ No newline at end of file + } + ) +} diff --git a/commons/src/main/java/fr/geonature/commons/util/PermissionUtils.kt b/commons/src/main/java/fr/geonature/commons/util/PermissionUtils.kt index 85e40e45..00276a3d 100644 --- a/commons/src/main/java/fr/geonature/commons/util/PermissionUtils.kt +++ b/commons/src/main/java/fr/geonature/commons/util/PermissionUtils.kt @@ -1,14 +1,9 @@ package fr.geonature.commons.util -import android.app.Activity -import android.content.Context import android.content.pm.PackageManager -import android.view.View -import androidx.annotation.NonNull +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat -import androidx.fragment.app.Fragment -import com.google.android.material.snackbar.BaseTransientBottomBar -import com.google.android.material.snackbar.Snackbar /** * Helper class about Android permissions. @@ -17,185 +12,81 @@ import com.google.android.material.snackbar.Snackbar */ object PermissionUtils { - /** - * Checks that all given permissions have been granted by verifying that each entry in the - * given array is of the value [PackageManager.PERMISSION_GRANTED]. - * - * @see Activity.onRequestPermissionsResult - */ - fun checkPermissions(grantResults: IntArray): Boolean { - // At least one result must be checked. - if (grantResults.isEmpty()) { - return false - } - - // Verify that each required permission has been granted, otherwise return false. - for (result in grantResults) { - if (result != PackageManager.PERMISSION_GRANTED) { - return false - } - } - - return true - } - - /** - * Determines whether the user have been granted a set of permissions. - * - * @param context the current `Context`. - * @param permissions a set of permissions being checked - */ - fun checkSelfPermissions( - context: Context, - vararg permissions: String - ): Boolean { - var granted = true - val iterator = permissions.iterator() - - while (iterator.hasNext() && granted) { - granted = ActivityCompat.checkSelfPermission( - context, - iterator.next() - ) == PackageManager.PERMISSION_GRANTED - } - - return granted - } - - /** - * Determines whether the user have been granted a set of permissions. - * - * @param context the current `Context`. - * @param onCheckSelfPermissionListener the callback to use to notify if these permissions was - * granted or not - * @param permissions a set of permissions being checked - */ - fun checkSelfPermissions( - context: Context, - onCheckSelfPermissionListener: OnCheckSelfPermissionListener, - @NonNull - vararg permissions: String - ) { - if (checkSelfPermissions( - context, - *permissions - ) - ) { - onCheckSelfPermissionListener.onPermissionsGranted() - } else { - onCheckSelfPermissionListener.onRequestPermissions(*permissions) - } - } - /** * Requests a set of permissions from `Activity`. * - * If a permission has been denied previously, a `Snackbar` will prompt the user to grant - * the permission, otherwise it is requested directly. - * - * @param activity the current `Activity` - * @param snackbarParentView the parent view on which to display the `Snackbar` - * @param snackbarMessageResourceId the message resource ID to display - * @param requestCode application specific request code to match with a result - * reported to `ActivityCompat.OnRequestPermissionsResultCallback#onRequestPermissionsResult(int, String[], int[])`. + * @param fromActivity the current `Activity` * @param permissions a set of permissions to request + * @param isGranted called when permissions were granted or not + * @param shouldShowRequestPermissionRationale called if we want to show UI with rationale + * before requesting a permission */ fun requestPermissions( - activity: Activity, - snackbarParentView: View, - snackbarMessageResourceId: Int, - requestCode: Int, - vararg permissions: String + fromActivity: AppCompatActivity, + permissions: List, + isGranted: (result: Map) -> Unit, + shouldShowRequestPermissionRationale: ((callback: () -> Unit) -> Unit)? = null ) { - var shouldShowRequestPermissions = false - val iterator = permissions.iterator() - - while (iterator.hasNext() && !shouldShowRequestPermissions) { - shouldShowRequestPermissions = ActivityCompat.shouldShowRequestPermissionRationale( - activity, - iterator.next() - ) - } - - if (shouldShowRequestPermissions) { - Snackbar.make( - snackbarParentView, - snackbarMessageResourceId, - BaseTransientBottomBar.LENGTH_INDEFINITE - ) - .setAction(android.R.string.ok) { - ActivityCompat.requestPermissions( - activity, - permissions, - requestCode + val requestPermissionLauncher = + fromActivity.registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { + isGranted( + mapOf( + Pair( + permissions.first(), + it + ) ) - } - .show() - } else { - ActivityCompat.requestPermissions( - activity, - permissions, - requestCode - ) - } - } - - /** - * Requests a set of permissions from a `Fragment`. - * - * If a permission has been denied previously, a `Snackbar` will prompt the user to grant - * the permission, otherwise it is requested directly. - * - * @param fragment the current `Fragment` - * @param snackbarParentView the parent view on which to display the `Snackbar` - * @param snackbarMessageResourceId the message resource ID to display - * @param requestCode application specific request code to match with a result - * reported to `ActivityCompat.OnRequestPermissionsResultCallback#onRequestPermissionsResult(int, String[], int[])`. - * @param permissions a set of permissions to request - */ - fun requestPermissions( - fragment: Fragment, - snackbarParentView: View, - snackbarMessageResourceId: Int, - requestCode: Int, - vararg permissions: String - ) { - var shouldShowRequestPermissions = false - val iterator = permissions.iterator() - - while (iterator.hasNext() && !shouldShowRequestPermissions) { - shouldShowRequestPermissions = - fragment.shouldShowRequestPermissionRationale(iterator.next()) - } + ) + } - if (shouldShowRequestPermissions) { - Snackbar.make( - snackbarParentView, - snackbarMessageResourceId, - BaseTransientBottomBar.LENGTH_INDEFINITE + val requestPermissionsLauncher = + fromActivity.registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { isGranted(it) } + + val checkSelfPermissions = permissions.asSequence() + .map { + Pair( + it, + ActivityCompat.checkSelfPermission( + fromActivity, + it + ) == PackageManager.PERMISSION_GRANTED + ) + } + .toMap() + + when { + // all permissions were granted + checkSelfPermissions.values.all { it } -> isGranted( + permissions.asSequence() + .map { + Pair( + it, + true + ) + } + .toMap() ) - .setAction(android.R.string.ok) { - fragment.requestPermissions( - permissions, - requestCode - ) - } - .show() - } else { - fragment.requestPermissions( - permissions, - requestCode + // show request permission rationale only for one non granted permission + shouldShowRequestPermissionRationale != null && permissions.size == 1 && ActivityCompat.shouldShowRequestPermissionRationale( + fromActivity, + permissions.first() + ) -> shouldShowRequestPermissionRationale { + requestPermissionLauncher.launch(permissions.first()) + } + // ask for one permission + permissions.size == 1 -> requestPermissionLauncher.launch(permissions.first()) + // ask for several permissions + else -> requestPermissionsLauncher.launch( + checkSelfPermissions.asSequence() + .filter { !it.value } + .map { it.key } + .toList() + .toTypedArray() ) } } - - /** - * Callback about [PermissionUtils.checkSelfPermissions]. - * - * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) - */ - interface OnCheckSelfPermissionListener { - fun onPermissionsGranted() - fun onRequestPermissions(vararg permissions: String) - } } diff --git a/commons/src/test/java/fr/geonature/commons/OneTimeObserver.kt b/commons/src/test/java/fr/geonature/commons/OneTimeObserver.kt index 6ae1c9a4..dcf8a70f 100644 --- a/commons/src/test/java/fr/geonature/commons/OneTimeObserver.kt +++ b/commons/src/test/java/fr/geonature/commons/OneTimeObserver.kt @@ -14,7 +14,8 @@ import androidx.lifecycle.Observer * * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) */ -class OneTimeObserver(private val handler: (T?) -> Unit) : Observer, +class OneTimeObserver(private val handler: (T?) -> Unit) : + Observer, LifecycleOwner { private val lifecycle = LifecycleRegistry(this) diff --git a/commons/src/test/java/fr/geonature/commons/data/AppSyncTest.kt b/commons/src/test/java/fr/geonature/commons/data/AppSyncTest.kt index 6db39b54..f6fd75e5 100644 --- a/commons/src/test/java/fr/geonature/commons/data/AppSyncTest.kt +++ b/commons/src/test/java/fr/geonature/commons/data/AppSyncTest.kt @@ -3,8 +3,6 @@ package fr.geonature.commons.data import android.database.Cursor import android.os.Parcel import fr.geonature.commons.data.AppSync.Companion.defaultProjection -import java.time.Instant -import java.util.Date import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull @@ -13,6 +11,8 @@ import org.junit.runner.RunWith import org.mockito.Mockito.`when` import org.mockito.Mockito.mock import org.robolectric.RobolectricTestRunner +import java.time.Instant +import java.util.Date /** * Unit tests about [AppSync]. diff --git a/commons/src/test/java/fr/geonature/commons/data/NomenclatureWithTaxonomyTest.kt b/commons/src/test/java/fr/geonature/commons/data/NomenclatureWithTaxonomyTest.kt index 8d51b3fa..d6ffef9d 100644 --- a/commons/src/test/java/fr/geonature/commons/data/NomenclatureWithTaxonomyTest.kt +++ b/commons/src/test/java/fr/geonature/commons/data/NomenclatureWithTaxonomyTest.kt @@ -76,25 +76,26 @@ class NomenclatureWithTaxonomyTest { ) ) - assertEquals(NomenclatureWithTaxonomy( - NomenclatureWithType( - 2, - "SN", - "1234:002", - "label", - 1234, - NomenclatureType( + assertEquals( + NomenclatureWithTaxonomy( + NomenclatureWithType( + 2, + "SN", + "1234:002", + "label", 1234, - "SGR", - "label" + NomenclatureType( + 1234, + "SGR", + "label" + ) ) - ) - ).also { - it.taxonony = Taxonomy( - "Animalia", - "Ascidies" - ) - }, + ).also { + it.taxonony = Taxonomy( + "Animalia", + "Ascidies" + ) + }, NomenclatureWithTaxonomy( NomenclatureWithType( 2, @@ -113,7 +114,8 @@ class NomenclatureWithTaxonomyTest { "Animalia", "Ascidies" ) - }) + } + ) } @Test diff --git a/commons/src/test/java/fr/geonature/commons/data/TaxonAreaTest.kt b/commons/src/test/java/fr/geonature/commons/data/TaxonAreaTest.kt index 2ce0faaa..d1beb6f8 100644 --- a/commons/src/test/java/fr/geonature/commons/data/TaxonAreaTest.kt +++ b/commons/src/test/java/fr/geonature/commons/data/TaxonAreaTest.kt @@ -5,8 +5,6 @@ import android.os.Parcel import fr.geonature.commons.data.TaxonArea.Companion.defaultProjection import fr.geonature.commons.data.TaxonArea.Companion.fromCursor import fr.geonature.commons.data.helper.EntityHelper.column -import java.time.Instant -import java.util.Date import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull @@ -16,6 +14,8 @@ import org.junit.runner.RunWith import org.mockito.Mockito.`when` import org.mockito.Mockito.mock import org.robolectric.RobolectricTestRunner +import java.time.Instant +import java.util.Date /** * Unit tests about [TaxonArea]. diff --git a/commons/src/test/java/fr/geonature/commons/data/TaxonWithAreaTest.kt b/commons/src/test/java/fr/geonature/commons/data/TaxonWithAreaTest.kt index c3ad67dd..e34b359a 100644 --- a/commons/src/test/java/fr/geonature/commons/data/TaxonWithAreaTest.kt +++ b/commons/src/test/java/fr/geonature/commons/data/TaxonWithAreaTest.kt @@ -417,8 +417,10 @@ class TaxonWithAreaTest { assertTrue(taxonFilterByAreaColors.second.isEmpty()) val taxonFilterByNameAndAreaColors = - (TaxonWithArea.Filter() - .byNameOrDescriptionOrRank("as") as TaxonWithArea.Filter) + ( + TaxonWithArea.Filter() + .byNameOrDescriptionOrRank("as") as TaxonWithArea.Filter + ) .byAreaColors( "red", "grey" diff --git a/commons/src/test/java/fr/geonature/commons/data/dao/BaseDaoTest.kt b/commons/src/test/java/fr/geonature/commons/data/dao/BaseDaoTest.kt index 30925a5e..0b904118 100644 --- a/commons/src/test/java/fr/geonature/commons/data/dao/BaseDaoTest.kt +++ b/commons/src/test/java/fr/geonature/commons/data/dao/BaseDaoTest.kt @@ -44,7 +44,7 @@ class BaseDaoTest { """ SELECT * FROM entity_table entity_table - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) } @@ -62,7 +62,7 @@ class BaseDaoTest { """ SELECT * FROM entity_table entity_table - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) } @@ -71,8 +71,10 @@ class BaseDaoTest { fun getQueryBuilderWithSelectionWithNoArgs() { // given a simple query builder from DAO val sqLiteQuery = - (SimpleEntityDao().QB() - .whereSelection("col = 1") as SimpleEntityDao.QB).getQueryBuilder() + ( + SimpleEntityDao().QB() + .whereSelection("col = 1") as SimpleEntityDao.QB + ).getQueryBuilder() .build() // then @@ -82,7 +84,7 @@ class BaseDaoTest { SELECT * FROM entity_table entity_table WHERE (col = 1) - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) assertEquals( @@ -94,14 +96,16 @@ class BaseDaoTest { @Test fun getQueryBuilderWithSelectionWithArgs() { // given a simple query builder from DAO - val sqLiteQuery = (SimpleEntityDao().QB() - .whereSelection( - "col = ? OR col = ?", - arrayOf( - 12, - "some_args" - ) - ) as SimpleEntityDao.QB).getQueryBuilder() + val sqLiteQuery = ( + SimpleEntityDao().QB() + .whereSelection( + "col = ? OR col = ?", + arrayOf( + 12, + "some_args" + ) + ) as SimpleEntityDao.QB + ).getQueryBuilder() .build() // then @@ -111,7 +115,7 @@ class BaseDaoTest { SELECT * FROM entity_table entity_table WHERE (col = ? OR col = ?) - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) assertEquals( diff --git a/commons/src/test/java/fr/geonature/commons/data/helper/ConvertersTest.kt b/commons/src/test/java/fr/geonature/commons/data/helper/ConvertersTest.kt index 34e0f868..dba3e9de 100644 --- a/commons/src/test/java/fr/geonature/commons/data/helper/ConvertersTest.kt +++ b/commons/src/test/java/fr/geonature/commons/data/helper/ConvertersTest.kt @@ -2,11 +2,11 @@ package fr.geonature.commons.data.helper import fr.geonature.commons.data.helper.Converters.dateToTimestamp import fr.geonature.commons.data.helper.Converters.fromTimestamp -import java.time.Instant -import java.util.Date import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test +import java.time.Instant +import java.util.Date /** * Unit tests about [Converters]. diff --git a/commons/src/test/java/fr/geonature/commons/data/helper/SQLiteSelectQueryBuilderTest.kt b/commons/src/test/java/fr/geonature/commons/data/helper/SQLiteSelectQueryBuilderTest.kt index 48c866ed..1359ae5a 100644 --- a/commons/src/test/java/fr/geonature/commons/data/helper/SQLiteSelectQueryBuilderTest.kt +++ b/commons/src/test/java/fr/geonature/commons/data/helper/SQLiteSelectQueryBuilderTest.kt @@ -26,7 +26,7 @@ class SQLiteSelectQueryBuilderTest { """ SELECT * FROM user - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) @@ -43,7 +43,7 @@ class SQLiteSelectQueryBuilderTest { """ SELECT * FROM user u - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) } @@ -61,7 +61,7 @@ class SQLiteSelectQueryBuilderTest { """ SELECT email FROM user - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) @@ -82,7 +82,7 @@ class SQLiteSelectQueryBuilderTest { """ SELECT u.email AS user_email FROM user u - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) @@ -109,7 +109,7 @@ class SQLiteSelectQueryBuilderTest { """ SELECT u.email, u.login FROM user u - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) @@ -128,7 +128,7 @@ class SQLiteSelectQueryBuilderTest { """ SELECT u.email, u.login FROM user u - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) } @@ -146,7 +146,7 @@ class SQLiteSelectQueryBuilderTest { """ SELECT * FROM user - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) @@ -166,7 +166,7 @@ class SQLiteSelectQueryBuilderTest { """ SELECT DISTINCT u.email, u.login FROM user u - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) } @@ -193,7 +193,7 @@ class SQLiteSelectQueryBuilderTest { """ SELECT u.email, u.login, p.role FROM user u, profile p - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) } @@ -223,7 +223,7 @@ class SQLiteSelectQueryBuilderTest { SELECT u.id, u.email, u.login, p.role FROM user u LEFT JOIN profile AS p ON p.user_id = u.id - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) @@ -265,7 +265,7 @@ class SQLiteSelectQueryBuilderTest { JOIN group AS g ON g.name = ? LEFT OUTER JOIN user_group AS ug ON ug.group_id = g.id AND ug.user_id = u.id LEFT JOIN profile AS p ON p.user_id = u.id - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) assertEquals( @@ -296,7 +296,7 @@ class SQLiteSelectQueryBuilderTest { SELECT u.email, u.login FROM user u WHERE u.email = ? - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) assertEquals( @@ -324,7 +324,7 @@ class SQLiteSelectQueryBuilderTest { SELECT u.email, u.login FROM user u WHERE (u.email = ?) - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) assertEquals( @@ -352,7 +352,7 @@ class SQLiteSelectQueryBuilderTest { SELECT u.email, u.login FROM user u WHERE (u.email = ?) - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) assertEquals( @@ -385,7 +385,7 @@ class SQLiteSelectQueryBuilderTest { FROM user u WHERE (u.email = ?) AND (u.login = ?) - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) assertEquals( @@ -415,7 +415,7 @@ class SQLiteSelectQueryBuilderTest { FROM user u WHERE (u.email = ?) OR (u.login = 'user') - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) assertEquals( @@ -447,7 +447,7 @@ class SQLiteSelectQueryBuilderTest { SELECT u.email, u.login FROM user u WHERE u.login = ? - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) assertEquals( @@ -479,7 +479,7 @@ class SQLiteSelectQueryBuilderTest { SELECT u.email, u.login FROM user u WHERE u.login = ? - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) assertEquals( @@ -510,7 +510,7 @@ class SQLiteSelectQueryBuilderTest { SELECT u.email, u.login FROM user u WHERE u.email = ? - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) assertEquals( @@ -552,7 +552,7 @@ class SQLiteSelectQueryBuilderTest { SELECT u.email, u.login AS login FROM user u GROUP BY login - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) } @@ -603,7 +603,7 @@ class SQLiteSelectQueryBuilderTest { LEFT JOIN input AS i ON i.user_id = p.id GROUP BY u.email HAVING SUM(i.id) > 0 - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) } @@ -637,7 +637,7 @@ class SQLiteSelectQueryBuilderTest { FROM user u INNER JOIN input AS i ON i.user_id = p.id ORDER BY count - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) } @@ -665,7 +665,7 @@ class SQLiteSelectQueryBuilderTest { SELECT u.email, u.login FROM user u ORDER BY u.email COLLATE NOCASE ASC - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) } @@ -702,7 +702,7 @@ class SQLiteSelectQueryBuilderTest { FROM user u INNER JOIN input AS i ON i.user_id = p.id ORDER BY count DESC - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) } @@ -730,7 +730,7 @@ class SQLiteSelectQueryBuilderTest { SELECT u.email, u.login FROM user u ORDER BY u.login COLLATE NOCASE DESC - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) } @@ -756,7 +756,7 @@ class SQLiteSelectQueryBuilderTest { SELECT u.email, u.login FROM user u ORDER BY u.login asc - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) } @@ -783,7 +783,7 @@ class SQLiteSelectQueryBuilderTest { SELECT u.email, u.login, u.username FROM user u ORDER BY u.email, COALESCE(u.login, u.username) desc - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) } @@ -807,7 +807,7 @@ class SQLiteSelectQueryBuilderTest { SELECT u.email, u.login FROM user u LIMIT 10 - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) @@ -831,7 +831,7 @@ class SQLiteSelectQueryBuilderTest { SELECT u.email, u.login FROM user u LIMIT 10, 3 - """.trimIndent(), + """.trimIndent(), sqLiteQuery.sql ) } diff --git a/commons/src/test/java/fr/geonature/commons/settings/AppSettingsManagerTest.kt b/commons/src/test/java/fr/geonature/commons/settings/AppSettingsManagerTest.kt index 352ee0bf..d0bcb15a 100644 --- a/commons/src/test/java/fr/geonature/commons/settings/AppSettingsManagerTest.kt +++ b/commons/src/test/java/fr/geonature/commons/settings/AppSettingsManagerTest.kt @@ -38,22 +38,24 @@ class AppSettingsManagerTest { initMocks(this) onAppSettingsJsonJsonReaderListener = - spy(object : AppSettingsJsonReader.OnAppSettingsJsonReaderListener { - override fun createAppSettings(): DummyAppSettings { - return DummyAppSettings() - } + spy( + object : AppSettingsJsonReader.OnAppSettingsJsonReaderListener { + override fun createAppSettings(): DummyAppSettings { + return DummyAppSettings() + } - override fun readAdditionalAppSettingsData( - reader: JsonReader, - keyName: String, - appSettings: DummyAppSettings - ) { - when (keyName) { - "attribute" -> appSettings.attribute = reader.nextString() - else -> reader.skipValue() + override fun readAdditionalAppSettingsData( + reader: JsonReader, + keyName: String, + appSettings: DummyAppSettings + ) { + when (keyName) { + "attribute" -> appSettings.attribute = reader.nextString() + else -> reader.skipValue() + } } } - }) + ) val application = getApplicationContext() appSettingsManager = spy( diff --git a/commons/src/test/java/fr/geonature/commons/util/DateHelperTest.kt b/commons/src/test/java/fr/geonature/commons/util/DateHelperTest.kt index 95d37127..e4f2d3de 100644 --- a/commons/src/test/java/fr/geonature/commons/util/DateHelperTest.kt +++ b/commons/src/test/java/fr/geonature/commons/util/DateHelperTest.kt @@ -3,11 +3,9 @@ package fr.geonature.commons.util import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue import org.junit.Test import java.text.SimpleDateFormat import java.util.Calendar -import java.util.Date import java.util.TimeZone /** @@ -67,4 +65,4 @@ class DateHelperTest { ?.toIsoDateString() ) } -} \ No newline at end of file +} diff --git a/commons/version.properties b/commons/version.properties index 12744b52..0bacf366 100644 --- a/commons/version.properties +++ b/commons/version.properties @@ -1,2 +1,2 @@ -#Sat Oct 10 14:42:14 CEST 2020 -VERSION_CODE=2560 +#Tue Feb 02 20:36:43 CET 2021 +VERSION_CODE=2680 diff --git a/docs/sync.adoc b/docs/sync.adoc index 1cd62bb0..7ff94455 100644 --- a/docs/sync.adoc +++ b/docs/sync.adoc @@ -2,7 +2,7 @@ == Check for update -[plantuml, images/sync_update, svg] +[plantuml,images/uml/sync_update,svg] .... participant "mobile/sync" as sync << mobile >> participant "GeoNature" as gn @@ -10,8 +10,17 @@ participant "GeoNature" as gn activate sync group Fetch common configuration data - sync -> gn ++ : **GET** : ""/api/gn_commons/t_mobile_apps"" - gn -> sync -- : **200** : ""array"" + sync -> gn ++ : **GET** ""/api/gn_commons/t_mobile_apps"" + gn -> sync -- : **200**: ""[AppPackage]"" + note left + **""AppPackage"":** + { + "url_apk": , + "package": , + "app_code": , + "version_code": + } + end note end group Check for update @@ -21,69 +30,183 @@ group Check for update sync -> sync : notify if we want to upgrade end end - .... -== Update local database from GeoNature +== Authentication -[plantuml, images/sync_data, svg] +[plantuml,images/uml/sync_auth,svg] .... participant "mobile/sync" as sync << mobile >> participant "GeoNature" as gn activate sync -group Fetch common configuration data - sync -> gn ++ : **GET** : ""/api/gn_commons/t_mobile_apps"" - gn -> sync -- : **200** : ""array"" - sync -> sync : update //settings_sync.json// +group Check for login + sync -> sync : Set login and password + sync -> gn ++ : **POST** ""/api/auth/login"" + gn -> sync -- : **200**: ""AuthLogin"" + note left + **""AuthLogin"":** + { + "user": { + "id_application": , + "id_organisme": , + "identifiant": + }, + "expires": + } + end note + sync -> sync : Set cookie end +.... -alt Check for login - sync -> sync : Set login and password - sync -> gn ++ : **POST** : ""/api/auth/login"" - gn -> sync -- : **200** : ""Cookie"" -end +== Update local database + +[plantuml,images/uml/sync_data,svg] +.... +participant "mobile/sync" as sync << mobile >> +participant "GeoNature" as gn + +activate sync + +ref over sync, gn : Check for login group Fetch GeoNature data - sync -> gn ++ : **GET** : ""/api/meta/datasets"" - gn -> sync -- : **200** : ""[dataset]"" - sync -> sync : update //dataset// table - sync -> gn ++ : **GET** : ""/api/users/menu/:observers_list_id"" - gn -> sync -- : **200** : ""[user]"" - sync -> sync : update //observers// table - sync -> gn ++ : **GET** : ""/api/taxref/regnewithgroupe2"" - gn -> sync -- : **200** : ""[taxonomy]"" - sync -> sync : update //taxonomy// table - sync -> gn ++ : **GET** : ""/api/taxref/allnamebylist/:taxa_list_id"" - gn -> sync -- : **200** : ""[taxon]"" - sync -> sync : update //taxa// table - sync -> gn ++ : **GET** : ""/api/synthese/color_taxon"" - gn -> sync -- : **200** : ""[taxrefArea]"" - sync -> sync : update //taxa_area// table - sync -> gn ++ : **GET** : ""/api/nomenclatures/nomenclatures/taxonomy"" - gn -> sync -- : **200** : ""[nomenclatureType]"" - sync -> sync : update //nomenclature_types// table - sync -> sync : update //nomenclatures// table - sync -> sync : update //nomenclatures_taxonomy// table - - note over sync : **TODO:**\nfetch registered modules from GeoNature - - loop for each registered module - sync -> gn ++ : **GET** : ""/api/:module/defaultNomenclatures"" - gn -> sync -- : **200** : ""[DefaultNomenclature]"" - sync -> sync : update //default_nomenclatures// table - end -end + group Dataset + sync -> gn ++ : **GET** ""/api/meta/datasets"" + gn -> sync -- : **200**: ""[Dataset]"" + note right of sync + **""Dataset"":** + { + "id_dataset": , + "dataset_name": , + "dataset_desc": , + "active": , + "meta_create_date": , + "modules": { + "module_path": + }[] + } + end note + sync -> sync : update //dataset// table + end -deactivate sync + group Observers + sync -> gn ++ : **GET** ""/api/users/menu/:observers_list_id"" + note right of sync + ""observers_list_id"" from settings + end note + gn -> sync -- : **200**: ""[User]"" + note right of sync + **""User"":** + { + "id_role": , + "nom_role": , + "prenom_role": + } + end note + sync -> sync : update //observers// table + end + + group Taxonomy + sync -> gn ++ : **GET** ""/api/taxref/regnewithgroupe2"" + gn -> sync -- : **200**: ""[Taxonomy]"" + note right of sync + **""Taxonomy"":** + { + : [] + } + end note + sync -> sync : update //taxonomy// table + end + + group Taxa + loop while response is not empty\nand response size matches ""page_size"" from settings\nand loop doesn't exceed ""page_max_retry"" from settings + sync -> gn ++ : **GET** ""/api/taxref/allnamebylist/:taxa_list_id"" + note right of sync + ""taxa_list_id"" from settings + end note + gn -> sync -- : **200**: ""[Taxref]"" + note right of sync + **""Taxref"":** + { + "cd_nom": , + "lb_nom": , + "nom_vern": , + "nom_valide": , + "regne": , + "group2_inpn": , + "search_name": + } + end note + sync -> sync : update //taxa// table + end + loop while response is not empty\nand response size matches ""page_size"" from settings\nand loop doesn't exceed ""page_max_retry"" from settings + sync -> gn ++ : **GET** ""/api/synthese/color_taxon?:code_area_type"" + note right of sync + ""code_area_type"" from settings + end note + gn -> sync -- : **200**: ""[TaxrefArea]"" + note right of sync + **""TaxrefArea"":** + { + "cd_nom": , + "id_area": , + "color": , + "nb_obs": , + "last_date": + } + end note + sync -> sync : update //taxa_area// table + end + end + + group Nomenclature + sync -> gn ++ : **GET** ""/api/nomenclatures/nomenclatures/taxonomy"" + gn -> sync -- : **200**: ""[NomenclatureType]"" + note right of sync + **""NomenclatureType"":** + { + "id_type": , + "mnemonique": , + "label_default": , + "nomenclatures": { + "id_nomenclature": , + "cd_nomenclature": , + "hierarchy": , + "label_default": , + "taxref": { + "regne": , + "group2_inpn": , + }[], + }[], + } + end note + note over sync #FFAA88 : **TODO:**\nfetch registered modules from GeoNature + loop for each registered module + sync -> gn ++ : **GET** ""/api/:module/defaultNomenclatures"" + gn -> sync -- : **200**: ""[DefaultNomenclature]"" + note right of sync + **""DefaultNomenclature"":** + { + : + } + end note + end + sync -> sync : update //nomenclature_types// table + sync -> sync : update //nomenclatures// table + sync -> sync : update //nomenclatures_taxonomy// table + sync -> sync : update //default_nomenclatures// table + end + +end .... == Synchronize local inputs -[plantuml, images/sync_input, svg] +[plantuml,images/uml/sync_input,svg] .... participant "mobile/sync" as sync << mobile >> participant "GeoNature" as gn @@ -92,30 +215,28 @@ activate sync group Fetch exported inputs from installed app - sync -> sync: fetch installed apps - note left : from Android ""PackageManager"" - - loop for each app - sync -> sync : read exported inputs - - loop for each input - sync -> sync : get module name from input - sync -> gn ++ : **POST** : ""api/:module/releve"" - note left - **input:** - { - "id": , - "module": , - ... - } - end note - gn -> sync -- : **200** - sync -> sync : delete input + sync -> sync: fetch installed apps + note left : from Android ""PackageManager"" + + loop for each app + sync -> sync : read exported inputs + + loop for each input + sync -> sync : get module name from input + sync -> gn ++ : **POST** ""api/:module/releve"" + note left + **""SyncInput"":** + { + "packageInfo": , + "filePath": , + "module": , + "payload": , + } + end note + gn -> sync -- : **200** + sync -> sync : delete input file + end end - - end - end -deactivate sync .... \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b917ab1f..192acd47 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jun 01 15:11:36 CEST 2020 +#Mon Dec 07 22:01:51 CET 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip diff --git a/mountpoint/build.gradle b/mountpoint/build.gradle index b7754a4b..df522cb8 100644 --- a/mountpoint/build.gradle +++ b/mountpoint/build.gradle @@ -1,8 +1,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' -version = "0.0.3" +version = "0.0.6" android { compileSdkVersion 29 @@ -43,12 +42,11 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - testImplementation 'androidx.test:core:1.2.0' - testImplementation 'junit:junit:4.13' + testImplementation 'androidx.test:core:1.3.0' + testImplementation 'junit:junit:4.13.1' testImplementation 'org.robolectric:robolectric:4.3.1' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + androidTestImplementation 'androidx.test:runner:1.3.0' } diff --git a/mountpoint/src/main/java/fr/geonature/mountpoint/model/MountPoint.kt b/mountpoint/src/main/java/fr/geonature/mountpoint/model/MountPoint.kt index 53dd7b08..dafbc876 100644 --- a/mountpoint/src/main/java/fr/geonature/mountpoint/model/MountPoint.kt +++ b/mountpoint/src/main/java/fr/geonature/mountpoint/model/MountPoint.kt @@ -4,7 +4,6 @@ import android.os.Environment import android.os.Parcel import android.os.Parcelable import android.util.Log -import androidx.annotation.NonNull import fr.geonature.mountpoint.BuildConfig import fr.geonature.mountpoint.util.DeviceUtils import java.io.File @@ -15,7 +14,8 @@ import java.io.IOException * * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) */ -class MountPoint : Parcelable, +class MountPoint : + Parcelable, Comparable { val mountPath: File @@ -54,7 +54,6 @@ class MountPoint : Parcelable, this.storageType = source.readSerializable() as StorageType } - @NonNull fun getStorageState(): String { if (DeviceUtils.isPostLollipop) { return Environment.getExternalStorageState(mountPath) diff --git a/mountpoint/src/test/java/fr/geonature/mountpoint/model/MountPointTest.kt b/mountpoint/src/test/java/fr/geonature/mountpoint/model/MountPointTest.kt index 5fd5b120..c8c971cd 100644 --- a/mountpoint/src/test/java/fr/geonature/mountpoint/model/MountPointTest.kt +++ b/mountpoint/src/test/java/fr/geonature/mountpoint/model/MountPointTest.kt @@ -89,4 +89,4 @@ class MountPointTest { MountPoint.CREATOR.createFromParcel(parcel) ) } -} \ No newline at end of file +} diff --git a/mountpoint/src/test/java/fr/geonature/mountpoint/util/FileUtilsTest.kt b/mountpoint/src/test/java/fr/geonature/mountpoint/util/FileUtilsTest.kt index efc658ff..02a88cae 100644 --- a/mountpoint/src/test/java/fr/geonature/mountpoint/util/FileUtilsTest.kt +++ b/mountpoint/src/test/java/fr/geonature/mountpoint/util/FileUtilsTest.kt @@ -56,4 +56,4 @@ class FileUtilsTest { ).absolutePath.contains("/Android/data/fr.geonature.sync") ) } -} \ No newline at end of file +} diff --git a/mountpoint/src/test/java/fr/geonature/mountpoint/util/MountPointUtilsTest.kt b/mountpoint/src/test/java/fr/geonature/mountpoint/util/MountPointUtilsTest.kt index 2b2f1d47..b024cc37 100644 --- a/mountpoint/src/test/java/fr/geonature/mountpoint/util/MountPointUtilsTest.kt +++ b/mountpoint/src/test/java/fr/geonature/mountpoint/util/MountPointUtilsTest.kt @@ -86,4 +86,4 @@ class MountPointUtilsTest { storageInGbFormatted ) } -} \ No newline at end of file +} diff --git a/mountpoint/version.properties b/mountpoint/version.properties index fab6c8d9..ecb14e44 100644 --- a/mountpoint/version.properties +++ b/mountpoint/version.properties @@ -1,2 +1,2 @@ -#Sat Feb 08 17:10:26 CET 2020 -VERSION_CODE=30 +#Mon Feb 01 21:59:56 CET 2021 +VERSION_CODE=150 diff --git a/sync/build.gradle b/sync/build.gradle index 095da70b..b573bbe5 100644 --- a/sync/build.gradle +++ b/sync/build.gradle @@ -1,9 +1,8 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' apply plugin: "kotlin-kapt" -version = "1.1.2" +version = "1.1.4" android { compileSdkVersion 29 @@ -69,26 +68,26 @@ dependencies { implementation project(':commons') - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1' - implementation 'androidx.core:core-ktx:1.5.0-alpha01' + implementation 'androidx.constraintlayout:constraintlayout:2.1.0-alpha2' + implementation 'androidx.core:core-ktx:1.5.0-beta01' implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" - implementation 'androidx.preference:preference:1.1.1' + implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation 'androidx.work:work-runtime:2.4.0' - implementation 'androidx.work:work-runtime-ktx:2.4.0' - implementation 'com.google.android.material:material:1.3.0-alpha02' - implementation 'com.google.code.gson:gson:2.8.5' + implementation 'androidx.work:work-runtime-ktx:2.5.0' + implementation 'com.google.android.material:material:1.3.0-rc01' + implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.squareup.okhttp3:logging-interceptor:3.12.0' - implementation 'com.squareup.retrofit2:retrofit:2.6.0' implementation 'com.squareup.retrofit2:converter-gson:2.6.0' - kapt 'androidx.room:room-compiler:2.2.5' + implementation 'com.squareup.retrofit2:retrofit:2.6.0' + + kapt 'androidx.room:room-compiler:2.2.6' - testImplementation 'junit:junit:4.13' - testImplementation 'androidx.test:core:1.2.0' + testImplementation 'androidx.test:core:1.3.0' + testImplementation 'junit:junit:4.13.1' testImplementation 'org.mockito:mockito-core:3.0.0' testImplementation 'org.robolectric:robolectric:4.3.1' - androidTestImplementation 'androidx.test:runner:1.3.0-rc03' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0-rc03' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha03' + androidTestImplementation 'androidx.test:runner:1.3.1-alpha03' } diff --git a/sync/src/main/AndroidManifest.xml b/sync/src/main/AndroidManifest.xml index b531df9e..32f0907e 100644 --- a/sync/src/main/AndroidManifest.xml +++ b/sync/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ package="fr.geonature.sync" android:sharedUserId="@string/sharedUserId"> + 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 2d8dc794..77850f7c 100644 --- a/sync/src/main/java/fr/geonature/sync/api/GeoNatureService.kt +++ b/sync/src/main/java/fr/geonature/sync/api/GeoNatureService.kt @@ -12,6 +12,7 @@ import retrofit2.Call import retrofit2.Response import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.Headers import retrofit2.http.POST import retrofit2.http.Path import retrofit2.http.Query @@ -25,12 +26,20 @@ import retrofit2.http.Url */ interface GeoNatureService { + @Headers( + "Accept: application/json", + "Content-Type: application/json;charset=UTF-8" + ) @POST("api/auth/login") suspend fun authLogin( @Body authCredentials: AuthCredentials ): Response + @Headers( + "Accept: application/json", + "Content-Type: application/json;charset=UTF-8" + ) @POST("api/{module}/releve") fun sendInput( @Path("module") @@ -39,15 +48,18 @@ interface GeoNatureService { input: RequestBody ): Call + @Headers("Accept: application/json") @GET("api/meta/datasets") fun getMetaDatasets(): Call + @Headers("Accept: application/json") @GET("api/users/menu/{id}") fun getUsers( @Path("id") menuId: Int ): Call> + @Headers("Accept: application/json") @GET("api/synthese/color_taxon") fun getTaxrefAreas( @Query("code_area_type") codeAreaType: String? = null, @@ -55,15 +67,18 @@ interface GeoNatureService { @Query("offset") offset: Int? = null ): Call> + @Headers("Accept: application/json") @GET("api/nomenclatures/nomenclatures/taxonomy") fun getNomenclatures(): Call> + @Headers("Accept: application/json") @GET("api/{module}/defaultNomenclatures") fun getDefaultNomenclaturesValues( @Path("module") module: String ): Call + @Headers("Accept: application/json") @GET("api/gn_commons/t_mobile_apps") fun getApplications(): 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 f0a0469e..5ef5248d 100644 --- a/sync/src/main/java/fr/geonature/sync/api/TaxHubService.kt +++ b/sync/src/main/java/fr/geonature/sync/api/TaxHubService.kt @@ -4,6 +4,7 @@ import fr.geonature.sync.api.model.Taxref import okhttp3.ResponseBody import retrofit2.Call import retrofit2.http.GET +import retrofit2.http.Headers import retrofit2.http.Path import retrofit2.http.Query @@ -14,13 +15,15 @@ import retrofit2.http.Query */ interface TaxHubService { + @Headers("Accept: application/json") @GET("api/taxref/regnewithgroupe2") fun getTaxonomyRanks(): Call + @Headers("Accept: application/json") @GET("api/taxref/allnamebylist/{id}") fun getTaxref( @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/api/model/AppPackage.kt b/sync/src/main/java/fr/geonature/sync/api/model/AppPackage.kt index 0b094238..ad85dca2 100644 --- a/sync/src/main/java/fr/geonature/sync/api/model/AppPackage.kt +++ b/sync/src/main/java/fr/geonature/sync/api/model/AppPackage.kt @@ -21,4 +21,4 @@ data class AppPackage( val versionCode: Int, val settings: Any -) \ No newline at end of file +) diff --git a/sync/src/main/java/fr/geonature/sync/data/MainContentProvider.kt b/sync/src/main/java/fr/geonature/sync/data/MainContentProvider.kt index 12336aa5..9a4cd1be 100644 --- a/sync/src/main/java/fr/geonature/sync/data/MainContentProvider.kt +++ b/sync/src/main/java/fr/geonature/sync/data/MainContentProvider.kt @@ -30,7 +30,7 @@ class MainContentProvider : ContentProvider() { return true } - override fun getType(uri: Uri): String? { + override fun getType(uri: Uri): String { return when (MATCHER.match(uri)) { APP_SYNC_ID -> "$VND_TYPE_ITEM_PREFIX/$AUTHORITY.${AppSync.TABLE_NAME}" DATASET, DATASET_ACTIVE -> "$VND_TYPE_DIR_PREFIX/$AUTHORITY.${Dataset.TABLE_NAME}" diff --git a/sync/src/main/java/fr/geonature/sync/sync/AppPackageDownloadStatus.kt b/sync/src/main/java/fr/geonature/sync/sync/AppPackageDownloadStatus.kt index 03df2831..229071f4 100644 --- a/sync/src/main/java/fr/geonature/sync/sync/AppPackageDownloadStatus.kt +++ b/sync/src/main/java/fr/geonature/sync/sync/AppPackageDownloadStatus.kt @@ -13,5 +13,4 @@ data class AppPackageDownloadStatus( val packageName: String, val progress: Int = -1, val apkFilePath: String? = null -) { -} \ No newline at end of file +) diff --git a/sync/src/main/java/fr/geonature/sync/sync/DataSyncStatus.kt b/sync/src/main/java/fr/geonature/sync/sync/DataSyncStatus.kt index f14548b5..076716c4 100644 --- a/sync/src/main/java/fr/geonature/sync/sync/DataSyncStatus.kt +++ b/sync/src/main/java/fr/geonature/sync/sync/DataSyncStatus.kt @@ -11,4 +11,4 @@ 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 b76f6427..2477d8eb 100644 --- a/sync/src/main/java/fr/geonature/sync/sync/DataSyncViewModel.kt +++ b/sync/src/main/java/fr/geonature/sync/sync/DataSyncViewModel.kt @@ -13,6 +13,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 fr.geonature.commons.util.add import fr.geonature.commons.util.toIsoDateString @@ -36,23 +37,33 @@ class DataSyncViewModel(application: Application) : AndroidViewModel(application val lastSynchronizedDate: LiveData = dataSyncManager.lastSynchronizedDate - fun startSync(appSettings: AppSettings): LiveData { - val lastSynchronizedDate = dataSyncManager.lastSynchronizedDate.value + var isSyncRunning: Boolean = false + private set - if (lastSynchronizedDate?.add( - Calendar.HOUR, - 1 - ) - ?.after(Date()) == true - ) { - Log.d( - TAG, - "data already synchronized at ${lastSynchronizedDate.toIsoDateString()}" - ) + fun startSync( + appSettings: AppSettings, + forceRefresh: Boolean = false + ): LiveData { + if (!forceRefresh) { + val lastSynchronizedDate = dataSyncManager.lastSynchronizedDate.value + + if (lastSynchronizedDate?.add( + Calendar.HOUR, + 1 + ) + ?.after(Date()) == true + ) { + Log.d( + TAG, + "data already synchronized at ${lastSynchronizedDate.toIsoDateString()}" + ) - return MutableLiveData(null) + return MutableLiveData(null) + } } + isSyncRunning = true + val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() @@ -97,13 +108,20 @@ class DataSyncViewModel(application: Application) : AndroidViewModel(application return@map null } - val serverStatus = ServerStatus.values()[it.progress.getInt( - DataSyncWorker.KEY_SERVER_STATUS, - it.outputData.getInt( + val serverStatus = ServerStatus.values()[ + it.progress.getInt( DataSyncWorker.KEY_SERVER_STATUS, - ServerStatus.OK.ordinal + it.outputData.getInt( + DataSyncWorker.KEY_SERVER_STATUS, + ServerStatus.OK.ordinal + ) ) - )] + ] + + isSyncRunning = it.state in arrayListOf( + WorkInfo.State.ENQUEUED, + WorkInfo.State.RUNNING + ) DataSyncStatus( it.state, 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 d395f990..b82d05bc 100644 --- a/sync/src/main/java/fr/geonature/sync/sync/PackageInfoManager.kt +++ b/sync/src/main/java/fr/geonature/sync/sync/PackageInfoManager.kt @@ -8,7 +8,6 @@ import androidx.lifecycle.MutableLiveData import fr.geonature.commons.util.getInputsFolder import fr.geonature.mountpoint.util.FileUtils import fr.geonature.sync.api.GeoNatureAPIClient -import fr.geonature.sync.api.model.AppPackage import fr.geonature.sync.sync.io.AppSettingsJsonWriter import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.withContext @@ -55,7 +54,7 @@ class PackageInfoManager private constructor(private val applicationContext: Con emptyList() } } catch (e: Exception) { - emptyList() + emptyList() } availableAppPackages.asSequence() 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 9cb38686..30f9ce7d 100644 --- a/sync/src/main/java/fr/geonature/sync/sync/PackageInfoViewModel.kt +++ b/sync/src/main/java/fr/geonature/sync/sync/PackageInfoViewModel.kt @@ -59,13 +59,15 @@ class PackageInfoViewModel(application: Application) : AndroidViewModel(applicat ?: 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( + state = WorkInfo.State.values()[ + workInfo.progress.getInt( InputsSyncWorker.KEY_PACKAGE_STATUS, - WorkInfo.State.ENQUEUED.ordinal + workInfo.outputData.getInt( + InputsSyncWorker.KEY_PACKAGE_STATUS, + WorkInfo.State.ENQUEUED.ordinal + ) ) - )] + ] inputs = workInfo.progress.getInt( InputsSyncWorker.KEY_PACKAGE_INPUTS, 0 @@ -162,7 +164,7 @@ class PackageInfoViewModel(application: Application) : AndroidViewModel(applicat DownloadPackageWorker.KEY_PROGRESS, -1 ) - .takeIf { it > 0 } + .takeIf { progress -> progress > 0 } ?: it.progress.getInt( DownloadPackageWorker.KEY_PROGRESS, -1 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 5198a068..5fe8fe88 100644 --- a/sync/src/main/java/fr/geonature/sync/sync/ServerStatus.kt +++ b/sync/src/main/java/fr/geonature/sync/sync/ServerStatus.kt @@ -7,6 +7,7 @@ package fr.geonature.sync.sync */ enum class ServerStatus(val httpStatus: Int) { OK(200), + UNAUTHORIZED(401), FORBIDDEN(403), INTERNAL_SERVER_ERROR(500) } diff --git a/sync/src/main/java/fr/geonature/sync/sync/io/AppSettingsJsonWriter.kt b/sync/src/main/java/fr/geonature/sync/sync/io/AppSettingsJsonWriter.kt index 6b524c2c..a7f23449 100644 --- a/sync/src/main/java/fr/geonature/sync/sync/io/AppSettingsJsonWriter.kt +++ b/sync/src/main/java/fr/geonature/sync/sync/io/AppSettingsJsonWriter.kt @@ -57,4 +57,4 @@ class AppSettingsJsonWriter(private val context: Context) { companion object { private val TAG = AppSettingsJsonWriter::class.java.name } -} \ No newline at end of file +} diff --git a/sync/src/main/java/fr/geonature/sync/sync/io/DatasetJsonReader.kt b/sync/src/main/java/fr/geonature/sync/sync/io/DatasetJsonReader.kt index d6d596c0..dab75849 100644 --- a/sync/src/main/java/fr/geonature/sync/sync/io/DatasetJsonReader.kt +++ b/sync/src/main/java/fr/geonature/sync/sync/io/DatasetJsonReader.kt @@ -151,8 +151,9 @@ class DatasetJsonReader { while (reader.hasNext()) { when (reader.nextName()) { - "module_path" -> module = reader.nextString() - .toLowerCase(Locale.ROOT) + "module_path" -> + module = reader.nextString() + .toLowerCase(Locale.ROOT) else -> reader.skipValue() } } diff --git a/sync/src/main/java/fr/geonature/sync/sync/io/TaxonomyJsonReader.kt b/sync/src/main/java/fr/geonature/sync/sync/io/TaxonomyJsonReader.kt index 6eabbf4b..cde31d23 100644 --- a/sync/src/main/java/fr/geonature/sync/sync/io/TaxonomyJsonReader.kt +++ b/sync/src/main/java/fr/geonature/sync/sync/io/TaxonomyJsonReader.kt @@ -116,8 +116,11 @@ class TaxonomyJsonReader { } ?: emptyList() } .filter { it.isNotEmpty() } - .flatMap { it.asSequence().distinct() } - .sortedWith(Comparator { o1, o2 -> + .flatMap { + it.asSequence() + .distinct() + } + .sortedWith { o1, o2 -> val kingdomCompare = o1.kingdom.compareTo(o2.kingdom) if (kingdomCompare != 0) { @@ -139,7 +142,7 @@ class TaxonomyJsonReader { groupCompare } } - }) + } .toList() } diff --git a/sync/src/main/java/fr/geonature/sync/sync/worker/CheckInputsToSynchronizeWorker.kt b/sync/src/main/java/fr/geonature/sync/sync/worker/CheckInputsToSynchronizeWorker.kt index 5f2a4af5..dc540a7d 100644 --- a/sync/src/main/java/fr/geonature/sync/sync/worker/CheckInputsToSynchronizeWorker.kt +++ b/sync/src/main/java/fr/geonature/sync/sync/worker/CheckInputsToSynchronizeWorker.kt @@ -48,9 +48,9 @@ class CheckInputsToSynchronizeWorker( notify( SYNC_NOTIFICATION_ID, NotificationCompat.Builder( - applicationContext, - MainApplication.SYNC_CHANNEL_ID - ) + applicationContext, + MainApplication.SYNC_CHANNEL_ID + ) .setContentTitle(applicationContext.getText(R.string.notification_inputs_to_synchronize_title)) .setContentText( applicationContext.resources.getQuantityString( @@ -90,4 +90,4 @@ class CheckInputsToSynchronizeWorker( 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/worker/DataSyncWorker.kt b/sync/src/main/java/fr/geonature/sync/sync/worker/DataSyncWorker.kt index 81e791f1..cedd9851 100644 --- a/sync/src/main/java/fr/geonature/sync/sync/worker/DataSyncWorker.kt +++ b/sync/src/main/java/fr/geonature/sync/sync/worker/DataSyncWorker.kt @@ -36,7 +36,8 @@ import java.util.Locale * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) */ class DataSyncWorker( - appContext: Context, workerParams: WorkerParameters + appContext: Context, + workerParams: WorkerParameters ) : CoroutineWorker( appContext, workerParams @@ -187,7 +188,8 @@ class DataSyncWorker( } private suspend fun syncInputObservers( - geoNatureServiceClient: GeoNatureAPIClient, menuId: Int + geoNatureServiceClient: GeoNatureAPIClient, + menuId: Int ): Result { return try { val response = geoNatureServiceClient.getUsers(menuId) @@ -396,6 +398,15 @@ class DataSyncWorker( "found ${taxrefAreas.size} taxa with areas from offset $offset" ) + setProgress( + workData( + applicationContext.getString( + R.string.sync_data_taxa_areas, + (offset + taxrefAreas.size) + ) + ) + ) + val taxonAreas = taxrefAreas.asSequence() .filter { taxrefArea -> validTaxaIds.any { it == taxrefArea.taxrefId } } .map { @@ -500,13 +511,15 @@ class DataSyncWorker( .flatMap { it.asSequence() } .filter { it.id > 0 } .map { nomenclature -> - (if (nomenclature.taxref.isEmpty()) arrayOf( - fr.geonature.sync.api.model.NomenclatureTaxonomy( - Taxonomy.ANY, - Taxonomy.ANY + ( + if (nomenclature.taxref.isEmpty()) arrayOf( + fr.geonature.sync.api.model.NomenclatureTaxonomy( + Taxonomy.ANY, + Taxonomy.ANY + ) ) - ) - else nomenclature.taxref.toTypedArray()).asSequence() + else nomenclature.taxref.toTypedArray() + ).asSequence() .map { Taxonomy( it.kingdom, @@ -619,11 +632,11 @@ class DataSyncWorker( private fun checkResponse(response: Response<*>): Result { // not connected - if (response.code() == 403) { + if (response.code() == ServerStatus.UNAUTHORIZED.httpStatus) { return Result.failure( workData( applicationContext.getString(R.string.sync_error_server_not_connected), - ServerStatus.FORBIDDEN + ServerStatus.UNAUTHORIZED ) ) } diff --git a/sync/src/main/java/fr/geonature/sync/sync/worker/DownloadPackageWorker.kt b/sync/src/main/java/fr/geonature/sync/sync/worker/DownloadPackageWorker.kt index 5c9d78b2..0b7ad4da 100644 --- a/sync/src/main/java/fr/geonature/sync/sync/worker/DownloadPackageWorker.kt +++ b/sync/src/main/java/fr/geonature/sync/sync/worker/DownloadPackageWorker.kt @@ -166,4 +166,4 @@ class DownloadPackageWorker( val workName: (packageName: String) -> String = { "download_package_worker:$it" } } -} \ No newline at end of file +} 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 b15b22b7..bf418e6b 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 @@ -2,7 +2,6 @@ package fr.geonature.sync.ui.home import android.Manifest import android.annotation.SuppressLint -import android.app.Activity import android.app.ProgressDialog import android.content.Context import android.content.Intent @@ -18,18 +17,20 @@ import android.view.animation.AnimationUtils.loadAnimation import android.widget.ProgressBar import android.widget.TextView import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresPermission import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.menu.MenuBuilder import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.FileProvider -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.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar import fr.geonature.commons.util.PermissionUtils import fr.geonature.commons.util.observeOnce @@ -43,7 +44,7 @@ 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.FORBIDDEN +import fr.geonature.sync.sync.ServerStatus.UNAUTHORIZED import fr.geonature.sync.ui.login.LoginActivity import fr.geonature.sync.ui.settings.PreferencesActivity import fr.geonature.sync.util.SettingsUtils.getGeoNatureServerUrl @@ -81,6 +82,8 @@ class HomeActivity : AppCompatActivity() { private var appSettings: AppSettings? = null private var isLoggedIn: Boolean = false + private lateinit var startSyncResultLauncher: ActivityResultLauncher + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -97,50 +100,52 @@ class HomeActivity : AppCompatActivity() { packageInfoViewModel = configurePackageInfoViewModel() dataSyncViewModel = configureDataSyncViewModel() - adapter = PackageInfoRecyclerViewAdapter(object : - PackageInfoRecyclerViewAdapter.OnPackageInfoRecyclerViewAdapterListener { - override fun onClick(item: PackageInfo) { - item.launchIntent?.run { - startActivity(this) + adapter = PackageInfoRecyclerViewAdapter( + object : + PackageInfoRecyclerViewAdapter.OnPackageInfoRecyclerViewAdapterListener { + override fun onClick(item: PackageInfo) { + item.launchIntent?.run { + startActivity(this) + } } - } - - override fun onLongClicked( - position: Int, - item: PackageInfo - ) { - // nothing to do... - } - override fun showEmptyTextView(show: Boolean) { - if (emptyTextView?.visibility == View.VISIBLE == show) { - return + override fun onLongClicked( + position: Int, + item: PackageInfo + ) { + // nothing to do... } - if (show) { - emptyTextView?.startAnimation( - loadAnimation( - this@HomeActivity, - android.R.anim.fade_in + override fun showEmptyTextView(show: Boolean) { + if (emptyTextView?.visibility == View.VISIBLE == show) { + return + } + + if (show) { + emptyTextView?.startAnimation( + loadAnimation( + this@HomeActivity, + android.R.anim.fade_in + ) ) - ) - emptyTextView?.visibility = View.VISIBLE - } else { - emptyTextView?.startAnimation( - loadAnimation( - this@HomeActivity, - android.R.anim.fade_out + emptyTextView?.visibility = View.VISIBLE + } else { + emptyTextView?.startAnimation( + loadAnimation( + this@HomeActivity, + android.R.anim.fade_out + ) ) - ) - emptyTextView?.visibility = View.GONE + emptyTextView?.visibility = View.GONE + } } - } - override fun onUpgrade(item: PackageInfo) { - packageInfoViewModel.cancelTasks() - downloadApk(item.packageName) + override fun onUpgrade(item: PackageInfo) { + packageInfoViewModel.cancelTasks() + downloadApk(item.packageName) + } } - }) + ) with(recyclerView as RecyclerView) { layoutManager = LinearLayoutManager(context) @@ -153,8 +158,23 @@ class HomeActivity : AppCompatActivity() { addItemDecoration(dividerItemDecoration) } + startSyncResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + when (result.resultCode) { + RESULT_OK -> { + if (appSettings == null) { + packageInfoViewModel.getAvailableApplications() + } else { + appSettings?.run { + startSync(this) + } + } + } + } + } + checkNetwork() - checkSelfPermissions() + checkPermissions() } override fun onResume() { @@ -181,6 +201,9 @@ class HomeActivity : AppCompatActivity() { override fun onPrepareOptionsMenu(menu: Menu?): Boolean { menu?.run { + findItem(R.id.menu_sync_refresh)?.also { + it.isEnabled = appSettings != null && !dataSyncViewModel.isSyncRunning + } findItem(R.id.menu_login)?.also { it.isEnabled = appSettings != null it.isVisible = !isLoggedIn @@ -197,105 +220,71 @@ class HomeActivity : AppCompatActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.menu_settings -> { - startActivityForResult( - PreferencesActivity.newIntent(this), - REQUEST_CODE_SYNC - ) + startSyncResultLauncher.launch(PreferencesActivity.newIntent(this)) + true + } + R.id.menu_sync_refresh -> { + appSettings?.run { + startSync( + this, + true + ) + } true } R.id.menu_login -> { - startActivityForResult( - LoginActivity.newIntent(this), - REQUEST_CODE_SYNC - ) + startSyncResultLauncher.launch(LoginActivity.newIntent(this)) true } R.id.menu_logout -> { authLoginViewModel.logout() - .observe(this, - Observer { + .observe( + this, + { Toast.makeText( this, R.string.toast_logout_success, Toast.LENGTH_SHORT ) .show() - }) + } + ) true } else -> super.onOptionsItemSelected(item) } } - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - when (requestCode) { - REQUEST_STORAGE_PERMISSIONS -> { - val requestPermissionsResult = PermissionUtils.checkPermissions(grantResults) - - if (requestPermissionsResult) { - makeSnackbar(getString(R.string.snackbar_permission_external_storage_available))?.show() - loadAppSettingsAndStartSync() - packageInfoViewModel.getAvailableApplications() - } else { - makeSnackbar(getString(R.string.snackbar_permissions_not_granted))?.show() - } - } - else -> super.onRequestPermissionsResult( - requestCode, - permissions, - grantResults - ) - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult( - requestCode, - resultCode, - data - ) - - if ((resultCode != Activity.RESULT_OK)) { - return - } - - if (requestCode == REQUEST_CODE_SYNC) { - if (appSettings == null) { - packageInfoViewModel.getAvailableApplications() - } else { - appSettings?.run { - startSync(this) - } - } - } - } - private fun configureAppSettingsViewModel(): AppSettingsViewModel { - return ViewModelProvider(this, + return ViewModelProvider( + this, fr.geonature.commons.settings.AppSettingsViewModel.Factory { AppSettingsViewModel(application) - }).get(AppSettingsViewModel::class.java) + } + ).get(AppSettingsViewModel::class.java) } private fun configureAuthLoginViewModel(): AuthLoginViewModel { - return ViewModelProvider(this, - AuthLoginViewModel.Factory { AuthLoginViewModel(application) }).get(AuthLoginViewModel::class.java) + return ViewModelProvider( + this, + AuthLoginViewModel.Factory { AuthLoginViewModel(application) } + ).get(AuthLoginViewModel::class.java) .also { vm -> - vm.isLoggedIn.observe(this@HomeActivity, - Observer { + vm.isLoggedIn.observe( + this@HomeActivity, + { this@HomeActivity.isLoggedIn = it invalidateOptionsMenu() - }) + } + ) } } private fun configurePackageInfoViewModel(): PackageInfoViewModel { - return ViewModelProvider(this, - PackageInfoViewModel.Factory { PackageInfoViewModel(application) }).get( + return ViewModelProvider( + this, + PackageInfoViewModel.Factory { PackageInfoViewModel(application) } + ).get( PackageInfoViewModel::class.java ) .also { vm -> @@ -309,51 +298,57 @@ class HomeActivity : AppCompatActivity() { "reloading settings after update..." ) - loadAppSettingsAndStartSync(true) + loadAppSettingsAndStartSync() } - vm.packageInfos.observe(this@HomeActivity, - Observer { + vm.packageInfos.observe( + this@HomeActivity, + { progressBar?.visibility = View.GONE adapter.setItems(it) - }) + } + ) } } private fun configureDataSyncViewModel(): DataSyncViewModel { - return ViewModelProvider(this, - DataSyncViewModel.Factory { DataSyncViewModel(application) }).get(DataSyncViewModel::class.java) + return ViewModelProvider( + this, + DataSyncViewModel.Factory { DataSyncViewModel(application) } + ).get(DataSyncViewModel::class.java) .also { vm -> - vm.lastSynchronizedDate.observe(this@HomeActivity, - Observer { + vm.lastSynchronizedDate.observe( + this@HomeActivity, + { dataSyncView?.setLastSynchronizedDate(it) - }) + } + ) } } - private fun checkSelfPermissions() { - PermissionUtils.checkSelfPermissions( - this@HomeActivity, - object : PermissionUtils.OnCheckSelfPermissionListener { - override fun onPermissionsGranted() { - loadAppSettingsAndStartSync() + private fun checkPermissions() { + PermissionUtils.requestPermissions( + this, + listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), + { result -> + if (result.values.all { it }) { packageInfoViewModel.getAvailableApplications() - } - - override fun onRequestPermissions(vararg permissions: String) { - homeContent?.also { - PermissionUtils.requestPermissions( - this@HomeActivity, - it, - R.string.snackbar_permission_external_storage_rationale, - REQUEST_STORAGE_PERMISSIONS, - *permissions - ) - } + } else { + Toast.makeText( + this, + R.string.snackbar_permissions_not_granted, + Toast.LENGTH_LONG + ) + .show() } }, - Manifest.permission.WRITE_EXTERNAL_STORAGE - ) + { callback -> + makeSnackbar( + getString(R.string.snackbar_permission_external_storage_rationale), + BaseTransientBottomBar.LENGTH_INDEFINITE + )?.setAction(android.R.string.ok) { callback() } + ?.show() + }) } @RequiresPermission(Manifest.permission.CHANGE_NETWORK_STATE) @@ -366,8 +361,9 @@ class HomeActivity : AppCompatActivity() { return } - connectivityManager.requestNetwork(NetworkRequest.Builder() - .build(), + connectivityManager.requestNetwork( + NetworkRequest.Builder() + .build(), object : ConnectivityManager.NetworkCallback() { @@ -382,7 +378,7 @@ class HomeActivity : AppCompatActivity() { ) } - private fun loadAppSettingsAndStartSync(updated: Boolean = false) { + private fun loadAppSettingsAndStartSync() { appSettingsViewModel.loadAppSettings() .observeOnce(this@HomeActivity) { if (it == null) { @@ -396,32 +392,24 @@ class HomeActivity : AppCompatActivity() { progressBar?.visibility = View.GONE if (!checkGeoNatureSettings()) { - startActivityForResult( - PreferencesActivity.newIntent(this), - REQUEST_CODE_SYNC - ) + startSyncResultLauncher.launch(PreferencesActivity.newIntent(this)) return@observeOnce } } else { - if (updated) { - makeSnackbar( - getString( - R.string.snackbar_settings_updated, - appSettingsViewModel.getAppSettingsFilename() - ) - )?.show() - } + makeSnackbar( + getString( + R.string.snackbar_settings_updated, + appSettingsViewModel.getAppSettingsFilename() + ) + )?.show() appSettings = it mergeAppSettingsWithSharedPreferences(it) invalidateOptionsMenu() if (!checkGeoNatureSettings()) { - startActivityForResult( - PreferencesActivity.newIntent(this), - REQUEST_CODE_SYNC - ) + startSyncResultLauncher.launch(PreferencesActivity.newIntent(this)) return@observeOnce } @@ -435,30 +423,38 @@ class HomeActivity : AppCompatActivity() { return GeoNatureAPIClient.instance(this) != null } - private fun startSync(appSettings: AppSettings) { + private fun startSync(appSettings: AppSettings, forceRefresh: Boolean = false) { GlobalScope.launch(Main) { - progressBar?.visibility = View.VISIBLE - - delay(500) + if (!forceRefresh) { + progressBar?.visibility = View.VISIBLE + delay(500) + } - dataSyncViewModel.startSync(appSettings) - .observeUntil(this@HomeActivity, + dataSyncViewModel.startSync( + appSettings, + forceRefresh + ) + .observeUntil( + this@HomeActivity, { it?.state in arrayListOf( WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED ) - }) { + } + ) { it?.run { + invalidateOptionsMenu() + dataSyncView?.setState(if (it.syncMessage.isNullOrBlank()) WorkInfo.State.ENQUEUED else it.state) if (!it.syncMessage.isNullOrBlank()) { dataSyncView?.setMessage(it.syncMessage) } - if (it.serverStatus == FORBIDDEN) { - Log.d( + if (it.serverStatus == UNAUTHORIZED) { + Log.i( TAG, "not connected, redirect to LoginActivity" ) @@ -470,10 +466,7 @@ class HomeActivity : AppCompatActivity() { ) .show() - startActivityForResult( - LoginActivity.newIntent(this@HomeActivity), - REQUEST_CODE_SYNC - ) + startSyncResultLauncher.launch(LoginActivity.newIntent(this@HomeActivity)) } } } @@ -484,13 +477,16 @@ class HomeActivity : AppCompatActivity() { } } - private fun makeSnackbar(text: CharSequence): Snackbar? { + private fun makeSnackbar( + text: CharSequence, + @BaseTransientBottomBar.Duration duration: Int = Snackbar.LENGTH_LONG + ): Snackbar? { val view = homeContent ?: return null return Snackbar.make( view, text, - Snackbar.LENGTH_LONG + duration ) } @@ -558,7 +554,8 @@ class HomeActivity : AppCompatActivity() { WorkInfo.State.FAILED, WorkInfo.State.CANCELLED ) - }) { + } + ) { it?.run { when (state) { WorkInfo.State.FAILED -> progressDialog?.dismiss() @@ -595,8 +592,5 @@ class HomeActivity : AppCompatActivity() { companion object { private val TAG = HomeActivity::class.java.name - - private const val REQUEST_STORAGE_PERMISSIONS = 0 - private const val REQUEST_CODE_SYNC = 0 } } 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 83784e6f..20a936df 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 @@ -5,6 +5,7 @@ import android.widget.Button import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView +import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.drawable.DrawableCompat import androidx.work.WorkInfo @@ -51,10 +52,10 @@ class PackageInfoRecyclerViewAdapter(private val listener: OnPackageInfoRecycler oldItemPosition: Int, newItemPosition: Int ): Boolean { - return oldItems[oldItemPosition] == newItems[newItemPosition] - && oldItems[oldItemPosition].apkUrl == newItems[newItemPosition].apkUrl - && oldItems[oldItemPosition].state == newItems[newItemPosition].state - && oldItems[oldItemPosition].inputs == newItems[newItemPosition].inputs + return oldItems[oldItemPosition] == newItems[newItemPosition] && + oldItems[oldItemPosition].apkUrl == newItems[newItemPosition].apkUrl && + oldItems[oldItemPosition].state == newItems[newItemPosition].state && + oldItems[oldItemPosition].inputs == newItems[newItemPosition].inputs } inner class ViewHolder(itemView: View) : @@ -90,7 +91,12 @@ class PackageInfoRecyclerViewAdapter(private val listener: OnPackageInfoRecycler } with(icon) { - setImageDrawable(item.icon ?: itemView.context.getDrawable(R.drawable.ic_upgrade)) + setImageDrawable( + item.icon ?: ContextCompat.getDrawable( + itemView.context, + R.drawable.ic_upgrade + ) + ) if (item.icon == null) { DrawableCompat.setTint( diff --git a/sync/src/main/java/fr/geonature/sync/ui/login/LoginActivity.kt b/sync/src/main/java/fr/geonature/sync/ui/login/LoginActivity.kt index 422aee0c..82a30682 100644 --- a/sync/src/main/java/fr/geonature/sync/ui/login/LoginActivity.kt +++ b/sync/src/main/java/fr/geonature/sync/ui/login/LoginActivity.kt @@ -41,10 +41,13 @@ class LoginActivity : AppCompatActivity() { setContentView(R.layout.activity_login) - authLoginViewModel = ViewModelProvider(this, - AuthLoginViewModel.Factory { AuthLoginViewModel(application) }).get(AuthLoginViewModel::class.java) + authLoginViewModel = ViewModelProvider( + this, + AuthLoginViewModel.Factory { AuthLoginViewModel(application) } + ).get(AuthLoginViewModel::class.java) .apply { - loginFormState.observe(this@LoginActivity, + loginFormState.observe( + this@LoginActivity, Observer { val loginState = it ?: return@Observer @@ -55,9 +58,11 @@ class LoginActivity : AppCompatActivity() { if (loginState.usernameError == null) null else getString(loginState.usernameError) editTextPassword?.error = if (loginState.passwordError == null) null else getString(loginState.passwordError) - }) + } + ) - loginResult.observe(this@LoginActivity, + loginResult.observe( + this@LoginActivity, Observer { val loginResult = it ?: return@Observer @@ -77,7 +82,8 @@ class LoginActivity : AppCompatActivity() { // Complete and destroy login activity once successful setResult(Activity.RESULT_OK) finish() - }) + } + ) } content = findViewById(R.id.content) @@ -141,12 +147,14 @@ class LoginActivity : AppCompatActivity() { } private fun loadAppSettings() { - ViewModelProvider(this, + ViewModelProvider( + this, fr.geonature.commons.settings.AppSettingsViewModel.Factory { AppSettingsViewModel( application ) - }).get(AppSettingsViewModel::class.java) + } + ).get(AppSettingsViewModel::class.java) .also { vm -> vm.loadAppSettings() .observeOnce(this) { @@ -156,21 +164,23 @@ class LoginActivity : AppCompatActivity() { R.string.snackbar_settings_not_found, vm.getAppSettingsFilename() ) - )?.addCallback(object : - BaseTransientBottomBar.BaseCallback() { - override fun onDismissed( - transientBottomBar: Snackbar?, - event: Int - ) { - super.onDismissed( - transientBottomBar, - event - ) - - setResult(Activity.RESULT_CANCELED) - finish() + )?.addCallback( + object : + BaseTransientBottomBar.BaseCallback() { + override fun onDismissed( + transientBottomBar: Snackbar?, + event: Int + ) { + super.onDismissed( + transientBottomBar, + event + ) + + setResult(Activity.RESULT_CANCELED) + finish() + } } - }) + ) ?.show() } else { appSettings = it 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 ddf588aa..c2aa6038 100644 --- a/sync/src/main/java/fr/geonature/sync/util/SettingsUtils.kt +++ b/sync/src/main/java/fr/geonature/sync/util/SettingsUtils.kt @@ -105,13 +105,13 @@ object SettingsUtils { } } - preferenceScreen.findPreference(context.getString(R.string.preference_category_about_app_version_key)) - ?.summary = context.getString( - R.string.app_version, - BuildConfig.VERSION_NAME, - BuildConfig.VERSION_CODE, - DateFormat.getDateTimeInstance() - .format(Date(BuildConfig.BUILD_DATE.toLong())) - ) + preferenceScreen.findPreference(context.getString(R.string.preference_category_about_app_version_key))?.summary = + context.getString( + R.string.app_version, + BuildConfig.VERSION_NAME, + BuildConfig.VERSION_CODE, + DateFormat.getDateTimeInstance() + .format(Date(BuildConfig.BUILD_DATE.toLong())) + ) } } diff --git a/sync/src/main/res/drawable/ic_action_refresh.xml b/sync/src/main/res/drawable/ic_action_refresh.xml new file mode 100644 index 00000000..5dc5ea09 --- /dev/null +++ b/sync/src/main/res/drawable/ic_action_refresh.xml @@ -0,0 +1,12 @@ + + + diff --git a/sync/src/main/res/layout/activity_login.xml b/sync/src/main/res/layout/activity_login.xml index f7e2ab80..5edb385d 100644 --- a/sync/src/main/res/layout/activity_login.xml +++ b/sync/src/main/res/layout/activity_login.xml @@ -1,5 +1,6 @@ - + app:srcCompat="@drawable/ic_person" + app:tint="@android:color/background_light" + tools:ignore="ContentDescription" /> + tools:text="@tools:sample/lorem/random" /> + + Authentification Paramètres + Synchroniser Se connecter Se déconnecter @@ -19,6 +20,7 @@ Synchronisation des jeux de données : %1$d Synchronisation des observateurs : %1$d Synchronisation des taxons : %1$d + Synchronisation des unités géographiques : %1$d Synchronisation des types de nomenclature : %1$d Synchronisation de la nomenclature : %1$d Synchronisation des valeurs par défaut de la nomenclature : %1$d @@ -68,8 +70,6 @@ Erreur lors du chargement des paramètres \'%1$s\' Paramètres \'%1$s\' à jour Les permissions n\'ont pas été accordées - Les permissions ont été accordées L\'application requiert la permission d\'accéder au contenu de la mémoire de stockage - L\'accès à la mémoire de stockage a été accordée diff --git a/sync/src/main/res/values/colors.xml b/sync/src/main/res/values/colors.xml index a5208abc..85dd9a8e 100644 --- a/sync/src/main/res/values/colors.xml +++ b/sync/src/main/res/values/colors.xml @@ -9,7 +9,7 @@ #40000000 @android:color/white - #80ffffff + #40FFFFFF #64DD17 #D50000 diff --git a/sync/src/main/res/values/dimens.xml b/sync/src/main/res/values/dimens.xml index 91503b8e..7113a493 100644 --- a/sync/src/main/res/values/dimens.xml +++ b/sync/src/main/res/values/dimens.xml @@ -2,6 +2,6 @@ 16dp 8dp - 2dp + 4dp 4dp diff --git a/sync/src/main/res/values/prefs.xml b/sync/src/main/res/values/prefs.xml index 63f1b519..92846177 100644 --- a/sync/src/main/res/values/prefs.xml +++ b/sync/src/main/res/values/prefs.xml @@ -9,7 +9,6 @@ https://demo.geonature.fr/taxhub server_taxhub_url Storage - storage Internal storage storage_internal External storage diff --git a/sync/src/main/res/values/strings.xml b/sync/src/main/res/values/strings.xml index 4d4c209a..6b6c533c 100644 --- a/sync/src/main/res/values/strings.xml +++ b/sync/src/main/res/values/strings.xml @@ -8,6 +8,7 @@ Sign in Settings + Start sync Sign in Sign out @@ -22,6 +23,7 @@ Synchronize dataset: %1$d Synchronize observers: %1$d Synchronize taxa: %1$d + Synchronize taxa by area: %1$d Synchronize nomenclature type: %1$d Synchronize nomenclature: %1$d Synchronize nomenclature default values: %1$d @@ -71,8 +73,6 @@ Unable to load settings \'%1$s\' Settings \'%1$s\' updated Permissions were not granted - Permissions were been granted External storage permission is needed - External storage Permissions have been granted diff --git a/sync/src/main/res/values/styles.xml b/sync/src/main/res/values/styles.xml index 25818d31..0b87d6a9 100644 --- a/sync/src/main/res/values/styles.xml +++ b/sync/src/main/res/values/styles.xml @@ -30,14 +30,4 @@ @color/primary_dark - - -