From c33b7b24f20308e12313fb5c13a798a1e67de8b9 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Mon, 18 May 2020 18:10:30 +0200 Subject: [PATCH 01/19] docs: synchronization workflow, app settings --- docs/settings.adoc | 53 ++++++++++++++++++++++++ docs/sync.adoc | 100 ++++++++++++++++++++++++++++++--------------- 2 files changed, 119 insertions(+), 34 deletions(-) create mode 100644 docs/settings.adoc diff --git a/docs/settings.adoc b/docs/settings.adoc new file mode 100644 index 00000000..234a6f70 --- /dev/null +++ b/docs/settings.adoc @@ -0,0 +1,53 @@ += App settings + +*Example:* + +[source,json] +---- +{ + "geonature_url": "http://demo.geonature/geonature", + "taxhub_url": "http://demo.geonature/taxhub", + "uh_application_id": 3, + "observers_list_id": 1, + "taxa_list_id": 100 +} +---- + +.Parameters description +|=== +| Parameter | UI | Description | Default value + +| `geonature_url` +| ☑ +| GeoNature URL +| + +| `taxhub_url` +| ☑ +| TaxHub URL +| + +| `uh_application_id` +| ☐ +| GeoNature application ID +| + +| `observers_list_id` +| ☐ +| GeoNature selected users menu ID +| + +| `taxa_list_id` +| ☐ +| GeoNature selected taxa list ID +| + +| `page_size` +| ☐ +| Default page size while fetching paginated values +| 1000 + +| `page_max_retry` +| ☐ +| Max attempt to fetch data according to given page size +| 20 diff --git a/docs/sync.adoc b/docs/sync.adoc index c0140de0..1cd62bb0 100644 --- a/docs/sync.adoc +++ b/docs/sync.adoc @@ -1,8 +1,8 @@ = Synchronization workflow -== Update local database from GeoNature +== Check for update -[plantuml, images/sync_data, svg] +[plantuml, images/sync_update, svg] .... participant "mobile/sync" as sync << mobile >> participant "GeoNature" as gn @@ -10,44 +10,75 @@ participant "GeoNature" as gn activate sync group Fetch common configuration data - sync -> gn ++ : **GET** : ""/sync"" - gn -> sync -- : **200** : ""settings"" - sync -> sync : update //settings.json// + sync -> gn ++ : **GET** : ""/api/gn_commons/t_mobile_apps"" + gn -> sync -- : **200** : ""array"" end -group Fetch common data - sync -> gn ++ : **GET** : ""/observers"" - gn -> sync -- : **200** : ""[observer]"" - sync -> sync : update //observer// table - sync -> gn ++ : **GET** : ""/taxa"" - gn -> sync -- : **200** : ""[taxon]"" - sync -> sync : update //taxon// table +group Check for update + sync -> sync : update //settings_sync.json// + + alt A new version is available + sync -> sync : notify if we want to upgrade + end end -... +.... -group Fetch data for each module +== Update local database from GeoNature - sync -> sync: fetch registered modules - note left : read from\n""settings.json"" +[plantuml, images/sync_data, svg] +.... +participant "mobile/sync" as sync << mobile >> +participant "GeoNature" as gn - loop for each registered module - sync -> gn ++ : **GET** : ""/module/sync"" - gn -> sync -- : **200** : ""settings"" - sync -> sync : update //module/settings.json// - sync -> sync : read registered endpoints - - loop for each registered endpoint - sync -> gn ++ : **GET** : ""/module/xxx"" - gn -> sync -- : **200** : ""[xxx]"" - sync -> sync : update //xxx// table - end +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// +end + +alt Check for login + sync -> sync : Set login and password + sync -> gn ++ : **POST** : ""/api/auth/login"" + gn -> sync -- : **200** : ""Cookie"" +end + +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 deactivate sync + .... == Synchronize local inputs @@ -59,16 +90,17 @@ participant "GeoNature" as gn activate sync -group Fetch exported inputs for each module +group Fetch exported inputs from installed app - sync -> sync: fetch registered modules - note left : read from\n""settings.json"" + sync -> sync: fetch installed apps + note left : from Android ""PackageManager"" - loop for each registered module - sync -> sync : read exported inputs matching module + loop for each app + sync -> sync : read exported inputs loop for each input - sync -> gn ++ : **POST** : ""/module/releve"" + sync -> sync : get module name from input + sync -> gn ++ : **POST** : ""api/:module/releve"" note left **input:** { @@ -77,7 +109,7 @@ group Fetch exported inputs for each module ... } end note - gn -> sync -- : **201** + gn -> sync -- : **200** sync -> sync : delete input end From 089baacb121aaeee811a56865e66980eced4aa23 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Mon, 18 May 2020 19:56:06 +0200 Subject: [PATCH 02/19] docs: Postman collection --- .../gn_mobile_core.postman_collection.json | 431 ++++++++++++++++++ 1 file changed, 431 insertions(+) create mode 100644 docs/postman/gn_mobile_core.postman_collection.json diff --git a/docs/postman/gn_mobile_core.postman_collection.json b/docs/postman/gn_mobile_core.postman_collection.json new file mode 100644 index 00000000..424cdc58 --- /dev/null +++ b/docs/postman/gn_mobile_core.postman_collection.json @@ -0,0 +1,431 @@ +{ + "info": { + "_postman_id": "e359d92f-c48a-4f00-a218-8863a7830fd7", + "name": "gn_mobile_core", + "description": "Data synchronization endpoints from GeoNature.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "/api/auth/login", + "event": [ + { + "listen": "test", + "script": { + "id": "64406aee-d4be-4aea-ae28-e4b9ca272b10", + "exec": [ + "var cookie = postman.getResponseHeader(\"Set-Cookie\")", + "pm.globals.set(\"cookie\", cookie.split(\";\")[0]);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json;charset=UTF-8", + "type": "text" + }, + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"login\": \"{{login}}\",\n \"password\": \"{{password}}\",\n \"id_application\": {{application_id}}\n}", + "options": { + "raw": {} + } + }, + "url": { + "raw": "{{geoNatureServerUrl}}/api/auth/login", + "host": [ + "{{geoNatureServerUrl}}" + ], + "path": [ + "api", + "auth", + "login" + ] + } + }, + "response": [] + }, + { + "name": "/api/occtax/releve", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Cookie", + "value": "{{cookie}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"id\": 137190440,\n \"module\": \"occtax\",\n \"geometry\": {\n \"type\": \"Point\",\n \"coordinates\": [\n -1.5386237274626353,\n 47.222886771502594\n ]\n },\n \"properties\": {\n \"meta_device_entry\": \"mobile\",\n \"date_min\": \"2020-05-06T19:27:20Z\",\n \"date_max\": \"2020-05-06T19:27:20Z\",\n \"id_dataset\": 2,\n \"id_digitiser\": 4,\n \"observers\": [\n 4\n ],\n \"comment\": null,\n \"default\": {\n \"technique_obs\": {\n \"label\": \"Non renseigné\",\n \"value\": 316\n },\n \"typ_grp\": {\n \"label\": \"NSP\",\n \"value\": 132\n }\n },\n \"id_nomenclature_obs_technique\": 316,\n \"id_nomenclature_grp_typ\": 132,\n \"t_occurrences_occtax\": [\n {\n \"cd_nom\": 531330,\n \"nom_cite\": \"Abarenicola claparedi\",\n \"regne\": \"Animalia\",\n \"group2_inpn\": \"Annélides\",\n \"properties\": {\n \"meth_obs\": {\n \"label\": \"Vu\",\n \"value\": 41\n },\n \"eta_bio\": {\n \"label\": \"Observé vivant\",\n \"value\": 157\n },\n \"meth_determin\": {\n \"label\": \"Non renseigné\",\n \"value\": 445\n },\n \"statut_bio\": {\n \"label\": \"Non renseigné\",\n \"value\": 29\n },\n \"naturalite\": {\n \"label\": \"Sauvage\",\n \"value\": 160\n },\n \"preuve_exist\": {\n \"label\": \"Inconnu\",\n \"value\": 81\n },\n \"counting\": [\n {\n \"index\": 1,\n \"stade_vie\": {\n \"label\": \"Indéterminé\",\n \"value\": 2\n },\n \"sexe\": {\n \"label\": \"Non renseigné\",\n \"value\": 171\n },\n \"obj_denbr\": {\n \"label\": \"Individu\",\n \"value\": 146\n },\n \"typ_denbr\": {\n \"label\": \"Ne sait pas\",\n \"value\": 94\n },\n \"min\": 1,\n \"max\": 1\n }\n ]\n },\n \"id_nomenclature_obs_meth\": 41,\n \"id_nomenclature_bio_condition\": 157,\n \"id_nomenclature_determination_method\": 445,\n \"id_nomenclature_bio_status\": 29,\n \"id_nomenclature_naturalness\": 160,\n \"id_nomenclature_exist_proof\": 81,\n \"cor_counting_occtax\": [\n {\n \"id_nomenclature_life_stage\": 2,\n \"id_nomenclature_sex\": 171,\n \"id_nomenclature_obj_count\": 146,\n \"id_nomenclature_type_count\": 94,\n \"count_min\": 1,\n \"count_max\": 1\n }\n ]\n }\n ]\n }\n}" + }, + "url": { + "raw": "{{geoNatureServerUrl}}/api/occtax/releve", + "host": [ + "{{geoNatureServerUrl}}" + ], + "path": [ + "api", + "occtax", + "releve" + ] + } + }, + "response": [] + }, + { + "name": "/api/gn_commons/t_mobile_apps", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json;charset=UTF-8", + "type": "text" + }, + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{geoNatureServerUrl}}/api/gn_commons/t_mobile_apps", + "host": [ + "{{geoNatureServerUrl}}" + ], + "path": [ + "api", + "gn_commons", + "t_mobile_apps" + ] + } + }, + "response": [] + }, + { + "name": "/api/meta/datasets", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json;charset=UTF-8", + "type": "text" + }, + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{geoNatureServerUrl}}/api/meta/datasets", + "host": [ + "{{geoNatureServerUrl}}" + ], + "path": [ + "api", + "meta", + "datasets" + ] + } + }, + "response": [] + }, + { + "name": "/api/nomenclatures/nomenclatures/taxonomy", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json;charset=UTF-8", + "type": "text" + }, + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{geoNatureServerUrl}}/api/nomenclatures/nomenclatures/taxonomy", + "host": [ + "{{geoNatureServerUrl}}" + ], + "path": [ + "api", + "nomenclatures", + "nomenclatures", + "taxonomy" + ] + } + }, + "response": [] + }, + { + "name": "/api/{{module}}/defaultNomenclatures", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json;charset=UTF-8", + "type": "text" + }, + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{geoNatureServerUrl}}/api/{{module}}/defaultNomenclatures", + "host": [ + "{{geoNatureServerUrl}}" + ], + "path": [ + "api", + "{{module}}", + "defaultNomenclatures" + ] + } + }, + "response": [] + }, + { + "name": "/api/users/menu/{{users_menu_id}}", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json;charset=UTF-8" + }, + { + "key": "Accept", + "type": "text", + "value": "application/json" + } + ], + "url": { + "raw": "{{geoNatureServerUrl}}/api/users/menu/{{users_menu_id}}", + "host": [ + "{{geoNatureServerUrl}}" + ], + "path": [ + "api", + "users", + "menu", + "{{users_menu_id}}" + ] + } + }, + "response": [] + }, + { + "name": "/api/taxref/allnamebylist/{{taxref_list_id}}", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json;charset=UTF-8" + }, + { + "key": "Accept", + "type": "text", + "value": "application/json" + } + ], + "url": { + "raw": "{{taxHubServerUrl}}/api/taxref/allnamebylist/{{taxref_list_id}}?limit=1000&offset=0", + "host": [ + "{{taxHubServerUrl}}" + ], + "path": [ + "api", + "taxref", + "allnamebylist", + "{{taxref_list_id}}" + ], + "query": [ + { + "key": "limit", + "value": "1000" + }, + { + "key": "offset", + "value": "0" + } + ] + } + }, + "response": [] + }, + { + "name": "/api/synthese/color_taxon", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json;charset=UTF-8", + "type": "text" + }, + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{geoNatureServerUrl}}/api/synthese/color_taxon?limit=1000&offset=0", + "host": [ + "{{geoNatureServerUrl}}" + ], + "path": [ + "api", + "synthese", + "color_taxon" + ], + "query": [ + { + "key": "limit", + "value": "1000" + }, + { + "key": "offset", + "value": "0" + } + ] + } + }, + "response": [] + }, + { + "name": "/api/taxref/regnewithgroupe2", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json;charset=UTF-8", + "type": "text" + }, + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{taxHubServerUrl}}/api/taxref/regnewithgroupe2", + "host": [ + "{{taxHubServerUrl}}" + ], + "path": [ + "api", + "taxref", + "regnewithgroupe2" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "id": "9e07829e-3973-4d78-8842-728046ed7d9b", + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "id": "2cd8b10d-2165-4124-8128-2e061c816fea", + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "id": "5058f04f-19e4-49e9-a714-f7d3a1297da1", + "key": "geoNatureServerUrl", + "value": "http://demo.geonature.fr/geonature", + "type": "string" + }, + { + "id": "162df62b-3019-4e1f-a911-1bc9c4ac1ad2", + "key": "taxHubServerUrl", + "value": "http://demo.geonature.fr/taxhub", + "type": "string" + }, + { + "id": "c62697a9-fa55-4e12-8b63-ce26edbb5cf1", + "key": "module", + "value": "occtax", + "type": "string" + }, + { + "id": "48904cdb-4688-4d6a-9db9-3482fd3d1575", + "key": "login", + "value": "admin", + "type": "string" + }, + { + "id": "904db166-dac6-4fa8-b0e1-d025e0da7ce2", + "key": "password", + "value": "admin", + "type": "string" + }, + { + "id": "6c972af3-79d0-48fe-b7af-3bccf877174f", + "key": "application_id", + "value": "3", + "type": "string" + }, + { + "id": "54ca48cf-858b-4282-9d94-ad42d29280e2", + "key": "users_menu_id", + "value": "1", + "type": "string" + }, + { + "id": "1b08a16a-2a84-4072-ad8c-6afc59a4de7e", + "key": "taxref_list_id", + "value": "100", + "type": "string" + } + ], + "protocolProfileBehavior": {} +} \ No newline at end of file From 299a1fc2aa8162ffd24b7be11b42434399c5f03c Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Mon, 18 May 2020 20:29:08 +0200 Subject: [PATCH 03/19] docs: developer guide --- docs/dev.adoc | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/dev.adoc diff --git a/docs/dev.adoc b/docs/dev.adoc new file mode 100644 index 00000000..b4c4e1c1 --- /dev/null +++ b/docs/dev.adoc @@ -0,0 +1,15 @@ += Developer guide + +== Prerequisites + +* http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html[JDK 8] or https://openjdk.java.net/install[OpenJDK 8] +* https://developer.android.com/studio#command-tools[Android SDK] or https://developer.android.com/studio#downloads[Android Studio] + +== Full Build + +A full build can be executed with the following command from project root dir: + +[source,bash] +---- +./gradlew clean assembleDebug +---- \ No newline at end of file From 2881dc6c13a1007a7238e0bc259a9148da3db6c3 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Mon, 18 May 2020 21:22:10 +0200 Subject: [PATCH 04/19] docs: styles and themes --- docs/styles_themes.adoc | 77 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 docs/styles_themes.adoc diff --git a/docs/styles_themes.adoc b/docs/styles_themes.adoc new file mode 100644 index 00000000..a7126abe --- /dev/null +++ b/docs/styles_themes.adoc @@ -0,0 +1,77 @@ += Styles and Themes + +Styles and themes on Android allow you to separate the details of your app design from the UI +structure and behavior, similar to stylesheets in web design. + +A style is a collection of attributes that specify the appearance for a single View. +A style can specify attributes such as font color, font size, background color, and much more. + +A theme is a type of style that's applied to an entire app, activity, or view hierarchy—not just an +individual view. When you apply your style as a theme, every view in the app or activity applies +each style attribute that it supports. Themes can also apply styles to non-view elements, such as +the status bar and window background. + +Styles and themes are declared in a style resource file in `res/values/`, usually named `styles.xml`. + +== Build variants + +Each applications use the https://developer.android.com/studio/build/build-variants[_flavor_] +concept at the build gradle level which allows to easily create variants of these applications. +Currently, the default variant is `pnx` which allows you to apply, among other things, the basic +colors of the theme and the icons of the mobile applications following the main colors of the French +National Park. When building applications, it is therefore necessary to specify the variant to be +used by gradle: + +In `debug` mode: + +[source,bash] +---- +./gradlew clean assemblePnxDebug +---- + +== How to register a new build variant + +You have to modify each `build.gradle` file located in each application directory and add a new +variant to `productFlavors`. For example, if you want to create the named `pnm` we'll get: + +[source,gradle] +---- +android { + ... + productFlavors { + pnx { + } + pne { + } + pnv { + } + pnm { + } + } + ... +} +---- + +When building applications, it will be necessary to specify the variant to be used by gradle. + +In `debug` mode: + +[source,bash] +---- +./gradlew clean assemblePnmDebug +---- + +== Adding a color theme + +Once the buildvariant has been created, you now need to add a new directory with the same name as +this variant in the `src/` directory of each application: + +* `main`: the directory of resources and sources for each application +* `pnm`: The directory for the `pnm` variant. + +Copy the `res/values/colors.xml` from an existing variant (e.g. `pne`) to the directory of the new +variant: + +* `primary`: the primary color of the theme +* `primary_dark`: the dark variant of the theme's main color +* `accent`: accent color \ No newline at end of file From 68f87fed0a7e7fb4f82e89eab0dd3c029fa4666a Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Tue, 19 May 2020 21:50:08 +0200 Subject: [PATCH 05/19] docs: styles and themes, set up a device for development --- .gitignore | 1 - docs/images/asset_studio.png | Bin 0 -> 77800 bytes docs/set_up_device_development.adoc | 87 ++++++++++++++++++++++++++++ docs/settings.adoc | 1 + docs/styles_themes.adoc | 41 ++++++++++++- 5 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 docs/images/asset_studio.png create mode 100644 docs/set_up_device_development.adoc diff --git a/.gitignore b/.gitignore index fde5fd0a..1e712c2d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ /build /captures /docs/.asciidoctor -/docs/images /docs/vendor /docs/*.html .externalNativeBuild diff --git a/docs/images/asset_studio.png b/docs/images/asset_studio.png new file mode 100644 index 0000000000000000000000000000000000000000..d596b24f8221fe29814e423b21e3628a3c8e2915 GIT binary patch literal 77800 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%0AW;nLa_aPqEWf+zP%HS9LjU@9G2iW4 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⋑!S9=0eXO`zw2t)f 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%0DA-=e*K&3PNqk|F9Pw8N%*+!LbI0D#pmS#-e2#@ zU+1^x)!v+o%UG0eJ$K>g8uoc48N0j+8b*=ljav9F^9HVwv|g|oxq^e9#Ll`VtNrdo zFAtBZ>gtzkSBa{#uoTfn8e-DVtg~&ztk(|$v_ost+u5;L&)J%8s@Rg*?!La9fj!E9 zSlAeazgXBK+29xrkPhk89`Vv~oTe&O!!?(+@D(Z>_iwA4hg8Fj`q8}2ty@FFC^C_uZMx|T<{Jsu8U z6EAOTP#rM;R#fDG&iHr44tk1&o6upR%yvN8D6P2ni=WaWb!Obim)63F2iW1B?Ba2l zkoSHY@r)uli}R_brluu>ee%)X>BPeof%&x!;u@HL`uweC7{KWh^dj+iL?z$rzz&PO z02mQl>=qj&alV?IoS-K;r~k0L{_MH0fDiS0K47>)LPU{v@=Cz!Ld&OWZQ>7aj}MQ7 zsA0mk(=Y;Gk8DZJr%!b;M$aaHSn`Ds;7<=1;t4<}=Pxg>>g(wlax{l4fH^DXTHmLv zP7h6GR*Go5SFb;>wb1J}w>O|}@M+4|8@o%cZG#ovgK;Fnf5{csM5GO5(mLBaQv5Sf z09!4Gfl}KNTh`E^m>F<40`?^zUl%u-Bbzi6?c}4cWZa(;cjxlusry=W>6X0~E2zt1|OeD40ZgCPqpH2_f4{;{=LBY3M znAO(6;~vBBbWxuXLBR6+@37&c2SMF-LP`-N;qlFQ#NJ~++!O5?Sb|B1F2?}C%+Z^Y z<*%x$q9ga6yv!5}Zng>~P1KvcOEkC!L!mM*H2z`+h#nHwF&r!u6A^`eSR%2F?7!s@-A*g5TP#oNMot76K&jK9f|f(jJQ;6{NxyGvWGW zlIZ_X0%$8-!L0QXG5!h;BK=HpdafagwhV`JngZVXP5m;>ew>{db-?_HVsUrZe)Z~A zI@}xA`2_4KO-dRB9bmC_@>a*$6|>t=SJ~j&8C+u9aO0dpXuwx_sL}G5@RdY0QIl;Oh{%kiG>+5*%c2C8Z- zxmfdq@81U58q%v+K7JwqEaVsB(tJ|r_2IlkQ469Dds;l7?gZUC68n``$TEH<&rQ!h z<82Rlp1j;f(z`|u{*QF-!FDNi?gQ2tD**O_6HyMHf6a9w~rrhyGMdq zh$!PHPM!HO~TcO-^t*8$>p+zLR*KLkj1fZD-Scx!EP zd%8x_i$L4w!IZSdr4HtA#lKth_ckuYatQ=S<*@tKM_QxzSk}0ff;3svw*Si}E@Su#)IB@pCh2(|3{T4=~1jGExH$4LPEx2~?P{rWjnWhY`ClqS7F zZ+=1qkSz4K%jR{^x0kN)R`~SG=T}b3N=PvFYz)5@kv;YAZR9*~`Ws$#xjU5b4atQa zMvx_NO7B&mS(_mlU8kysP1QzbR1DZf;eZ|BWZ|-dw2{J5d4M5NT`&Ql8 zl*zUL8e@r54X{5_N!N}G=9rh zmL2%@Yw)hT=$%dmwYbo{u1?Xj9zDN857x~?PW7yPANmEBmuFAAD}#%Syp$W{_uu6a zq5WISe|972{ds_WotBn%czB2}VWz+GsT+5m?&Eg`m*#P~^y1b@hIOljt)lQV>0qd) z`g5jxhBxdUVqgjBd-E-U%gf7V&pJEQrm`HatDYr459I8@g4VdU1vtZ~fP{weP{#=F zj*Py0E2z#3<7bRgKBZ>?_W=lXy{g?KXnQ{k3V&0AAAkni+ zfNTqWWg8GZ_V@Sa4p=hX^Q4afuU{kP&D6U5TcO@TS_AMEP`Ybg zj5bX(y#ce>PU~KX`hc+i-pF|7f6O97C2~ng$=FymKsHu3t3POBxfG{-;~w$>MgV{r z6+54)s-OM-r2ctpS$u!-&X-#+b0~^x=9hya55K+th73K30)hLxx@H|3U@@8*>ehCgD3y6D;O#UOy}( zCrFwDPS2|K^UE)%uyhA9yk|C{9N;=Xtdpy2MVjp|a={W8h_j&oHS5B{K9+V#H+9cl zX(D_MnON%$3-9ag?Hw3^9^y3ss7t%o|I~lVg8_w@`MFq#4fXX?)6>Z2Z}%}#gS&f6 zd^PBq29LEl+%LOJ^+7e3c?S$S3u|0I2UII4I z>{*?8@7zWlNYCM%Q~4q*H=)QG16YFeoZMM5p`|ZHicyq@MfxM8XzRo5Ui8tGbZaTi z)~9C~;Kze)SZo`Hw#QTTI75kb0_J1|kg-{JwQ{brpzefuQ5lr%$$QJtmPW#Hrle zT+YRT-d<(vYaMTn7TPXx9#cig06Wp+<;9s9Q+EX^C$i(B|H~;?DdE{QVcN{eYkStQ znLJzNs*n9F+lZHI@41mpw{#4X6?p3T%2dz6p2~GDknQg|&w`@;4>)A_W$wtl>+Tl3 zQ=g5nE3;$%BVqepZ<`+^gCC|k6>MY zG-&zgwXCdcsH~u~kYHE4;iJ(oW-)OI3G|%Sp8WMDD{SWV@ANI7$%czvF@x7m zdEpo*qw(CzG9x0zb1E-NmJ-iLN+f_xGtt&-3~Wq{Q6c1x2v$JruibP}c`kvACZR>ou@PB6M<^q8nePQ;FP~U2tyxIUv>EvEI~|!hc>2SprB%_o zH*K?{rKh?MH^b?ITQ$Ze_{ zU@#cy3=t6`u;B7@Gcr!a$grGy@$%)%7cai%<|_E?FE9L5g@PZ2hJOkT34i#2wcKTo zS=6-Aef{avr$bc^aG6Q@{y2fRhXq zO}ihAeF+SiGAGm3t68`2yScdm9)yjJjgish=6Gr7`ssWHDJf=4BH)uaA6}^p08T~& z%Aw7{pvDLd`PV&2mNz&zcGVg66$M4(RZX$+P7wb1I25r$H(S7;Q< z&e2hY9yvJp)~*?t|AmEu-<*L2@UQM}-@V0lVE;r+oJzX2pn#G1U0Cw3p!fFxLCM)a zs%gd2yfYVq1Z)`uxf`SR&JZsEW#4`S84B*0U^vyEdOLqRdB8Pve<~FH7w-5|w-0#z zJLdv_{`x>mG(`@1NP{yY1RYuwvWac8OjLJ%KrX$+$>2}Y5Se~Kh{QcMh4_9PFpe{2AYYwMqrH30g#|u zcLs@EVyA@J{?jIK91473OHBEu*EndGwrn*Ac(K14&Wk%8my%Z8Y)WlkoX&uYWsvP2 zKwqe7RHflk)+xGC8@Bkh!lLYw`A>BtR|=HG?9Zq`Enb#FZw5!cANaeUSyE77PDqH0 z;Yh@m$D!Bghga&x*C)4+Uq@L;sSx#S4CwZ-RK6YZHv%I!WywN2(T@W(~Xm#Z>6 zS`*7tC)$^kkZ0vucWf#;jYHAv@y84Gg6Ml<#DmQ)3kXgiF`{VzlIie(yBH=0R| z_Ix#cgq0%T(Fu7|&9zycTL&JBYqspA)+V;(>z{=?9g+PrgQs5F&T#CBd@rcnxjw9lP;jL?-LCAu=ek4r6v7|M zCW{&Qj`j!Vg1p`ZJg)hM?G5LS%C*1pS~voZ#mPZBB(PAl{91Yp#BI$kSE&Y1+uShF zs+v-B+_0J?i4GDlFfb=|>9ec? z-md!xop}SMLu(2XHN)z%F`B|mM0&6IX>?iO8nq6?oSUcO%kYXt*&bJjO3rXcjjd2h zjbrR^dcH-Y?9$ilkob|y#8A?y5K@>5xl;k()u7;DzC%|;AfgjF zh@ggTu9|=?>)-!E-KLv1rL;(6cg(CxmWk-q5+DC1IOO>hT1b@wZ46&)4`cSdY#<+Z z*RI*szWzJOzyL{j)=?t@56spXV&G$QSitR$9wjbz_l_b{FV$O2;v0%2 zKquatjC3oIHM8k}P>NeB42Y$T#7&SMWih@x0qTP_#z6C=HqZ)31%6w)_;%c!`o#%jqEN0hK9v-N45co_Jqd#B8j!&?VJ)w+2D0$Hdn+z~5O zkTSi#lGsDC*Pp`pJ78Q8 zPc@(d9ll@jy0z%A1(=5Vn3`?=atA(kn@Ccm%)h#?k_IdRwY$_T)HutgS3)}afKF3O z-gNGj_9~I}Chb=hk&yky#LI`Pc(aFStSm=g< z-)9hsFdxWx;6*|fNf?mt#+;97k8S68* zP>u~|7VB#y7m?Z|VVL9yl7zFCtsH68ws{|O1WV?9G_kdr6E+4sL>L%XW6>m1C`JYa zdiLSF$Hht~P-K-b(fZxoCP+FCiTJ7?#Z5=rCngC&y~}5oG!X+(e1tdU{4BrB!B7x$ zRFOe|)wkPt3*Q_br%?(;CZzvLU)UNH^Nkh`e>L6YnGM%p@PhiftZqX^N&74w!?>DW z-3gF)?nw^a%LF3~9J7kWde0EGQgX#k@@^g?^v6^ib_@2lwn1K3h{$8yV^yw6-x9cn zsu^A@@PF%Cup7=wtHX`16t^#<<<1yu(u{U#z|KuW zpGBaU9a$$hahOhyDd3oL81vB}k8T}iA^}}vHBe7{npZ<5=Q?19l{=2B-`M@wss9W? zE|2PkQnnyxIHrz&_WO5P7lHg-XYs$-BUCn-+z`;!CN#4GW-qS?3h}8Pdu7ofusU>@R)Hro{#stP zDUn#?g#+J|Ias$nb8wU^_kHdPQOwNW$1ZWBKx_sVsx`Csc6=cbT(&(|x6(~J$`FMl zBBm7%$}Dip02nIEEBYl^F?akY7P2~wmCyQxh7ird6e~=#;Omm>;2Bo;s|+BB_-d!V zCa=(y@8tQUx&6W-nB9xg|6X@Biga=&Bf zH6V8CuP1p`;898cyVG!o_y2S8Uk2sB7iWS0e|}^L>HqMQ^njuN?_2jVYvUAunxG5? zZjKH8f3{=Z+FTEi6lacws=E2AvWjks=|S8)xwT5KDW5d-{kSq zhM0ZpiDJK3wj;D8?w!}L4BIJSB?No$qwBz)5PfJi4vvKm~?J6wH8#s#_uqW)K9*B#Z=((Umoa+M|^ zy(u6Ff(S@&qCf;G(gmc0)DU`yC?dTINQX;FkRlP0E<~hC51j;vv``bILnPrH@AuwX z-+OPp@2zkCn03}!XJ*bhQ}*w-_srgq<5Pc!R*v7*+1wC#K-|J1a!Qd+H_5+l%J9Wu z=NHhx5eFVI%LaCh`d#DwsmG^F|EOtNSk~DZvA(lJ(FC|V?7VN6whrY}by=mkGC*jn zwW6hz%tm~$LYr~|(qoNOk zS2AZulH=FEy>t=st{wN4Qs2oXMr%e>wdiG(3*Q=U6r>fClQ7O(gycq~ zT1!cKhd4Ct4pHSq=<8d$>O^~kZgz0eeWusfOFTF}^fwVWWk0rVZzfjHpdX*lULE+* z{28uO08#q3*ik4&C7Gc@9nz_&fM{tB^a^qr9S!*`Fczd{T->kf*D`)jPEuhsKu#Yo zg;mXqcekM7FMU)1(ms%&j2S+nO-VW`n8{hcW9}h-q_}c})6{L}SNI*7#PCrOuPOg-=R1 zwjSGT4c~qfG@)p}HQUZFe<8m4Vx0nY{F`#=uSZ;BW7e&mBQ00f0IQ}F+`DT-|t%!luu5!0geZCsXY3{}IL_447EKjB~LOf`F~62ppikU;EW{+|`Uk zTbJ1qQ@Q!v=O*Z>I(>2Ln~&2)S``#qH}}s3Bu8Sw&Y|=!VYmQ+aRR}uIY12RQL_J_ zlH_3Nr~9ViOWm$qn*F=Af8R zj)0BMv(>9_UhB?cA|cvNP;Bv_SDgA{*2i*x`!kn!(5I-1S1qCtBeLm^$BuI9?E+Pr zR?BzMFSzlFb+@$PBJ#7AMPP~0g<T|Lz~qbxkXC)EnqM#bz8#VjjL^b=jP zU1fmNI|)5`~BkszRr*)zxU=I!{9PY0(BDpl@!jharpx?psP^#~{@01_yZ~d~+FT{Tke;Q(u zct_E>V+q*n2BIU5wlu;5@;;VHY$y>&ilpP73!J>#yq>fF%6TV_-@wyV(pV@Y0oRM) zUS}*{h|s^Ql#;;pr8_r46B^U%s}OPl5VSVu>qx1|&ium z^z^hT`FULOZ@jC@Res0HIs}23EGL7BH`E=gJfY2Id6G;zGstZ?0 z6sOrfE}Oj2I}9;8SS1{eeG@F+*9)}B)|QDOk9z=Ec}XLVNEXZr|Gf_|2%U*}Tx_ePjDmWZj20 z(cuAm{$x5sMicY|=Balz;NPiwVi?P7qexAySA{RV+b~_y2&2pW7$wo%XL5eJwbWBZ zemyBHS?=6VW&-ii-&L73%i%y)wSk?hvq}p;t5C4gE$AIU>SM`ujaD<<02X}7$x9mX z2$_=Hva~v6?YdgyAyl^FXR{jZUIz=8awF=q*JZe)u$@Au41k(o3%MO`rga z)wpG2Yu%ccb;iR>9Oe&P{Gw8v*k}_BsD8zM2)+zAJC@0e8X!^YB^KU3kku?nr8Q|= z(`(J>Q_d%J6JAlyz2p}2`Dv;)x&YsXbjW^aL6%F3>F}lfatSCBH#pG;PvGEy2r9N#GnZC&Zd4Dp3Dt7-Wf9e{Dgee6V)dTa#HH!p6+T~*kRpBA zr9MY%wePO zm=`4a?Fi)`0B}MC9;DFi6Rj}Q`5N=gg6%~bI=waVk zuJ^x|6v?A~^e9}~RW0F5>N7uG@2F7y0Q%(QB)a1Q0DaUpy%!!QD9tM>Nh-;WVnUkK z9nMNk&ZEFd-1B%b=MgTz>x3^Z3K)kQ*4u%7Ni%8tV6>yDV&11#%D6#sJLgBoA2*i0 zWmrx8CWmk@c4m~7m%#xwV2p=(@thwVQG8?OAgk+D^I^DiLnV$JIrX>L$l>(h_5aXD z9rNn7V3U$yf#&(pff;5sLLd_`4nIAdNYc~?CK$E_-hGOWx{PyxwM{rJYz z@5W`JFM8_07ktJ>Q<(;<0l53%fNCyN^o9HQXrHa6)4Q>Rh5wF;{}~_u>+63PH~-`L z{|h(&>-zu;0n)#j1D^^M`ro-%dg-~@f3kw#)L<@TvGRBQU!!<`xAIQ};a^_^-0trd zV#&XY{myMEcQg7QW>yA$FwIK+@j|^+eO&#b)h1QW!(uFBwh=?V&FW?KN9w-nrO#bk zvy9<}AlHMCW4WD@ndSUJq3j* zRqyqp;@j-^1rGP^?9~|$}C&nctjJr)r)Gjh?J~b(mV_LHDww&W9 zu%^WOpS+@`_&RheGLrLLljV0aPD5YkuF{hTnLI?XI2;@2>O+jQLV+P(uo@rcjbmAt#)V$?yc10O-@%({Luul`|kcaFHB5nbW{Zhts(F2`F?Bg z)nK-zC^4^yVgHGNVeG`|*MPJ2c;K&SXDSFzRgPm6TQ#Oql8eH%Zg|F?$18&MQ{FrE z0Do#gg}Ao$aD|7ldBO%ZaCbP7_NU{w6kTBB5Er`8h|H>fzPFA>#T8FwH?4A!`0Zs2 z*tpzOp60VKF>@DdeZSq8Xs{58Qnlf&{F%|OOOAZivQz@~^Y92NL!Zh&eyqogLe{lT zFJy_o?`AnU1^P}q7T_B`*pvLoE-!rs^S!qjcIeM%OH0P}#(e3Q)NeWBSXr2HPJ*{d z*LlXi&TK~lwi86=A<+S4xKw9U6eK0&$FPm%2S13} zJ|?R_P7YRPFJ_MPWPpt6b#?Uf7tEMz8>jEdgRKQ~IolkvoDNd@DmHl)Og(-r-c2KZ z0F4~c(4-{O224TpOh_7oQc>AFr9^f zrg~_QV-o@fzpd8BS8_flWp5YBD!V7E4!Y%pV`XM$brjSjJ{IGt=N^Q|+^_xKbC`ew zb_AfXCqs7d9lVb^Hz)LLf}MdBn1OEyH=ekAhyfP2uS0QU!#7Iz42z61I5yl9MY}bE zUD38SkT@N}O={E%P6%{4Nb9r`p9d4!#nsgkRb}5C z%&iaUb!V~iKBD4d!>tz#Dw>6J->3eZnTW4RBih5{+l%tHE5zcHp1$c?G4-dPTm?~9 z`BoAyb38HPH`#n_W$zayA}6_Tn^GL%YyYgt!YHH?3Dc>1{vCyE5|A~3rS>Z^+qKMn z>+_vKWkKkq%i%(%6- zwt40(?$!UA>FOtGwZfwkMFXNC$96@b@XNg4$xXh5Vlje=MLP1zVz>zx=FhXcC-0P6h00_j7(g#3>F(o4-#AaQg^0WrYq*vh+&5~}+!2-% z;y`j4pE_Z_Hw|?#C=O;2w}ZVyA#V%awLKF1^rI{Czy;~vxe#yU!RWNf6(R-`=Vvj#)vh?lG~24-LCQd-aJZ5Nm^P?Nkv-PwAmFQPlW79 zX`aulo;kN~Mxr}jp~JRPf()3YY5b=hd)URPVph4iKgE(Qt4p9qi)6xU>KY=@%YdHY zw%JV4sY3qGWlO5xvJtl$1@2^4erzf`VKeb^G}Qp6`_|>m+XmfkhKZbh1M3jydx$1L zo#{(j%R!C|eBW5~1$44rG3yYTEjOzWYdddmZ-a;z_GQQ0q=>W)&L^Liow3(GzL%^i zN;7+A{5(6WxS-%Wzu9|6sr^0QPm<$Ce%{}M`G}!!TqF~DBMhIV78P(Y@mWnVMd+MQ za*0Uk=<3@1isBuqj-DTSbaB}zooRU?}6Jrak#L&o&GRGXy5wIGVgoTCKu@@g5N&e;FjCzA~m3` zg|2FzyOt;9X+S|k0z~XX6jo_jRYoO3OGvYNL=^fP%sxL{t5iin-)KN@N{VQgonC|8 znf{g3Y*RgwVnspWX|Me=qXoRBKK?v~yk$Qxa-4v>MuK$g?hvz)Dky9i-llX?KKEEv z6*xZIc^Cndr~zPWQu%y5y!oVC?Nu`~4{@GwouuKLnB-H8=P1ytNcJh86VK)6@OMv! zPWPVqCy;S%Jzd@1uJxPQGe7^VKgG0nA(tAOKy%_eP7wfpk+pJZGQ30yIj1;oJrd?{ zI_pPy!R~C`Imo-{BcklrI{yQWbcy^M@@Cutn`u-^D*y-2^2(s7YbY+%drR<{I51O3 z)*-lm!rLuTmv#|NVuVFsr3)0#RJ?B*yPjo$ck|Fgv6^x4X10yO{kQ%8d4KqdMDG6= zpayEU+1Y@_$4;yt61*2Q{zmwo9lOXf2`H%)c+;=(Kl5Xz>qb5VN|hLV=Kh;ugkA}1 zi^+4u9`ZctZ*TO$sQnKv@efAv|K%tw+u!DhCgs1$k(awVw00EeVv3iTHS#%qrn*xX T0drdYo1kcE=&9E{w2k}^Jh`{5 literal 0 HcmV?d00001 diff --git a/docs/set_up_device_development.adoc b/docs/set_up_device_development.adoc new file mode 100644 index 00000000..d897281d --- /dev/null +++ b/docs/set_up_device_development.adoc @@ -0,0 +1,87 @@ += Set up a device for development + +Before you can start debugging on your device, there are a few things you must do: + +. On the device, open the *Settings* app, select *Developer options*, and then enable +*USB debugging*. ++ +*Note:* If you do not see *Developer options*, follow the instructions to <<_enable_developer_options,enable developer options>>. + +. Set up your system to detect your device. +*Windows*:: +Install a USB driver for Android Debug Bridge (adb). For an installation guide and +links to OEM drivers, follow this https://developer.android.com/studio/run/win-usb.html[link]. +*Mac OS X*:: It just works. Skip this step. +*Debian based Linux*:: +Use `apt install` to install the `android-tools-adb package`. This gives +you a community-maintained default set of `udev` rules for all Android devices. +Make sure that you are in the `plugdev` group. If you see the following error message, adb did not +find you in the `plugdev` group: ++ + error: insufficient permissions for device: udev requires plugdev group membership ++ +Use `id` to see what groups you are in. Use `sudo usermod -aG plugdev $LOGNAME` to add yourself to +the `plugdev` group. + +== Enable developer options + +On Android 4.1 and lower, the *Developer options* screen is available by default. +On Android 4.2 and higher, you must enable this screen as follows: + +. Open the *Settings* app. +. (Only on Android 8.0 or higher) Select *System*. +. Scroll to the bottom and select *About phone*. +. Scroll to the bottom and tap *Build number* 7 times. +. Return to the previous screen to find *Developer options* near the bottom. + +At the top of the *Developer options* screen, you can toggle the options on and off. You probably +want to keep this on. When off, most options are disabled except those that don't require +communication between the device and your development computer. + +Next, you should scroll down a little and enable *USB debugging*. This allows Android Studio and +other SDK tools to recognize your device when connected via USB, so you can use the debugger and +other tools. + +== How to fix "insufficient permissions for device: verify udev rules." + +Check if you are in `plugdev` group and if not, use `sudo usermod -aG plugdev $LOGNAME` to add +yourself to the `plugdev` group. + +Find the bus and device id assigned by the kernel with `lsusb` command: + +[source,bash] +---- +lsusb +... +Bus 003 Device 010: ID 18d1:4ee7 Google Inc. +... +---- + +Create a udev rules file `/etc/udev/rules.d/51-android.rules` as root if it doesn't exist and add +this line: + +[source] +---- +SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0664", GROUP="plugdev" +---- + +where `idVendor` value come from the output of lsusb. From the above example, the right `idVendor` +value is *18d1*. + +Now assign read permissions on the file, reload `udev` and reload the `adb` daemon: + +[source,bash] +---- +sudo chmod a+r /etc/udev/rules.d/51-android.rules +sudo udevadm control --reload-rules +adb kill-server +adb start-server +---- + +You may have to disconnect and connect again your device to the USB port. You should see it by +issuing this command: + +[source,bash] +---- +adb devices +---- \ No newline at end of file diff --git a/docs/settings.adoc b/docs/settings.adoc index 234a6f70..1a1480eb 100644 --- a/docs/settings.adoc +++ b/docs/settings.adoc @@ -51,3 +51,4 @@ | ☐ | Max attempt to fetch data according to given page size | 20 +|=== diff --git a/docs/styles_themes.adoc b/docs/styles_themes.adoc index a7126abe..3301086a 100644 --- a/docs/styles_themes.adoc +++ b/docs/styles_themes.adoc @@ -63,7 +63,7 @@ In `debug` mode: == Adding a color theme -Once the buildvariant has been created, you now need to add a new directory with the same name as +Once the build variant has been created, you now need to add a new directory with the same name as this variant in the `src/` directory of each application: * `main`: the directory of resources and sources for each application @@ -74,4 +74,41 @@ variant: * `primary`: the primary color of the theme * `primary_dark`: the dark variant of the theme's main color -* `accent`: accent color \ No newline at end of file +* `accent`: accent color + +== Application icon + +The `artwork/` directory contains the icons in SVG format (including `*_launcher.svg`). +You can use this to create a new icon that will be used as the launcher of each application. +Ideally, you should stick to the following principles: + +* SVG format +* No margin +* Icon in black only with no background (transparent) + +Then use Asset Studio (from Android Studio) to generate a new set of icons. + +image::images/asset_studio.png[Asset Studio,width=50%,pdfwidth=50%,scaledwidth=50%] + +== Application name + +To change the name of the application, copy the files `res/values/strings.xml` and +`res/values-fr/strings.xml` from the `src/main` directory to the directory of the new +variant, respecting the tree structure. + +Then, we can edit each `strings.xml` file and keep only the node containing the key +`app_name`: + +[source,xml] +---- + + + + My application + + +---- + +Gradle will simply merge the default resources (`src/main/res`) with the resources of the variant +selected during the build. So you don't need to keep everything copied to the variant but just take +the resources we want to replace. \ No newline at end of file From 9035d153e3634930c565545fc0b306fe4179b3f4 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Tue, 19 May 2020 21:55:00 +0200 Subject: [PATCH 06/19] fix: styles --- docs/set_up_device_development.adoc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/set_up_device_development.adoc b/docs/set_up_device_development.adoc index d897281d..19f7df7b 100644 --- a/docs/set_up_device_development.adoc +++ b/docs/set_up_device_development.adoc @@ -5,7 +5,7 @@ Before you can start debugging on your device, there are a few things you must d . On the device, open the *Settings* app, select *Developer options*, and then enable *USB debugging*. + -*Note:* If you do not see *Developer options*, follow the instructions to <<_enable_developer_options,enable developer options>>. +*Note:* If you do not see *Developer options*, follow the instructions to <>. . Set up your system to detect your device. *Windows*:: @@ -23,6 +23,7 @@ find you in the `plugdev` group: Use `id` to see what groups you are in. Use `sudo usermod -aG plugdev $LOGNAME` to add yourself to the `plugdev` group. +[[enable_developer_options]] == Enable developer options On Android 4.1 and lower, the *Developer options* screen is available by default. @@ -42,7 +43,7 @@ Next, you should scroll down a little and enable *USB debugging*. This allows An other SDK tools to recognize your device when connected via USB, so you can use the debugger and other tools. -== How to fix "insufficient permissions for device: verify udev rules." +== How to fix "insufficient permissions for device: verify udev rules." Check if you are in `plugdev` group and if not, use `sudo usermod -aG plugdev $LOGNAME` to add yourself to the `plugdev` group. From 62c713cc16ab61f1b509f8c10876c17659d0b073 Mon Sep 17 00:00:00 2001 From: Camille Monchicourt Date: Wed, 20 May 2020 13:40:18 +0200 Subject: [PATCH 07/19] Minor French typo (#14) * Minor French typo * README Sync - Additional minor details and typo --- sync/README.md | 6 +++--- sync/src/main/res/values-fr/strings.xml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sync/README.md b/sync/README.md index 780a7716..3912c262 100644 --- a/sync/README.md +++ b/sync/README.md @@ -32,15 +32,15 @@ Example: | ------------------- | ------- | ------------------------------------------------------ | ------------- | | `geonature_url` | ☑ | GeoNature URL | | | `taxhub_url` | ☑ | TaxHub URL | | -| `uh_application_id` | ☐ | GeoNature application ID | | -| `observers_list_id` | ☐ | GeoNature selected users menu ID | | +| `uh_application_id` | ☐ | GeoNature application ID in UsersHub | | +| `observers_list_id` | ☐ | GeoNature selected observer list ID in UsersHub | | | `taxa_list_id` | ☐ | GeoNature selected taxa list ID | | | `page_size` | ☐ | Default page size while fetching paginated values | 1000 | | `page_max_retry` | ☐ | Max attempt to fetch data according to given page size | 20 | ## Content Provider -This app expose synchronized data from a GeoNature instance through a content provider. +This app exposes synchronized data from a GeoNature instance through a content provider. The authority of this content provider is `fr.geonature.sync.provider`. ### Exposed content URIs diff --git a/sync/src/main/res/values-fr/strings.xml b/sync/src/main/res/values-fr/strings.xml index df35d891..16ab5432 100644 --- a/sync/src/main/res/values-fr/strings.xml +++ b/sync/src/main/res/values-fr/strings.xml @@ -36,8 +36,8 @@ Aucune application compatible trouvée %1$s (%2$s) - Relevé à synchronizer : %d - Relevés à synchronizer : %d + Relevé à synchroniser : %d + Relevés à synchroniser : %d Installer Mettre à jour From b8cf49320d8fe489ba75223fa24ad921c956ac89 Mon Sep 17 00:00:00 2001 From: Sebastien Grimault Date: Wed, 20 May 2020 13:42:39 +0200 Subject: [PATCH 08/19] docs: merge from sync/README.md --- docs/settings.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/settings.adoc b/docs/settings.adoc index 1a1480eb..dba4d7be 100644 --- a/docs/settings.adoc +++ b/docs/settings.adoc @@ -34,12 +34,12 @@ | `observers_list_id` | ☐ -| GeoNature selected users menu ID +| GeoNature selected users menu ID in UsersHub | | `taxa_list_id` | ☐ -| GeoNature selected taxa list ID +| GeoNature selected taxa list ID in UsersHub | | `page_size` From cfc0a05927ef53c8781b111c192c20afab78ba4a Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sat, 23 May 2020 15:37:52 +0200 Subject: [PATCH 09/19] fix: do not try to update app settings during installation (storage permission from sharedUserId may be not defined for device targeting Android 10 or above) --- .../fr/geonature/sync/sync/worker/DownloadPackageWorker.kt | 3 --- 1 file changed, 3 deletions(-) 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 c08b25a3..e21cfa5a 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 @@ -56,9 +56,6 @@ class DownloadPackageWorker( setProgress(workData(packageInfoToUpdate.packageName)) try { - // update app settings as JSON file - packageInfoManager.updateAppSettings(packageInfoToUpdate) - val response = geoNatureAPIClient.downloadPackage(apkUrl) .awaitResponse() From b5f8cbece2e537e77486ff81e2c67a90f24ca5e1 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sun, 7 Jun 2020 14:35:24 +0200 Subject: [PATCH 10/19] chore: upgrade libraries dependencies --- viewpager/build.gradle | 6 +++--- .../ui/AbstractNavigationHistoryPagerFragmentActivity.kt | 2 +- viewpager/version.properties | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/viewpager/build.gradle b/viewpager/build.gradle index c54fc55b..db5d5f50 100644 --- a/viewpager/build.gradle +++ b/viewpager/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' -version = "0.1.3" +version = "0.1.4" android { compileSdkVersion 29 @@ -42,11 +42,11 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.1" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.4" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0" implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.preference:preference:1.1.0' + implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.13' diff --git a/viewpager/src/main/java/fr/geonature/viewpager/ui/AbstractNavigationHistoryPagerFragmentActivity.kt b/viewpager/src/main/java/fr/geonature/viewpager/ui/AbstractNavigationHistoryPagerFragmentActivity.kt index 0d15ff8a..8c851ceb 100644 --- a/viewpager/src/main/java/fr/geonature/viewpager/ui/AbstractNavigationHistoryPagerFragmentActivity.kt +++ b/viewpager/src/main/java/fr/geonature/viewpager/ui/AbstractNavigationHistoryPagerFragmentActivity.kt @@ -240,7 +240,7 @@ abstract class AbstractNavigationHistoryPagerFragmentActivity : AbstractPagerFra * * @return the number of pages in history for a given page key * - * @see IValidateFragment.resourceTitle + * @see IValidateFragment.getResourceTitle */ fun countPagesInHistory(key: Int): Int { var count = 0 diff --git a/viewpager/version.properties b/viewpager/version.properties index e85cb90e..ebc3823f 100644 --- a/viewpager/version.properties +++ b/viewpager/version.properties @@ -1,2 +1,2 @@ -#Wed Oct 16 21:05:59 CEST 2019 -VERSION_CODE=590 +#Sun Jun 07 13:36:46 CEST 2020 +VERSION_CODE=830 From 491145545b2b322c6547447d045a1f877ed67483 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sun, 7 Jun 2020 14:38:54 +0200 Subject: [PATCH 11/19] feat: StickyHeaderItemDecorator as standalone class, expose --- .../AbstractStickyRecyclerViewAdapter.kt | 251 +----------------- .../ui/adapter/IStickyRecyclerViewAdapter.kt | 56 ++++ .../ui/adapter/StickyHeaderItemDecorator.kt | 228 ++++++++++++++++ 3 files changed, 294 insertions(+), 241 deletions(-) create mode 100644 commons/src/main/java/fr/geonature/commons/ui/adapter/IStickyRecyclerViewAdapter.kt create mode 100644 commons/src/main/java/fr/geonature/commons/ui/adapter/StickyHeaderItemDecorator.kt 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 5168f55d..02e0fc0c 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 @@ -1,9 +1,5 @@ package fr.geonature.commons.ui.adapter -import android.graphics.Canvas -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView /** @@ -13,54 +9,19 @@ import androidx.recyclerview.widget.RecyclerView * * @see https://github.com/shuhart/StickyHeader */ -abstract class AbstractStickyRecyclerViewAdapter : RecyclerView.Adapter() { - - /** - * This method gets called by [StickyHeaderItemDecorator] to fetch the position of the header - * item in the adapter that is used for (represents) item at specified position. - * - * @param itemPosition Adapter's position of the item for which to do the search of the position - * of the header item. - * - * @return int. Position of the header for an item in the adapter or - * [RecyclerView.NO_POSITION] (-1) if an item has no header. - */ - abstract fun getHeaderPositionForItem(itemPosition: Int): Int - - /** - * This method gets called by [StickyHeaderItemDecorator] to setup the header View. - * - * @param holder Holder to bind the data on. - * @param headerPosition Position of the header item in the adapter. - */ - abstract fun onBindHeaderViewHolder(holder: SVH, headerPosition: Int) - - /** - * Called when [StickyHeaderItemDecorator] needs a new [RecyclerView.ViewHolder] to represent a - * sticky header item. - * Those two instances will be cached and used to represent a current top sticky header and the - * moving one. - * - * We can either create a new View manually or inflate it from an XML layout file. - * - * The new ViewHolder will be used to display items of the adapter using - * [onBindHeaderViewHolder]. - * Since it will be re-used to display different items in the data set, it is a good idea to - * cache references to sub views of the View to avoid unnecessary [View.findViewById] calls. - * - * @param parent The ViewGroup to resolve a layout params. - * - * @return A new ViewHolder that holds a View of the given view type. - * - * @see [onBindHeaderViewHolder] - */ - abstract fun onCreateHeaderViewHolder(parent: ViewGroup): SVH +abstract class AbstractStickyRecyclerViewAdapter : + RecyclerView.Adapter(), + IStickyRecyclerViewAdapter { override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { super.onAttachedToRecyclerView(recyclerView) - recyclerView.addItemDecoration(StickyHeaderItemDecorator(this, recyclerView)) + recyclerView.addItemDecoration( + StickyHeaderItemDecorator( + this, + recyclerView + ) + ) } override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { @@ -69,201 +30,9 @@ abstract class AbstractStickyRecyclerViewAdapter) { + if (decorator is StickyHeaderItemDecorator<*>) { recyclerView.removeItemDecoration(decorator) } } } - - /** - * Decorator used for sticky header. - */ - class StickyHeaderItemDecorator( - private val adapter: AbstractStickyRecyclerViewAdapter, - recyclerView: RecyclerView - ) : RecyclerView.ItemDecoration() { - - private var currentStickyPosition = RecyclerView.NO_POSITION - private var currentStickyHolder: SVH? = null - private var lastViewOverlappedByHeader: View? = null - - init { - currentStickyHolder = adapter.onCreateHeaderViewHolder(recyclerView) - fixLayoutSize(recyclerView) - } - - override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { - super.onDrawOver(c, parent, state) - - val layoutManager = parent.layoutManager ?: return - var topChildPosition = RecyclerView.NO_POSITION - - if (layoutManager is LinearLayoutManager) { - topChildPosition = layoutManager.findFirstVisibleItemPosition() - } else { - val topChild = parent.getChildAt(0) - - if (topChild != null) { - topChildPosition = parent.getChildAdapterPosition(topChild) - } - } - - if (topChildPosition == RecyclerView.NO_POSITION) { - return - } - - var viewOverlappedByHeader: View? = - getChildInContact(parent, currentStickyHolder!!.itemView.bottom) - - if (viewOverlappedByHeader == null) { - viewOverlappedByHeader = if (lastViewOverlappedByHeader != null) { - lastViewOverlappedByHeader - } else { - parent.getChildAt(topChildPosition) - } - } - lastViewOverlappedByHeader = viewOverlappedByHeader - - val overlappedByHeaderPosition = - parent.getChildAdapterPosition(viewOverlappedByHeader!!) - val overlappedHeaderPosition: Int - val preOverlappedPosition: Int - - if (overlappedByHeaderPosition > 0) { - preOverlappedPosition = - adapter.getHeaderPositionForItem(overlappedByHeaderPosition - 1) - overlappedHeaderPosition = - adapter.getHeaderPositionForItem(overlappedByHeaderPosition) - } else { - preOverlappedPosition = adapter.getHeaderPositionForItem(topChildPosition) - overlappedHeaderPosition = preOverlappedPosition - } - - if (preOverlappedPosition == RecyclerView.NO_POSITION) { - return - } - - if (preOverlappedPosition != overlappedHeaderPosition && shouldMoveHeader( - viewOverlappedByHeader - ) - ) { - updateStickyHeader(topChildPosition) - moveHeader(c, viewOverlappedByHeader) - } else { - updateStickyHeader(topChildPosition) - drawHeader(c) - } - } - - /** - * Whether the sticky header should move or not. - * - * This method is for avoiding sinking/departing the sticky header into/from top of screen - */ - private fun shouldMoveHeader(viewOverlappedByHeader: View): Boolean { - val dy = viewOverlappedByHeader.top - viewOverlappedByHeader.height - return viewOverlappedByHeader.top >= 0 && dy <= 0 - } - - private fun updateStickyHeader(topChildPosition: Int) { - val currentStickyHolder = currentStickyHolder ?: return - val headerPositionForItem = adapter.getHeaderPositionForItem(topChildPosition) - - if (headerPositionForItem != currentStickyPosition && headerPositionForItem != RecyclerView.NO_POSITION) { - adapter.onBindHeaderViewHolder(currentStickyHolder, headerPositionForItem) - currentStickyPosition = headerPositionForItem - } else if (headerPositionForItem != RecyclerView.NO_POSITION) { - adapter.onBindHeaderViewHolder(currentStickyHolder, headerPositionForItem) - } - } - - private fun drawHeader(c: Canvas) { - c.save() - c.translate(0f, 0f) - currentStickyHolder!!.itemView.draw(c) - c.restore() - } - - private fun moveHeader( - c: Canvas, - nextHeader: View - ) { - c.save() - c.translate(0f, nextHeader.top - nextHeader.height.toFloat()) - currentStickyHolder!!.itemView.draw(c) - c.restore() - } - - private fun getChildInContact( - parent: RecyclerView, - contactPoint: Int - ): View? { - var childInContact: View? = null - - for (i in 0 until parent.childCount) { - val child = parent.getChildAt(i) - - if (child.bottom > contactPoint) { - if (child.top <= contactPoint) { // This child overlaps the contact point - childInContact = child - break - } - } - } - - return childInContact - } - - 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 - ) - } - }) - } - } } \ No newline at end of file diff --git a/commons/src/main/java/fr/geonature/commons/ui/adapter/IStickyRecyclerViewAdapter.kt b/commons/src/main/java/fr/geonature/commons/ui/adapter/IStickyRecyclerViewAdapter.kt new file mode 100644 index 00000000..b9c6440f --- /dev/null +++ b/commons/src/main/java/fr/geonature/commons/ui/adapter/IStickyRecyclerViewAdapter.kt @@ -0,0 +1,56 @@ +package fr.geonature.commons.ui.adapter + +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView + +/** + * Common interface about [RecyclerView.Adapter] with sticky headers. + * + * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) + * + * @see https://github.com/shuhart/StickyHeader + */ +interface IStickyRecyclerViewAdapter { + + /** + * This method gets called by [StickyHeaderItemDecorator] to fetch the position of the header + * item in the adapter that is used for (represents) item at specified position. + * + * @param itemPosition Adapter's position of the item for which to do the search of the position + * of the header item. + * + * @return int. Position of the header for an item in the adapter or + * [RecyclerView.NO_POSITION] (-1) if an item has no header. + */ + fun getHeaderPositionForItem(itemPosition: Int): Int + + /** + * This method gets called by [StickyHeaderItemDecorator] to setup the header View. + * + * @param holder Holder to bind the data on. + * @param headerPosition Position of the header item in the adapter. + */ + fun onBindHeaderViewHolder(holder: SVH, headerPosition: Int) + + /** + * Called when [StickyHeaderItemDecorator] needs a new [RecyclerView.ViewHolder] to represent a + * sticky header item. + * Those two instances will be cached and used to represent a current top sticky header and the + * moving one. + * + * We can either create a new View manually or inflate it from an XML layout file. + * + * The new ViewHolder will be used to display items of the adapter using + * [onBindHeaderViewHolder]. + * Since it will be re-used to display different items in the data set, it is a good idea to + * cache references to sub views of the View to avoid unnecessary [View.findViewById] calls. + * + * @param parent The ViewGroup to resolve a layout params. + * + * @return A new ViewHolder that holds a View of the given view type. + * + * @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 new file mode 100644 index 00000000..3005bcf0 --- /dev/null +++ b/commons/src/main/java/fr/geonature/commons/ui/adapter/StickyHeaderItemDecorator.kt @@ -0,0 +1,228 @@ +package fr.geonature.commons.ui.adapter + +import android.graphics.Canvas +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +/** + * Decorator used for sticky header. + * + * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) + * + * @see https://github.com/shuhart/StickyHeader + */ +class StickyHeaderItemDecorator( + private val adapter: IStickyRecyclerViewAdapter, + recyclerView: RecyclerView +) : RecyclerView.ItemDecoration() { + + private var currentStickyPosition = RecyclerView.NO_POSITION + private var currentStickyHolder: SVH? = null + private var lastViewOverlappedByHeader: View? = null + + init { + currentStickyHolder = adapter.onCreateHeaderViewHolder(recyclerView) + fixLayoutSize(recyclerView) + } + + override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + super.onDrawOver( + c, + parent, + state + ) + + val layoutManager = parent.layoutManager ?: return + var topChildPosition = RecyclerView.NO_POSITION + + if (layoutManager is LinearLayoutManager) { + topChildPosition = layoutManager.findFirstVisibleItemPosition() + } else { + val topChild = parent.getChildAt(0) + + if (topChild != null) { + topChildPosition = parent.getChildAdapterPosition(topChild) + } + } + + if (topChildPosition == RecyclerView.NO_POSITION) { + return + } + + var viewOverlappedByHeader: View? = + getChildInContact( + parent, + currentStickyHolder!!.itemView.bottom + ) + + if (viewOverlappedByHeader == null) { + viewOverlappedByHeader = if (lastViewOverlappedByHeader != null) { + lastViewOverlappedByHeader + } else { + parent.getChildAt(topChildPosition) + } + } + lastViewOverlappedByHeader = viewOverlappedByHeader + + val overlappedByHeaderPosition = + parent.getChildAdapterPosition(viewOverlappedByHeader!!) + val overlappedHeaderPosition: Int + val preOverlappedPosition: Int + + if (overlappedByHeaderPosition > 0) { + preOverlappedPosition = + adapter.getHeaderPositionForItem(overlappedByHeaderPosition - 1) + overlappedHeaderPosition = + adapter.getHeaderPositionForItem(overlappedByHeaderPosition) + } else { + preOverlappedPosition = adapter.getHeaderPositionForItem(topChildPosition) + overlappedHeaderPosition = preOverlappedPosition + } + + if (preOverlappedPosition == RecyclerView.NO_POSITION) { + return + } + + if (preOverlappedPosition != overlappedHeaderPosition && shouldMoveHeader( + viewOverlappedByHeader + ) + ) { + updateStickyHeader(topChildPosition) + moveHeader( + c, + viewOverlappedByHeader + ) + } else { + updateStickyHeader(topChildPosition) + drawHeader(c) + } + } + + /** + * Whether the sticky header should move or not. + * + * This method is for avoiding sinking/departing the sticky header into/from top of screen + */ + private fun shouldMoveHeader(viewOverlappedByHeader: View): Boolean { + val dy = viewOverlappedByHeader.top - viewOverlappedByHeader.height + return viewOverlappedByHeader.top >= 0 && dy <= 0 + } + + private fun updateStickyHeader(topChildPosition: Int) { + val currentStickyHolder = currentStickyHolder ?: return + val headerPositionForItem = adapter.getHeaderPositionForItem(topChildPosition) + + if (headerPositionForItem != currentStickyPosition && headerPositionForItem != RecyclerView.NO_POSITION) { + adapter.onBindHeaderViewHolder( + currentStickyHolder, + headerPositionForItem + ) + currentStickyPosition = headerPositionForItem + } else if (headerPositionForItem != RecyclerView.NO_POSITION) { + adapter.onBindHeaderViewHolder( + currentStickyHolder, + headerPositionForItem + ) + } + } + + private fun drawHeader(c: Canvas) { + c.save() + c.translate( + 0f, + 0f + ) + currentStickyHolder!!.itemView.draw(c) + c.restore() + } + + private fun moveHeader( + c: Canvas, + nextHeader: View + ) { + c.save() + c.translate( + 0f, + nextHeader.top - nextHeader.height.toFloat() + ) + currentStickyHolder!!.itemView.draw(c) + c.restore() + } + + private fun getChildInContact( + parent: RecyclerView, + contactPoint: Int + ): View? { + var childInContact: View? = null + + for (i in 0 until parent.childCount) { + val child = parent.getChildAt(i) + + if (child.bottom > contactPoint) { + if (child.top <= contactPoint) { // This child overlaps the contact point + childInContact = child + break + } + } + } + + return childInContact + } + + 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 + ) + } + }) + } +} \ No newline at end of file From 7f2748e97be6fcd1c2824987ac0361ff28c6a103 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sun, 7 Jun 2020 14:39:44 +0200 Subject: [PATCH 12/19] fix: expose immutable list of items from adapter --- .../AbstractListItemRecyclerViewAdapter.kt | 81 +++++++++---------- 1 file changed, 39 insertions(+), 42 deletions(-) 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 00924637..aa8438c6 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 @@ -12,17 +12,18 @@ import androidx.recyclerview.widget.RecyclerView * * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) */ -abstract class AbstractListItemRecyclerViewAdapter(private val listener: OnListItemRecyclerViewAdapterListener) : +abstract class AbstractListItemRecyclerViewAdapter(private val listener: OnListItemRecyclerViewAdapterListener? = null) : RecyclerView.Adapter.AbstractViewHolder>() { - internal val items = mutableListOf() + private val _items = mutableListOf() + val items: List = _items init { this.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onChanged() { super.onChanged() - listener.showEmptyTextView(itemCount == 0) + listener?.showEmptyTextView(itemCount == 0) } override fun onItemRangeChanged( @@ -34,7 +35,7 @@ abstract class AbstractListItemRecyclerViewAdapter(private val listener: OnLi itemCount ) - listener.showEmptyTextView(itemCount == 0) + listener?.showEmptyTextView(itemCount == 0) } override fun onItemRangeInserted( @@ -46,7 +47,7 @@ abstract class AbstractListItemRecyclerViewAdapter(private val listener: OnLi itemCount ) - listener.showEmptyTextView(false) + listener?.showEmptyTextView(false) } override fun onItemRangeRemoved( @@ -58,7 +59,7 @@ abstract class AbstractListItemRecyclerViewAdapter(private val listener: OnLi itemCount ) - listener.showEmptyTextView(itemCount == 0) + listener?.showEmptyTextView(itemCount == 0) } }) } @@ -79,23 +80,20 @@ abstract class AbstractListItemRecyclerViewAdapter(private val listener: OnLi } override fun getItemCount(): Int { - return items.size + return _items.size } override fun onBindViewHolder( holder: AbstractViewHolder, position: Int ) { - holder.bind( - position, - items[position] - ) + holder.bind(_items[position]) } override fun getItemViewType(position: Int): Int { return getLayoutResourceId( position, - items[position] + _items[position] ) } @@ -103,13 +101,13 @@ abstract class AbstractListItemRecyclerViewAdapter(private val listener: OnLi * Sets new items. */ fun setItems(newItems: List) { - if (this.items.isEmpty()) { - this.items.addAll(newItems) + if (this._items.isEmpty()) { + this._items.addAll(newItems) - if (this.items.isNotEmpty()) { + if (this._items.isNotEmpty()) { notifyItemRangeInserted( 0, - this.items.size + this._items.size ) } else { notifyDataSetChanged() @@ -119,7 +117,7 @@ abstract class AbstractListItemRecyclerViewAdapter(private val listener: OnLi } if (newItems.isEmpty()) { - this.items.clear() + this._items.clear() notifyDataSetChanged() return @@ -127,7 +125,7 @@ abstract class AbstractListItemRecyclerViewAdapter(private val listener: OnLi val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() { override fun getOldListSize(): Int { - return this@AbstractListItemRecyclerViewAdapter.items.size + return this@AbstractListItemRecyclerViewAdapter._items.size } override fun getNewListSize(): Int { @@ -139,7 +137,7 @@ abstract class AbstractListItemRecyclerViewAdapter(private val listener: OnLi newItemPosition: Int ): Boolean { return this@AbstractListItemRecyclerViewAdapter.areItemsTheSame( - this@AbstractListItemRecyclerViewAdapter.items, + this@AbstractListItemRecyclerViewAdapter._items, newItems, oldItemPosition, newItemPosition @@ -151,7 +149,7 @@ abstract class AbstractListItemRecyclerViewAdapter(private val listener: OnLi newItemPosition: Int ): Boolean { return this@AbstractListItemRecyclerViewAdapter.areContentsTheSame( - this@AbstractListItemRecyclerViewAdapter.items, + this@AbstractListItemRecyclerViewAdapter._items, newItems, oldItemPosition, newItemPosition @@ -159,8 +157,8 @@ abstract class AbstractListItemRecyclerViewAdapter(private val listener: OnLi } }) - this.items.clear() - this.items.addAll(newItems) + this._items.clear() + this._items.addAll(newItems) diffResult.dispatchUpdatesTo(this) } @@ -172,8 +170,8 @@ abstract class AbstractListItemRecyclerViewAdapter(private val listener: OnLi item: T, index: Int = -1 ) { - val position = if (index < 0 || index > this.items.size) this.items.size else index - this.items.add( + val position = if (index < 0 || index > this._items.size) this._items.size else index + this._items.add( position, item ) @@ -187,13 +185,13 @@ abstract class AbstractListItemRecyclerViewAdapter(private val listener: OnLi * @return item position if successfully removed, -1 otherwise */ fun remove(item: T): Int { - val itemPosition = this.items.indexOf(item) - val removed = this.items.remove(item) + val itemPosition = this._items.indexOf(item) + val removed = this._items.remove(item) if (removed) { notifyItemRemoved(itemPosition) - if (this.items.isEmpty()) { + if (this._items.isEmpty()) { notifyDataSetChanged() } } @@ -205,7 +203,7 @@ abstract class AbstractListItemRecyclerViewAdapter(private val listener: OnLi * Clear the list. */ fun clear(notify: Boolean = true) { - this.items.clear() + this._items.clear() if (notify) { notifyDataSetChanged() @@ -250,22 +248,21 @@ abstract class AbstractListItemRecyclerViewAdapter(private val listener: OnLi ): Boolean abstract inner class AbstractViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - fun bind( - position: Int, - item: T - ) { + open fun bind(item: T) { onBind(item) - with(itemView) { - setOnClickListener { - listener.onClick(item) - } - setOnLongClickListener { - listener.onLongClicked( - position, - item - ) - true + listener?.also { l -> + with(itemView) { + setOnClickListener { + l.onClick(item) + } + setOnLongClickListener { + l.onLongClicked( + adapterPosition, + item + ) + true + } } } } From fdb2a6dd37be7ef22e0c3c8e65ab9570af7e7410 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sun, 7 Jun 2020 14:47:37 +0200 Subject: [PATCH 13/19] fix: base Filter from AbstractTaxon --- .../geonature/commons/data/AbstractTaxon.kt | 52 ++++++++++++++++ .../java/fr/geonature/commons/data/Taxon.kt | 60 +++---------------- .../fr/geonature/commons/data/TaxonArea.kt | 10 ++-- .../geonature/commons/data/TaxonWithArea.kt | 36 +++++++++++ .../fr/geonature/commons/data/TaxonTest.kt | 22 ++++--- .../commons/data/TaxonWithAreaTest.kt | 43 ++++++++++++- 6 files changed, 155 insertions(+), 68 deletions(-) diff --git a/commons/src/main/java/fr/geonature/commons/data/AbstractTaxon.kt b/commons/src/main/java/fr/geonature/commons/data/AbstractTaxon.kt index dc3adcfb..b1718cb9 100644 --- a/commons/src/main/java/fr/geonature/commons/data/AbstractTaxon.kt +++ b/commons/src/main/java/fr/geonature/commons/data/AbstractTaxon.kt @@ -191,6 +191,58 @@ abstract class AbstractTaxon : Parcelable { return this } + /** + * Filter by taxonomy. + * + * @return this + */ + fun byTaxonomy(taxonomy: Taxonomy): Filter { + if (taxonomy.isAny()) { + return this + } + + if (taxonomy.group == Taxonomy.ANY) { + return byKingdom(taxonomy.kingdom) + } + + this.wheres.add( + Pair( + "((${Taxonomy.getColumnAlias( + Taxonomy.COLUMN_KINGDOM, + tableAlias + )} = ?) AND (${Taxonomy.getColumnAlias( + Taxonomy.COLUMN_GROUP, + tableAlias + )} = ?))", + arrayOf( + taxonomy.kingdom, + taxonomy.group + ) + ) + ) + + return this + } + + /** + * Filter by taxonomy kingdom. + * + * @return this + */ + fun byKingdom(kingdom: String): Filter { + this.wheres.add( + Pair( + "(${Taxonomy.getColumnAlias( + Taxonomy.COLUMN_KINGDOM, + tableAlias + )} = ?)", + arrayOf(kingdom) + ) + ) + + return this + } + /** * Builds the WHERE clause as selection for this filter. */ 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 b240ac24..d0c853d4 100644 --- a/commons/src/main/java/fr/geonature/commons/data/Taxon.kt +++ b/commons/src/main/java/fr/geonature/commons/data/Taxon.kt @@ -120,10 +120,12 @@ class Taxon : AbstractTaxon { ) ) } catch (e: Exception) { - Log.w( - TAG, - e - ) + e.message?.run { + Log.w( + TAG, + this + ) + } null } @@ -145,53 +147,5 @@ class Taxon : AbstractTaxon { /** * Filter query builder. */ - class Filter : AbstractTaxon.Filter(TABLE_NAME) { - - /** - * Filter by taxonomy. - * - * @return this - */ - fun byTaxonomy(taxonomy: Taxonomy): Filter { - if (taxonomy.isAny()) { - return this - } - - if (taxonomy.group == Taxonomy.ANY) { - return byKingdom(taxonomy.kingdom) - } - - this.wheres.add( - Pair( - "((${Taxonomy.getColumnAlias( - Taxonomy.COLUMN_KINGDOM, tableAlias - )} = ?) AND (${Taxonomy.getColumnAlias( - Taxonomy.COLUMN_GROUP, - tableAlias - )} = ?))", - arrayOf(taxonomy.kingdom, taxonomy.group) - ) - ) - - return this - } - - /** - * Filter by taxonomy kingdom. - * - * @return this - */ - fun byKingdom(kingdom: String): Filter { - this.wheres.add( - Pair( - "(${Taxonomy.getColumnAlias( - Taxonomy.COLUMN_KINGDOM, tableAlias - )} = ?)", - arrayOf(kingdom) - ) - ) - - return this - } - } + class Filter : AbstractTaxon.Filter(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 0ed7d6c1..f228b252 100644 --- a/commons/src/main/java/fr/geonature/commons/data/TaxonArea.kt +++ b/commons/src/main/java/fr/geonature/commons/data/TaxonArea.kt @@ -188,10 +188,12 @@ data class TaxonArea( ) ) } catch (e: Exception) { - Log.w( - TAG, - e - ) + e.message?.run { + Log.w( + TAG, + this + ) + } null } 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 021c4b82..d8489bcc 100644 --- a/commons/src/main/java/fr/geonature/commons/data/TaxonWithArea.kt +++ b/commons/src/main/java/fr/geonature/commons/data/TaxonWithArea.kt @@ -116,4 +116,40 @@ class TaxonWithArea : AbstractTaxon { } } } + + /** + * Filter query builder. + */ + class Filter : AbstractTaxon.Filter(Taxon.TABLE_NAME) { + + /** + * Filter by area 'colors'. + * + * @return this + */ + fun byAreaColors(vararg color: String): AbstractTaxon.Filter { + if (color.isEmpty()) { + return this + } + + this.wheres.add( + Pair( + "(${getColumnAlias( + TaxonArea.COLUMN_COLOR, + TaxonArea.TABLE_NAME + )} IN (${color.filter { it != "none" } + .joinToString(", ") { "'${it}'" }})${color.find { it == "none" } + ?.let { + " OR (${getColumnAlias( + TaxonArea.COLUMN_COLOR, + TaxonArea.TABLE_NAME + )} IS NULL)" + } ?: ""})", + null + ) + ) + + return this + } + } } diff --git a/commons/src/test/java/fr/geonature/commons/data/TaxonTest.kt b/commons/src/test/java/fr/geonature/commons/data/TaxonTest.kt index c54ede89..4dcc46a2 100644 --- a/commons/src/test/java/fr/geonature/commons/data/TaxonTest.kt +++ b/commons/src/test/java/fr/geonature/commons/data/TaxonTest.kt @@ -291,12 +291,14 @@ class TaxonTest { @Test fun testFilter() { val taxonFilterByNameAndTaxonomy = - (Taxon.Filter().byNameOrDescription("as") as Taxon.Filter).byTaxonomy( - Taxonomy( - "Animalia", - "Ascidies" + Taxon.Filter() + .byNameOrDescription("as") + .byTaxonomy( + Taxonomy( + "Animalia", + "Ascidies" + ) ) - ) .build() assertEquals( @@ -314,11 +316,13 @@ class TaxonTest { ) val taxonFilterByNameAndKingdom = - (Taxon.Filter().byNameOrDescription("as") as Taxon.Filter).byTaxonomy( - Taxonomy( - "Animalia" + Taxon.Filter() + .byNameOrDescription("as") + .byTaxonomy( + Taxonomy( + "Animalia" + ) ) - ) .build() assertEquals( 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 68eac083..adfa4b3a 100644 --- a/commons/src/test/java/fr/geonature/commons/data/TaxonWithAreaTest.kt +++ b/commons/src/test/java/fr/geonature/commons/data/TaxonWithAreaTest.kt @@ -4,17 +4,18 @@ import android.database.Cursor import android.os.Parcel import fr.geonature.commons.data.TaxonWithArea.Companion.defaultProjection import fr.geonature.commons.data.TaxonWithArea.Companion.fromCursor -import java.time.Instant -import java.util.Date import org.junit.Assert.assertArrayEquals 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 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 [TaxonWithArea]. @@ -381,4 +382,42 @@ class TaxonWithAreaTest { defaultProjection() ) } + + @Test + fun testFilter() { + val taxonFilterByAreaColors = + TaxonWithArea.Filter() + .byAreaColors( + "red", + "grey" + ) + .build() + + assertEquals( + "(${TaxonArea.TABLE_NAME}_${TaxonArea.COLUMN_COLOR} IN ('red', 'grey'))", + taxonFilterByAreaColors.first + ) + assertTrue(taxonFilterByAreaColors.second.isEmpty()) + + val taxonFilterByNameAndAreaColors = + (TaxonWithArea.Filter() + .byNameOrDescription("as") as TaxonWithArea.Filter) + .byAreaColors( + "red", + "grey" + ) + .build() + + assertEquals( + "(${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME} LIKE ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_DESCRIPTION} LIKE ?) AND (${TaxonArea.TABLE_NAME}_${TaxonArea.COLUMN_COLOR} IN ('red', 'grey'))", + taxonFilterByNameAndAreaColors.first + ) + assertArrayEquals( + arrayOf( + "%as%", + "%as%" + ), + taxonFilterByNameAndAreaColors.second + ) + } } From 3dfc26b8b2fb28746fd05f81dd213c20687563db Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sun, 7 Jun 2020 14:48:21 +0200 Subject: [PATCH 14/19] feat: expose getColor() --- .../fr/geonature/commons/util/ThemeUtils.kt | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/commons/src/main/java/fr/geonature/commons/util/ThemeUtils.kt b/commons/src/main/java/fr/geonature/commons/util/ThemeUtils.kt index cbec5ad8..fb03a429 100644 --- a/commons/src/main/java/fr/geonature/commons/util/ThemeUtils.kt +++ b/commons/src/main/java/fr/geonature/commons/util/ThemeUtils.kt @@ -1,6 +1,7 @@ package fr.geonature.commons.util import android.content.Context +import androidx.annotation.AttrRes import androidx.annotation.ColorInt import fr.geonature.commons.R @@ -13,26 +14,38 @@ object ThemeUtils { @ColorInt fun getPrimaryColor(context: Context): Int { - return getColor(context, R.attr.colorPrimary) + return getColor( + context, + R.attr.colorPrimary + ) } @ColorInt fun getPrimaryDarkColor(context: Context): Int { - return getColor(context, R.attr.colorPrimaryDark) + return getColor( + context, + R.attr.colorPrimaryDark + ) } @ColorInt fun getAccentColor(context: Context): Int { - return getColor(context, R.attr.colorAccent) + return getColor( + context, + R.attr.colorAccent + ) } @ColorInt - private fun getColor( + fun getColor( context: Context, - colorAttribute: Int + @AttrRes colorAttribute: Int ): Int { val typedArray = context.theme.obtainStyledAttributes(intArrayOf(colorAttribute)) - val color = typedArray.getColor(0, 0) + val color = typedArray.getColor( + 0, + 0 + ) typedArray.recycle() From 5201dd4f8b8b9c346c71f88b56ccee1728dd5e92 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sun, 7 Jun 2020 14:49:14 +0200 Subject: [PATCH 15/19] chore: upgrade libraries dependencies, fix unit tests... --- commons/build.gradle | 4 ++-- .../commons/settings/AppSettingsManagerTest.kt | 10 +--------- .../commons/settings/AppSettingsViewModelTest.kt | 4 ++-- commons/version.properties | 4 ++-- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/commons/build.gradle b/commons/build.gradle index f1dbeae6..c100488a 100644 --- a/commons/build.gradle +++ b/commons/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' -version = "0.6.8" +version = "0.6.9" android { compileSdkVersion 29 @@ -44,7 +44,7 @@ dependencies { api project(':mountpoint') implementation 'androidx.appcompat:appcompat:1.1.0' - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-alpha02" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-alpha03" 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' 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 cd2ab4f0..352ee0bf 100644 --- a/commons/src/test/java/fr/geonature/commons/settings/AppSettingsManagerTest.kt +++ b/commons/src/test/java/fr/geonature/commons/settings/AppSettingsManagerTest.kt @@ -6,9 +6,7 @@ import androidx.test.core.app.ApplicationProvider.getApplicationContext import fr.geonature.commons.FixtureHelper.getFixtureAsFile import fr.geonature.commons.MockitoKotlinHelper.any import fr.geonature.commons.MockitoKotlinHelper.eq -import fr.geonature.commons.observeOnce import fr.geonature.commons.settings.io.AppSettingsJsonReader -import java.io.File import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull @@ -22,6 +20,7 @@ import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations.initMocks import org.robolectric.RobolectricTestRunner +import java.io.File /** * Unit tests about [AppSettingsManager]. @@ -126,12 +125,5 @@ class AppSettingsManagerTest { DummyAppSettings("value"), appSettings ) - appSettingsManager.appSettings.observeOnce { - assertNotNull(it) - assertEquals( - appSettings, - it - ) - } } } diff --git a/commons/src/test/java/fr/geonature/commons/settings/AppSettingsViewModelTest.kt b/commons/src/test/java/fr/geonature/commons/settings/AppSettingsViewModelTest.kt index de5e0d5e..e9a7e56d 100644 --- a/commons/src/test/java/fr/geonature/commons/settings/AppSettingsViewModelTest.kt +++ b/commons/src/test/java/fr/geonature/commons/settings/AppSettingsViewModelTest.kt @@ -31,7 +31,7 @@ class AppSettingsViewModelTest { private lateinit var onAppSettingsJsonJsonReaderListener: AppSettingsJsonReader.OnAppSettingsJsonReaderListener @Mock - private lateinit var observer: Observer + private lateinit var observer: Observer private lateinit var application: Application private lateinit var appSettingsViewModel: DummyAppSettingsViewModel @@ -52,7 +52,7 @@ class AppSettingsViewModelTest { ) ) appSettingsManager = spy(appSettingsViewModel.appSettingsManager) - appSettingsManager.appSettings.observeForever(observer) + appSettingsViewModel.loadAppSettings().observeForever(observer) } @Test diff --git a/commons/version.properties b/commons/version.properties index 49416784..f1f32665 100644 --- a/commons/version.properties +++ b/commons/version.properties @@ -1,2 +1,2 @@ -#Sat Feb 08 17:10:26 CET 2020 -VERSION_CODE=2210 +#Sun Jun 07 14:41:23 CEST 2020 +VERSION_CODE=2450 From 62c66af2fc86e118b67bcab8492e63f1331d8829 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sun, 7 Jun 2020 14:52:08 +0200 Subject: [PATCH 16/19] fix: missing 'TAXA_AREA' uri matcher from types --- sync/src/main/java/fr/geonature/sync/auth/AuthManager.kt | 2 +- .../java/fr/geonature/sync/data/MainContentProvider.kt | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/sync/src/main/java/fr/geonature/sync/auth/AuthManager.kt b/sync/src/main/java/fr/geonature/sync/auth/AuthManager.kt index 742536d0..b09265d2 100644 --- a/sync/src/main/java/fr/geonature/sync/auth/AuthManager.kt +++ b/sync/src/main/java/fr/geonature/sync/auth/AuthManager.kt @@ -10,12 +10,12 @@ import androidx.preference.PreferenceManager import fr.geonature.sync.api.model.AuthLogin import fr.geonature.sync.auth.io.AuthLoginJsonReader import fr.geonature.sync.auth.io.AuthLoginJsonWriter -import java.util.Calendar import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.util.Calendar /** * [AuthLogin] manager. 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 0b886c59..e0e1caa7 100644 --- a/sync/src/main/java/fr/geonature/sync/data/MainContentProvider.kt +++ b/sync/src/main/java/fr/geonature/sync/data/MainContentProvider.kt @@ -36,7 +36,7 @@ class MainContentProvider : ContentProvider() { DATASET_ID -> "$VND_TYPE_ITEM_PREFIX/$AUTHORITY.${Dataset.TABLE_NAME}" INPUT_OBSERVERS, INPUT_OBSERVERS_IDS -> "$VND_TYPE_DIR_PREFIX/$AUTHORITY.${InputObserver.TABLE_NAME}" INPUT_OBSERVER_ID -> "$VND_TYPE_ITEM_PREFIX/$AUTHORITY.${InputObserver.TABLE_NAME}" - TAXA -> "$VND_TYPE_DIR_PREFIX/$AUTHORITY.${Taxon.TABLE_NAME}" + TAXA, TAXA_AREA -> "$VND_TYPE_DIR_PREFIX/$AUTHORITY.${Taxon.TABLE_NAME}" TAXON_ID, TAXON_AREA_ID -> "$VND_TYPE_ITEM_PREFIX/$AUTHORITY.${Taxon.TABLE_NAME}" TAXONOMY, TAXONOMY_KINGDOM -> "$VND_TYPE_DIR_PREFIX/$AUTHORITY.${Taxonomy.TABLE_NAME}" TAXONOMY_KINGDOM_GROUP -> "$VND_TYPE_ITEM_PREFIX/$AUTHORITY.${Taxonomy.TABLE_NAME}" @@ -224,7 +224,10 @@ class MainContentProvider : ContentProvider() { uri: Uri ): Cursor { val selectedObserverIds = - uri.lastPathSegment?.split(",")?.mapNotNull { it.toLongOrNull() }?.distinct()?.toLongArray() + uri.lastPathSegment?.split(",") + ?.mapNotNull { it.toLongOrNull() } + ?.distinct() + ?.toLongArray() ?: longArrayOf() if (selectedObserverIds.size == 1) { From d166b4094c81bec1cb65a3bffa100f3c24d822a0 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sun, 7 Jun 2020 14:52:51 +0200 Subject: [PATCH 17/19] chore: gradle upgrade --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 68022c22..b1456ef1 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.6.3' + classpath 'com.android.tools.build:gradle:4.0.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jlleitschuh.gradle:ktlint-gradle:9.1.1" // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8cc31f2d..b917ab1f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Feb 24 20:50:38 CET 2020 +#Mon Jun 01 15:11:36 CEST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip From 98633ffcca591b4fe8e5086ed7c3b75fe756ae77 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sun, 7 Jun 2020 14:55:27 +0200 Subject: [PATCH 18/19] docs: Postman collection --- .../gn_mobile_core.postman_collection.json | 863 +++++++++--------- 1 file changed, 434 insertions(+), 429 deletions(-) diff --git a/docs/postman/gn_mobile_core.postman_collection.json b/docs/postman/gn_mobile_core.postman_collection.json index 424cdc58..3fc32ad3 100644 --- a/docs/postman/gn_mobile_core.postman_collection.json +++ b/docs/postman/gn_mobile_core.postman_collection.json @@ -1,431 +1,436 @@ { - "info": { - "_postman_id": "e359d92f-c48a-4f00-a218-8863a7830fd7", - "name": "gn_mobile_core", - "description": "Data synchronization endpoints from GeoNature.", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "/api/auth/login", - "event": [ - { - "listen": "test", - "script": { - "id": "64406aee-d4be-4aea-ae28-e4b9ca272b10", - "exec": [ - "var cookie = postman.getResponseHeader(\"Set-Cookie\")", - "pm.globals.set(\"cookie\", cookie.split(\";\")[0]);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json;charset=UTF-8", - "type": "text" - }, - { - "key": "Accept", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"login\": \"{{login}}\",\n \"password\": \"{{password}}\",\n \"id_application\": {{application_id}}\n}", - "options": { - "raw": {} - } - }, - "url": { - "raw": "{{geoNatureServerUrl}}/api/auth/login", - "host": [ - "{{geoNatureServerUrl}}" - ], - "path": [ - "api", - "auth", - "login" - ] - } - }, - "response": [] - }, - { - "name": "/api/occtax/releve", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - }, - { - "key": "Cookie", - "value": "{{cookie}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"id\": 137190440,\n \"module\": \"occtax\",\n \"geometry\": {\n \"type\": \"Point\",\n \"coordinates\": [\n -1.5386237274626353,\n 47.222886771502594\n ]\n },\n \"properties\": {\n \"meta_device_entry\": \"mobile\",\n \"date_min\": \"2020-05-06T19:27:20Z\",\n \"date_max\": \"2020-05-06T19:27:20Z\",\n \"id_dataset\": 2,\n \"id_digitiser\": 4,\n \"observers\": [\n 4\n ],\n \"comment\": null,\n \"default\": {\n \"technique_obs\": {\n \"label\": \"Non renseigné\",\n \"value\": 316\n },\n \"typ_grp\": {\n \"label\": \"NSP\",\n \"value\": 132\n }\n },\n \"id_nomenclature_obs_technique\": 316,\n \"id_nomenclature_grp_typ\": 132,\n \"t_occurrences_occtax\": [\n {\n \"cd_nom\": 531330,\n \"nom_cite\": \"Abarenicola claparedi\",\n \"regne\": \"Animalia\",\n \"group2_inpn\": \"Annélides\",\n \"properties\": {\n \"meth_obs\": {\n \"label\": \"Vu\",\n \"value\": 41\n },\n \"eta_bio\": {\n \"label\": \"Observé vivant\",\n \"value\": 157\n },\n \"meth_determin\": {\n \"label\": \"Non renseigné\",\n \"value\": 445\n },\n \"statut_bio\": {\n \"label\": \"Non renseigné\",\n \"value\": 29\n },\n \"naturalite\": {\n \"label\": \"Sauvage\",\n \"value\": 160\n },\n \"preuve_exist\": {\n \"label\": \"Inconnu\",\n \"value\": 81\n },\n \"counting\": [\n {\n \"index\": 1,\n \"stade_vie\": {\n \"label\": \"Indéterminé\",\n \"value\": 2\n },\n \"sexe\": {\n \"label\": \"Non renseigné\",\n \"value\": 171\n },\n \"obj_denbr\": {\n \"label\": \"Individu\",\n \"value\": 146\n },\n \"typ_denbr\": {\n \"label\": \"Ne sait pas\",\n \"value\": 94\n },\n \"min\": 1,\n \"max\": 1\n }\n ]\n },\n \"id_nomenclature_obs_meth\": 41,\n \"id_nomenclature_bio_condition\": 157,\n \"id_nomenclature_determination_method\": 445,\n \"id_nomenclature_bio_status\": 29,\n \"id_nomenclature_naturalness\": 160,\n \"id_nomenclature_exist_proof\": 81,\n \"cor_counting_occtax\": [\n {\n \"id_nomenclature_life_stage\": 2,\n \"id_nomenclature_sex\": 171,\n \"id_nomenclature_obj_count\": 146,\n \"id_nomenclature_type_count\": 94,\n \"count_min\": 1,\n \"count_max\": 1\n }\n ]\n }\n ]\n }\n}" - }, - "url": { - "raw": "{{geoNatureServerUrl}}/api/occtax/releve", - "host": [ - "{{geoNatureServerUrl}}" - ], - "path": [ - "api", - "occtax", - "releve" - ] - } - }, - "response": [] - }, - { - "name": "/api/gn_commons/t_mobile_apps", - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json;charset=UTF-8", - "type": "text" - }, - { - "key": "Accept", - "value": "application/json", - "type": "text" - } - ], - "url": { - "raw": "{{geoNatureServerUrl}}/api/gn_commons/t_mobile_apps", - "host": [ - "{{geoNatureServerUrl}}" - ], - "path": [ - "api", - "gn_commons", - "t_mobile_apps" - ] - } - }, - "response": [] - }, - { - "name": "/api/meta/datasets", - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json;charset=UTF-8", - "type": "text" - }, - { - "key": "Accept", - "value": "application/json", - "type": "text" - } - ], - "url": { - "raw": "{{geoNatureServerUrl}}/api/meta/datasets", - "host": [ - "{{geoNatureServerUrl}}" - ], - "path": [ - "api", - "meta", - "datasets" - ] - } - }, - "response": [] - }, - { - "name": "/api/nomenclatures/nomenclatures/taxonomy", - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json;charset=UTF-8", - "type": "text" - }, - { - "key": "Accept", - "value": "application/json", - "type": "text" - } - ], - "url": { - "raw": "{{geoNatureServerUrl}}/api/nomenclatures/nomenclatures/taxonomy", - "host": [ - "{{geoNatureServerUrl}}" - ], - "path": [ - "api", - "nomenclatures", - "nomenclatures", - "taxonomy" - ] - } - }, - "response": [] - }, - { - "name": "/api/{{module}}/defaultNomenclatures", - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json;charset=UTF-8", - "type": "text" - }, - { - "key": "Accept", - "value": "application/json", - "type": "text" - } - ], - "url": { - "raw": "{{geoNatureServerUrl}}/api/{{module}}/defaultNomenclatures", - "host": [ - "{{geoNatureServerUrl}}" - ], - "path": [ - "api", - "{{module}}", - "defaultNomenclatures" - ] - } - }, - "response": [] - }, - { - "name": "/api/users/menu/{{users_menu_id}}", - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "type": "text", - "value": "application/json;charset=UTF-8" - }, - { - "key": "Accept", - "type": "text", - "value": "application/json" - } - ], - "url": { - "raw": "{{geoNatureServerUrl}}/api/users/menu/{{users_menu_id}}", - "host": [ - "{{geoNatureServerUrl}}" - ], - "path": [ - "api", - "users", - "menu", - "{{users_menu_id}}" - ] - } - }, - "response": [] - }, - { - "name": "/api/taxref/allnamebylist/{{taxref_list_id}}", - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "type": "text", - "value": "application/json;charset=UTF-8" - }, - { - "key": "Accept", - "type": "text", - "value": "application/json" - } - ], - "url": { - "raw": "{{taxHubServerUrl}}/api/taxref/allnamebylist/{{taxref_list_id}}?limit=1000&offset=0", - "host": [ - "{{taxHubServerUrl}}" - ], - "path": [ - "api", - "taxref", - "allnamebylist", - "{{taxref_list_id}}" - ], - "query": [ - { - "key": "limit", - "value": "1000" - }, - { - "key": "offset", - "value": "0" - } - ] - } - }, - "response": [] - }, - { - "name": "/api/synthese/color_taxon", - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json;charset=UTF-8", - "type": "text" - }, - { - "key": "Accept", - "value": "application/json", - "type": "text" - } - ], - "url": { - "raw": "{{geoNatureServerUrl}}/api/synthese/color_taxon?limit=1000&offset=0", - "host": [ - "{{geoNatureServerUrl}}" - ], - "path": [ - "api", - "synthese", - "color_taxon" - ], - "query": [ - { - "key": "limit", - "value": "1000" - }, - { - "key": "offset", - "value": "0" - } - ] - } - }, - "response": [] - }, - { - "name": "/api/taxref/regnewithgroupe2", - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json;charset=UTF-8", - "type": "text" - }, - { - "key": "Accept", - "value": "application/json", - "type": "text" - } - ], - "url": { - "raw": "{{taxHubServerUrl}}/api/taxref/regnewithgroupe2", - "host": [ - "{{taxHubServerUrl}}" - ], - "path": [ - "api", - "taxref", - "regnewithgroupe2" - ] - } - }, - "response": [] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "id": "9e07829e-3973-4d78-8842-728046ed7d9b", - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "id": "2cd8b10d-2165-4124-8128-2e061c816fea", - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ], - "variable": [ - { - "id": "5058f04f-19e4-49e9-a714-f7d3a1297da1", - "key": "geoNatureServerUrl", - "value": "http://demo.geonature.fr/geonature", - "type": "string" - }, - { - "id": "162df62b-3019-4e1f-a911-1bc9c4ac1ad2", - "key": "taxHubServerUrl", - "value": "http://demo.geonature.fr/taxhub", - "type": "string" - }, - { - "id": "c62697a9-fa55-4e12-8b63-ce26edbb5cf1", - "key": "module", - "value": "occtax", - "type": "string" - }, - { - "id": "48904cdb-4688-4d6a-9db9-3482fd3d1575", - "key": "login", - "value": "admin", - "type": "string" - }, - { - "id": "904db166-dac6-4fa8-b0e1-d025e0da7ce2", - "key": "password", - "value": "admin", - "type": "string" - }, - { - "id": "6c972af3-79d0-48fe-b7af-3bccf877174f", - "key": "application_id", - "value": "3", - "type": "string" - }, - { - "id": "54ca48cf-858b-4282-9d94-ad42d29280e2", - "key": "users_menu_id", - "value": "1", - "type": "string" - }, - { - "id": "1b08a16a-2a84-4072-ad8c-6afc59a4de7e", - "key": "taxref_list_id", - "value": "100", - "type": "string" - } - ], - "protocolProfileBehavior": {} + "info": { + "_postman_id": "e359d92f-c48a-4f00-a218-8863a7830fd7", + "name": "gn_mobile_core", + "description": "Data synchronization endpoints from GeoNature.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "/api/auth/login", + "event": [ + { + "listen": "test", + "script": { + "id": "64406aee-d4be-4aea-ae28-e4b9ca272b10", + "exec": [ + "var cookie = postman.getResponseHeader(\"Set-Cookie\")", + "pm.globals.set(\"cookie\", cookie.split(\";\")[0]);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json;charset=UTF-8", + "type": "text" + }, + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"login\": \"{{login}}\",\n \"password\": \"{{password}}\",\n \"id_application\": {{application_id}}\n}", + "options": { + "raw": {} + } + }, + "url": { + "raw": "{{geoNatureServerUrl}}/api/auth/login", + "host": [ + "{{geoNatureServerUrl}}" + ], + "path": [ + "api", + "auth", + "login" + ] + } + }, + "response": [] + }, + { + "name": "/api/occtax/releve", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Cookie", + "value": "{{cookie}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"id\": 137190440,\n \"module\": \"occtax\",\n \"geometry\": {\n \"type\": \"Point\",\n \"coordinates\": [\n -1.5386237274626353,\n 47.222886771502594\n ]\n },\n \"properties\": {\n \"meta_device_entry\": \"mobile\",\n \"date_min\": \"2020-05-06T19:27:20Z\",\n \"date_max\": \"2020-05-06T19:27:20Z\",\n \"id_dataset\": 2,\n \"id_digitiser\": 4,\n \"observers\": [\n 4\n ],\n \"comment\": null,\n \"default\": {\n \"technique_obs\": {\n \"label\": \"Non renseigné\",\n \"value\": 316\n },\n \"typ_grp\": {\n \"label\": \"NSP\",\n \"value\": 132\n }\n },\n \"id_nomenclature_obs_technique\": 316,\n \"id_nomenclature_grp_typ\": 132,\n \"t_occurrences_occtax\": [\n {\n \"cd_nom\": 531330,\n \"nom_cite\": \"Abarenicola claparedi\",\n \"regne\": \"Animalia\",\n \"group2_inpn\": \"Annélides\",\n \"properties\": {\n \"meth_obs\": {\n \"label\": \"Vu\",\n \"value\": 41\n },\n \"eta_bio\": {\n \"label\": \"Observé vivant\",\n \"value\": 157\n },\n \"meth_determin\": {\n \"label\": \"Non renseigné\",\n \"value\": 445\n },\n \"statut_bio\": {\n \"label\": \"Non renseigné\",\n \"value\": 29\n },\n \"naturalite\": {\n \"label\": \"Sauvage\",\n \"value\": 160\n },\n \"preuve_exist\": {\n \"label\": \"Inconnu\",\n \"value\": 81\n },\n \"counting\": [\n {\n \"index\": 1,\n \"stade_vie\": {\n \"label\": \"Indéterminé\",\n \"value\": 2\n },\n \"sexe\": {\n \"label\": \"Non renseigné\",\n \"value\": 171\n },\n \"obj_denbr\": {\n \"label\": \"Individu\",\n \"value\": 146\n },\n \"typ_denbr\": {\n \"label\": \"Ne sait pas\",\n \"value\": 94\n },\n \"min\": 1,\n \"max\": 1\n }\n ]\n },\n \"id_nomenclature_obs_meth\": 41,\n \"id_nomenclature_bio_condition\": 157,\n \"id_nomenclature_determination_method\": 445,\n \"id_nomenclature_bio_status\": 29,\n \"id_nomenclature_naturalness\": 160,\n \"id_nomenclature_exist_proof\": 81,\n \"cor_counting_occtax\": [\n {\n \"id_nomenclature_life_stage\": 2,\n \"id_nomenclature_sex\": 171,\n \"id_nomenclature_obj_count\": 146,\n \"id_nomenclature_type_count\": 94,\n \"count_min\": 1,\n \"count_max\": 1\n }\n ]\n }\n ]\n }\n}" + }, + "url": { + "raw": "{{geoNatureServerUrl}}/api/occtax/releve", + "host": [ + "{{geoNatureServerUrl}}" + ], + "path": [ + "api", + "occtax", + "releve" + ] + } + }, + "response": [] + }, + { + "name": "/api/gn_commons/t_mobile_apps", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json;charset=UTF-8", + "type": "text" + }, + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{geoNatureServerUrl}}/api/gn_commons/t_mobile_apps", + "host": [ + "{{geoNatureServerUrl}}" + ], + "path": [ + "api", + "gn_commons", + "t_mobile_apps" + ] + } + }, + "response": [] + }, + { + "name": "/api/meta/datasets", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json;charset=UTF-8", + "type": "text" + }, + { + "key": "Accept", + "value": "application/json", + "type": "text" + }, + { + "key": "Cookie", + "value": "{{cookie}}", + "type": "text" + } + ], + "url": { + "raw": "{{geoNatureServerUrl}}/api/meta/datasets", + "host": [ + "{{geoNatureServerUrl}}" + ], + "path": [ + "api", + "meta", + "datasets" + ] + } + }, + "response": [] + }, + { + "name": "/api/nomenclatures/nomenclatures/taxonomy", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json;charset=UTF-8", + "type": "text" + }, + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{geoNatureServerUrl}}/api/nomenclatures/nomenclatures/taxonomy", + "host": [ + "{{geoNatureServerUrl}}" + ], + "path": [ + "api", + "nomenclatures", + "nomenclatures", + "taxonomy" + ] + } + }, + "response": [] + }, + { + "name": "/api/{{module}}/defaultNomenclatures", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json;charset=UTF-8", + "type": "text" + }, + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{geoNatureServerUrl}}/api/{{module}}/defaultNomenclatures", + "host": [ + "{{geoNatureServerUrl}}" + ], + "path": [ + "api", + "{{module}}", + "defaultNomenclatures" + ] + } + }, + "response": [] + }, + { + "name": "/api/users/menu/{{users_menu_id}}", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json;charset=UTF-8" + }, + { + "key": "Accept", + "type": "text", + "value": "application/json" + } + ], + "url": { + "raw": "{{geoNatureServerUrl}}/api/users/menu/{{users_menu_id}}", + "host": [ + "{{geoNatureServerUrl}}" + ], + "path": [ + "api", + "users", + "menu", + "{{users_menu_id}}" + ] + } + }, + "response": [] + }, + { + "name": "/api/taxref/allnamebylist/{{taxref_list_id}}", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json;charset=UTF-8" + }, + { + "key": "Accept", + "type": "text", + "value": "application/json" + } + ], + "url": { + "raw": "{{taxHubServerUrl}}/api/taxref/allnamebylist/{{taxref_list_id}}?limit=1000&offset=0", + "host": [ + "{{taxHubServerUrl}}" + ], + "path": [ + "api", + "taxref", + "allnamebylist", + "{{taxref_list_id}}" + ], + "query": [ + { + "key": "limit", + "value": "1000" + }, + { + "key": "offset", + "value": "0" + } + ] + } + }, + "response": [] + }, + { + "name": "/api/synthese/color_taxon", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json;charset=UTF-8", + "type": "text" + }, + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{geoNatureServerUrl}}/api/synthese/color_taxon?limit=1000&offset=0", + "host": [ + "{{geoNatureServerUrl}}" + ], + "path": [ + "api", + "synthese", + "color_taxon" + ], + "query": [ + { + "key": "limit", + "value": "1000" + }, + { + "key": "offset", + "value": "0" + } + ] + } + }, + "response": [] + }, + { + "name": "/api/taxref/regnewithgroupe2", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json;charset=UTF-8", + "type": "text" + }, + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{taxHubServerUrl}}/api/taxref/regnewithgroupe2", + "host": [ + "{{taxHubServerUrl}}" + ], + "path": [ + "api", + "taxref", + "regnewithgroupe2" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "id": "9e07829e-3973-4d78-8842-728046ed7d9b", + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "id": "2cd8b10d-2165-4124-8128-2e061c816fea", + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "id": "f6cd18af-0dc0-4727-9cd2-70ec6e929203", + "key": "geoNatureServerUrl", + "value": "http://demo.geonature.fr/geonature", + "type": "string" + }, + { + "id": "cb293c28-d001-4495-8fc6-91db8501fe0b", + "key": "taxHubServerUrl", + "value": "http://demo.geonature.fr/taxhub", + "type": "string" + }, + { + "id": "d4a1b7a1-f8ed-454f-8dce-523d0945ffa7", + "key": "module", + "value": "occtax", + "type": "string" + }, + { + "id": "f0a02508-c55f-4c31-9935-ccc32dcf00cb", + "key": "login", + "value": "admin", + "type": "string" + }, + { + "id": "2ac4db67-1cef-406f-a4c6-b3e384213129", + "key": "password", + "value": "", + "type": "string" + }, + { + "id": "2728c59e-b46e-40e1-bc20-24022eee8119", + "key": "application_id", + "value": "3", + "type": "string" + }, + { + "id": "30e1467f-3cdd-4e24-bcbd-2b7f38b85ba4", + "key": "users_menu_id", + "value": "1", + "type": "string" + }, + { + "id": "49418d17-f5ed-41f5-8048-da8f8b632167", + "key": "taxref_list_id", + "value": "100", + "type": "string" + } + ], + "protocolProfileBehavior": {} } \ No newline at end of file From 8f49fe20c0f43e484bda2e3e4ddba8c9e83f077a Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sun, 7 Jun 2020 16:52:44 +0200 Subject: [PATCH 19/19] chore: 0.2.9 release --- sync/build.gradle | 12 ++++++------ sync/version.properties | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sync/build.gradle b/sync/build.gradle index 87615390..a9b8468f 100644 --- a/sync/build.gradle +++ b/sync/build.gradle @@ -3,7 +3,7 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: "kotlin-kapt" -version = "0.2.8" +version = "0.2.9" android { compileSdkVersion 29 @@ -43,7 +43,7 @@ android { flavorDimensions "version" productFlavors { - pnx { + generic { } pne { } @@ -66,13 +66,13 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta6' - implementation 'androidx.core:core-ktx:1.3.0-rc01' + implementation 'androidx.core:core-ktx:1.4.0-alpha01' implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.work:work-runtime:2.3.4' implementation 'androidx.work:work-runtime-ktx:2.3.4' - implementation 'com.google.android.material:material:1.2.0-alpha06' + implementation 'com.google.android.material:material:1.2.0-beta01' implementation 'com.google.code.gson:gson:2.8.5' implementation 'com.squareup.okhttp3:logging-interceptor:3.12.0' implementation 'com.squareup.retrofit2:retrofit:2.6.0' @@ -83,6 +83,6 @@ dependencies { testImplementation 'androidx.test:core:1.2.0' testImplementation 'org.mockito:mockito-core:3.0.0' testImplementation 'org.robolectric:robolectric:4.3.1' - androidTestImplementation 'androidx.test:runner:1.3.0-beta01' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0-beta01' + androidTestImplementation 'androidx.test:runner:1.3.0-rc01' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0-rc01' } diff --git a/sync/version.properties b/sync/version.properties index 45f1ee40..e4b275a7 100644 --- a/sync/version.properties +++ b/sync/version.properties @@ -1,2 +1,2 @@ -#Thu May 14 21:20:39 CEST 2020 -VERSION_CODE=2280 +#Sun Jun 07 16:47:58 CEST 2020 +VERSION_CODE=2310