From 3054db34b298261aa978b632ad043ced81d5213d Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Mon, 10 Jun 2019 16:11:34 +0200 Subject: [PATCH 01/15] chore: update core modules dependencies --- gn_mobile_core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gn_mobile_core b/gn_mobile_core index bda13bfa..494d65d5 160000 --- a/gn_mobile_core +++ b/gn_mobile_core @@ -1 +1 @@ -Subproject commit bda13bfa539cbe61a8bc357e34cf31a2436415eb +Subproject commit 494d65d5b1eae7799a22bc4a039cbda84e1deabd From 523ae19c2547b866360a16237b1c6c5123d09ac0 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Mon, 10 Jun 2019 16:12:39 +0200 Subject: [PATCH 02/15] feat: manage inputs from HomeFragment --- occtax/build.gradle | 2 +- .../input/io/OnInputJsonReaderListenerImpl.kt | 8 +- .../input/io/OnInputJsonWriterListenerImpl.kt | 16 +- .../geonature/occtax/ui/home/HomeActivity.kt | 27 ++- .../geonature/occtax/ui/home/HomeFragment.kt | 176 +++++++++++++++--- .../ui/home/InputRecyclerViewAdapter.kt | 98 ++++++++++ .../ui/input/InputPagerFragmentActivity.kt | 33 ++-- .../{home_fragment.xml => fragment_home.xml} | 30 +-- occtax/src/main/res/values-fr/strings.xml | 4 + occtax/src/main/res/values/strings.xml | 4 + .../occtax/input/io/InputJsonReaderTest.kt | 2 +- .../occtax/input/io/InputJsonWriterTest.kt | 2 +- 12 files changed, 326 insertions(+), 76 deletions(-) create mode 100644 occtax/src/main/java/fr/geonature/occtax/ui/home/InputRecyclerViewAdapter.kt rename occtax/src/main/res/layout/{home_fragment.xml => fragment_home.xml} (80%) diff --git a/occtax/build.gradle b/occtax/build.gradle index be2f8364..1ad8970b 100644 --- a/occtax/build.gradle +++ b/occtax/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' -version = "0.0.7" +version = "0.0.8" android { compileSdkVersion 28 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..a5da75e4 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 @@ -14,15 +14,15 @@ import java.util.Date * * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) */ -class OnInputJsonReaderListenerImpl : InputJsonReader.OnInputJsonReaderListener { +class OnInputJsonReaderListenerImpl : InputJsonReader.OnInputJsonReaderListener { - override fun createInput(): AbstractInput { + override fun createInput(): Input { return Input() } override fun readAdditionalInputData(reader: JsonReader, keyName: String, - input: AbstractInput) { + input: Input) { when (keyName) { "geometry" -> readGeometry(reader, input) @@ -33,7 +33,7 @@ class OnInputJsonReaderListenerImpl : InputJsonReader.OnInputJsonReaderListener } private fun readGeometry(reader: JsonReader, - input: AbstractInput) { + input: Input) { reader.beginObject() // TODO: read geometry object 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..7ae82494 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,20 @@ 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.occtax.input.Input /** * Default implementation of [InputJsonWriter.OnInputJsonWriterListener]. * * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) */ -class OnInputJsonWriterListenerImpl : InputJsonWriter.OnInputJsonWriterListener { +class OnInputJsonWriterListenerImpl : InputJsonWriter.OnInputJsonWriterListener { override fun writeAdditionalInputData(writer: JsonWriter, - input: AbstractInput) { + input: Input) { writeGeometry(writer, input) writeProperties(writer, @@ -22,7 +22,7 @@ class OnInputJsonWriterListenerImpl : InputJsonWriter.OnInputJsonWriterListener } private fun writeGeometry(writer: JsonWriter, - input: AbstractInput) { + input: Input) { writer.name("geometry") .beginObject() @@ -32,7 +32,7 @@ class OnInputJsonWriterListenerImpl : InputJsonWriter.OnInputJsonWriterListener } private fun writeProperties(writer: JsonWriter, - input: AbstractInput) { + input: Input) { writer.name("properties") .beginObject() @@ -54,7 +54,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 +63,7 @@ class OnInputJsonWriterListenerImpl : InputJsonWriter.OnInputJsonWriterListener } private fun writeInputObserverIds(writer: JsonWriter, - input: AbstractInput) { + input: Input) { writer.name("observers") .beginArray() @@ -74,7 +74,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/ui/home/HomeActivity.kt b/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeActivity.kt index f27df65e..99493f55 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,10 @@ package fr.geonature.occtax.ui.home import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import fr.geonature.commons.input.InputManager +import fr.geonature.occtax.input.Input +import fr.geonature.occtax.input.io.OnInputJsonReaderListenerImpl +import fr.geonature.occtax.input.io.OnInputJsonWriterListenerImpl import fr.geonature.occtax.ui.input.InputPagerFragmentActivity import fr.geonature.occtax.ui.settings.PreferencesActivity import fr.geonature.occtax.util.IntentUtils @@ -14,19 +18,29 @@ import fr.geonature.occtax.util.IntentUtils * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) */ class HomeActivity : AppCompatActivity(), - HomeFragment.OnHomeFragmentFragmentListener { + HomeFragment.OnHomeFragmentFragmentListener { + + private lateinit var inputManager: InputManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + inputManager = InputManager(application, + OnInputJsonReaderListenerImpl(), + OnInputJsonWriterListenerImpl()) + 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 onShowSettings() { startActivity(PreferencesActivity.newIntent(this)) } @@ -35,7 +49,8 @@ class HomeActivity : AppCompatActivity(), startActivity(IntentUtils.syncActivity(this)) } - override fun onStartInput() { - startActivity(InputPagerFragmentActivity.newIntent(this)) + override fun onStartInput(input: Input?) { + startActivity(InputPagerFragmentActivity.newIntent(this, + 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..a76e5867 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 @@ -3,21 +3,39 @@ package fr.geonature.occtax.ui.home 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.commons.input.InputManager +import fr.geonature.occtax.R +import fr.geonature.occtax.input.Input import fr.geonature.occtax.ui.settings.PreferencesFragment 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]. @@ -27,7 +45,8 @@ import fr.geonature.occtax.ui.shared.view.ListItemActionView class HomeFragment : Fragment() { private var listener: OnHomeFragmentFragmentListener? = null - private lateinit var appSyncView: AppSyncView + private lateinit var adapter: InputRecyclerViewAdapter + private var selectedInputToDelete: Pair? = null private val loaderCallbacks = object : LoaderManager.LoaderCallbacks { override fun onCreateLoader( @@ -36,7 +55,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), @@ -67,11 +87,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,15 +116,93 @@ 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 { listener?.onStartInput() } + + adapter = InputRecyclerViewAdapter(object : InputRecyclerViewAdapter.OnInputRecyclerViewAdapterListener { + override fun onInputClicked(input: Input) { + Log.i(TAG, + "input selected: ${input.id}") + + listener?.onStartInput(input) + } + + override fun onInputLongClicked(index: Int, + input: Input) { + selectedInputToDelete = Pair.create(index, + input) + + GlobalScope.launch(Dispatchers.Main) { + listener?.getInputManager() + ?.deleteInput(input.id) + (inputRecyclerView.adapter as InputRecyclerViewAdapter).removeAt(index) + } + + 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 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) + + GlobalScope.launch(Dispatchers.Main) { + val inputs: List = listener?.getInputManager()?.readInputs() + ?: emptyList() + (inputRecyclerView.adapter as InputRecyclerViewAdapter).setInputs(inputs) + } } override fun onAttach(context: Context) { @@ -112,28 +222,33 @@ 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 - } - */ - 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,26 +256,39 @@ class HomeFragment : Fragment() { } } - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + private fun showEmptyTextView(show: Boolean) { + if (inputEmptyTextView.visibility == View.VISIBLE == show) { + return + } - LoaderManager.getInstance(this) - .initLoader(LOADER_APP_SYNC, - bundleOf(AppSync.COLUMN_ID to requireContext().packageName), - loaderCallbacks) + 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]. */ interface OnHomeFragmentFragmentListener { + fun getInputManager(): InputManager fun onShowSettings() fun onStartSync() - fun onStartInput() + fun onStartInput(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" /** * Use this factory method to create a new instance of [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..9b458b7b --- /dev/null +++ b/occtax/src/main/java/fr/geonature/occtax/ui/home/InputRecyclerViewAdapter.kt @@ -0,0 +1,98 @@ +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(position, + 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) + notifyItemInserted(this.inputs.size - 1) + } + else { + this.inputs.add(index, + input) + notifyItemRangeChanged(index, + this.inputs.size - index) + } + } + + fun removeAt(index: Int) { + this.inputs.removeAt(index) + notifyItemRemoved(index) + } + + 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(position: Int, + 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(position, + 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(index: 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..7c9b87b9 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 @@ -26,7 +26,7 @@ import kotlinx.coroutines.launch */ class InputPagerFragmentActivity : AbstractNavigationHistoryPagerFragmentActivity() { - private lateinit var inputManager: InputManager + private lateinit var inputManager: InputManager private lateinit var input: Input override fun onCreate(savedInstanceState: Bundle?) { @@ -36,7 +36,14 @@ class InputPagerFragmentActivity : AbstractNavigationHistoryPagerFragmentActivit OnInputJsonReaderListenerImpl(), OnInputJsonWriterListenerImpl()) - readCurrentInput() + input = intent.getParcelableExtra(EXTRA_INPUT) ?: Input() + + Log.i(TAG, + "loading input: ${input.id}") + + GlobalScope.launch(Dispatchers.Main) { + pagerManager.load(input.id) + } } override fun onPause() { @@ -69,19 +76,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() @@ -95,10 +89,15 @@ class InputPagerFragmentActivity : AbstractNavigationHistoryPagerFragmentActivit companion object { private val TAG = InputPagerFragmentActivity::class.java.name + const val EXTRA_INPUT = "extra_input" - fun newIntent(context: Context): Intent { + fun newIntent(context: Context, + input: Input? = null): Intent { return Intent(context, - InputPagerFragmentActivity::class.java) + InputPagerFragmentActivity::class.java).apply { + putExtra(EXTRA_INPUT, + input) + } } } } diff --git a/occtax/src/main/res/layout/home_fragment.xml b/occtax/src/main/res/layout/fragment_home.xml similarity index 80% rename from occtax/src/main/res/layout/home_fragment.xml rename to occtax/src/main/res/layout/fragment_home.xml index d8f57518..072307ef 100644 --- a/occtax/src/main/res/layout/home_fragment.xml +++ b/occtax/src/main/res/layout/fragment_home.xml @@ -3,6 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/homeContent" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" @@ -39,26 +40,27 @@ android:textStyle="bold" app:layout_constraintTop_toBottomOf="@+id/cardViewAppSyncView" /> - - + + - + + + 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". diff --git a/occtax/src/main/res/values/strings.xml b/occtax/src/main/res/values/strings.xml index 899ce72b..db0e3dd8 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. 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..5bda5aca 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() { 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() { From ec061b866199e5310d5ebf71da917698532ff06a Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Mon, 10 Jun 2019 17:04:32 +0200 Subject: [PATCH 03/15] fix: compute element position from 'remove' action --- .../geonature/occtax/ui/home/HomeFragment.kt | 6 +++--- .../ui/home/InputRecyclerViewAdapter.kt | 21 +++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) 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 a76e5867..ddf74af7 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 @@ -132,15 +132,15 @@ class HomeFragment : Fragment() { listener?.onStartInput(input) } - override fun onInputLongClicked(index: Int, + override fun onInputLongClicked(position: Int, input: Input) { - selectedInputToDelete = Pair.create(index, + selectedInputToDelete = Pair.create(position, input) GlobalScope.launch(Dispatchers.Main) { listener?.getInputManager() ?.deleteInput(input.id) - (inputRecyclerView.adapter as InputRecyclerViewAdapter).removeAt(index) + (inputRecyclerView.adapter as InputRecyclerViewAdapter).remove(input) } Snackbar.make(homeContent, 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 index 9b458b7b..9671908f 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/home/InputRecyclerViewAdapter.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/home/InputRecyclerViewAdapter.kt @@ -25,8 +25,7 @@ class InputRecyclerViewAdapter(private val listener: OnInputRecyclerViewAdapterL override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(position, - inputs[position]) + holder.bind(inputs[position]) } fun setInputs(inputs: List) { @@ -50,9 +49,14 @@ class InputRecyclerViewAdapter(private val listener: OnInputRecyclerViewAdapterL } } - fun removeAt(index: Int) { - this.inputs.removeAt(index) - notifyItemRemoved(index) + 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, @@ -61,14 +65,13 @@ class InputRecyclerViewAdapter(private val listener: OnInputRecyclerViewAdapterL private val text1: TextView = itemView.findViewById(android.R.id.text1) - fun bind(position: Int, - input: Input) { + 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(position, + listener.onInputLongClicked(inputs.indexOf(input), input) true } @@ -92,7 +95,7 @@ class InputRecyclerViewAdapter(private val listener: OnInputRecyclerViewAdapterL * * @param input the selected [Input] */ - fun onInputLongClicked(index: Int, + fun onInputLongClicked(position: Int, input: Input) } } \ No newline at end of file From 92655c9503491bba01d6cba7593c12ca95d097bf Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sat, 22 Jun 2019 19:00:31 +0200 Subject: [PATCH 04/15] chore: upgrade Kotlin version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 93172c6d..49ad9597 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.40' repositories { google() From 5eda52987b506d1f77251e421dca3abba6340a70 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sat, 22 Jun 2019 19:01:23 +0200 Subject: [PATCH 05/15] chore: IDE global settings --- .idea/codeStyles/Project.xml | 7 +++++++ 1 file changed, 7 insertions(+) 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/occtax/build.gradle b/occtax/build.gradle index 1ad8970b..7e96eb67 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.8" +version = "0.0.9" android { compileSdkVersion 28 + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { applicationId "fr.geonature.occtax" minSdkVersion 21 @@ -43,12 +48,13 @@ 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' 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 99493f55..5a7fd514 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 @@ -3,9 +3,12 @@ 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 @@ -21,6 +24,7 @@ class HomeActivity : AppCompatActivity(), HomeFragment.OnHomeFragmentFragmentListener { private lateinit var inputManager: InputManager + private lateinit var appSettingsManager: AppSettingsManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -28,6 +32,8 @@ class HomeActivity : AppCompatActivity(), inputManager = InputManager(application, OnInputJsonReaderListenerImpl(), OnInputJsonWriterListenerImpl()) + appSettingsManager = AppSettingsManager(application, + OnAppSettingsJsonReaderListenerImpl()) if (savedInstanceState == null) { supportFragmentManager.beginTransaction() @@ -41,6 +47,10 @@ class HomeActivity : AppCompatActivity(), return inputManager } + override fun getAppSettingsManager(): AppSettingsManager { + return appSettingsManager + } + override fun onShowSettings() { startActivity(PreferencesActivity.newIntent(this)) } 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 ddf74af7..3ed2ac9c 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 @@ -11,6 +11,7 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.view.animation.AnimationUtils +import android.widget.Toast import androidx.core.os.bundleOf import androidx.core.util.Pair import androidx.fragment.app.Fragment @@ -24,8 +25,10 @@ import com.google.android.material.snackbar.Snackbar import fr.geonature.commons.data.AppSync import fr.geonature.commons.data.Provider.buildUri import fr.geonature.commons.input.InputManager +import fr.geonature.commons.settings.AppSettingsManager import fr.geonature.occtax.R import fr.geonature.occtax.input.Input +import fr.geonature.occtax.settings.AppSettings import fr.geonature.occtax.ui.settings.PreferencesFragment import fr.geonature.occtax.ui.shared.view.ListItemActionView import kotlinx.android.synthetic.main.fragment_home.appSyncView @@ -46,6 +49,7 @@ class HomeFragment : Fragment() { private var listener: OnHomeFragmentFragmentListener? = null private lateinit var adapter: InputRecyclerViewAdapter + private var appSettings: AppSettings? = null private var selectedInputToDelete: Pair? = null private val loaderCallbacks = object : LoaderManager.LoaderCallbacks { @@ -199,9 +203,21 @@ class HomeFragment : Fragment() { loaderCallbacks) GlobalScope.launch(Dispatchers.Main) { - val inputs: List = listener?.getInputManager()?.readInputs() - ?: emptyList() - (inputRecyclerView.adapter as InputRecyclerViewAdapter).setInputs(inputs) + appSettings = listener?.getAppSettingsManager() + ?.loadAppSettings() + + if (appSettings == null) { + showToastMessage(getString(R.string.message_settings_not_found, + listener?.getAppSettingsManager()?.getAppSettingsFilename())) + } + else { + fab.show() + activity?.invalidateOptionsMenu() + + val inputs: List = listener?.getInputManager()?.readInputs() + ?: emptyList() + (inputRecyclerView.adapter as InputRecyclerViewAdapter).setInputs(inputs) + } } } @@ -246,6 +262,13 @@ class HomeFragment : Fragment() { menu) } + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + + val menuItemSettings = menu.findItem(R.id.menu_settings) + menuItemSettings.isEnabled = appSettings != null + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.menu_settings -> { @@ -274,11 +297,21 @@ class HomeFragment : Fragment() { } } + private fun showToastMessage(message: CharSequence) { + val context = context ?: return + + Toast.makeText(context, + message, + Toast.LENGTH_LONG) + .show() + } + /** * Callback used by [PreferencesFragment]. */ interface OnHomeFragmentFragmentListener { fun getInputManager(): InputManager + fun getAppSettingsManager(): AppSettingsManager fun onShowSettings() fun onStartSync() fun onStartInput(input: Input? = null) diff --git a/occtax/src/main/res/layout/fragment_home.xml b/occtax/src/main/res/layout/fragment_home.xml index 072307ef..fb55caee 100644 --- a/occtax/src/main/res/layout/fragment_home.xml +++ b/occtax/src/main/res/layout/fragment_home.xml @@ -71,6 +71,7 @@ android:clickable="true" android:focusable="true" android:tint="@android:color/white" + android:visibility="invisible" app:elevation="@dimen/fab_elevation" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/occtax/src/main/res/values-fr/strings.xml b/occtax/src/main/res/values-fr/strings.xml index 2875ab60..853a4428 100644 --- a/occtax/src/main/res/values-fr/strings.xml +++ b/occtax/src/main/res/values-fr/strings.xml @@ -33,6 +33,8 @@ OK Annuler + Erreur lors du chargement des paramètres \'%1$s\' + Observateur & date Taxons diff --git a/occtax/src/main/res/values/strings.xml b/occtax/src/main/res/values/strings.xml index db0e3dd8..97a2b093 100644 --- a/occtax/src/main/res/values/strings.xml +++ b/occtax/src/main/res/values/strings.xml @@ -37,6 +37,8 @@ OK Cancel + Unable to load settings \'%1$s\' + Observers & date Taxa 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..e24c4529 --- /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 = false, + showCompass = false, + zoom = 8.0, + minZoomLevel = 7.0, + maxZoomLevel = 12.0, + minZoomEditing = 10.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/settings_occtax.json b/occtax/src/test/resources/fixtures/settings_occtax.json new file mode 100644 index 00000000..f582ae6b --- /dev/null +++ b/occtax/src/test/resources/fixtures/settings_occtax.json @@ -0,0 +1,30 @@ +{ + "map": { + "show_scale": false, + "show_compass": false, + "max_bounds": [ + [ + 47.253369, + -1.605721 + ], + [ + 47.173845, + -1.482811 + ] + ], + "center": [ + 47.225827, + -1.554470 + ], + "start_zoom": 8.0, + "min_zoom": 7.0, + "max_zoom": 12.0, + "min_zoom_editing": 10.0, + "layers": [ + { + "label": "Nantes", + "source": "nantes.mbtiles" + } + ] + } +} \ No newline at end of file From 9753b2b8d8a89f0730e8d8113ba0a495f70ab3f4 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sun, 23 Jun 2019 17:47:38 +0200 Subject: [PATCH 08/15] feat: manage app permissions at startup --- occtax/build.gradle | 2 +- .../geonature/occtax/ui/home/HomeFragment.kt | 111 ++++++++++++++---- .../ui/home/InputRecyclerViewAdapter.kt | 8 +- occtax/src/main/res/layout/fragment_home.xml | 101 ++++++++-------- occtax/src/main/res/values-fr/strings.xml | 6 +- occtax/src/main/res/values/strings.xml | 6 +- 6 files changed, 156 insertions(+), 78 deletions(-) diff --git a/occtax/build.gradle b/occtax/build.gradle index 7e96eb67..55725847 100644 --- a/occtax/build.gradle +++ b/occtax/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' -version = "0.0.9" +version = "0.1.0" android { compileSdkVersion 28 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 3ed2ac9c..4116b433 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,5 +1,6 @@ package fr.geonature.occtax.ui.home +import android.Manifest import android.content.Context import android.database.Cursor import android.os.Bundle @@ -11,7 +12,6 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.view.animation.AnimationUtils -import android.widget.Toast import androidx.core.os.bundleOf import androidx.core.util.Pair import androidx.fragment.app.Fragment @@ -26,6 +26,10 @@ import fr.geonature.commons.data.AppSync import fr.geonature.commons.data.Provider.buildUri 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 @@ -51,6 +55,7 @@ class HomeFragment : Fragment() { 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( @@ -175,6 +180,14 @@ class HomeFragment : Fragment() { 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, @@ -202,22 +215,23 @@ class HomeFragment : Fragment() { bundleOf(AppSync.COLUMN_ID to requireContext().packageName), loaderCallbacks) - GlobalScope.launch(Dispatchers.Main) { - appSettings = listener?.getAppSettingsManager() - ?.loadAppSettings() - - if (appSettings == null) { - showToastMessage(getString(R.string.message_settings_not_found, - listener?.getAppSettingsManager()?.getAppSettingsFilename())) - } - else { - fab.show() - activity?.invalidateOptionsMenu() - - val inputs: List = listener?.getInputManager()?.readInputs() - ?: emptyList() - (inputRecyclerView.adapter as InputRecyclerViewAdapter).setInputs(inputs) - } + 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) } } @@ -279,6 +293,59 @@ class HomeFragment : Fragment() { } } + override fun onRequestPermissionsResult(requestCode: Int, + permissions: Array, + grantResults: IntArray) { + when (requestCode) { + REQUEST_EXTERNAL_STORAGE -> { + requestPermissionsResult = checkPermissions(grantResults) + + 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 @@ -297,15 +364,6 @@ class HomeFragment : Fragment() { } } - private fun showToastMessage(message: CharSequence) { - val context = context ?: return - - Toast.makeText(context, - message, - Toast.LENGTH_LONG) - .show() - } - /** * Callback used by [PreferencesFragment]. */ @@ -322,6 +380,7 @@ class HomeFragment : Fragment() { 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]. 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 index 9671908f..e8380142 100644 --- a/occtax/src/main/java/fr/geonature/occtax/ui/home/InputRecyclerViewAdapter.kt +++ b/occtax/src/main/java/fr/geonature/occtax/ui/home/InputRecyclerViewAdapter.kt @@ -39,7 +39,8 @@ class InputRecyclerViewAdapter(private val listener: OnInputRecyclerViewAdapterL index: Int? = null) { if (index == null) { this.inputs.add(input) - notifyItemInserted(this.inputs.size - 1) + notifyItemRangeInserted(this.inputs.size - 1, + 1) } else { this.inputs.add(index, @@ -49,6 +50,11 @@ class InputRecyclerViewAdapter(private val listener: OnInputRecyclerViewAdapterL } } + fun clear() { + this.inputs.clear() + notifyDataSetChanged() + } + fun remove(input: Input) { val inputPosition = this.inputs.indexOf(input) this.inputs.remove(input) diff --git a/occtax/src/main/res/layout/fragment_home.xml b/occtax/src/main/res/layout/fragment_home.xml index fb55caee..5ba0b166 100644 --- a/occtax/src/main/res/layout/fragment_home.xml +++ b/occtax/src/main/res/layout/fragment_home.xml @@ -1,71 +1,78 @@ - + android:layout_height="match_parent"> - + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="@dimen/padding_default" + tools:context=".ui.home.HomeActivity"> - + app:cardCornerRadius="@dimen/cardview_radius" + app:cardElevation="@dimen/cardview_elevation" + app:contentPadding="@dimen/padding_default" + app:layout_constraintTop_toTopOf="parent"> - + - - - - - - + + android:layout_marginBottom="@dimen/padding_default" + android:layout_marginTop="@dimen/padding_default" + android:text="@string/home_last_inputs" + android:textAppearance="@style/TextAppearance.AppCompat.Medium" + android:textStyle="bold" + app:layout_constraintTop_toBottomOf="@+id/cardViewAppSyncView" /> + + + + + + + + + - + - + diff --git a/occtax/src/main/res/values-fr/strings.xml b/occtax/src/main/res/values-fr/strings.xml index 853a4428..47348d08 100644 --- a/occtax/src/main/res/values-fr/strings.xml +++ b/occtax/src/main/res/values-fr/strings.xml @@ -33,7 +33,11 @@ OK Annuler - Erreur lors du chargement des paramètres \'%1$s\' + 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 Taxons diff --git a/occtax/src/main/res/values/strings.xml b/occtax/src/main/res/values/strings.xml index 97a2b093..879bfafa 100644 --- a/occtax/src/main/res/values/strings.xml +++ b/occtax/src/main/res/values/strings.xml @@ -37,7 +37,11 @@ OK Cancel - Unable to load settings \'%1$s\' + 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 Taxa From e881e6f5cee9c4ce282a107537449f85b3fdfbd6 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Fri, 12 Jul 2019 17:51:34 +0200 Subject: [PATCH 09/15] fix: linter, typo, logger, unused resources --- .../geonature/occtax/ui/home/HomeActivity.kt | 2 +- .../geonature/occtax/ui/home/HomeFragment.kt | 18 ++++-- .../ObserversAndDateInputFragment.kt | 8 ++- .../occtax/ui/input/taxa/TaxaFragment.kt | 8 ++- .../ui/input/taxa/TaxaRecyclerViewAdapter.kt | 10 +-- .../InputObserverRecyclerViewAdapter.kt | 35 ++++++++-- .../src/main/res/layout/list_title_taxon.xml | 64 ------------------- occtax/src/main/res/values-fr/strings.xml | 2 +- 8 files changed, 62 insertions(+), 85 deletions(-) delete mode 100644 occtax/src/main/res/layout/list_title_taxon.xml 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 5a7fd514..ed985997 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 @@ -21,7 +21,7 @@ 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 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 4116b433..996364a3 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 @@ -33,7 +33,6 @@ 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.settings.PreferencesFragment import fr.geonature.occtax.ui.shared.view.ListItemActionView import kotlinx.android.synthetic.main.fragment_home.appSyncView import kotlinx.android.synthetic.main.fragment_home.fab @@ -51,7 +50,7 @@ import kotlinx.coroutines.launch */ class HomeFragment : Fragment() { - private var listener: OnHomeFragmentFragmentListener? = null + private var listener: OnHomeFragmentListener? = null private lateinit var adapter: InputRecyclerViewAdapter private var appSettings: AppSettings? = null private var selectedInputToDelete: Pair? = null @@ -80,7 +79,11 @@ 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 -> { @@ -238,11 +241,11 @@ class HomeFragment : Fragment() { 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") } } @@ -365,9 +368,9 @@ class HomeFragment : Fragment() { } /** - * Callback used by [PreferencesFragment]. + * Callback used by [HomeFragment]. */ - interface OnHomeFragmentFragmentListener { + interface OnHomeFragmentListener { fun getInputManager(): InputManager fun getAppSettingsManager(): AppSettingsManager fun onShowSettings() @@ -387,6 +390,7 @@ class HomeFragment : Fragment() { * * @return A new instance of [HomeFragment] */ + @JvmStatic fun newInstance() = HomeFragment() } } 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..086bc525 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,7 +70,7 @@ class TaxaRecyclerViewAdapter(private val listener: OnTaxaRecyclerViewAdapterLis val name = taxon.name ?: return "" return name.elementAt(0) - .toString() + .toString() } fun setSelectedTaxon(selectedTaxon: Taxon) { @@ -119,7 +119,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 +138,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 +147,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/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 47348d08..283a639a 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 From db88d1ddc01ae4348479e25520a92da4c81b23ec Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Fri, 12 Jul 2019 17:52:08 +0200 Subject: [PATCH 10/15] chore: upgrade RecyclerView dependency --- occtax/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/occtax/build.gradle b/occtax/build.gradle index 55725847..42710870 100644 --- a/occtax/build.gradle +++ b/occtax/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' -version = "0.1.0" +version = "0.1.1" android { compileSdkVersion 28 @@ -57,7 +57,7 @@ dependencies { 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' From 20f6ba27729d14a7f9c0d78203dab7fec19a712d Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sat, 13 Jul 2019 13:44:37 +0200 Subject: [PATCH 11/15] chore: gradle upgrade, kotlin upgrade --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 49ad9597..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.40' + 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 From c5936f39a631ec82d88921dc0fe25bb82cb6816c Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sat, 13 Jul 2019 13:47:12 +0200 Subject: [PATCH 12/15] chore: update core and maps modules dependencies --- gn_mobile_core | 2 +- gn_mobile_maps | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gn_mobile_core b/gn_mobile_core index 568f1dcc..d06cea5d 160000 --- a/gn_mobile_core +++ b/gn_mobile_core @@ -1 +1 @@ -Subproject commit 568f1dccae3b745e6726f91bf8fa4dc28902bf6e +Subproject commit d06cea5d67cbfdf974a2a6f7c1a2818fd43359b0 diff --git a/gn_mobile_maps b/gn_mobile_maps index 13addbd3..8e36cff3 160000 --- a/gn_mobile_maps +++ b/gn_mobile_maps @@ -1 +1 @@ -Subproject commit 13addbd38b0f6cae0ed93441cca14b392e3ca3de +Subproject commit 8e36cff33f4f55972d53d9072cb4d22f514c83d1 From ca4947266252c18ce28a8e120341e6b92028a070 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sun, 14 Jul 2019 17:05:34 +0200 Subject: [PATCH 13/15] chore: update core and maps modules dependencies --- README.md | 8 ++++++-- gn_mobile_core | 2 +- gn_mobile_maps | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 57250423..08bc00a8 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,12 @@ ## 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 diff --git a/gn_mobile_core b/gn_mobile_core index d06cea5d..983793e6 160000 --- a/gn_mobile_core +++ b/gn_mobile_core @@ -1 +1 @@ -Subproject commit d06cea5d67cbfdf974a2a6f7c1a2818fd43359b0 +Subproject commit 983793e6317734a05c6c390ab5eab555e27672f9 diff --git a/gn_mobile_maps b/gn_mobile_maps index 8e36cff3..30654751 160000 --- a/gn_mobile_maps +++ b/gn_mobile_maps @@ -1 +1 @@ -Subproject commit 8e36cff33f4f55972d53d9072cb4d22f514c83d1 +Subproject commit 306547510f982ad61631843cf81a3298c35e0583 From c0091cbafc51a5ecbccaf5e41a68b545227a983a Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sun, 14 Jul 2019 17:08:18 +0200 Subject: [PATCH 14/15] feat: InputMapFragment, bunch of fixes --- occtax/build.gradle | 2 +- .../java/fr/geonature/occtax/input/Input.kt | 15 +++- .../input/io/OnInputJsonReaderListenerImpl.kt | 15 ++-- .../input/io/OnInputJsonWriterListenerImpl.kt | 14 ++- .../geonature/occtax/ui/home/HomeActivity.kt | 4 +- .../geonature/occtax/ui/home/HomeFragment.kt | 16 +++- .../ui/input/InputPagerFragmentActivity.kt | 18 +++- .../occtax/ui/input/map/InputMapFragment.kt | 90 +++++++++++++++++++ .../ui/input/taxa/TaxaRecyclerViewAdapter.kt | 1 + occtax/src/main/res/values-fr/strings.xml | 1 + occtax/src/main/res/values/strings.xml | 1 + .../occtax/input/io/InputJsonReaderTest.kt | 1 + .../settings/io/AppSettingsJsonReaderTest.kt | 12 +-- .../fixtures/input_no_observer_no_taxon.json | 2 +- .../test/resources/fixtures/input_simple.json | 2 +- .../resources/fixtures/settings_occtax.json | 12 +-- 16 files changed, 176 insertions(+), 30 deletions(-) create mode 100644 occtax/src/main/java/fr/geonature/occtax/ui/input/map/InputMapFragment.kt diff --git a/occtax/build.gradle b/occtax/build.gradle index 42710870..c6b563f9 100644 --- a/occtax/build.gradle +++ b/occtax/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' -version = "0.1.1" +version = "0.1.2" android { compileSdkVersion 28 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 a5da75e4..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 @@ -16,6 +17,8 @@ import java.util.Date */ class OnInputJsonReaderListenerImpl : InputJsonReader.OnInputJsonReaderListener { + private val geoJsonReader = GeoJsonReader() + override fun createInput(): Input { return Input() } @@ -34,11 +37,13 @@ class OnInputJsonReaderListenerImpl : InputJsonReader.OnInputJsonReaderListener< private fun readGeometry(reader: JsonReader, input: Input) { - reader.beginObject() - - // TODO: read geometry object - - reader.endObject() + 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 7ae82494..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 @@ -4,6 +4,7 @@ import android.util.JsonWriter 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 /** @@ -13,6 +14,8 @@ import fr.geonature.occtax.input.Input */ class OnInputJsonWriterListenerImpl : InputJsonWriter.OnInputJsonWriterListener { + private val geoJsonWriter = GeoJsonWriter() + override fun writeAdditionalInputData(writer: JsonWriter, input: Input) { writeGeometry(writer, @@ -24,11 +27,16 @@ class OnInputJsonWriterListenerImpl : InputJsonWriter.OnInputJsonWriterListener< private fun writeGeometry(writer: JsonWriter, 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, 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 ed985997..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 @@ -59,8 +59,10 @@ class HomeActivity : AppCompatActivity(), startActivity(IntentUtils.syncActivity(this)) } - override fun onStartInput(input: Input?) { + 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 996364a3..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 @@ -80,7 +80,8 @@ class HomeFragment : Fragment() { data: Cursor?) { if (data == null) { - Log.w(TAG, "Failed to load data from '${(loader as CursorLoader).uri}'") + Log.w(TAG, + "Failed to load data from '${(loader as CursorLoader).uri}'") return } @@ -134,14 +135,20 @@ class HomeFragment : Fragment() { } }) - 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(input) + listener?.onStartInput(appSettings, + input) } override fun onInputLongClicked(position: Int, @@ -375,7 +382,8 @@ class HomeFragment : Fragment() { fun getAppSettingsManager(): AppSettingsManager fun onShowSettings() fun onStartSync() - fun onStartInput(input: Input? = null) + fun onStartInput(appSettings: AppSettings, + input: Input? = null) } companion object { 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 7c9b87b9..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 @@ -27,6 +29,7 @@ import kotlinx.coroutines.launch class InputPagerFragmentActivity : AbstractNavigationHistoryPagerFragmentActivity() { private lateinit var inputManager: InputManager + private lateinit var appSettings: AppSettings private lateinit var input: Input override fun onCreate(savedInstanceState: Bundle?) { @@ -36,7 +39,13 @@ class InputPagerFragmentActivity : AbstractNavigationHistoryPagerFragmentActivit OnInputJsonReaderListenerImpl(), OnInputJsonWriterListenerImpl()) + 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}") @@ -58,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()) } @@ -89,12 +100,17 @@ class InputPagerFragmentActivity : AbstractNavigationHistoryPagerFragmentActivit companion object { private val TAG = InputPagerFragmentActivity::class.java.name - const val EXTRA_INPUT = "extra_input" + + 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).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/taxa/TaxaRecyclerViewAdapter.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaRecyclerViewAdapter.kt index 086bc525..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 @@ -75,6 +75,7 @@ class TaxaRecyclerViewAdapter(private val listener: OnTaxaRecyclerViewAdapterLis fun setSelectedTaxon(selectedTaxon: Taxon) { this.selectedTaxon = selectedTaxon + scrollToFirstItemSelected() notifyDataSetChanged() } diff --git a/occtax/src/main/res/values-fr/strings.xml b/occtax/src/main/res/values-fr/strings.xml index 283a639a..2cba44a1 100644 --- a/occtax/src/main/res/values-fr/strings.xml +++ b/occtax/src/main/res/values-fr/strings.xml @@ -40,6 +40,7 @@ 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 879bfafa..b9f11bbe 100644 --- a/occtax/src/main/res/values/strings.xml +++ b/occtax/src/main/res/values/strings.xml @@ -44,6 +44,7 @@ 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 5bda5aca..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 @@ -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/settings/io/AppSettingsJsonReaderTest.kt b/occtax/src/test/java/fr/geonature/occtax/settings/io/AppSettingsJsonReaderTest.kt index e24c4529..a7dc6cd8 100644 --- a/occtax/src/test/java/fr/geonature/occtax/settings/io/AppSettingsJsonReaderTest.kt +++ b/occtax/src/test/java/fr/geonature/occtax/settings/io/AppSettingsJsonReaderTest.kt @@ -49,12 +49,12 @@ class AppSettingsJsonReaderTest { ) ), null, - showScale = false, - showCompass = false, - zoom = 8.0, - minZoomLevel = 7.0, - maxZoomLevel = 12.0, - minZoomEditing = 10.0, + showScale = true, + showCompass = true, + zoom = 10.0, + minZoomLevel = 8.0, + maxZoomLevel = 19.0, + minZoomEditing = 12.0, maxBounds = BoundingBox.fromGeoPoints( arrayListOf( GeoPoint( 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 index f582ae6b..09f603c1 100644 --- a/occtax/src/test/resources/fixtures/settings_occtax.json +++ b/occtax/src/test/resources/fixtures/settings_occtax.json @@ -1,7 +1,7 @@ { "map": { - "show_scale": false, - "show_compass": false, + "show_scale": true, + "show_compass": true, "max_bounds": [ [ 47.253369, @@ -16,10 +16,10 @@ 47.225827, -1.554470 ], - "start_zoom": 8.0, - "min_zoom": 7.0, - "max_zoom": 12.0, - "min_zoom_editing": 10.0, + "start_zoom": 10.0, + "min_zoom": 8.0, + "max_zoom": 19.0, + "min_zoom_editing": 12.0, "layers": [ { "label": "Nantes", From 9f13bda247ca7c8d7affae2bb90fbe14b2f6cb4a Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Sun, 14 Jul 2019 17:13:21 +0200 Subject: [PATCH 15/15] chore: 0.1.2 --- occtax/version.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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