From b2bfd47bcbefb3d685d0ca74d7b28d784256d7e6 Mon Sep 17 00:00:00 2001 From: Andries Smit Date: Mon, 28 Mar 2022 11:44:58 +0200 Subject: [PATCH] chore: shared action helper sort and remove --- .../activities/QuickWizardListActivity.kt | 220 +++++------------- .../plugins/source/BGSourceFragment.kt | 155 ++++-------- app/src/main/res/drawable/ic_settings.xml | 2 +- app/src/main/res/drawable/ic_sort.xml | 11 + app/src/main/res/menu/menu_bgsource.xml | 10 - .../main/res/menu/menu_delete_selection.xml | 4 +- .../main/res/menu/menu_single_fragment.xml | 3 + .../general/automation/AutomationFragment.kt | 210 +++++++---------- .../src/main/res/menu/menu_automation.xml | 16 -- .../androidaps/utils/ActionHelper.kt | 185 +++++++++++++++ core/src/main/res/drawable/ic_sort.xml | 11 + core/src/main/res/drawable/ic_trash.xml | 10 + .../src/main/res/menu/menu_actions.xml | 33 +-- .../main/res/menu/menu_delete_selection.xml | 4 +- core/src/main/res/values/strings.xml | 5 + 15 files changed, 436 insertions(+), 443 deletions(-) create mode 100644 app/src/main/res/drawable/ic_sort.xml delete mode 100644 app/src/main/res/menu/menu_bgsource.xml delete mode 100644 automation/src/main/res/menu/menu_automation.xml create mode 100644 core/src/main/java/info/nightscout/androidaps/utils/ActionHelper.kt create mode 100644 core/src/main/res/drawable/ic_sort.xml create mode 100644 core/src/main/res/drawable/ic_trash.xml rename app/src/main/res/menu/menu_quickwizard.xml => core/src/main/res/menu/menu_actions.xml (52%) rename {automation => core}/src/main/res/menu/menu_delete_selection.xml (75%) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/activities/QuickWizardListActivity.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/activities/QuickWizardListActivity.kt index c19899c75a..2bd847f955 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/activities/QuickWizardListActivity.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/activities/QuickWizardListActivity.kt @@ -7,11 +7,6 @@ import android.view.* import androidx.core.util.forEach import androidx.fragment.app.FragmentManager import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_DRAG -import androidx.recyclerview.widget.ItemTouchHelper.DOWN -import androidx.recyclerview.widget.ItemTouchHelper.END -import androidx.recyclerview.widget.ItemTouchHelper.START -import androidx.recyclerview.widget.ItemTouchHelper.UP import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import info.nightscout.androidaps.R @@ -20,8 +15,12 @@ import info.nightscout.androidaps.databinding.OverviewQuickwizardlistActivityBin import info.nightscout.androidaps.databinding.OverviewQuickwizardlistItemBinding import info.nightscout.androidaps.extensions.toVisibility import info.nightscout.androidaps.plugins.bus.RxBus +import info.nightscout.androidaps.utils.dragHelpers.ItemTouchHelperAdapter +import info.nightscout.androidaps.utils.dragHelpers.OnStartDragListener +import info.nightscout.androidaps.utils.dragHelpers.SimpleItemTouchHelperCallback import info.nightscout.androidaps.plugins.general.overview.dialogs.EditQuickWizardDialog import info.nightscout.androidaps.plugins.general.overview.events.EventQuickWizardChange +import info.nightscout.androidaps.utils.ActionHelper import info.nightscout.androidaps.utils.DateUtil import info.nightscout.androidaps.utils.FabricPrivacy import info.nightscout.androidaps.utils.alertDialogs.OKDialog @@ -33,7 +32,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign import javax.inject.Inject -class QuickWizardListActivity : DaggerAppCompatActivityWithResult() { +class QuickWizardListActivity : DaggerAppCompatActivityWithResult(), OnStartDragListener { @Inject lateinit var aapsSchedulers: AapsSchedulers @Inject lateinit var rxBus: RxBus @@ -43,49 +42,15 @@ class QuickWizardListActivity : DaggerAppCompatActivityWithResult() { @Inject lateinit var sp: SP private var disposable: CompositeDisposable = CompositeDisposable() - private var selectedItems: SparseArray = SparseArray() - private var removeActionMode: ActionMode? = null - private var sortActionMode: ActionMode? = null - + private lateinit var actionHelper: ActionHelper + private val itemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback()) private lateinit var binding: OverviewQuickwizardlistActivityBinding - private val itemTouchHelper by lazy { - val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback(UP or DOWN or START or END, 0) { - - override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { - val adapter = recyclerView.adapter as RecyclerViewAdapter - val from = viewHolder.layoutPosition - val to = target.layoutPosition - adapter.moveItem(from, to) - adapter.notifyItemMoved(from, to) - - return true - } - - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} - - override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { - super.onSelectedChanged(viewHolder, actionState) - if (actionState == ACTION_STATE_DRAG) { - viewHolder?.itemView?.alpha = 0.5f - } - } - - override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { - super.clearView(recyclerView, viewHolder) - viewHolder.itemView.alpha = 1.0f - (recyclerView.adapter as RecyclerViewAdapter).onDrop() - } - } - - ItemTouchHelper(simpleItemTouchCallback) - } - - fun startDragging(viewHolder: RecyclerView.ViewHolder) { + override fun onStartDrag(viewHolder: RecyclerView.ViewHolder) { itemTouchHelper.startDrag(viewHolder) } - private inner class RecyclerViewAdapter(var fragmentManager: FragmentManager) : RecyclerView.Adapter() { + private inner class RecyclerViewAdapter(var fragmentManager: FragmentManager) : RecyclerView.Adapter(), ItemTouchHelperAdapter { private inner class QuickWizardEntryViewHolder(val binding: OverviewQuickwizardlistItemBinding, val fragmentManager: FragmentManager) : RecyclerView.ViewHolder(binding.root) @@ -113,60 +78,48 @@ class QuickWizardListActivity : DaggerAppCompatActivityWithResult() { ) } - if (sortActionMode != null && removeActionMode != null) { - holder.binding.cardview.setOnClickListener { + fun click() { + if (actionHelper.isNoAction) { val manager = fragmentManager val editQuickWizardDialog = EditQuickWizardDialog() val bundle = Bundle() bundle.putInt("position", position) editQuickWizardDialog.arguments = bundle editQuickWizardDialog.show(manager, "EditQuickWizardDialog") - } - } - - fun updateSelection(selected: Boolean) { - if (selected) { - selectedItems.put(position, entry) - } else { - selectedItems.remove(position) - } - removeActionMode?.title = rh.gs(R.string.count_selected, selectedItems.size()) - } - - holder.binding.cardview.setOnTouchListener { _, event -> - if (event.actionMasked == MotionEvent.ACTION_UP && sortActionMode == null && removeActionMode == null) { - val manager = fragmentManager - val editQuickWizardDialog = EditQuickWizardDialog() - val bundle = Bundle() - bundle.putInt("position", position) - editQuickWizardDialog.arguments = bundle - editQuickWizardDialog.show(manager, "EditQuickWizardDialog") - } - if (event.actionMasked == MotionEvent.ACTION_DOWN && sortActionMode != null) { - startDragging(holder) - } - if (event.actionMasked == MotionEvent.ACTION_UP && removeActionMode != null) { + } else if (actionHelper.isRemoving) { holder.binding.cbRemove.toggle() - updateSelection(holder.binding.cbRemove.isChecked) + actionHelper.updateSelection(position, entry, holder.binding.cbRemove.isChecked) + } + } + // For accessibility add click lister too, unfortunately sort can not made accessible + holder.binding.root.setOnClickListener { + click() + } + holder.binding.root.setOnTouchListener { _, event -> + if (event.actionMasked == MotionEvent.ACTION_UP) { + click() + } else if (event.actionMasked == MotionEvent.ACTION_DOWN && actionHelper.isSorting) { + onStartDrag(holder) } return@setOnTouchListener true } - holder.binding.cbRemove.isChecked = selectedItems.get(position) != null - holder.binding.cbRemove.setOnCheckedChangeListener { _, value -> updateSelection(value) } - holder.binding.handleView.visibility = (sortActionMode != null).toVisibility() - holder.binding.cbRemove.visibility = (removeActionMode != null).toVisibility() - removeActionMode?.title = rh.gs(R.string.count_selected, selectedItems.size()) + holder.binding.cbRemove.isChecked = actionHelper.isSelected(position) + holder.binding.cbRemove.setOnCheckedChangeListener { _, value -> + actionHelper.updateSelection(position, entry, value) + } + holder.binding.handleView.visibility = actionHelper.isSorting.toVisibility() + holder.binding.cbRemove.visibility = actionHelper.isRemoving.toVisibility() } - override fun getItemCount(): Int = quickWizard.size() + override fun getItemCount() = quickWizard.size() - fun moveItem(from: Int, to: Int) { - quickWizard.move(from, to) + override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { + binding.recyclerview.adapter?.notifyItemMoved(fromPosition, toPosition) + quickWizard.move(fromPosition, toPosition) + return true } - fun onDrop() { - rxBus.send(EventQuickWizardChange()) - } + override fun onDrop() = rxBus.send(EventQuickWizardChange()) } override fun onCreate(savedInstanceState: Bundle?) { @@ -174,6 +127,11 @@ class QuickWizardListActivity : DaggerAppCompatActivityWithResult() { binding = OverviewQuickwizardlistActivityBinding.inflate(layoutInflater) setContentView(binding.root) + actionHelper = ActionHelper(rh, this) + actionHelper.setUpdateListHandler { binding.recyclerview.adapter?.notifyDataSetChanged() } + actionHelper.setOnRemoveHandler { removeSelected(it) } + actionHelper.enableSort = true + title = rh.gs(R.string.quickwizard) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true) @@ -184,6 +142,7 @@ class QuickWizardListActivity : DaggerAppCompatActivityWithResult() { itemTouchHelper.attachToRecyclerView(binding.recyclerview) binding.addButton.setOnClickListener { + actionHelper.finish() val manager = supportFragmentManager val editQuickWizardDialog = EditQuickWizardDialog() editQuickWizardDialog.show(manager, "EditQuickWizardDialog") @@ -203,105 +162,37 @@ class QuickWizardListActivity : DaggerAppCompatActivityWithResult() { override fun onPause() { disposable.clear() + actionHelper.finish() super.onPause() } - private fun removeSelected() { - if (selectedItems.size() > 0) - OKDialog.showConfirmation(this, rh.gs(R.string.removerecord), getConfirmationText(), Runnable { - selectedItems.forEach { _, item -> - quickWizard.remove(item.position) - rxBus.send(EventQuickWizardChange()) - } - removeActionMode?.finish() - }) - else - removeActionMode?.finish() + private fun removeSelected(selectedItems: SparseArray) { + OKDialog.showConfirmation(this, rh.gs(R.string.removerecord), getConfirmationText(selectedItems), Runnable { + selectedItems.forEach { _, item -> + quickWizard.remove(item.position) + rxBus.send(EventQuickWizardChange()) + } + actionHelper.finish() + }) } override fun onCreateOptionsMenu(menu: Menu?): Boolean { - val inflater = menuInflater - inflater.inflate(R.menu.menu_quickwizard, menu) + menuInflater.inflate(R.menu.menu_actions, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { - android.R.id.home -> { + android.R.id.home -> { finish() true } - R.id.nav_remove_items -> { - removeActionMode = startActionMode(RemoveActionModeCallback()) - true - } + else -> actionHelper.onOptionsItemSelected(item) - R.id.nav_sort_items -> { - sortActionMode = startActionMode(SortActionModeCallback()) - true - } - - else -> false } - inner class RemoveActionModeCallback : ActionMode.Callback { - - override fun onCreateActionMode(mode: ActionMode, menu: Menu?): Boolean { - mode.menuInflater.inflate(R.menu.menu_delete_selection, menu) - selectedItems.clear() - mode.title = rh.gs(R.string.count_selected, selectedItems.size()) - binding.recyclerview.adapter?.notifyDataSetChanged() - return true - } - - override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?) = false - - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - return when (item.itemId) { - R.id.remove_selected -> { - removeSelected() - true - } - - else -> false - } - } - - override fun onDestroyActionMode(mode: ActionMode?) { - removeActionMode = null - binding.recyclerview.adapter?.notifyDataSetChanged() - } - } - - inner class SortActionModeCallback : ActionMode.Callback { - - override fun onCreateActionMode(mode: ActionMode, menu: Menu?): Boolean { - mode.title = rh.gs(R.string.sort_label) - binding.recyclerview.adapter?.notifyDataSetChanged() - return true - } - - override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?) = false - - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - return when (item.itemId) { - R.id.remove_selected -> { - removeSelected() - true - } - - else -> false - } - } - - override fun onDestroyActionMode(mode: ActionMode?) { - sortActionMode = null - binding.recyclerview.adapter?.notifyDataSetChanged() - } - } - - private fun getConfirmationText(): String { + private fun getConfirmationText(selectedItems: SparseArray): String { if (selectedItems.size() == 1) { val entry = selectedItems.valueAt(0) return "${rh.gs(R.string.remove_button)} ${entry.buttonText()} ${rh.gs(R.string.format_carbs, entry.carbs())}\n" + @@ -309,4 +200,5 @@ class QuickWizardListActivity : DaggerAppCompatActivityWithResult() { } return rh.gs(R.string.confirm_remove_multiple_items, selectedItems.size()) } + } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/source/BGSourceFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/source/BGSourceFragment.kt index e175939d4c..0f1386afbb 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/source/BGSourceFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/source/BGSourceFragment.kt @@ -3,7 +3,6 @@ package info.nightscout.androidaps.plugins.source import android.os.Bundle import android.util.SparseArray import android.view.* -import androidx.appcompat.widget.Toolbar import androidx.core.util.forEach import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -26,6 +25,7 @@ import info.nightscout.androidaps.interfaces.PluginBase import info.nightscout.androidaps.interfaces.ProfileFunction import info.nightscout.androidaps.logging.UserEntryLogger import info.nightscout.androidaps.plugins.bus.RxBus +import info.nightscout.androidaps.utils.ActionHelper import info.nightscout.androidaps.utils.DateUtil import info.nightscout.androidaps.utils.FabricPrivacy import info.nightscout.androidaps.utils.T @@ -54,11 +54,9 @@ class BGSourceFragment : DaggerFragment() { private val disposable = CompositeDisposable() private val millsToThePast = T.hours(36).msecs() - private var selectedItems: SparseArray = SparseArray() - private var toolbar: Toolbar? = null - private var removeActionMode: ActionMode? = null - + private lateinit var actionHelper: ActionHelper private var _binding: BgsourceFragmentBinding? = null + // This property is only valid between onCreateView and onDestroyView. private val binding get() = _binding!! @@ -67,8 +65,10 @@ class BGSourceFragment : DaggerFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - toolbar = activity?.findViewById(R.id.toolbar) - setHasOptionsMenu(true) + actionHelper = ActionHelper(rh, activity) + actionHelper.setUpdateListHandler { binding.recyclerview.adapter?.notifyDataSetChanged() } + actionHelper.setOnRemoveHandler { removeSelected(it) } + setHasOptionsMenu(actionHelper.inMenu) binding.recyclerview.setHasFixedSize(true) binding.recyclerview.layoutManager = LinearLayoutManager(view.context) @@ -97,44 +97,30 @@ class BGSourceFragment : DaggerFragment() { @Synchronized override fun onPause() { - removeActionMode?.finish() + actionHelper.finish() disposable.clear() super.onPause() } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.menu_bgsource, menu) + actionHelper.onCreateOptionsMenu(menu, inflater) } override fun onPrepareOptionsMenu(menu: Menu) { - // Only show when tab bg source is shown - menu.findItem(R.id.nav_remove_items)?.isVisible = isResumed super.onPrepareOptionsMenu(menu) + actionHelper.onPrepareOptionsMenu(menu) } @Synchronized override fun onDestroyView() { super.onDestroyView() - removeActionMode?.finish() binding.recyclerview.adapter = null // avoid leaks _binding = null } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.nav_remove_items -> { - if (toolbar != null) { - removeActionMode = toolbar?.startActionMode(RemoveActionModeCallback()) // in overview - } else { - removeActionMode = activity?.startActionMode(RemoveActionModeCallback()) // in Single FragmentActivity - } - true - } - - else -> false - } - } + override fun onOptionsItemSelected(item: MenuItem) = + actionHelper.onOptionsItemSelected(item) inner class RecyclerViewAdapter internal constructor(private var glucoseValues: List) : RecyclerView.Adapter() { @@ -159,34 +145,27 @@ class BGSourceFragment : DaggerFragment() { if (diff < T.secs(20).msecs()) holder.binding.root.setBackgroundColor(rh.gc(R.color.errorAlertBackground)) } - fun updateSelection(selected: Boolean) { - if (selected) { - selectedItems.put(position, glucoseValue) - } else { - selectedItems.remove(position) - } - removeActionMode?.title = rh.gs(R.string.count_selected, selectedItems.size()) - } + holder.binding.root.setOnLongClickListener { - if (removeActionMode == null) { - removeActionMode = toolbar?.startActionMode(RemoveActionModeCallback()) - } + actionHelper.startRemove() holder.binding.cbRemove.toggle() - updateSelection(holder.binding.cbRemove.isChecked) + actionHelper.updateSelection(position, glucoseValue, holder.binding.cbRemove.isChecked) true } holder.binding.root.setOnClickListener { - if (removeActionMode != null) { + if (actionHelper.isRemoving) { holder.binding.cbRemove.toggle() - updateSelection(holder.binding.cbRemove.isChecked) + actionHelper.updateSelection(position, glucoseValue, holder.binding.cbRemove.isChecked) } } - holder.binding.cbRemove.setOnCheckedChangeListener { _, value -> updateSelection(value) } - holder.binding.cbRemove.isChecked = selectedItems.get(position) != null - holder.binding.cbRemove.visibility = (removeActionMode != null).toVisibility() + holder.binding.cbRemove.setOnCheckedChangeListener { _, value -> + actionHelper.updateSelection(position, glucoseValue, value) + } + holder.binding.cbRemove.isChecked = actionHelper.isSelected(position) + holder.binding.cbRemove.visibility = actionHelper.isRemoving.toVisibility() } - override fun getItemCount(): Int = glucoseValues.size + override fun getItemCount() = glucoseValues.size inner class GlucoseValuesViewHolder(view: View) : RecyclerView.ViewHolder(view) { @@ -194,36 +173,7 @@ class BGSourceFragment : DaggerFragment() { } } - inner class RemoveActionModeCallback : ActionMode.Callback { - - override fun onCreateActionMode(mode: ActionMode, menu: Menu?): Boolean { - mode.menuInflater.inflate(R.menu.menu_delete_selection, menu) - selectedItems.clear() - mode.title = rh.gs(R.string.count_selected, selectedItems.size()) - binding.recyclerview.adapter?.notifyDataSetChanged() - return true - } - - override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?) = false - - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - return when (item.itemId) { - R.id.remove_selected -> { - removeSelected() - true - } - - else -> false - } - } - - override fun onDestroyActionMode(mode: ActionMode?) { - removeActionMode = null - binding.recyclerview.adapter?.notifyDataSetChanged() - } - } - - private fun getConfirmationText(): String { + private fun getConfirmationText(selectedItems: SparseArray): String { if (selectedItems.size() == 1) { val glucoseValue = selectedItems.valueAt(0) return dateUtil.dateAndTimeString(glucoseValue.timestamp) + "\n" + glucoseValue.valueToUnitsString(profileFunction.getUnits()) @@ -231,36 +181,33 @@ class BGSourceFragment : DaggerFragment() { return rh.gs(R.string.confirm_remove_multiple_items, selectedItems.size()) } - private fun removeSelected() { - if (selectedItems.size() > 0) - activity?.let { activity -> - OKDialog.showConfirmation(activity, rh.gs(R.string.removerecord), getConfirmationText(), Runnable { - selectedItems.forEach { _, glucoseValue -> - val source = when ((activePlugin.activeBgSource as PluginBase).pluginDescription.pluginName) { - R.string.dexcom_app_patched -> Sources.Dexcom - R.string.eversense -> Sources.Eversense - R.string.Glimp -> Sources.Glimp - R.string.MM640g -> Sources.MM640g - R.string.nsclientbg -> Sources.NSClientSource - R.string.poctech -> Sources.PocTech - R.string.tomato -> Sources.Tomato - R.string.glunovo -> Sources.Glunovo - R.string.xdrip -> Sources.Xdrip - else -> Sources.Unknown - } - uel.log( - Action.BG_REMOVED, source, - ValueWithUnit.Timestamp(glucoseValue.timestamp) - ) - repository.runTransactionForResult(InvalidateGlucoseValueTransaction(glucoseValue.id)) - .doOnError { aapsLogger.error(LTag.DATABASE, "Error while invalidating BG value", it) } - .blockingGet() - .also { result -> result.invalidated.forEach { aapsLogger.debug(LTag.DATABASE, "Invalidated bg $it") } } + private fun removeSelected(selectedItems: SparseArray) { + activity?.let { activity -> + OKDialog.showConfirmation(activity, rh.gs(R.string.removerecord), getConfirmationText(selectedItems), Runnable { + selectedItems.forEach { _, glucoseValue -> + val source = when ((activePlugin.activeBgSource as PluginBase).pluginDescription.pluginName) { + R.string.dexcom_app_patched -> Sources.Dexcom + R.string.eversense -> Sources.Eversense + R.string.Glimp -> Sources.Glimp + R.string.MM640g -> Sources.MM640g + R.string.nsclientbg -> Sources.NSClientSource + R.string.poctech -> Sources.PocTech + R.string.tomato -> Sources.Tomato + R.string.glunovo -> Sources.Glunovo + R.string.xdrip -> Sources.Xdrip + else -> Sources.Unknown } - removeActionMode?.finish() - }) - } - else - removeActionMode?.finish() + uel.log( + Action.BG_REMOVED, source, + ValueWithUnit.Timestamp(glucoseValue.timestamp) + ) + repository.runTransactionForResult(InvalidateGlucoseValueTransaction(glucoseValue.id)) + .doOnError { aapsLogger.error(LTag.DATABASE, "Error while invalidating BG value", it) } + .blockingGet() + .also { result -> result.invalidated.forEach { aapsLogger.debug(LTag.DATABASE, "Invalidated bg $it") } } + } + actionHelper.finish() + }) + } } } diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml index 8b7d553260..9553b51a99 100644 --- a/app/src/main/res/drawable/ic_settings.xml +++ b/app/src/main/res/drawable/ic_settings.xml @@ -6,6 +6,6 @@ android:viewportHeight="24" android:viewportWidth="24"> diff --git a/app/src/main/res/drawable/ic_sort.xml b/app/src/main/res/drawable/ic_sort.xml new file mode 100644 index 0000000000..4b1317ac20 --- /dev/null +++ b/app/src/main/res/drawable/ic_sort.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/menu/menu_bgsource.xml b/app/src/main/res/menu/menu_bgsource.xml deleted file mode 100644 index 87d741a9eb..0000000000 --- a/app/src/main/res/menu/menu_bgsource.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/app/src/main/res/menu/menu_delete_selection.xml b/app/src/main/res/menu/menu_delete_selection.xml index be03cd47cd..4fb8d9e96a 100644 --- a/app/src/main/res/menu/menu_delete_selection.xml +++ b/app/src/main/res/menu/menu_delete_selection.xml @@ -4,8 +4,8 @@ + app:showAsAction="ifRoom" /> diff --git a/app/src/main/res/menu/menu_single_fragment.xml b/app/src/main/res/menu/menu_single_fragment.xml index 0bc5ee19b8..2a1d4db202 100644 --- a/app/src/main/res/menu/menu_single_fragment.xml +++ b/app/src/main/res/menu/menu_single_fragment.xml @@ -2,9 +2,12 @@ xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" tools:context=".MainActivity"> + + diff --git a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationFragment.kt b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationFragment.kt index 8543558515..6da7293296 100644 --- a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationFragment.kt +++ b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationFragment.kt @@ -3,13 +3,15 @@ package info.nightscout.androidaps.plugins.general.automation import android.annotation.SuppressLint import android.content.Context import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.text.method.ScrollingMovementMethod import android.util.SparseArray import android.view.* -import androidx.core.util.forEach import android.widget.ImageView import android.widget.LinearLayout import androidx.annotation.DrawableRes +import androidx.core.util.forEach import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -20,24 +22,24 @@ import info.nightscout.androidaps.automation.databinding.AutomationEventItemBind import info.nightscout.androidaps.automation.databinding.AutomationFragmentBinding import info.nightscout.androidaps.database.entities.UserEntry.Action import info.nightscout.androidaps.database.entities.UserEntry.Sources +import info.nightscout.androidaps.extensions.toVisibility import info.nightscout.androidaps.logging.UserEntryLogger import info.nightscout.androidaps.plugins.bus.RxBus import info.nightscout.androidaps.plugins.general.automation.dialogs.EditEventDialog +import info.nightscout.androidaps.utils.dragHelpers.ItemTouchHelperAdapter +import info.nightscout.androidaps.utils.dragHelpers.OnStartDragListener +import info.nightscout.androidaps.utils.dragHelpers.SimpleItemTouchHelperCallback import info.nightscout.androidaps.plugins.general.automation.events.EventAutomationDataChanged import info.nightscout.androidaps.plugins.general.automation.events.EventAutomationUpdateGui import info.nightscout.androidaps.plugins.general.automation.triggers.TriggerConnector +import info.nightscout.androidaps.utils.ActionHelper import info.nightscout.androidaps.utils.FabricPrivacy import info.nightscout.androidaps.utils.HtmlHelper import info.nightscout.androidaps.utils.alertDialogs.OKDialog -import io.reactivex.rxjava3.kotlin.plusAssign -import info.nightscout.androidaps.extensions.toVisibility -import info.nightscout.androidaps.plugins.general.automation.dragHelpers.ItemTouchHelperAdapter -import info.nightscout.androidaps.plugins.general.automation.dragHelpers.OnStartDragListener -import info.nightscout.androidaps.plugins.general.automation.dragHelpers.SimpleItemTouchHelperCallback import info.nightscout.androidaps.utils.resources.ResourceHelper import info.nightscout.androidaps.utils.rx.AapsSchedulers import io.reactivex.rxjava3.disposables.CompositeDisposable -import java.util.* +import io.reactivex.rxjava3.kotlin.plusAssign import javax.inject.Inject class AutomationFragment : DaggerFragment(), OnStartDragListener { @@ -52,11 +54,8 @@ class AutomationFragment : DaggerFragment(), OnStartDragListener { private var disposable: CompositeDisposable = CompositeDisposable() private lateinit var eventListAdapter: EventListAdapter - private var selectedItems: SparseArray = SparseArray() - private var removeActionMode: ActionMode? = null - private var sortActionMode: ActionMode? = null + private lateinit var actionHelper: ActionHelper private val itemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback()) - private var _binding: AutomationFragmentBinding? = null // This property is only valid between onCreateView and onDestroyView. @@ -64,20 +63,22 @@ class AutomationFragment : DaggerFragment(), OnStartDragListener { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = AutomationFragmentBinding.inflate(inflater, container, false) + actionHelper = ActionHelper(rh, activity) + actionHelper.setUpdateListHandler { binding.eventListView.adapter?.notifyDataSetChanged() } + actionHelper.setOnRemoveHandler { removeSelected(it) } + actionHelper.enableSort = true + setHasOptionsMenu(actionHelper.inMenu) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setHasOptionsMenu(true) - eventListAdapter = EventListAdapter() binding.eventListView.layoutManager = LinearLayoutManager(context) binding.eventListView.adapter = eventListAdapter binding.logView.movementMethod = ScrollingMovementMethod() binding.fabAddEvent.setOnClickListener { - removeActionMode?.finish() - sortActionMode?.finish() + actionHelper.finish() val dialog = EditEventDialog() val args = Bundle() args.putString("event", AutomationEvent(injector).toJSON()) @@ -110,8 +111,7 @@ class AutomationFragment : DaggerFragment(), OnStartDragListener { @Synchronized override fun onPause() { - removeActionMode?.finish() - sortActionMode?.finish() + actionHelper.finish() super.onPause() disposable.clear() } @@ -119,38 +119,16 @@ class AutomationFragment : DaggerFragment(), OnStartDragListener { @Synchronized override fun onDestroyView() { super.onDestroyView() - removeActionMode?.finish() - sortActionMode?.finish() _binding = null } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.menu_automation, menu) + actionHelper.onCreateOptionsMenu(menu, inflater) } - override fun onPrepareOptionsMenu(menu: Menu) { - // Only show when tab automation is shown - menu.findItem(R.id.nav_remove_automation_items)?.isVisible = isResumed - menu.findItem(R.id.nav_sort_automation_items)?.isVisible = isResumed - super.onPrepareOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.nav_remove_automation_items -> { - removeActionMode = activity?.startActionMode(RemoveActionModeCallback()) - true - } - - R.id.nav_sort_automation_items -> { - sortActionMode = activity?.startActionMode(SortActionModeCallback()) - true - } - - else -> false - } - } + override fun onOptionsItemSelected(item: MenuItem): Boolean = + actionHelper.onOptionsItemSelected(item) @Synchronized private fun updateGui() { @@ -235,52 +213,74 @@ class AutomationFragment : DaggerFragment(), OnStartDragListener { } holder.binding.aapsLogo.visibility = (automation.systemAction).toVisibility() - fun updateSelection(selected: Boolean) { - if (selected) { - selectedItems.put(position, automation) - } else { - selectedItems.remove(position) - } - removeActionMode?.title = rh.gs(R.string.count_selected, selectedItems.size()) + var longPress = false + val handler = Handler(Looper.getMainLooper()) + val mLongPressed = Runnable { + longPress = true + actionHelper.startAction() } - holder.binding.rootLayout.setOnTouchListener { _, touchEvent -> - if (touchEvent.actionMasked == MotionEvent.ACTION_UP && sortActionMode == null && removeActionMode == null) { + fun click() { + if (actionHelper.isNoAction && !longPress) { val dialog = EditEventDialog() val args = Bundle() args.putString("event", automation.toJSON()) args.putInt("position", position) dialog.arguments = args dialog.show(childFragmentManager, "EditEventDialog") - } - if (touchEvent.actionMasked == MotionEvent.ACTION_DOWN && sortActionMode != null) { - onStartDrag(holder) - } - if (touchEvent.actionMasked == MotionEvent.ACTION_UP && removeActionMode != null) { + } else if (actionHelper.isRemoving) { holder.binding.cbRemove.toggle() - updateSelection(holder.binding.cbRemove.isChecked) - removeActionMode?.title = rh.gs(R.string.count_selected, selectedItems.size()) + actionHelper.updateSelection(position, automation, holder.binding.cbRemove.isChecked) + } + } + // Implement click listeners and touch handler for accessibility, unfortunately drag and drop for sorting can not be made accessible + holder.binding.rootLayout.setOnClickListener { + click() + } + holder.binding.rootLayout.setOnLongClickListener { + actionHelper.startAction() + true + } + holder.binding.rootLayout.setOnTouchListener { _, touchEvent -> + when (touchEvent.actionMasked) { + MotionEvent.ACTION_UP -> { + handler.removeCallbacks(mLongPressed) + click() + longPress = false + } + + MotionEvent.ACTION_DOWN -> { + if (actionHelper.isSorting) { + onStartDrag(holder) + } + if (actionHelper.isNoAction && !actionHelper.inMenu) { + handler.postDelayed(mLongPressed, ViewConfiguration.getLongPressTimeout().toLong()) + } + } + } return@setOnTouchListener true } - holder.binding.cbRemove.isChecked = selectedItems.get(position) != null - holder.binding.cbRemove.setOnCheckedChangeListener { _, value -> updateSelection(value) } - holder.binding.iconSort.visibility = (sortActionMode != null).toVisibility() - holder.binding.cbRemove.visibility = (removeActionMode != null).toVisibility() - holder.binding.cbRemove.isEnabled = !automation.readOnly - holder.binding.enabled.visibility = if (removeActionMode == null) View.VISIBLE else View.INVISIBLE + + holder.binding.cbRemove.isChecked = actionHelper.isSelected(position) + holder.binding.cbRemove.setOnCheckedChangeListener { _, value -> + actionHelper.updateSelection(position, automation, value) + } + holder.binding.iconSort.visibility = actionHelper.isSorting.toVisibility() + holder.binding.cbRemove.visibility = actionHelper.isRemoving.toVisibility() + holder.binding.cbRemove.isEnabled = automation.readOnly.not() + holder.binding.enabled.visibility = if (actionHelper.isRemoving) View.INVISIBLE else View.VISIBLE } - override fun getItemCount(): Int = automationPlugin.size() + override fun getItemCount() = automationPlugin.size() override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { + binding.eventListView.adapter?.notifyItemMoved(fromPosition, toPosition) automationPlugin.swap(fromPosition, toPosition) return true } - override fun onDrop() { - rxBus.send(EventAutomationDataChanged()) - } + override fun onDrop() = rxBus.send(EventAutomationDataChanged()) inner class ViewHolder(view: View, val context: Context) : RecyclerView.ViewHolder(view) { @@ -288,54 +288,7 @@ class AutomationFragment : DaggerFragment(), OnStartDragListener { } } - inner class SortActionModeCallback : ActionMode.Callback { - - override fun onCreateActionMode(mode: ActionMode, menu: Menu?): Boolean { - mode.title = rh.gs(R.string.sort_label) - binding.eventListView.adapter?.notifyDataSetChanged() - return true - } - - override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?) = false - - override fun onActionItemClicked(mode: ActionMode, item: MenuItem) = false - - override fun onDestroyActionMode(mode: ActionMode?) { - sortActionMode = null - binding.eventListView.adapter?.notifyDataSetChanged() - } - } - - inner class RemoveActionModeCallback : ActionMode.Callback { - - override fun onCreateActionMode(mode: ActionMode, menu: Menu?): Boolean { - mode.menuInflater.inflate(R.menu.menu_delete_selection, menu) - selectedItems.clear() - mode.title = rh.gs(R.string.count_selected, selectedItems.size()) - binding.eventListView.adapter?.notifyDataSetChanged() - return true - } - - override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?) = false - - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - return when (item.itemId) { - R.id.remove_selected -> { - removeSelected() - true - } - - else -> false - } - } - - override fun onDestroyActionMode(mode: ActionMode?) { - removeActionMode = null - binding.eventListView.adapter?.notifyDataSetChanged() - } - } - - private fun getConfirmationText(): String { + private fun getConfirmationText(selectedItems: SparseArray): String { if (selectedItems.size() == 1) { val event = selectedItems.valueAt(0) return rh.gs(R.string.removerecord) + " " + event.title @@ -343,20 +296,17 @@ class AutomationFragment : DaggerFragment(), OnStartDragListener { return rh.gs(R.string.confirm_remove_multiple_items, selectedItems.size()) } - private fun removeSelected() { - if (selectedItems.size() > 0) { - activity?.let { activity -> - OKDialog.showConfirmation(activity, rh.gs(R.string.removerecord), getConfirmationText(), Runnable { - selectedItems.forEach { _, event -> - uel.log(Action.AUTOMATION_REMOVED, Sources.Automation, event.title) - automationPlugin.removeAt(event.position) - rxBus.send(EventAutomationDataChanged()) - } - removeActionMode?.finish() - }) - } - } else { - removeActionMode?.finish() + private fun removeSelected(selectedItems: SparseArray) { + activity?.let { activity -> + OKDialog.showConfirmation(activity, rh.gs(R.string.removerecord), getConfirmationText(selectedItems), Runnable { + selectedItems.forEach { _, event -> + uel.log(Action.AUTOMATION_REMOVED, Sources.Automation, event.title) + automationPlugin.removeAt(event.position) + rxBus.send(EventAutomationDataChanged()) + } + actionHelper.finish() + }) } } + } diff --git a/automation/src/main/res/menu/menu_automation.xml b/automation/src/main/res/menu/menu_automation.xml deleted file mode 100644 index 3f51834c0a..0000000000 --- a/automation/src/main/res/menu/menu_automation.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/core/src/main/java/info/nightscout/androidaps/utils/ActionHelper.kt b/core/src/main/java/info/nightscout/androidaps/utils/ActionHelper.kt new file mode 100644 index 0000000000..2844da68fb --- /dev/null +++ b/core/src/main/java/info/nightscout/androidaps/utils/ActionHelper.kt @@ -0,0 +1,185 @@ +package info.nightscout.androidaps.utils + +import android.util.SparseArray +import android.view.ActionMode +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.fragment.app.FragmentActivity +import info.nightscout.androidaps.core.R + +import info.nightscout.androidaps.utils.resources.ResourceHelper + +class ActionHelper(val rh: ResourceHelper, val activity: FragmentActivity?) { + + var enableSort = false + private var selectedItems: SparseArray = SparseArray() + private var actionMode: ActionMode? = null + private var removeActionMode: ActionMode? = null + private var sortActionMode: ActionMode? = null + private var onRemove: ((selectedItems: SparseArray) -> Unit)? = null + private var onUpdate: (() -> Unit)? = null + + val inMenu: Boolean + get() { + val parentClass = this.activity?.let { it::class.simpleName } + return parentClass == "SingleFragmentActivity" + } + + val enableRemove: Boolean + get() = onRemove != null + + val isNoAction: Boolean + get() = actionMode == null && removeActionMode == null && sortActionMode == null + + val isAction: Boolean + get() = actionMode != null + + val isSorting: Boolean + get() = sortActionMode != null + + val isRemoving: Boolean + get() = removeActionMode != null + + fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.nav_remove_items -> { + removeActionMode = activity?.startActionMode(RemoveActionModeCallback()) + true + } + + R.id.nav_sort_items -> { + sortActionMode = activity?.startActionMode(SortActionModeCallback()) + true + } + + else -> false + } + } + + fun updateSelection(position: Int, item: T, selected: Boolean) { + if (selected) { + selectedItems.put(position, item) + } else { + selectedItems.remove(position) + } + removeActionMode?.title = rh.gs(R.string.count_selected, selectedItems.size()) + } + + fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + if (inMenu) { + inflater.inflate(R.menu.menu_actions, menu) + } + } + + fun onPrepareOptionsMenu(menu: Menu) { + menu.findItem(R.id.nav_remove_items)?.isVisible = enableRemove + menu.findItem(R.id.nav_sort_items)?.isVisible = enableSort + } + + fun startAction() { + if (actionMode == null) { + actionMode = activity?.startActionMode(ActionModeCallback()) + } + } + + fun startRemove() { + if (removeActionMode == null) { + removeActionMode = activity?.startActionMode(RemoveActionModeCallback()) + } + } + + fun startSort() { + if (sortActionMode == null) { + sortActionMode = activity?.startActionMode(SortActionModeCallback()) + } + } + + fun isSelected(position: Int) = + selectedItems.get(position) != null + + fun setOnRemoveHandler(onRemove: (selectedItems: SparseArray) -> Unit) { + this.onRemove = onRemove + } + + fun setUpdateListHandler(onUpdate: () -> Unit) { + this.onUpdate = onUpdate + } + + fun finish() { + actionMode?.finish() + removeActionMode?.finish() + sortActionMode?.finish() + } + + private inner class ActionModeCallback : ActionMode.Callback { + + override fun onCreateActionMode(mode: ActionMode, menu: Menu?): Boolean { + mode.title = activity?.title + mode.menuInflater.inflate(R.menu.menu_actions, menu) + onUpdate?.let { it() } + return true + } + + override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?) = false + + override fun onActionItemClicked(mode: ActionMode, item: MenuItem) = + onOptionsItemSelected(item) + + override fun onDestroyActionMode(mode: ActionMode?) { + actionMode = null + } + } + + private inner class SortActionModeCallback : ActionMode.Callback { + + override fun onCreateActionMode(mode: ActionMode, menu: Menu?): Boolean { + mode.title = rh.gs(R.string.sort_label) + onUpdate?.let { it() } + return true + } + + override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?) = false + + override fun onActionItemClicked(mode: ActionMode, item: MenuItem) = false + + override fun onDestroyActionMode(mode: ActionMode?) { + sortActionMode = null + onUpdate?.let { it() } + } + } + + private inner class RemoveActionModeCallback : ActionMode.Callback { + + override fun onCreateActionMode(mode: ActionMode, menu: Menu?): Boolean { + mode.menuInflater.inflate(R.menu.menu_delete_selection, menu) + selectedItems.clear() + mode.title = rh.gs(R.string.count_selected, selectedItems.size()) + onUpdate?.let { it() } + return true + } + + override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?) = false + + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + return when (item.itemId) { + R.id.remove_selected -> { + if (selectedItems.size() > 0) { + onRemove?.let { it(selectedItems) } + } else { + finish() + } + true + } + + else -> false + } + } + + override fun onDestroyActionMode(mode: ActionMode?) { + removeActionMode = null + onUpdate?.let { it() } + } + } + +} diff --git a/core/src/main/res/drawable/ic_sort.xml b/core/src/main/res/drawable/ic_sort.xml new file mode 100644 index 0000000000..4b1317ac20 --- /dev/null +++ b/core/src/main/res/drawable/ic_sort.xml @@ -0,0 +1,11 @@ + + + diff --git a/core/src/main/res/drawable/ic_trash.xml b/core/src/main/res/drawable/ic_trash.xml new file mode 100644 index 0000000000..770ca733c8 --- /dev/null +++ b/core/src/main/res/drawable/ic_trash.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/menu/menu_quickwizard.xml b/core/src/main/res/menu/menu_actions.xml similarity index 52% rename from app/src/main/res/menu/menu_quickwizard.xml rename to core/src/main/res/menu/menu_actions.xml index 7a3e86905d..798c6a5da2 100644 --- a/app/src/main/res/menu/menu_quickwizard.xml +++ b/core/src/main/res/menu/menu_actions.xml @@ -1,14 +1,19 @@ - - - - - - - + + + + + + + diff --git a/automation/src/main/res/menu/menu_delete_selection.xml b/core/src/main/res/menu/menu_delete_selection.xml similarity index 75% rename from automation/src/main/res/menu/menu_delete_selection.xml rename to core/src/main/res/menu/menu_delete_selection.xml index 1090412c5a..4fb8d9e96a 100644 --- a/automation/src/main/res/menu/menu_delete_selection.xml +++ b/core/src/main/res/menu/menu_delete_selection.xml @@ -4,8 +4,8 @@ + app:showAsAction="ifRoom" /> diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 1e2c53c19e..74e30a005c 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -556,6 +556,11 @@ %1$.0f%% Basal Basal % + %1$d selected + Sort + Remove Items + Sort Items + Remove Selected Items %1$d day