diff --git a/.gitmodules b/.gitmodules index 07770fff..5daf7267 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,7 @@ path = gn_mobile_core url = https://github.com/PnX-SI/gn_mobile_core.git branch = develop +[submodule "gn_mobile_maps"] + path = gn_mobile_maps + url = https://github.com/PnX-SI/gn_mobile_maps.git + branch = develop \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 5801bf00..b55e8d05 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,5 +1,12 @@ + diff --git a/.idea/vcs.xml b/.idea/vcs.xml index fdb32216..7b7b2fe7 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -3,5 +3,6 @@ + \ No newline at end of file diff --git a/README.md b/README.md index 012d0b4e..08bc00a8 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,19 @@ ## Upgrade git sub modules -Do **NOT** modify directly `commons` and `viewpager`. Any changes should be made from -[gn_mobile_core](https://github.com/PnX-SI/gn_mobile_core) git repository. +Do **NOT** modify directly any git sub modules (e.g. `commons`, `viewpager` and `maps`). +Any changes should be made from each underlying git repository: + +* `commons`: [gn_mobile_core](https://github.com/PnX-SI/gn_mobile_core) git repository +* `viewpager`: [gn_mobile_core](https://github.com/PnX-SI/gn_mobile_core) git repository +* `maps`: [gn_mobile_maps](https://github.com/PnX-SI/gn_mobile_maps) git repository ```bash ./upgrade_submodules.sh -``` \ No newline at end of file +``` + +## Troubleshooting + +* Kotlin error, Redeclaration from class within imported module: + + clean project from menu *Build -> Clean Project*, then rebuild project. diff --git a/build.gradle b/build.gradle index 93172c6d..fc0b5243 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.31' + ext.kotlin_version = '1.3.41' repositories { google() @@ -10,7 +10,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.4.1' + classpath 'com.android.tools.build:gradle:3.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gn_mobile_core b/gn_mobile_core index bda13bfa..983793e6 160000 --- a/gn_mobile_core +++ b/gn_mobile_core @@ -1 +1 @@ -Subproject commit bda13bfa539cbe61a8bc357e34cf31a2436415eb +Subproject commit 983793e6317734a05c6c390ab5eab555e27672f9 diff --git a/gn_mobile_maps b/gn_mobile_maps new file mode 160000 index 00000000..30654751 --- /dev/null +++ b/gn_mobile_maps @@ -0,0 +1 @@ +Subproject commit 306547510f982ad61631843cf81a3298c35e0583 diff --git a/gn_mobile_occtax.iml b/gn_mobile_occtax.iml index 7ece0648..8facbe76 100644 --- a/gn_mobile_occtax.iml +++ b/gn_mobile_occtax.iml @@ -8,7 +8,7 @@ - + diff --git a/maps b/maps new file mode 120000 index 00000000..15f2186d --- /dev/null +++ b/maps @@ -0,0 +1 @@ +gn_mobile_maps/maps \ No newline at end of file diff --git a/occtax/build.gradle b/occtax/build.gradle index be2f8364..c6b563f9 100644 --- a/occtax/build.gradle +++ b/occtax/build.gradle @@ -2,11 +2,16 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' -version = "0.0.7" +version = "0.1.2" android { compileSdkVersion 28 + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { applicationId "fr.geonature.occtax" minSdkVersion 21 @@ -43,15 +48,16 @@ dependencies { implementation project(':commons') implementation project(':viewpager') + implementation project(':maps') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1" - implementation 'androidx.core:core-ktx:1.2.0-alpha01' + implementation 'androidx.core:core-ktx:1.2.0-alpha02' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'com.google.android.material:material:1.0.0' - implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha06' + implementation 'androidx.recyclerview:recyclerview:1.1.0-beta01' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.preference:preference:1.0.0' implementation 'com.l4digital.fastscroll:fastscroll:2.0.1' diff --git a/occtax/src/main/java/fr/geonature/occtax/input/Input.kt b/occtax/src/main/java/fr/geonature/occtax/input/Input.kt index 000478b2..baee058b 100644 --- a/occtax/src/main/java/fr/geonature/occtax/input/Input.kt +++ b/occtax/src/main/java/fr/geonature/occtax/input/Input.kt @@ -4,6 +4,7 @@ import android.os.Parcel import android.os.Parcelable import fr.geonature.commons.input.AbstractInput import fr.geonature.commons.input.AbstractInputTaxon +import org.locationtech.jts.geom.Geometry /** * Describes a current input. @@ -12,8 +13,20 @@ import fr.geonature.commons.input.AbstractInputTaxon */ class Input : AbstractInput { + var geometry: Geometry? = null + constructor() : super("occtax") - constructor(source: Parcel) : super(source) + constructor(source: Parcel) : super(source) { + this.geometry = source.readSerializable() as Geometry? + } + + override fun writeToParcel(dest: Parcel, + flags: Int) { + super.writeToParcel(dest, + flags) + + dest.writeSerializable(geometry) + } override fun getTaxaFromParcel(source: Parcel): List { val inputTaxa = source.createTypedArrayList(InputTaxon.CREATOR) diff --git a/occtax/src/main/java/fr/geonature/occtax/input/io/OnInputJsonReaderListenerImpl.kt b/occtax/src/main/java/fr/geonature/occtax/input/io/OnInputJsonReaderListenerImpl.kt index 8f42431b..f7b967da 100644 --- a/occtax/src/main/java/fr/geonature/occtax/input/io/OnInputJsonReaderListenerImpl.kt +++ b/occtax/src/main/java/fr/geonature/occtax/input/io/OnInputJsonReaderListenerImpl.kt @@ -5,6 +5,7 @@ import android.util.JsonToken import fr.geonature.commons.input.AbstractInput import fr.geonature.commons.input.io.InputJsonReader import fr.geonature.commons.util.IsoDateUtils +import fr.geonature.maps.jts.geojson.io.GeoJsonReader import fr.geonature.occtax.input.Input import fr.geonature.occtax.input.InputTaxon import java.util.Date @@ -14,15 +15,17 @@ import java.util.Date * * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) */ -class OnInputJsonReaderListenerImpl : InputJsonReader.OnInputJsonReaderListener { +class OnInputJsonReaderListenerImpl : InputJsonReader.OnInputJsonReaderListener { - override fun createInput(): AbstractInput { + private val geoJsonReader = GeoJsonReader() + + override fun createInput(): Input { return Input() } override fun readAdditionalInputData(reader: JsonReader, keyName: String, - input: AbstractInput) { + input: Input) { when (keyName) { "geometry" -> readGeometry(reader, input) @@ -33,12 +36,14 @@ class OnInputJsonReaderListenerImpl : InputJsonReader.OnInputJsonReaderListener } private fun readGeometry(reader: JsonReader, - input: AbstractInput) { - reader.beginObject() - - // TODO: read geometry object - - reader.endObject() + input: Input) { + when (reader.peek()) { + JsonToken.NULL -> reader.nextNull() + JsonToken.BEGIN_OBJECT -> { + input.geometry = geoJsonReader.readGeometry(reader) + } + else -> reader.skipValue() + } } private fun readProperties(reader: JsonReader, diff --git a/occtax/src/main/java/fr/geonature/occtax/input/io/OnInputJsonWriterListenerImpl.kt b/occtax/src/main/java/fr/geonature/occtax/input/io/OnInputJsonWriterListenerImpl.kt index f8d2c483..6d4cf91b 100644 --- a/occtax/src/main/java/fr/geonature/occtax/input/io/OnInputJsonWriterListenerImpl.kt +++ b/occtax/src/main/java/fr/geonature/occtax/input/io/OnInputJsonWriterListenerImpl.kt @@ -1,20 +1,23 @@ package fr.geonature.occtax.input.io import android.util.JsonWriter -import fr.geonature.commons.input.AbstractInput import fr.geonature.commons.input.AbstractInputTaxon import fr.geonature.commons.input.io.InputJsonWriter import fr.geonature.commons.util.IsoDateUtils +import fr.geonature.maps.jts.geojson.io.GeoJsonWriter +import fr.geonature.occtax.input.Input /** * Default implementation of [InputJsonWriter.OnInputJsonWriterListener]. * * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) */ -class OnInputJsonWriterListenerImpl : InputJsonWriter.OnInputJsonWriterListener { +class OnInputJsonWriterListenerImpl : InputJsonWriter.OnInputJsonWriterListener { + + private val geoJsonWriter = GeoJsonWriter() override fun writeAdditionalInputData(writer: JsonWriter, - input: AbstractInput) { + input: Input) { writeGeometry(writer, input) writeProperties(writer, @@ -22,17 +25,22 @@ class OnInputJsonWriterListenerImpl : InputJsonWriter.OnInputJsonWriterListener } private fun writeGeometry(writer: JsonWriter, - input: AbstractInput) { + input: Input) { writer.name("geometry") - .beginObject() - // TODO: write geometry object + val geometry = input.geometry - writer.endObject() + if (geometry == null) { + writer.nullValue() + } + else { + geoJsonWriter.writeGeometry(writer, + geometry) + } } private fun writeProperties(writer: JsonWriter, - input: AbstractInput) { + input: Input) { writer.name("properties") .beginObject() @@ -54,7 +62,7 @@ class OnInputJsonWriterListenerImpl : InputJsonWriter.OnInputJsonWriterListener } private fun writeDate(writer: JsonWriter, - input: AbstractInput) { + input: Input) { val dateToIsoString = IsoDateUtils.toIsoDateString(input.date) writer.name("date_min") .value(dateToIsoString) @@ -63,7 +71,7 @@ class OnInputJsonWriterListenerImpl : InputJsonWriter.OnInputJsonWriterListener } private fun writeInputObserverIds(writer: JsonWriter, - input: AbstractInput) { + input: Input) { writer.name("observers") .beginArray() @@ -74,7 +82,7 @@ class OnInputJsonWriterListenerImpl : InputJsonWriter.OnInputJsonWriterListener } private fun writeInputTaxa(writer: JsonWriter, - input: AbstractInput) { + input: Input) { writer.name("t_occurrences_occtax") .beginArray() diff --git a/occtax/src/main/java/fr/geonature/occtax/settings/AppSettings.kt b/occtax/src/main/java/fr/geonature/occtax/settings/AppSettings.kt new file mode 100644 index 00000000..0b744caa --- /dev/null +++ b/occtax/src/main/java/fr/geonature/occtax/settings/AppSettings.kt @@ -0,0 +1,53 @@ +package fr.geonature.occtax.settings + +import android.os.Parcel +import android.os.Parcelable +import fr.geonature.commons.settings.IAppSettings +import fr.geonature.maps.settings.MapSettings + +/** + * Global internal settings. + * + * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) + */ +data class AppSettings(var mapSettings: MapSettings? = null) : IAppSettings { + + private constructor(source: Parcel) : this(source.readParcelable(MapSettings::class.java.classLoader) as MapSettings) + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel?, + flags: Int) { + dest?.writeParcelable( + mapSettings, + 0 + ) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AppSettings + + if (mapSettings != other.mapSettings) return false + + return true + } + + override fun hashCode(): Int { + return mapSettings.hashCode() + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): AppSettings { + return AppSettings(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/occtax/src/main/java/fr/geonature/occtax/settings/io/OnAppSettingsJsonReaderListenerImpl.kt b/occtax/src/main/java/fr/geonature/occtax/settings/io/OnAppSettingsJsonReaderListenerImpl.kt new file mode 100644 index 00000000..355be54f --- /dev/null +++ b/occtax/src/main/java/fr/geonature/occtax/settings/io/OnAppSettingsJsonReaderListenerImpl.kt @@ -0,0 +1,40 @@ +package fr.geonature.occtax.settings.io + +import android.util.JsonReader +import android.util.JsonToken +import fr.geonature.commons.settings.io.AppSettingsJsonReader +import fr.geonature.maps.settings.io.MapSettingsReader +import fr.geonature.occtax.settings.AppSettings + +/** + * Default implementation of [AppSettingsJsonReader.OnAppSettingsJsonReaderListener]. + * + * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) + */ +class OnAppSettingsJsonReaderListenerImpl : AppSettingsJsonReader.OnAppSettingsJsonReaderListener { + + override fun createAppSettings(): AppSettings { + return AppSettings() + } + + override fun readAdditionalAppSettingsData(reader: JsonReader, + keyName: String, + appSettings: AppSettings) { + when (keyName) { + "map" -> { + if (reader.peek() == JsonToken.BEGIN_OBJECT) { + readMapSettings(reader, + appSettings) + } + else { + reader.skipValue() + } + } + } + } + + private fun readMapSettings(reader: JsonReader, + appSettings: AppSettings) { + appSettings.mapSettings = MapSettingsReader().read(reader) + } +} \ No newline at end of file diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeActivity.kt b/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeActivity.kt index f27df65e..532b1949 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeActivity.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeActivity.kt @@ -2,6 +2,13 @@ package fr.geonature.occtax.ui.home import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import fr.geonature.commons.input.InputManager +import fr.geonature.commons.settings.AppSettingsManager +import fr.geonature.occtax.input.Input +import fr.geonature.occtax.input.io.OnInputJsonReaderListenerImpl +import fr.geonature.occtax.input.io.OnInputJsonWriterListenerImpl +import fr.geonature.occtax.settings.AppSettings +import fr.geonature.occtax.settings.io.OnAppSettingsJsonReaderListenerImpl import fr.geonature.occtax.ui.input.InputPagerFragmentActivity import fr.geonature.occtax.ui.settings.PreferencesActivity import fr.geonature.occtax.util.IntentUtils @@ -14,19 +21,36 @@ import fr.geonature.occtax.util.IntentUtils * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) */ class HomeActivity : AppCompatActivity(), - HomeFragment.OnHomeFragmentFragmentListener { + HomeFragment.OnHomeFragmentListener { + + private lateinit var inputManager: InputManager + private lateinit var appSettingsManager: AppSettingsManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + inputManager = InputManager(application, + OnInputJsonReaderListenerImpl(), + OnInputJsonWriterListenerImpl()) + appSettingsManager = AppSettingsManager(application, + OnAppSettingsJsonReaderListenerImpl()) + if (savedInstanceState == null) { supportFragmentManager.beginTransaction() - .replace(android.R.id.content, - HomeFragment.newInstance()) - .commit() + .replace(android.R.id.content, + HomeFragment.newInstance()) + .commit() } } + override fun getInputManager(): InputManager { + return inputManager + } + + override fun getAppSettingsManager(): AppSettingsManager { + return appSettingsManager + } + override fun onShowSettings() { startActivity(PreferencesActivity.newIntent(this)) } @@ -35,7 +59,10 @@ class HomeActivity : AppCompatActivity(), startActivity(IntentUtils.syncActivity(this)) } - override fun onStartInput() { - startActivity(InputPagerFragmentActivity.newIntent(this)) + override fun onStartInput(appSettings: AppSettings, + input: Input?) { + startActivity(InputPagerFragmentActivity.newIntent(this, + appSettings, + input)) } } diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeFragment.kt index 89c19f5e..d8f9c1c2 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeFragment.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeFragment.kt @@ -1,23 +1,47 @@ package fr.geonature.occtax.ui.home +import android.Manifest import android.content.Context import android.database.Cursor import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import android.view.animation.AnimationUtils import androidx.core.os.bundleOf +import androidx.core.util.Pair import androidx.fragment.app.Fragment import androidx.loader.app.LoaderManager import androidx.loader.content.CursorLoader import androidx.loader.content.Loader +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.snackbar.Snackbar import fr.geonature.commons.data.AppSync import fr.geonature.commons.data.Provider.buildUri -import fr.geonature.occtax.ui.settings.PreferencesFragment +import fr.geonature.commons.input.InputManager +import fr.geonature.commons.settings.AppSettingsManager +import fr.geonature.commons.util.PermissionUtils.OnCheckSelfPermissionListener +import fr.geonature.commons.util.PermissionUtils.checkPermissions +import fr.geonature.commons.util.PermissionUtils.checkSelfPermissions +import fr.geonature.commons.util.PermissionUtils.requestPermissions +import fr.geonature.occtax.R +import fr.geonature.occtax.input.Input +import fr.geonature.occtax.settings.AppSettings import fr.geonature.occtax.ui.shared.view.ListItemActionView +import kotlinx.android.synthetic.main.fragment_home.appSyncView +import kotlinx.android.synthetic.main.fragment_home.fab +import kotlinx.android.synthetic.main.fragment_home.homeContent +import kotlinx.android.synthetic.main.fragment_home.inputEmptyTextView +import kotlinx.android.synthetic.main.fragment_home.inputRecyclerView +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch /** * Home screen [Fragment]. @@ -26,8 +50,11 @@ import fr.geonature.occtax.ui.shared.view.ListItemActionView */ class HomeFragment : Fragment() { - private var listener: OnHomeFragmentFragmentListener? = null - private lateinit var appSyncView: AppSyncView + private var listener: OnHomeFragmentListener? = null + private lateinit var adapter: InputRecyclerViewAdapter + private var appSettings: AppSettings? = null + private var selectedInputToDelete: Pair? = null + private var requestPermissionsResult = true private val loaderCallbacks = object : LoaderManager.LoaderCallbacks { override fun onCreateLoader( @@ -36,7 +63,8 @@ class HomeFragment : Fragment() { when (id) { LOADER_APP_SYNC -> return CursorLoader(requireContext(), buildUri(AppSync.TABLE_NAME, - args!!.getString(AppSync.COLUMN_ID)!!), + args?.getString(AppSync.COLUMN_ID) + ?: ""), arrayOf(AppSync.COLUMN_ID, AppSync.COLUMN_LAST_SYNC, AppSync.COLUMN_INPUTS_TO_SYNCHRONIZE), @@ -51,7 +79,12 @@ class HomeFragment : Fragment() { loader: Loader, data: Cursor?) { - if (data == null) return + if (data == null) { + Log.w(TAG, + "Failed to load data from '${(loader as CursorLoader).uri}'") + + return + } when (loader.id) { LOADER_APP_SYNC -> { @@ -67,11 +100,23 @@ class HomeFragment : Fragment() { } } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val selectedInputPositionToDelete = savedInstanceState?.getInt(STATE_SELECTED_INPUT_POSITION_TO_DELETE) + val selectedInputToDelete = savedInstanceState?.getParcelable(STATE_SELECTED_INPUT_TO_DELETE) + + if (selectedInputPositionToDelete != null && selectedInputToDelete != null) { + this.selectedInputToDelete = Pair.create(selectedInputPositionToDelete, + selectedInputToDelete) + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return inflater.inflate(fr.geonature.occtax.R.layout.home_fragment, + return inflater.inflate(R.layout.fragment_home, container, false) } @@ -84,25 +129,130 @@ class HomeFragment : Fragment() { setHasOptionsMenu(true) - appSyncView = view.findViewById(fr.geonature.occtax.R.id.appSyncView) appSyncView.setListener(object : ListItemActionView.OnListItemActionViewListener { override fun onAction() { listener?.onStartSync() } }) - view.findViewById(fr.geonature.occtax.R.id.fab) - .setOnClickListener { listener?.onStartInput() } + fab.setOnClickListener { + val appSettings = appSettings ?: return@setOnClickListener + listener?.onStartInput(appSettings) + } + + adapter = InputRecyclerViewAdapter(object : InputRecyclerViewAdapter.OnInputRecyclerViewAdapterListener { + override fun onInputClicked(input: Input) { + val appSettings = appSettings ?: return + + Log.i(TAG, + "input selected: ${input.id}") + + listener?.onStartInput(appSettings, + input) + } + + override fun onInputLongClicked(position: Int, + input: Input) { + selectedInputToDelete = Pair.create(position, + input) + + GlobalScope.launch(Dispatchers.Main) { + listener?.getInputManager() + ?.deleteInput(input.id) + (inputRecyclerView.adapter as InputRecyclerViewAdapter).remove(input) + } + + Snackbar.make(homeContent, + R.string.home_snackbar_input_deleted, + Snackbar.LENGTH_SHORT) + .setAction(R.string.home_snackbar_input_undo + ) { + GlobalScope.launch(Dispatchers.Main) { + val inputToRestore = selectedInputToDelete?.second + + if (inputToRestore != null) { + listener?.getInputManager() + ?.saveInput(inputToRestore) + (inputRecyclerView.adapter as InputRecyclerViewAdapter).addInput(inputToRestore, + selectedInputToDelete?.first) + } + + selectedInputToDelete = null + } + } + .show() + } + }) + adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onChanged() { + super.onChanged() + + showEmptyTextView(adapter.itemCount == 0) + } + + override fun onItemRangeChanged(positionStart: Int, + itemCount: Int) { + super.onItemRangeChanged(positionStart, + itemCount) + + showEmptyTextView(adapter.itemCount == 0) + } + + override fun onItemRangeInserted(positionStart: Int, + itemCount: Int) { + super.onItemRangeInserted(positionStart, + itemCount) + + showEmptyTextView(false) + } + }) + + with(inputRecyclerView) { + layoutManager = LinearLayoutManager(context) + adapter = this@HomeFragment.adapter + } + + val dividerItemDecoration = DividerItemDecoration(inputRecyclerView.context, + (inputRecyclerView.layoutManager as LinearLayoutManager).orientation) + inputRecyclerView.addItemDecoration(dividerItemDecoration) + } + + override fun onResume() { + super.onResume() + + LoaderManager.getInstance(this) + .initLoader(LOADER_APP_SYNC, + bundleOf(AppSync.COLUMN_ID to requireContext().packageName), + loaderCallbacks) + + if (requestPermissionsResult) { + val context = context ?: return + checkSelfPermissions(context, + object : OnCheckSelfPermissionListener { + override fun onPermissionsGranted() { + loadAppSettings() + } + + override fun onRequestPermissions(vararg permissions: String) { + requestPermissions(this@HomeFragment, + homeContent, + R.string.snackbar_permission_external_storage_rationale, + REQUEST_EXTERNAL_STORAGE, + *permissions) + } + }, + Manifest.permission.WRITE_EXTERNAL_STORAGE) + } } override fun onAttach(context: Context) { super.onAttach(context) - if (context is OnHomeFragmentFragmentListener) { + if (context is OnHomeFragmentListener) { listener = context } else { - throw RuntimeException("$context must implement OnHomeFragmentFragmentListener") + throw RuntimeException("$context must implement OnHomeFragmentListener") } } @@ -112,28 +262,40 @@ class HomeFragment : Fragment() { listener = null } + override fun onSaveInstanceState(outState: Bundle) { + val selectedInputPositionToDelete = this.selectedInputToDelete?.first + val selectedInputToDelete = this.selectedInputToDelete?.second + + if (selectedInputPositionToDelete != null && selectedInputToDelete != null) { + outState.putInt(STATE_SELECTED_INPUT_POSITION_TO_DELETE, + selectedInputPositionToDelete) + outState.putParcelable(STATE_SELECTED_INPUT_TO_DELETE, + selectedInputToDelete) + } + + super.onSaveInstanceState(outState) + } + override fun onCreateOptionsMenu( menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(fr.geonature.occtax.R.menu.settings, + inflater.inflate(R.menu.settings, menu) } - /* override fun onPrepareOptionsMenu(menu: Menu) { super.onPrepareOptionsMenu(menu) val menuItemSettings = menu.findItem(R.id.menu_settings) - menuItemSettings.isEnabled = true + menuItemSettings.isEnabled = appSettings != null } - */ override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { - fr.geonature.occtax.R.id.menu_settings -> { + R.id.menu_settings -> { listener?.onShowSettings() true } @@ -141,32 +303,102 @@ class HomeFragment : Fragment() { } } - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onRequestPermissionsResult(requestCode: Int, + permissions: Array, + grantResults: IntArray) { + when (requestCode) { + REQUEST_EXTERNAL_STORAGE -> { + requestPermissionsResult = checkPermissions(grantResults) - LoaderManager.getInstance(this) - .initLoader(LOADER_APP_SYNC, - bundleOf(AppSync.COLUMN_ID to requireContext().packageName), - loaderCallbacks) + if (requestPermissionsResult) { + Snackbar.make(homeContent, + R.string.snackbar_permission_external_storage_available, + Snackbar.LENGTH_LONG) + .show() + } + else { + Snackbar.make(homeContent, + R.string.snackbar_permissions_not_granted, + Snackbar.LENGTH_LONG) + .show() + } + } + else -> super.onRequestPermissionsResult(requestCode, + permissions, + grantResults) + } + } + + private fun loadAppSettings() { + GlobalScope.launch(Dispatchers.Main) { + appSettings = listener?.getAppSettingsManager() + ?.loadAppSettings() + + if (appSettings == null) { + fab.hide() + adapter.clear() + activity?.invalidateOptionsMenu() + + Snackbar.make(homeContent, + getString(R.string.snackbar_settings_not_found, + listener?.getAppSettingsManager()?.getAppSettingsFilename()), + Snackbar.LENGTH_LONG) + .show() + } + else { + fab.show() + activity?.invalidateOptionsMenu() + + val inputs: List = listener?.getInputManager()?.readInputs() + ?: emptyList() + (inputRecyclerView.adapter as InputRecyclerViewAdapter).setInputs(inputs) + } + } + } + + private fun showEmptyTextView(show: Boolean) { + if (inputEmptyTextView.visibility == View.VISIBLE == show) { + return + } + + if (show) { + inputEmptyTextView.startAnimation(AnimationUtils.loadAnimation(context, + android.R.anim.fade_in)) + inputEmptyTextView.visibility = View.VISIBLE + + } + else { + inputEmptyTextView.startAnimation(AnimationUtils.loadAnimation(context, + android.R.anim.fade_out)) + inputEmptyTextView.visibility = View.GONE + } } /** - * Callback used by [PreferencesFragment]. + * Callback used by [HomeFragment]. */ - interface OnHomeFragmentFragmentListener { + interface OnHomeFragmentListener { + fun getInputManager(): InputManager + fun getAppSettingsManager(): AppSettingsManager fun onShowSettings() fun onStartSync() - fun onStartInput() + fun onStartInput(appSettings: AppSettings, + input: Input? = null) } companion object { + private val TAG = HomeFragment::class.java.name private const val LOADER_APP_SYNC = 1 + private const val STATE_SELECTED_INPUT_POSITION_TO_DELETE = "state_selected_input_position_to_delete" + private const val STATE_SELECTED_INPUT_TO_DELETE = "state_selected_input_to_delete" + private const val REQUEST_EXTERNAL_STORAGE = 0 /** * Use this factory method to create a new instance of [HomeFragment]. * * @return A new instance of [HomeFragment] */ + @JvmStatic fun newInstance() = HomeFragment() } } diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/home/InputRecyclerViewAdapter.kt b/occtax/src/main/java/fr/geonature/occtax/ui/home/InputRecyclerViewAdapter.kt new file mode 100644 index 00000000..e8380142 --- /dev/null +++ b/occtax/src/main/java/fr/geonature/occtax/ui/home/InputRecyclerViewAdapter.kt @@ -0,0 +1,107 @@ +package fr.geonature.occtax.ui.home + +import android.text.format.DateFormat +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import fr.geonature.occtax.R +import fr.geonature.occtax.input.Input + +/** + * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) + */ +class InputRecyclerViewAdapter(private val listener: OnInputRecyclerViewAdapterListener) : RecyclerView.Adapter() { + private val inputs: MutableList = mutableListOf() + + override fun onCreateViewHolder(parent: ViewGroup, + viewType: Int): ViewHolder { + return ViewHolder(parent) + } + + override fun getItemCount(): Int { + return inputs.size + } + + override fun onBindViewHolder(holder: ViewHolder, + position: Int) { + holder.bind(inputs[position]) + } + + fun setInputs(inputs: List) { + this.inputs.clear() + this.inputs.addAll(inputs) + + notifyDataSetChanged() + } + + fun addInput(input: Input, + index: Int? = null) { + if (index == null) { + this.inputs.add(input) + notifyItemRangeInserted(this.inputs.size - 1, + 1) + } + else { + this.inputs.add(index, + input) + notifyItemRangeChanged(index, + this.inputs.size - index) + } + } + + fun clear() { + this.inputs.clear() + notifyDataSetChanged() + } + + fun remove(input: Input) { + val inputPosition = this.inputs.indexOf(input) + this.inputs.remove(input) + notifyItemRemoved(inputPosition) + + if (this.inputs.isEmpty()) { + notifyDataSetChanged() + } + } + + inner class ViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.list_item_2, + parent, + false)) { + + private val text1: TextView = itemView.findViewById(android.R.id.text1) + + fun bind(input: Input) { + text1.text = itemView.context.getString(R.string.home_input_created_at, + DateFormat.format(itemView.context.getString(R.string.home_input_date), + input.date)) + itemView.setOnClickListener { listener.onInputClicked(input) } + itemView.setOnLongClickListener { + listener.onInputLongClicked(inputs.indexOf(input), + input) + true + } + } + } + + /** + * Callback used by [InputRecyclerViewAdapter]. + */ + interface OnInputRecyclerViewAdapterListener { + + /** + * Called when a [Input] has been clicked. + * + * @param input the selected [Input] + */ + fun onInputClicked(input: Input) + + /** + * Called when a [Input] has been clicked and held. + * + * @param input the selected [Input] + */ + fun onInputLongClicked(position: Int, + input: Input) + } +} \ No newline at end of file diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/InputPagerFragmentActivity.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/InputPagerFragmentActivity.kt index 60b135ea..c7acba01 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/InputPagerFragmentActivity.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/InputPagerFragmentActivity.kt @@ -10,6 +10,8 @@ import fr.geonature.occtax.R import fr.geonature.occtax.input.Input import fr.geonature.occtax.input.io.OnInputJsonReaderListenerImpl import fr.geonature.occtax.input.io.OnInputJsonWriterListenerImpl +import fr.geonature.occtax.settings.AppSettings +import fr.geonature.occtax.ui.input.map.InputMapFragment import fr.geonature.occtax.ui.input.observers.ObserversAndDateInputFragment import fr.geonature.occtax.ui.input.taxa.TaxaFragment import fr.geonature.viewpager.ui.AbstractNavigationHistoryPagerFragmentActivity @@ -26,7 +28,8 @@ import kotlinx.coroutines.launch */ class InputPagerFragmentActivity : AbstractNavigationHistoryPagerFragmentActivity() { - private lateinit var inputManager: InputManager + private lateinit var inputManager: InputManager + private lateinit var appSettings: AppSettings private lateinit var input: Input override fun onCreate(savedInstanceState: Bundle?) { @@ -36,7 +39,20 @@ class InputPagerFragmentActivity : AbstractNavigationHistoryPagerFragmentActivit OnInputJsonReaderListenerImpl(), OnInputJsonWriterListenerImpl()) - readCurrentInput() + appSettings = intent.getParcelableExtra(EXTRA_APP_SETTINGS) + input = intent.getParcelableExtra(EXTRA_INPUT) ?: Input() + val lastAddedInputTaxon = input.getLastAddedInputTaxon() + + if (lastAddedInputTaxon != null) { + input.setCurrentSelectedInputTaxonId(lastAddedInputTaxon.id) + } + + Log.i(TAG, + "loading input: ${input.id}") + + GlobalScope.launch(Dispatchers.Main) { + pagerManager.load(input.id) + } } override fun onPause() { @@ -51,6 +67,8 @@ class InputPagerFragmentActivity : AbstractNavigationHistoryPagerFragmentActivit get() = LinkedHashMap().apply { put(R.string.pager_fragment_observers_and_date_input_title, ObserversAndDateInputFragment.newInstance()) + put(R.string.pager_fragment_map_title, + InputMapFragment.newInstance(appSettings.mapSettings!!)) put(R.string.pager_fragment_taxa_title, TaxaFragment.newInstance()) } @@ -69,19 +87,6 @@ class InputPagerFragmentActivity : AbstractNavigationHistoryPagerFragmentActivit setInputToCurrentPage() } - private fun readCurrentInput() { - GlobalScope.launch(Dispatchers.Main) { - val input = inputManager.readCurrentInput() ?: Input() - - Log.i(TAG, "loading input: ${input.id}") - - pagerHelper.load(input.id) - - this@InputPagerFragmentActivity.input = input as Input - this@InputPagerFragmentActivity.setInputToCurrentPage() - } - } - private fun setInputToCurrentPage() { val pageFragment = getCurrentPageFragment() @@ -96,9 +101,19 @@ class InputPagerFragmentActivity : AbstractNavigationHistoryPagerFragmentActivit private val TAG = InputPagerFragmentActivity::class.java.name - fun newIntent(context: Context): Intent { + private const val EXTRA_APP_SETTINGS = "extra_app_settings" + private const val EXTRA_INPUT = "extra_input" + + fun newIntent(context: Context, + appSettings: AppSettings, + input: Input? = null): Intent { return Intent(context, - InputPagerFragmentActivity::class.java) + InputPagerFragmentActivity::class.java).apply { + putExtra(EXTRA_APP_SETTINGS, + appSettings) + putExtra(EXTRA_INPUT, + input) + } } } } diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/map/InputMapFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/map/InputMapFragment.kt new file mode 100644 index 00000000..4fa94575 --- /dev/null +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/map/InputMapFragment.kt @@ -0,0 +1,90 @@ +package fr.geonature.occtax.ui.input.map + +import android.os.Bundle +import androidx.fragment.app.Fragment +import fr.geonature.commons.input.AbstractInput +import fr.geonature.maps.jts.geojson.GeometryUtils.fromPoint +import fr.geonature.maps.jts.geojson.GeometryUtils.toPoint +import fr.geonature.maps.settings.MapSettings +import fr.geonature.maps.ui.MapFragment +import fr.geonature.maps.ui.widget.EditFeatureButton +import fr.geonature.occtax.R +import fr.geonature.occtax.input.Input +import fr.geonature.occtax.ui.input.IInputFragment +import fr.geonature.viewpager.ui.AbstractPagerFragmentActivity +import fr.geonature.viewpager.ui.IValidateFragment +import org.locationtech.jts.geom.Point +import org.osmdroid.util.GeoPoint +import org.osmdroid.views.MapView + +/** + * Simple [Fragment] embedding a [MapView] instance to edit a single POI on the map. + * + * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) + */ +class InputMapFragment : MapFragment(), + IValidateFragment, + IInputFragment { + + private var input: Input? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + onSelectedPOIsListener = object : OnSelectedPOIsListener { + override fun onSelectedPOIs(pois: List) { + if (pois.isNotEmpty()) { + input?.geometry = toPoint(pois[0]) + } + + (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage() + } + } + } + + override fun getResourceTitle(): Int { + return R.string.pager_fragment_map_title + } + + override fun pagingEnabled(): Boolean { + return false + } + + override fun validate(): Boolean { + return this.input?.geometry != null + } + + override fun refreshView() { + val geometry = input?.geometry ?: return + + if (geometry is Point) { + setSelectedPOIs(listOf(fromPoint(geometry))) + } + } + + override fun setInput(input: AbstractInput) { + this.input = input as Input + } + + companion object { + + /** + * Use this factory method to create a new instance of [InputMapFragment]. + * + * @return A new instance of [InputMapFragment] + */ + @JvmStatic + fun newInstance(mapSettings: MapSettings) = InputMapFragment().apply { + arguments = Bundle().apply { + putParcelable( + ARG_MAP_SETTINGS, + mapSettings + ) + putSerializable( + ARG_EDIT_MODE, + EditFeatureButton.EditMode.SINGLE + ) + } + } + } +} diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt index ad0c25a1..59fd27a0 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt @@ -5,6 +5,7 @@ import android.content.Intent import android.database.Cursor import android.os.Bundle import android.text.format.DateFormat +import android.util.Log import android.util.Pair import android.view.LayoutInflater import android.view.View @@ -71,7 +72,11 @@ class ObserversAndDateInputFragment : Fragment(), loader: Loader, data: Cursor?) { - if (data == null) return + if (data == null) { + Log.w(TAG, "Failed to load data from '${(loader as CursorLoader).uri}'") + + return + } when (loader.id) { LOADER_OBSERVERS_IDS -> { @@ -221,6 +226,7 @@ class ObserversAndDateInputFragment : Fragment(), companion object { + private val TAG = ObserversAndDateInputFragment::class.java.name private const val DATE_PICKER_DIALOG_FRAGMENT = "date_picker_dialog_fragment" private const val LOADER_OBSERVERS_IDS = 1 private const val KEY_SELECTED_INPUT_OBSERVER_IDS = "selected_input_observer_ids" diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaFragment.kt index 66ec0214..b742c04a 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaFragment.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaFragment.kt @@ -2,6 +2,7 @@ package fr.geonature.occtax.ui.input.taxa import android.database.Cursor import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater @@ -86,7 +87,11 @@ class TaxaFragment : Fragment(), override fun onLoadFinished(loader: Loader, data: Cursor?) { - if (data == null) return + if (data == null) { + Log.w(TAG, "Failed to load data from '${(loader as CursorLoader).uri}'") + + return + } when (loader.id) { LOADER_TAXA -> adapter?.bind(data) @@ -250,6 +255,7 @@ class TaxaFragment : Fragment(), companion object { + private val TAG = TaxaFragment::class.java.name private const val LOADER_TAXA = 1 private const val LOADER_TAXON = 2 private const val KEY_FILTER = "filter" diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaRecyclerViewAdapter.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaRecyclerViewAdapter.kt index 90c6a427..a760d537 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaRecyclerViewAdapter.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaRecyclerViewAdapter.kt @@ -70,11 +70,12 @@ class TaxaRecyclerViewAdapter(private val listener: OnTaxaRecyclerViewAdapterLis val name = taxon.name ?: return "" return name.elementAt(0) - .toString() + .toString() } fun setSelectedTaxon(selectedTaxon: Taxon) { this.selectedTaxon = selectedTaxon + scrollToFirstItemSelected() notifyDataSetChanged() } @@ -119,7 +120,7 @@ class TaxaRecyclerViewAdapter(private val listener: OnTaxaRecyclerViewAdapterLis } } - inner class ViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.list_title_taxon, + inner class ViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.list_title_item_2, parent, false)) { @@ -138,8 +139,8 @@ class TaxaRecyclerViewAdapter(private val listener: OnTaxaRecyclerViewAdapterLis val previousTitle = if (position > 0) { cursor.moveToPosition(position - 1) Taxon.fromCursor(cursor) - ?.name?.elementAt(0) - .toString() + ?.name?.elementAt(0) + .toString() } else { "" @@ -147,7 +148,7 @@ class TaxaRecyclerViewAdapter(private val listener: OnTaxaRecyclerViewAdapterLis if (taxon != null) { val currentTitle = taxon.name?.elementAt(0) - .toString() + .toString() title.text = if (previousTitle == currentTitle) "" else currentTitle text1.text = taxon.name text2.text = taxon.description diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/observers/InputObserverRecyclerViewAdapter.kt b/occtax/src/main/java/fr/geonature/occtax/ui/observers/InputObserverRecyclerViewAdapter.kt index 447e48d5..5b5810df 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/observers/InputObserverRecyclerViewAdapter.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/observers/InputObserverRecyclerViewAdapter.kt @@ -30,12 +30,15 @@ class InputObserverRecyclerViewAdapter(private val listener: OnInputObserverRecy val checkbox: CheckBox = v.findViewById(android.R.id.checkbox) val inputObserver = v.tag as InputObserver + val isSelected = selectedInputObservers.contains(inputObserver) if (isSingleChoice()) { + val selectedItemsPositions = selectedInputObservers.map { getItemPosition(it) } selectedInputObservers.clear() + selectedItemsPositions.forEach { notifyItemChanged(it) } } - if (selectedInputObservers.contains(inputObserver)) { + if (isSelected) { selectedInputObservers.remove(inputObserver) checkbox.isChecked = false } @@ -72,7 +75,7 @@ class InputObserverRecyclerViewAdapter(private val listener: OnInputObserverRecy val lastname = inputObserver.lastname ?: return "" return lastname.elementAt(0) - .toString() + .toString() } fun setChoiceMode(choiceMode: Int = ListView.CHOICE_MODE_SINGLE) { @@ -99,6 +102,28 @@ class InputObserverRecyclerViewAdapter(private val listener: OnInputObserverRecy notifyDataSetChanged() } + private fun getItemPosition(inputObserver: InputObserver?): Int { + var itemPosition = -1 + val cursor = cursor ?: return itemPosition + if (inputObserver == null) return itemPosition + + cursor.moveToFirst() + + while (!cursor.isAfterLast && itemPosition < 0) { + val currentInputObserver = InputObserver.fromCursor(cursor) + + if (inputObserver.id == currentInputObserver?.id) { + itemPosition = cursor.position + } + + cursor.moveToNext() + } + + cursor.moveToFirst() + + return itemPosition + } + private fun scrollToFirstItemSelected() { val cursor = cursor ?: return @@ -142,8 +167,8 @@ class InputObserverRecyclerViewAdapter(private val listener: OnInputObserverRecy val previousTitle = if (position > 0) { cursor.moveToPosition(position - 1) InputObserver.fromCursor(cursor) - ?.lastname?.elementAt(0) - .toString() + ?.lastname?.elementAt(0) + .toString() } else { "" @@ -151,7 +176,7 @@ class InputObserverRecyclerViewAdapter(private val listener: OnInputObserverRecy if (inputObserver != null) { val currentTitle = inputObserver.lastname?.elementAt(0) - .toString() + .toString() title.text = if (previousTitle == currentTitle) "" else currentTitle text1.text = inputObserver.lastname?.toUpperCase() text2.text = inputObserver.firstname diff --git a/occtax/src/main/res/layout/fragment_home.xml b/occtax/src/main/res/layout/fragment_home.xml new file mode 100644 index 00000000..5ba0b166 --- /dev/null +++ b/occtax/src/main/res/layout/fragment_home.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/occtax/src/main/res/layout/home_fragment.xml b/occtax/src/main/res/layout/home_fragment.xml deleted file mode 100644 index d8f57518..00000000 --- a/occtax/src/main/res/layout/home_fragment.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/occtax/src/main/res/layout/list_title_taxon.xml b/occtax/src/main/res/layout/list_title_taxon.xml deleted file mode 100644 index 0f227995..00000000 --- a/occtax/src/main/res/layout/list_title_taxon.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/occtax/src/main/res/values-fr/strings.xml b/occtax/src/main/res/values-fr/strings.xml index 50d3a38e..2cba44a1 100644 --- a/occtax/src/main/res/values-fr/strings.xml +++ b/occtax/src/main/res/values-fr/strings.xml @@ -1,5 +1,5 @@ - + Paramètres Observateurs @@ -13,6 +13,10 @@ Relevés en cours Aucun relevé en cours.\nCréer un nouveau relevé via le bouton +. + EEE dd MMM yyyy + Relevé créé le %s + Relevé supprimé + Annuler Aucune donnée Aucune sélection.\nAjouter en un via le bouton "Ajouter". @@ -29,7 +33,14 @@ OK Annuler + Erreur lors du chargement des paramètres \'%1$s\' + Les permissions n\'ont pas été accordées + Les permissions ont été accordées + L\'application requiert la permission d\'accéder au contenu de la mémoire de stockage + L\'accès à la mémoire de stockage a été accordée + Observateur & date + Pointage Taxons Observateurs diff --git a/occtax/src/main/res/values/strings.xml b/occtax/src/main/res/values/strings.xml index 899ce72b..b9f11bbe 100644 --- a/occtax/src/main/res/values/strings.xml +++ b/occtax/src/main/res/values/strings.xml @@ -17,6 +17,10 @@ Last inputs No existing input.\nCreate a new one by tapping the + button. + EEE dd MMM yyyy + Input created at %s + Input deleted + Undo No data No selected item.\nAdd one by tapping the "Add" button. @@ -33,7 +37,14 @@ OK Cancel + Unable to load settings \'%1$s\' + Permissions were not granted + Permissions were been granted + External storage permission is needed + External storage Permissions have been granted + Observers & date + Pointing Taxa Selected observers diff --git a/occtax/src/test/java/fr/geonature/occtax/input/io/InputJsonReaderTest.kt b/occtax/src/test/java/fr/geonature/occtax/input/io/InputJsonReaderTest.kt index 116d351c..a49d29d5 100644 --- a/occtax/src/test/java/fr/geonature/occtax/input/io/InputJsonReaderTest.kt +++ b/occtax/src/test/java/fr/geonature/occtax/input/io/InputJsonReaderTest.kt @@ -23,7 +23,7 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class InputJsonReaderTest { - private lateinit var inputJsonReader: InputJsonReader + private lateinit var inputJsonReader: InputJsonReader @Before fun setUp() { @@ -108,6 +108,7 @@ class InputJsonReaderTest { assertArrayEquals(longArrayOf(), input.getInputObserverIds() .toLongArray()) + assertNull(input.geometry) assertEquals(listOf(), input.getInputTaxa()) } diff --git a/occtax/src/test/java/fr/geonature/occtax/input/io/InputJsonWriterTest.kt b/occtax/src/test/java/fr/geonature/occtax/input/io/InputJsonWriterTest.kt index f63b5a0d..fba474c2 100644 --- a/occtax/src/test/java/fr/geonature/occtax/input/io/InputJsonWriterTest.kt +++ b/occtax/src/test/java/fr/geonature/occtax/input/io/InputJsonWriterTest.kt @@ -21,7 +21,7 @@ import java.util.Date @RunWith(RobolectricTestRunner::class) class InputJsonWriterTest { - private lateinit var inputJsonWriter: InputJsonWriter + private lateinit var inputJsonWriter: InputJsonWriter @Before fun setUp() { diff --git a/occtax/src/test/java/fr/geonature/occtax/settings/io/AppSettingsJsonReaderTest.kt b/occtax/src/test/java/fr/geonature/occtax/settings/io/AppSettingsJsonReaderTest.kt new file mode 100644 index 00000000..a7dc6cd8 --- /dev/null +++ b/occtax/src/test/java/fr/geonature/occtax/settings/io/AppSettingsJsonReaderTest.kt @@ -0,0 +1,97 @@ +package fr.geonature.occtax.settings.io + +import fr.geonature.commons.settings.io.AppSettingsJsonReader +import fr.geonature.maps.settings.LayerSettings +import fr.geonature.maps.settings.MapSettings +import fr.geonature.occtax.FixtureHelper.getFixture +import fr.geonature.occtax.settings.AppSettings +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.osmdroid.util.BoundingBox +import org.osmdroid.util.GeoPoint +import org.robolectric.RobolectricTestRunner + +/** + * Unit tests about [AppSettingsJsonReader]. + * + * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) + */ +@RunWith(RobolectricTestRunner::class) +class AppSettingsJsonReaderTest { + lateinit var appSettingsJsonReader: AppSettingsJsonReader + + @Before + fun setUp() { + appSettingsJsonReader = AppSettingsJsonReader(OnAppSettingsJsonReaderListenerImpl()) + } + + @Test + fun testReadAppSettingsFromJsonString() { + // given a JSON settings + val json = getFixture("settings_occtax.json") + + // when read the JSON as AppSettings + val appSettings = appSettingsJsonReader.read(json) + + // then + assertNotNull(appSettings) + assertEquals( + AppSettings( + MapSettings( + arrayListOf( + LayerSettings( + "Nantes", + "nantes.mbtiles" + ) + ), + null, + showScale = true, + showCompass = true, + zoom = 10.0, + minZoomLevel = 8.0, + maxZoomLevel = 19.0, + minZoomEditing = 12.0, + maxBounds = BoundingBox.fromGeoPoints( + arrayListOf( + GeoPoint( + 47.253369, + -1.605721 + ), + GeoPoint( + 47.173845, + -1.482811 + ) + ) + ), + center = GeoPoint( + 47.225827, + -1.554470 + ) + )), + appSettings + ) + } + + @Test + fun testReadAppSettingsFromInvalidJsonString() { + // when read an invalid JSON as AppSettings + val appSettings = appSettingsJsonReader.read("") + + // then + assertNull(appSettings) + } + + @Test + fun testReadAppSettingsFromJsonStringWithNoMapSettings() { + // when read an empty JSON as AppSettings + val appSettings = appSettingsJsonReader.read("{\"map\":null}") + + // then + assertNotNull(appSettings) + assertNull(appSettings?.mapSettings) + } +} \ No newline at end of file diff --git a/occtax/src/test/resources/fixtures/input_no_observer_no_taxon.json b/occtax/src/test/resources/fixtures/input_no_observer_no_taxon.json index fd02a715..93742e98 100644 --- a/occtax/src/test/resources/fixtures/input_no_observer_no_taxon.json +++ b/occtax/src/test/resources/fixtures/input_no_observer_no_taxon.json @@ -1,7 +1,7 @@ { "id": 1234, "module": "occtax", - "geometry": {}, + "geometry": null, "properties": { "meta_device_entry": "mobile", "date_min": "2016-10-28T00:00:00Z", diff --git a/occtax/src/test/resources/fixtures/input_simple.json b/occtax/src/test/resources/fixtures/input_simple.json index efe55bd0..1fab43a7 100644 --- a/occtax/src/test/resources/fixtures/input_simple.json +++ b/occtax/src/test/resources/fixtures/input_simple.json @@ -1,7 +1,7 @@ { "id": 1234, "module": "occtax", - "geometry": {}, + "geometry": null, "properties": { "meta_device_entry": "mobile", "date_min": "2016-10-28T00:00:00Z", diff --git a/occtax/src/test/resources/fixtures/settings_occtax.json b/occtax/src/test/resources/fixtures/settings_occtax.json new file mode 100644 index 00000000..09f603c1 --- /dev/null +++ b/occtax/src/test/resources/fixtures/settings_occtax.json @@ -0,0 +1,30 @@ +{ + "map": { + "show_scale": true, + "show_compass": true, + "max_bounds": [ + [ + 47.253369, + -1.605721 + ], + [ + 47.173845, + -1.482811 + ] + ], + "center": [ + 47.225827, + -1.554470 + ], + "start_zoom": 10.0, + "min_zoom": 8.0, + "max_zoom": 19.0, + "min_zoom_editing": 12.0, + "layers": [ + { + "label": "Nantes", + "source": "nantes.mbtiles" + } + ] + } +} \ No newline at end of file diff --git a/occtax/version.properties b/occtax/version.properties index 99810155..4b3b54b9 100644 --- a/occtax/version.properties +++ b/occtax/version.properties @@ -1,2 +1,2 @@ -#Wed Jun 05 22:37:37 CEST 2019 -VERSION_CODE=560 +#Sun Jul 14 17:10:50 CEST 2019 +VERSION_CODE=730 diff --git a/settings.gradle b/settings.gradle index a4d14882..54d50263 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':commons', ':viewpager', ':occtax' +include ':commons', ':viewpager', ':maps', ':occtax' diff --git a/upgrade_submodules.sh b/upgrade_submodules.sh index 202eda61..410b206c 100755 --- a/upgrade_submodules.sh +++ b/upgrade_submodules.sh @@ -3,11 +3,20 @@ APP_HOME="`pwd -P`" cd $APP_HOME/commons -git checkout -- version.properties +git checkout -- . + cd $APP_HOME/viewpager -git checkout -- version.properties +git checkout -- . + +cd $APP_HOME/maps +git checkout -- . + cd $APP_HOME/gn_mobile_core git pull origin develop + +cd $APP_HOME/gn_mobile_maps +git pull origin develop + cd $APP_HOME git submodule update --remote git submodule foreach git checkout develop