From 43bce022d5917716a4588e05ca157e298bbc86ba Mon Sep 17 00:00:00 2001 From: bellerbrock Date: Mon, 17 Feb 2025 09:10:03 -0500 Subject: [PATCH 01/11] stashed changes --- .../tracker/activities/FieldDetailFragment.kt | 55 +++++++++++++ .../tracker/database/DataHelper.java | 10 +++ .../tracker/database/dao/StudyDao.kt | 77 +++++++++++++++++++ app/src/main/res/drawable/ic_tag_text.xml | 1 + .../main/res/layout/fragment_field_detail.xml | 13 ++++ app/src/main/res/values/strings.xml | 3 + 6 files changed, 159 insertions(+) create mode 100644 app/src/main/res/drawable/ic_tag_text.xml diff --git a/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt b/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt index 62c816dc9..8886a39be 100644 --- a/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt +++ b/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt @@ -11,6 +11,8 @@ import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.AutoCompleteTextView import android.widget.EditText import android.widget.ImageView import android.widget.LinearLayout @@ -73,6 +75,7 @@ class FieldDetailFragment : Fragment(), FieldSyncController { private lateinit var entryCountChip: Chip private lateinit var attributeCountChip: Chip private lateinit var sortOrderChip: Chip + private lateinit var editUniqueChip: Chip private lateinit var traitCountChip: Chip private lateinit var observationCountChip: Chip private lateinit var trialNameChip: Chip @@ -97,6 +100,7 @@ class FieldDetailFragment : Fragment(), FieldSyncController { entryCountChip = rootView.findViewById(R.id.entryCountChip) attributeCountChip = rootView.findViewById(R.id.attributeCountChip) sortOrderChip = rootView.findViewById(R.id.sortOrderChip) + editUniqueChip = rootView.findViewById(R.id.editUniqueChip) traitCountChip = rootView.findViewById(R.id.traitCountChip) observationCountChip = rootView.findViewById(R.id.observationCountChip) detailRecyclerView = rootView.findViewById(R.id.fieldDetailRecyclerView) @@ -186,6 +190,15 @@ class FieldDetailFragment : Fragment(), FieldSyncController { } } + editUniqueChip.setOnClickListener { + fieldId?.let { id -> + val field = database.getFieldObject(id) + field?.let { + showEditUniqueIdDialog(it) + } + } + } + disableDataChipRipples() Log.d("FieldDetailFragment", "onCreateView End") @@ -295,6 +308,7 @@ class FieldDetailFragment : Fragment(), FieldSyncController { entryCountChip.text = entryCount attributeCountChip.text = attributeCount sortOrderChip.text = getString(R.string.field_sort_entries) + editUniqueChip.text = getString(R.string.field_edit_unique_id) val lastEdit = field.date_edit if (!lastEdit.isNullOrEmpty()) { @@ -434,6 +448,47 @@ class FieldDetailFragment : Fragment(), FieldSyncController { dialog.show() } + private fun showEditUniqueIdDialog(field: FieldObject) { + val inflater = requireActivity().layoutInflater + val dialogView = inflater.inflate(R.layout.dialog_field_edit_unique, null) + + val uniqueDropdown = dialogView.findViewById(R.id.unique_dropdown) + val errorMessageView = dialogView.findViewById(R.id.error_message) + + // Get list of possible unique attributes + val uniqueOptions = database.getPossibleUniqueAttributes(field.exp_id) + val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, uniqueOptions) + uniqueDropdown.setAdapter(adapter) + + // Set current value if exists + field.unique_id?.let { uniqueDropdown.setText(it) } + + val builder = AlertDialog.Builder(requireContext(), R.style.AppAlertDialog) + .setTitle(getString(R.string.field_edit_unique_id)) + .setView(dialogView) + .setPositiveButton(getString(R.string.dialog_save), null) + .setNegativeButton(getString(R.string.dialog_cancel), null) + + val dialog = builder.create() + + dialog.setOnShowListener { + val positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) + positiveButton.setOnClickListener { + val selectedAttribute = uniqueDropdown.text.toString() + + if (selectedAttribute.isNotBlank() && uniqueOptions.contains(selectedAttribute)) { + database.updateFieldUniqueId(field.exp_id, selectedAttribute) + loadFieldDetails() + dialog.dismiss() + } else { + showErrorMessage(errorMessageView, getString(R.string.invalid_unique_id_selection)) + } + } + } + + dialog.show() + } + /** * Checks if the given newName is unique among all fields, considering both import names and aliases. */ diff --git a/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java b/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java index 08584549a..cf952eadb 100644 --- a/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java +++ b/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java @@ -888,6 +888,16 @@ public void updateObservations(List observations) { // db.endTransaction(); } + public List getPossibleUniqueAttributes(int studyId) { + open(); + return StudyDao.Companion.getPossibleUniqueAttributes(studyId); + } + + public void updateFieldUniqueId(int studyId, String newUniqueAttribute) { + open(); + StudyDao.Companion.updateFieldUniqueId(studyId, newUniqueAttribute); + } + public void updateImages(List images) { ArrayList ids = new ArrayList<>(); diff --git a/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt b/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt index d5ab3e9b5..762509788 100644 --- a/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt +++ b/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt @@ -33,6 +33,83 @@ class StudyDao { companion object { + fun getPossibleUniqueAttributes(studyId: Int): List = withDatabase { db -> + val query = """ + SELECT oua.observation_unit_attribute_name + FROM observation_units_attributes oua + WHERE oua.study_id = ? + AND ( + SELECT COUNT(DISTINCT ouv.observation_unit_value_name) + FROM observation_units_values ouv + WHERE ouv.observation_unit_attribute_db_id = oua.internal_id_observation_unit_attribute + ) = ( + SELECT COUNT(*) + FROM observation_units_values ouv + WHERE ouv.observation_unit_attribute_db_id = oua.internal_id_observation_unit_attribute + ) + """ + + db.rawQuery(query, arrayOf(studyId.toString())).use { cursor -> + val attributes = mutableListOf() + while (cursor.moveToNext()) { + cursor.getString(0)?.let { attributes.add(it) } + } + attributes + } + } ?: emptyList() + + fun updateFieldUniqueId(studyId: Int, newUniqueAttribute: String) = withDatabase { db -> + db.beginTransaction() + try { + // Get the attribute ID for the new unique identifier + val attrIdQuery = """ + SELECT internal_id_observation_unit_attribute + FROM observation_units_attributes + WHERE study_id = ? AND observation_unit_attribute_name = ? + """ + + val attrId = db.rawQuery(attrIdQuery, arrayOf(studyId.toString(), newUniqueAttribute)) + .use { cursor -> + if (cursor.moveToFirst()) cursor.getInt(0) else null + } ?: return@withDatabase + + // Update studies table + db.execSQL( + "UPDATE studies SET study_unique_id_name = ? WHERE internal_id_study = ?", + arrayOf(newUniqueAttribute, studyId.toString()) + ) + + // Update observation_units table using values from observation_units_values + val updateUnitsQuery = """ + UPDATE observation_units + SET observation_unit_db_id = ( + SELECT ouv.observation_unit_value_name + FROM observation_units_values ouv + WHERE ouv.observation_unit_id = observation_units.internal_id_observation_unit + AND ouv.observation_unit_attribute_db_id = ? + ) + WHERE study_id = ? + """ + db.execSQL(updateUnitsQuery, arrayOf(attrId.toString(), studyId.toString())) + + // Update observations table using the new observation_unit_db_id values + val updateObsQuery = """ + UPDATE observations + SET observation_unit_id = ( + SELECT ou.observation_unit_db_id + FROM observation_units ou + WHERE ou.internal_id_observation_unit = observations.observation_unit_id + ) + WHERE study_id = ? + """ + db.execSQL(updateObsQuery, arrayOf(studyId.toString())) + + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + private fun fixPlotAttributes(db: SQLiteDatabase) { db.rawQuery("PRAGMA foreign_keys=OFF;", null).close() diff --git a/app/src/main/res/drawable/ic_tag_text.xml b/app/src/main/res/drawable/ic_tag_text.xml new file mode 100644 index 000000000..f0c0cb4a2 --- /dev/null +++ b/app/src/main/res/drawable/ic_tag_text.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_field_detail.xml b/app/src/main/res/layout/fragment_field_detail.xml index 560bb67ef..6509217a1 100644 --- a/app/src/main/res/layout/fragment_field_detail.xml +++ b/app/src/main/res/layout/fragment_field_detail.xml @@ -182,6 +182,19 @@ app:chipStrokeColor="?attr/selectableChipStroke" app:chipStrokeWidth="2dp" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 33521bf32..4f2ecc5e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -204,6 +204,9 @@ import name display name + Change Unique Id + Invalid Selection + Data From 0995bf69173722caa9eeffe30f96cdd8270f5972 Mon Sep 17 00:00:00 2001 From: bellerbrock Date: Mon, 17 Feb 2025 09:19:47 -0500 Subject: [PATCH 02/11] original dialog --- .../res/layout/dialog_field_edit_unique.xml | 30 +++++++++++++++++++ app/src/main/res/values/strings.xml | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/layout/dialog_field_edit_unique.xml diff --git a/app/src/main/res/layout/dialog_field_edit_unique.xml b/app/src/main/res/layout/dialog_field_edit_unique.xml new file mode 100644 index 000000000..ccbdcf3fa --- /dev/null +++ b/app/src/main/res/layout/dialog_field_edit_unique.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4f2ecc5e1..56514d07c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -204,7 +204,7 @@ import name display name - Change Unique Id + Change Unique ID Invalid Selection From 965c4a01b2d4870f0fa6858fb5ac77cde46f5ac1 Mon Sep 17 00:00:00 2001 From: bellerbrock Date: Mon, 17 Feb 2025 14:51:30 -0500 Subject: [PATCH 03/11] search attribute approach --- .../tracker/activities/FieldDetailFragment.kt | 88 +++++++++++-------- .../tracker/database/DataHelper.java | 21 ++++- .../tracker/database/dao/StudyDao.kt | 28 +++--- .../tracker/dialogs/AttributeChooserDialog.kt | 21 ++++- .../tracker/objects/FieldObject.java | 8 ++ .../main/res/layout/fragment_field_detail.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 7 files changed, 118 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt b/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt index 8886a39be..d326c37e1 100644 --- a/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt +++ b/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt @@ -29,6 +29,7 @@ import com.fieldbook.tracker.adapters.FieldDetailAdapter import com.fieldbook.tracker.adapters.FieldDetailItem import com.fieldbook.tracker.brapi.service.BrAPIService import com.fieldbook.tracker.database.DataHelper +import com.fieldbook.tracker.dialogs.AttributeChooserDialog import com.fieldbook.tracker.dialogs.BrapiSyncObsDialog import com.fieldbook.tracker.interfaces.FieldAdapterController import com.fieldbook.tracker.interfaces.FieldSortController @@ -278,6 +279,7 @@ class FieldDetailFragment : Fragment(), FieldSyncController { var importFormat: ImportFormat? = field.import_format var entryCount = field.count.toString() val attributeCount = field.attribute_count.toString() + val searchAttribute = (field.search_attribute ?: field.unique_id).toString() if (importFormat == ImportFormat.BRAPI) { cardViewSync.visibility = View.VISIBLE @@ -308,7 +310,8 @@ class FieldDetailFragment : Fragment(), FieldSyncController { entryCountChip.text = entryCount attributeCountChip.text = attributeCount sortOrderChip.text = getString(R.string.field_sort_entries) - editUniqueChip.text = getString(R.string.field_edit_unique_id) +// editUniqueChip.text = getString(R.string.field_edit_unique_id) + editUniqueChip.text = searchAttribute val lastEdit = field.date_edit if (!lastEdit.isNullOrEmpty()) { @@ -448,45 +451,58 @@ class FieldDetailFragment : Fragment(), FieldSyncController { dialog.show() } - private fun showEditUniqueIdDialog(field: FieldObject) { - val inflater = requireActivity().layoutInflater - val dialogView = inflater.inflate(R.layout.dialog_field_edit_unique, null) - - val uniqueDropdown = dialogView.findViewById(R.id.unique_dropdown) - val errorMessageView = dialogView.findViewById(R.id.error_message) - - // Get list of possible unique attributes - val uniqueOptions = database.getPossibleUniqueAttributes(field.exp_id) - val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, uniqueOptions) - uniqueDropdown.setAdapter(adapter) - - // Set current value if exists - field.unique_id?.let { uniqueDropdown.setText(it) } - - val builder = AlertDialog.Builder(requireContext(), R.style.AppAlertDialog) - .setTitle(getString(R.string.field_edit_unique_id)) - .setView(dialogView) - .setPositiveButton(getString(R.string.dialog_save), null) - .setNegativeButton(getString(R.string.dialog_cancel), null) - - val dialog = builder.create() +// private fun showEditUniqueIdDialog(field: FieldObject) { +// val inflater = requireActivity().layoutInflater +// val dialogView = inflater.inflate(R.layout.dialog_field_edit_unique, null) +// +// val uniqueDropdown = dialogView.findViewById(R.id.unique_dropdown) +// val errorMessageView = dialogView.findViewById(R.id.error_message) +// +// // Get list of possible unique attributes +// val uniqueOptions = database.getPossibleUniqueAttributes(field.exp_id) +// val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, uniqueOptions) +// uniqueDropdown.setAdapter(adapter) +// +// // Set current value if exists +// field.unique_id?.let { uniqueDropdown.setText(it) } +// +// val builder = AlertDialog.Builder(requireContext(), R.style.AppAlertDialog) +// .setTitle(getString(R.string.field_edit_unique_id)) +// .setView(dialogView) +// .setPositiveButton(getString(R.string.dialog_save), null) +// .setNegativeButton(getString(R.string.dialog_cancel), null) +// +// val dialog = builder.create() +// +// dialog.setOnShowListener { +// val positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) +// positiveButton.setOnClickListener { +// val selectedAttribute = uniqueDropdown.text.toString() +// +// if (selectedAttribute.isNotBlank() && uniqueOptions.contains(selectedAttribute)) { +// database.updateFieldUniqueId(field.exp_id, selectedAttribute) +// loadFieldDetails() +// dialog.dismiss() +// } else { +// showErrorMessage(errorMessageView, getString(R.string.invalid_unique_id_selection)) +// } +// } +// } +// +// dialog.show() +// } - dialog.setOnShowListener { - val positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) - positiveButton.setOnClickListener { - val selectedAttribute = uniqueDropdown.text.toString() - - if (selectedAttribute.isNotBlank() && uniqueOptions.contains(selectedAttribute)) { - database.updateFieldUniqueId(field.exp_id, selectedAttribute) + private fun showEditUniqueIdDialog(field: FieldObject) { + val dialog = AttributeChooserDialog().apply { + loadUniqueAttributes() + setOnAttributeSelectedListener(object : AttributeChooserDialog.OnAttributeSelectedListener { + override fun onAttributeSelected(label: String) { + database.updateFieldUniqueId(field.exp_id, label) loadFieldDetails() - dialog.dismiss() - } else { - showErrorMessage(errorMessageView, getString(R.string.invalid_unique_id_selection)) } - } + }) } - - dialog.show() + dialog.show(parentFragmentManager, AttributeChooserDialog.TAG) } /** diff --git a/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java b/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java index cf952eadb..f37a35157 100644 --- a/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java +++ b/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java @@ -74,7 +74,7 @@ public class DataHelper { public static final String RANGE = "range"; public static final String TRAITS = "traits"; - public static final int DATABASE_VERSION = 11; + public static final int DATABASE_VERSION = 12; private static final String DATABASE_NAME = "fieldbook.db"; private static final String USER_TRAITS = "user_traits"; private static final String EXP_INDEX = "exp_id"; @@ -3062,6 +3062,25 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { helper.fixStudyAliases(db); } + + if (oldVersion <= 10 && newVersion >= 11) { + + // modify studies table for better handling of brapi study attributes + db.execSQL("ALTER TABLE studies ADD COLUMN import_format TEXT"); + db.execSQL("ALTER TABLE studies ADD COLUMN date_sync TEXT"); + helper.populateImportFormat(db); + helper.fixStudyAliases(db); + + } + + if (oldVersion <= 11 && newVersion >= 12) { + + // Add observation_unit_search_attribute column to studies table + db.execSQL("ALTER TABLE studies ADD COLUMN observation_unit_search_attribute TEXT"); + + // Populate the new column with the default value from study_unique_id_name + db.execSQL("UPDATE studies SET observation_unit_search_attribute = study_unique_id_name"); + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt b/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt index 762509788..9e9617d8e 100644 --- a/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt +++ b/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt @@ -35,24 +35,26 @@ class StudyDao { fun getPossibleUniqueAttributes(studyId: Int): List = withDatabase { db -> val query = """ - SELECT oua.observation_unit_attribute_name - FROM observation_units_attributes oua - WHERE oua.study_id = ? - AND ( - SELECT COUNT(DISTINCT ouv.observation_unit_value_name) - FROM observation_units_values ouv - WHERE ouv.observation_unit_attribute_db_id = oua.internal_id_observation_unit_attribute - ) = ( - SELECT COUNT(*) - FROM observation_units_values ouv - WHERE ouv.observation_unit_attribute_db_id = oua.internal_id_observation_unit_attribute + SELECT observation_unit_attribute_name + FROM observation_units_attributes + WHERE internal_id_observation_unit_attribute IN ( + SELECT distinct(observation_unit_attribute_db_id) + FROM observation_units_values + WHERE study_id = ? + GROUP BY observation_unit_attribute_db_id, observation_unit_value_name + HAVING COUNT(*) = 1 ) """ + + Log.d("StudyDao", "Running query: $query") db.rawQuery(query, arrayOf(studyId.toString())).use { cursor -> val attributes = mutableListOf() while (cursor.moveToNext()) { - cursor.getString(0)?.let { attributes.add(it) } + cursor.getString(0)?.let { + attributes.add(it) + Log.d("StudyDao", "Found unique attribute: $it") + } } attributes } @@ -265,6 +267,7 @@ class StudyDao { it.trait_count = this["trait_count"]?.toString() it.observation_count = this["observation_count"]?.toString() it.trial_name = this["trial_name"]?.toString() + it.search_attribute = this["observation_unit_search_attribute"]?.toString() } fun getAllFieldObjects(sortOrder: String): ArrayList = withDatabase { db -> @@ -315,6 +318,7 @@ class StudyDao { study_sort_name, trial_name, count, + observation_unit_search_attribute, (SELECT COUNT(*) FROM observation_units_attributes WHERE study_id = Studies.${Study.PK}) AS attribute_count, (SELECT COUNT(DISTINCT observation_variable_name) FROM observations WHERE study_id = Studies.${Study.PK} AND observation_variable_db_id > 0) AS trait_count, (SELECT COUNT(*) FROM observations WHERE study_id = Studies.${Study.PK} AND observation_variable_db_id > 0) AS observation_count diff --git a/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt b/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt index 8feadbefc..776637402 100644 --- a/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt +++ b/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt @@ -10,6 +10,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.fieldbook.tracker.R import com.fieldbook.tracker.activities.CollectActivity +import com.fieldbook.tracker.activities.FieldEditorActivity import com.fieldbook.tracker.adapters.AttributeAdapter import com.fieldbook.tracker.objects.TraitObject import com.fieldbook.tracker.preferences.GeneralKeys @@ -21,13 +22,21 @@ import com.google.android.material.tabs.TabLayout */ open class AttributeChooserDialog : DialogFragment(), AttributeAdapter.AttributeAdapterController { + companion object { const val TAG = "AttributeChooserDialog" } + interface OnAttributeSelectedListener { fun onAttributeSelected(label: String) } + private var loadUniqueAttributesOnly = false + + fun loadUniqueAttributes() { + loadUniqueAttributesOnly = true + } + private lateinit var tabLayout: TabLayout private lateinit var recyclerView: RecyclerView private var attributes = arrayOf() @@ -53,7 +62,17 @@ open class AttributeChooserDialog : DialogFragment(), AttributeAdapter.Attribute // Call loadData after dialog is shown dialog.setOnShowListener { - loadData() + if (loadUniqueAttributesOnly) { + val prefs = PreferenceManager.getDefaultSharedPreferences(requireActivity()) + val activeFieldId = prefs.getInt(GeneralKeys.SELECTED_FIELD_ID, -1) + Log.d(TAG, "Loading unique attributes for field ID: $activeFieldId") + val activity = requireActivity() as FieldEditorActivity + attributes = activity.getDatabase().getPossibleUniqueAttributes(activeFieldId)?.toTypedArray() ?: emptyArray() + Log.d(TAG, "Final attributes array size: ${attributes.size}") + loadTab(getString(R.string.dialog_att_chooser_attributes)) + } else { + loadData() + } } return dialog diff --git a/app/src/main/java/com/fieldbook/tracker/objects/FieldObject.java b/app/src/main/java/com/fieldbook/tracker/objects/FieldObject.java index f686bca7b..019d41ad5 100644 --- a/app/src/main/java/com/fieldbook/tracker/objects/FieldObject.java +++ b/app/src/main/java/com/fieldbook/tracker/objects/FieldObject.java @@ -29,6 +29,7 @@ public class FieldObject { private String trait_count; private String observation_count; private String trial_name; + private String search_attribute; public String getTrial_name() { return trial_name; @@ -242,4 +243,11 @@ public String getObservation_count() { public void setObservation_count(String observation_count) { this.observation_count = observation_count; } + public String getSearch_attribute() { + return search_attribute; + } + + public void setSearch_attribute(String search_attribute) { + this.search_attribute = search_attribute; + } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_field_detail.xml b/app/src/main/res/layout/fragment_field_detail.xml index 6509217a1..b37bb033c 100644 --- a/app/src/main/res/layout/fragment_field_detail.xml +++ b/app/src/main/res/layout/fragment_field_detail.xml @@ -186,7 +186,7 @@ android:id="@+id/editUniqueChip" android:layout_width="wrap_content" android:layout_height="wrap_content" - app:chipIcon="@drawable/ic_tag_text" + app:chipIcon="@drawable/ic_tb_barcode_scan" app:chipIconSize="24dp" app:ensureMinTouchTargetSize="false" app:closeIconVisible="false" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 56514d07c..4c60d249d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -204,7 +204,7 @@ import name display name - Change Unique ID + Change Search Attribute Invalid Selection From 9ed9785b962e88eac2d18294cf7f0360e2f7f23a Mon Sep 17 00:00:00 2001 From: bellerbrock Date: Tue, 18 Feb 2025 13:24:22 -0500 Subject: [PATCH 04/11] update db functions, add and use pref --- .../tracker/activities/CollectActivity.java | 24 ++++++- .../tracker/activities/FieldDetailFragment.kt | 2 +- .../tracker/database/DataHelper.java | 20 ++---- .../tracker/database/dao/StudyDao.kt | 67 ++++--------------- .../tracker/preferences/GeneralKeys.java | 1 + .../tracker/utilities/FieldSwitchImpl.kt | 1 + 6 files changed, 43 insertions(+), 72 deletions(-) diff --git a/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java b/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java index 1e33f0480..b86b48f9d 100644 --- a/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java +++ b/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java @@ -925,7 +925,7 @@ public boolean moveToSearch( } //move to plot id - if (command.equals("id") || command.equals("barcode")) { + if (command.equals("id")) { int rangeSize = plotIndices.length; for (int j = 1; j <= rangeSize; j++) { rangeBox.setRangeByIndex(j - 1); @@ -941,6 +941,26 @@ public boolean moveToSearch( } } + if (command.equals("barcode")) { + int rangeSize = plotIndices.length; + String searchAttribute = preferences.getString(GeneralKeys.SEARCH_ATTRIBUTE, ""); + + for (int j = 1; j <= rangeSize; j++) { + rangeBox.setRangeByIndex(j - 1); + RangeObject ro = rangeBox.getCRange(); + + // Get the search attribute value for this plot + String[] attributeValue = database.getDropDownRange(searchAttribute, ro.plot_id); + String searchValue = attributeValue != null && attributeValue.length > 0 ? attributeValue[0] : ""; + + // Match against search attribute first, fallback to plot_id + if (searchValue.equals(data) || ro.plot_id.equals(data)) { + moveToResultCore(j); + return true; + } + } + } + if (!command.equals("quickgoto") && !command.equals("barcode")) Utils.makeToast(this, getString(R.string.main_toolbar_moveto_no_match)); @@ -1847,7 +1867,7 @@ public void onClick(DialogInterface dialog, int which) { inputPlotId = barcodeId.getText().toString(); rangeBox.setAllRangeID(); int[] rangeID = rangeBox.getRangeID(); - moveToSearch("id", rangeID, null, null, inputPlotId, -1); + moveToSearch("barcode", rangeID, null, null, inputPlotId, -1); goToId.dismiss(); } }); diff --git a/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt b/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt index d326c37e1..474e67eea 100644 --- a/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt +++ b/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt @@ -497,7 +497,7 @@ class FieldDetailFragment : Fragment(), FieldSyncController { loadUniqueAttributes() setOnAttributeSelectedListener(object : AttributeChooserDialog.OnAttributeSelectedListener { override fun onAttributeSelected(label: String) { - database.updateFieldUniqueId(field.exp_id, label) + database.updateSearchAttribute(field.exp_id, label) loadFieldDetails() } }) diff --git a/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java b/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java index f37a35157..3f08900d7 100644 --- a/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java +++ b/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java @@ -893,9 +893,9 @@ public List getPossibleUniqueAttributes(int studyId) { return StudyDao.Companion.getPossibleUniqueAttributes(studyId); } - public void updateFieldUniqueId(int studyId, String newUniqueAttribute) { + public void updateSearchAttribute(int studyId, String newSearchAttribute) { open(); - StudyDao.Companion.updateFieldUniqueId(studyId, newUniqueAttribute); + StudyDao.Companion.updateSearchAttribute(studyId, newSearchAttribute); } public void updateImages(List images) { @@ -3063,24 +3063,12 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } - if (oldVersion <= 10 && newVersion >= 11) { - - // modify studies table for better handling of brapi study attributes - db.execSQL("ALTER TABLE studies ADD COLUMN import_format TEXT"); - db.execSQL("ALTER TABLE studies ADD COLUMN date_sync TEXT"); - helper.populateImportFormat(db); - helper.fixStudyAliases(db); - - } - if (oldVersion <= 11 && newVersion >= 12) { - - // Add observation_unit_search_attribute column to studies table + // Add observation_unit_search_attribute column to studies table, use study_unique_id_name as default value db.execSQL("ALTER TABLE studies ADD COLUMN observation_unit_search_attribute TEXT"); - - // Populate the new column with the default value from study_unique_id_name db.execSQL("UPDATE studies SET observation_unit_search_attribute = study_unique_id_name"); } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt b/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt index 9e9617d8e..f635a674e 100644 --- a/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt +++ b/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt @@ -38,11 +38,11 @@ class StudyDao { SELECT observation_unit_attribute_name FROM observation_units_attributes WHERE internal_id_observation_unit_attribute IN ( - SELECT distinct(observation_unit_attribute_db_id) + SELECT observation_unit_attribute_db_id FROM observation_units_values WHERE study_id = ? - GROUP BY observation_unit_attribute_db_id, observation_unit_value_name - HAVING COUNT(*) = 1 + GROUP BY observation_unit_attribute_db_id + HAVING COUNT(DISTINCT observation_unit_value_name) = COUNT(observation_unit_value_name) ) """ @@ -60,56 +60,17 @@ class StudyDao { } } ?: emptyList() - fun updateFieldUniqueId(studyId: Int, newUniqueAttribute: String) = withDatabase { db -> - db.beginTransaction() - try { - // Get the attribute ID for the new unique identifier - val attrIdQuery = """ - SELECT internal_id_observation_unit_attribute - FROM observation_units_attributes - WHERE study_id = ? AND observation_unit_attribute_name = ? - """ - - val attrId = db.rawQuery(attrIdQuery, arrayOf(studyId.toString(), newUniqueAttribute)) - .use { cursor -> - if (cursor.moveToFirst()) cursor.getInt(0) else null - } ?: return@withDatabase - - // Update studies table - db.execSQL( - "UPDATE studies SET study_unique_id_name = ? WHERE internal_id_study = ?", - arrayOf(newUniqueAttribute, studyId.toString()) - ) - - // Update observation_units table using values from observation_units_values - val updateUnitsQuery = """ - UPDATE observation_units - SET observation_unit_db_id = ( - SELECT ouv.observation_unit_value_name - FROM observation_units_values ouv - WHERE ouv.observation_unit_id = observation_units.internal_id_observation_unit - AND ouv.observation_unit_attribute_db_id = ? - ) - WHERE study_id = ? - """ - db.execSQL(updateUnitsQuery, arrayOf(attrId.toString(), studyId.toString())) - - // Update observations table using the new observation_unit_db_id values - val updateObsQuery = """ - UPDATE observations - SET observation_unit_id = ( - SELECT ou.observation_unit_db_id - FROM observation_units ou - WHERE ou.internal_id_observation_unit = observations.observation_unit_id - ) - WHERE study_id = ? - """ - db.execSQL(updateObsQuery, arrayOf(studyId.toString())) - - db.setTransactionSuccessful() - } finally { - db.endTransaction() - } + /** + * Updates the observation unit search attribute for a study record. + * This attribute is used to identify entries in the barcode search, by default it's the same as the unique_id + */ + + fun updateSearchAttribute(studyId: Int, newSearchAttribute: String) = withDatabase { db -> + db.update(Study.tableName, + contentValuesOf("observation_unit_search_attribute" to newSearchAttribute), + "${Study.PK} = $studyId", + null + ) } private fun fixPlotAttributes(db: SQLiteDatabase) { diff --git a/app/src/main/java/com/fieldbook/tracker/preferences/GeneralKeys.java b/app/src/main/java/com/fieldbook/tracker/preferences/GeneralKeys.java index 84b6149a6..cc617fa02 100644 --- a/app/src/main/java/com/fieldbook/tracker/preferences/GeneralKeys.java +++ b/app/src/main/java/com/fieldbook/tracker/preferences/GeneralKeys.java @@ -183,6 +183,7 @@ public class GeneralKeys { public static final String UNIQUE_NAME = "ImportUniqueName"; public static final String PRIMARY_NAME = "ImportFirstName"; public static final String SECONDARY_NAME = "ImportSecondName"; + public static final String SEARCH_ATTRIBUTE = "SearchAttribute"; //Used to get name of observation level of currently selected field public static final String FIELD_OBS_LEVEL = "FieldObsLevel"; diff --git a/app/src/main/java/com/fieldbook/tracker/utilities/FieldSwitchImpl.kt b/app/src/main/java/com/fieldbook/tracker/utilities/FieldSwitchImpl.kt index 05293d5f1..9e8ccbaca 100644 --- a/app/src/main/java/com/fieldbook/tracker/utilities/FieldSwitchImpl.kt +++ b/app/src/main/java/com/fieldbook/tracker/utilities/FieldSwitchImpl.kt @@ -44,6 +44,7 @@ class FieldSwitchImpl @Inject constructor(@ActivityContext private val context: .putString(GeneralKeys.UNIQUE_NAME, field.unique_id) .putString(GeneralKeys.PRIMARY_NAME, field.primary_id) .putString(GeneralKeys.SECONDARY_NAME, field.secondary_id) + .putString(GeneralKeys.SEARCH_ATTRIBUTE, field.search_attribute) .putBoolean(GeneralKeys.IMPORT_FIELD_FINISHED, true) .putString(GeneralKeys.LAST_PLOT, null).apply() From 680371231e813cda9fdb1ae64c74ec559b491a68 Mon Sep 17 00:00:00 2001 From: bellerbrock Date: Tue, 18 Feb 2025 14:58:48 -0500 Subject: [PATCH 05/11] cleanup --- .../tracker/activities/FieldDetailFragment.kt | 48 ++----------------- .../tracker/dialogs/AttributeChooserDialog.kt | 23 ++++----- app/src/main/res/values/strings.xml | 1 + 3 files changed, 16 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt b/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt index 474e67eea..1c9e903ed 100644 --- a/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt +++ b/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt @@ -195,7 +195,7 @@ class FieldDetailFragment : Fragment(), FieldSyncController { fieldId?.let { id -> val field = database.getFieldObject(id) field?.let { - showEditUniqueIdDialog(it) + showChangeSearchAttributeDialog(it) } } } @@ -451,50 +451,8 @@ class FieldDetailFragment : Fragment(), FieldSyncController { dialog.show() } -// private fun showEditUniqueIdDialog(field: FieldObject) { -// val inflater = requireActivity().layoutInflater -// val dialogView = inflater.inflate(R.layout.dialog_field_edit_unique, null) -// -// val uniqueDropdown = dialogView.findViewById(R.id.unique_dropdown) -// val errorMessageView = dialogView.findViewById(R.id.error_message) -// -// // Get list of possible unique attributes -// val uniqueOptions = database.getPossibleUniqueAttributes(field.exp_id) -// val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, uniqueOptions) -// uniqueDropdown.setAdapter(adapter) -// -// // Set current value if exists -// field.unique_id?.let { uniqueDropdown.setText(it) } -// -// val builder = AlertDialog.Builder(requireContext(), R.style.AppAlertDialog) -// .setTitle(getString(R.string.field_edit_unique_id)) -// .setView(dialogView) -// .setPositiveButton(getString(R.string.dialog_save), null) -// .setNegativeButton(getString(R.string.dialog_cancel), null) -// -// val dialog = builder.create() -// -// dialog.setOnShowListener { -// val positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) -// positiveButton.setOnClickListener { -// val selectedAttribute = uniqueDropdown.text.toString() -// -// if (selectedAttribute.isNotBlank() && uniqueOptions.contains(selectedAttribute)) { -// database.updateFieldUniqueId(field.exp_id, selectedAttribute) -// loadFieldDetails() -// dialog.dismiss() -// } else { -// showErrorMessage(errorMessageView, getString(R.string.invalid_unique_id_selection)) -// } -// } -// } -// -// dialog.show() -// } - - private fun showEditUniqueIdDialog(field: FieldObject) { - val dialog = AttributeChooserDialog().apply { - loadUniqueAttributes() + private fun showChangeSearchAttributeDialog(field: FieldObject) { + val dialog = AttributeChooserDialog(showTraits = false, showOther = false, uniqueOnly = true).apply { setOnAttributeSelectedListener(object : AttributeChooserDialog.OnAttributeSelectedListener { override fun onAttributeSelected(label: String) { database.updateSearchAttribute(field.exp_id, label) diff --git a/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt b/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt index b4720addb..8f923275c 100644 --- a/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt +++ b/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt @@ -23,7 +23,11 @@ import com.google.android.material.tabs.TabLayout * Each tab will load data into a recycler view that lets user choose infobar prefixes. */ -open class AttributeChooserDialog : DialogFragment(), AttributeAdapter.AttributeAdapterController { +open class AttributeChooserDialog( + private val showTraits: Boolean = true, + private val showOther: Boolean = true, + private val uniqueOnly: Boolean = false +) : DialogFragment(), AttributeAdapter.AttributeAdapterController { companion object { const val TAG = "AttributeChooserDialog" @@ -33,12 +37,6 @@ open class AttributeChooserDialog : DialogFragment(), AttributeAdapter.Attribute fun onAttributeSelected(label: String) } - private var loadUniqueAttributesOnly = false - - fun loadUniqueAttributes() { - loadUniqueAttributesOnly = true - } - private lateinit var tabLayout: TabLayout private lateinit var recyclerView: RecyclerView private lateinit var progressBar: ProgressBar @@ -60,6 +58,10 @@ open class AttributeChooserDialog : DialogFragment(), AttributeAdapter.Attribute recyclerView.layoutManager = LinearLayoutManager(requireActivity()) recyclerView.adapter = AttributeAdapter(this, null) + //toggle view of traits/other based on class param + tabLayout.getTabAt(1)?.view?.visibility = if (showTraits) TabLayout.VISIBLE else TabLayout.GONE + tabLayout.getTabAt(2)?.view?.visibility = if (showOther) TabLayout.VISIBLE else TabLayout.GONE + val dialog = AlertDialog.Builder(requireActivity(), R.style.AppAlertDialog) .setView(view) .setNegativeButton(android.R.string.cancel, null) @@ -71,19 +73,18 @@ open class AttributeChooserDialog : DialogFragment(), AttributeAdapter.Attribute toggleProgressVisibility(true) BackgroundUiTask.execute( backgroundBlock = { - if (loadUniqueAttributesOnly) { + if (uniqueOnly) { val prefs = PreferenceManager.getDefaultSharedPreferences(requireActivity()) val activeFieldId = prefs.getInt(GeneralKeys.SELECTED_FIELD_ID, -1) - Log.d(TAG, "Loading unique attributes for field ID: $activeFieldId") val activity = requireActivity() as FieldEditorActivity attributes = activity.getDatabase().getPossibleUniqueAttributes(activeFieldId)?.toTypedArray() ?: emptyArray() - Log.d(TAG, "Final attributes array size: ${attributes.size}") } else { loadData() } }, uiBlock = { - if (loadUniqueAttributesOnly) { + toggleProgressVisibility(false) + if (uniqueOnly) { loadTab(getString(R.string.dialog_att_chooser_attributes)) } else { setupTabLayout() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c2e06dd2c..acf1213f1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -346,6 +346,7 @@ Next entry with no data + Unique Attributes Attributes Traits Other From 4a8b2bf65811a10f9d6fe175aa8c0c5d9073de6b Mon Sep 17 00:00:00 2001 From: bellerbrock Date: Tue, 25 Feb 2025 12:50:31 -0500 Subject: [PATCH 06/11] one source of truth for search attribute --- .../fieldbook/tracker/activities/CollectActivity.java | 3 ++- .../com/fieldbook/tracker/database/DataHelper.java | 5 +++++ .../com/fieldbook/tracker/database/dao/StudyDao.kt | 10 ++++++++++ .../com/fieldbook/tracker/preferences/GeneralKeys.java | 1 - .../com/fieldbook/tracker/utilities/FieldSwitchImpl.kt | 1 - 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java b/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java index 687cf861d..b08ed6f1f 100644 --- a/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java +++ b/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java @@ -943,7 +943,8 @@ public boolean moveToSearch( if (command.equals("barcode")) { int rangeSize = plotIndices.length; - String searchAttribute = preferences.getString(GeneralKeys.SEARCH_ATTRIBUTE, ""); + int currentFieldId = preferences.getInt(GeneralKeys.SELECTED_FIELD_ID, 0); + String searchAttribute = database.getSearchAttribute(currentFieldId); for (int j = 1; j <= rangeSize; j++) { rangeBox.setRangeByIndex(j - 1); diff --git a/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java b/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java index 3f08900d7..a1d6b1405 100644 --- a/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java +++ b/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java @@ -898,6 +898,11 @@ public void updateSearchAttribute(int studyId, String newSearchAttribute) { StudyDao.Companion.updateSearchAttribute(studyId, newSearchAttribute); } + public String getSearchAttribute(int studyId) { + open(); + return StudyDao.Companion.getSearchAttribute(studyId); + } + public void updateImages(List images) { ArrayList ids = new ArrayList<>(); diff --git a/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt b/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt index bb5f19b06..4030eebd9 100644 --- a/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt +++ b/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt @@ -73,6 +73,16 @@ class StudyDao { ) } + fun getSearchAttribute(studyId: Int): String? = withDatabase { db -> + val query = "SELECT observation_unit_search_attribute FROM ${Study.tableName} WHERE ${Study.PK} = $studyId" + db.rawQuery(query, null).use { cursor -> + if (cursor.moveToFirst()) { + return@withDatabase cursor.getString(0) + } + null + } + } + private fun fixPlotAttributes(db: SQLiteDatabase) { db.rawQuery("PRAGMA foreign_keys=OFF;", null).close() diff --git a/app/src/main/java/com/fieldbook/tracker/preferences/GeneralKeys.java b/app/src/main/java/com/fieldbook/tracker/preferences/GeneralKeys.java index cc617fa02..84b6149a6 100644 --- a/app/src/main/java/com/fieldbook/tracker/preferences/GeneralKeys.java +++ b/app/src/main/java/com/fieldbook/tracker/preferences/GeneralKeys.java @@ -183,7 +183,6 @@ public class GeneralKeys { public static final String UNIQUE_NAME = "ImportUniqueName"; public static final String PRIMARY_NAME = "ImportFirstName"; public static final String SECONDARY_NAME = "ImportSecondName"; - public static final String SEARCH_ATTRIBUTE = "SearchAttribute"; //Used to get name of observation level of currently selected field public static final String FIELD_OBS_LEVEL = "FieldObsLevel"; diff --git a/app/src/main/java/com/fieldbook/tracker/utilities/FieldSwitchImpl.kt b/app/src/main/java/com/fieldbook/tracker/utilities/FieldSwitchImpl.kt index 9e8ccbaca..05293d5f1 100644 --- a/app/src/main/java/com/fieldbook/tracker/utilities/FieldSwitchImpl.kt +++ b/app/src/main/java/com/fieldbook/tracker/utilities/FieldSwitchImpl.kt @@ -44,7 +44,6 @@ class FieldSwitchImpl @Inject constructor(@ActivityContext private val context: .putString(GeneralKeys.UNIQUE_NAME, field.unique_id) .putString(GeneralKeys.PRIMARY_NAME, field.primary_id) .putString(GeneralKeys.SECONDARY_NAME, field.secondary_id) - .putString(GeneralKeys.SEARCH_ATTRIBUTE, field.search_attribute) .putBoolean(GeneralKeys.IMPORT_FIELD_FINISHED, true) .putString(GeneralKeys.LAST_PLOT, null).apply() From 5f7c5f11211ca2b34beae5169de2bd3eccb4c07b Mon Sep 17 00:00:00 2001 From: bellerbrock Date: Thu, 27 Feb 2025 14:09:03 -0500 Subject: [PATCH 07/11] updated attribute serarch and fallback code --- .../tracker/activities/CollectActivity.java | 142 ++++++++++++++---- .../tracker/database/DataHelper.java | 4 +- .../database/dao/ObservationUnitDao.kt | 39 +++++ .../dao/ObservationUnitPropertyDao.kt | 2 +- .../tracker/database/dao/StudyDao.kt | 10 -- app/src/main/res/values/strings.xml | 2 + 6 files changed, 158 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java b/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java index b08ed6f1f..939ac32c0 100644 --- a/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java +++ b/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java @@ -941,25 +941,90 @@ public boolean moveToSearch( } } + // if (command.equals("barcode")) { + // int rangeSize = plotIndices.length; + // int currentFieldId = preferences.getInt(GeneralKeys.SELECTED_FIELD_ID, 0); + + // String searchAttribute = database.getSearchAttribute(currentFieldId); + // boolean isSearchAttrEmpty = searchAttribute == null || searchAttribute.isEmpty(); + + // for (int j = 1; j <= rangeSize; j++) { + // rangeBox.setRangeByIndex(j - 1); + // RangeObject ro = rangeBox.getCRange(); + + // // Match against search attribute first (if available) + // if (!isSearchAttrEmpty) { + // String[] attributeValue = database.getDropDownRange(searchAttribute, ro.plot_id); + // String searchValue = attributeValue != null && attributeValue.length > 0 ? attributeValue[0] : ""; + + // if (searchValue.equals(data)) { + // moveToResultCore(j); + // return true; + // } + // } + + // // Fall back to plot_id check + // if (ro.plot_id.equals(data)) { + // moveToResultCore(j); + // return true; + // } + // } + // } + if (command.equals("barcode")) { - int rangeSize = plotIndices.length; int currentFieldId = preferences.getInt(GeneralKeys.SELECTED_FIELD_ID, 0); - String searchAttribute = database.getSearchAttribute(currentFieldId); + Log.d(TAG, "Barcode search in current field: " + currentFieldId + ", searching for: " + data); - for (int j = 1; j <= rangeSize; j++) { + ObservationUnitModel[] matchingUnits = database.getObservationUnitsBySearchAttribute( + currentFieldId, data); + Log.d(TAG, "Search attribute results: " + matchingUnits.length + " units found"); + + // if (matchingUnits.length > 0) { + // // If found by search attribute, move to that observation unit + // String matchingObsUnitId = matchingUnits[0].getObservation_unit_db_id(); + // rangeBox.setAllRangeID(); + // int[] rangeID = rangeBox.getRangeID(); + // if (moveToSearch("id", rangeID, null, null, matchingObsUnitId, -1)) { + // return true; + // } + // } + if (matchingUnits.length > 0) { + // If found by search attribute, move to that observation unit + String matchingObsUnitId = matchingUnits[0].getObservation_unit_db_id(); + Log.d(TAG, "Found match by search attribute. Unit ID: " + matchingObsUnitId); + + // If multiple matches were found, show a toast notification + if (matchingUnits.length > 1) { + Utils.makeToast(this, getString(R.string.search_multiple_matches_found, matchingUnits.length)); + } + + for (int j = 1; j <= plotIndices.length; j++) { + rangeBox.setRangeByIndex(j - 1); + RangeObject ro = rangeBox.getCRange(); + Log.d(TAG, "Checking against plot_id: " + ro.plot_id); + + if (ro.plot_id.equals(matchingObsUnitId)) { + moveToResultCore(j); + return true; + } + } + Log.d(TAG, "Couldn't find matching plot in range box despite database match"); + } + + // Fallback: check if the barcode directly matches a plot_id + Log.d(TAG, "Falling back to direct plot_id matching"); + for (int j = 1; j <= plotIndices.length; j++) { rangeBox.setRangeByIndex(j - 1); RangeObject ro = rangeBox.getCRange(); - // Get the search attribute value for this plot - String[] attributeValue = database.getDropDownRange(searchAttribute, ro.plot_id); - String searchValue = attributeValue != null && attributeValue.length > 0 ? attributeValue[0] : ""; - - // Match against search attribute first, fallback to plot_id - if (searchValue.equals(data) || ro.plot_id.equals(data)) { + if (ro.plot_id.equals(data)) { + Log.d(TAG, "Direct match found at index: " + j); moveToResultCore(j); return true; } } + + return false; } if (!command.equals("quickgoto") && !command.equals("barcode")) @@ -2016,16 +2081,19 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { if(resultCode == RESULT_OK) { if (geoNavHelper.getSnackbar() != null) geoNavHelper.getSnackbar().dismiss(); + String barcodeValue; + if(mlkitEnabled) { - inputPlotId = data.getStringExtra("barcode"); + barcodeValue = data.getStringExtra("barcode"); } else { IntentResult plotSearchResult = IntentIntegrator.parseActivityResult(resultCode, data); - inputPlotId = plotSearchResult.getContents(); + barcodeValue = plotSearchResult.getContents(); } + rangeBox.setAllRangeID(); int[] rangeID = rangeBox.getRangeID(); - boolean success = moveToSearch("barcode", rangeID, null, null, inputPlotId, -1); + boolean success = moveToSearch("barcode", rangeID, null, null, barcodeValue, -1); //play success or error sound if the plotId was not found if (success) { @@ -2033,35 +2101,53 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { } else { boolean found = false; FieldObject studyObj = null; - ObservationUnitModel[] models = database.getAllObservationUnits(); - for (ObservationUnitModel m : models) { - if (m.getObservation_unit_db_id().equals(inputPlotId)) { - - FieldObject study = database.getFieldObject(m.getStudy_id()); - if (study != null && study.getExp_name() != null) { - studyObj = study; - found = true; - break; - } + + // Check all other fields by search attribute + ArrayList allFields = database.getAllFieldObjects(); + for (FieldObject field : allFields) { + + // Skip the current field + if (field.getExp_id() == preferences.getInt(GeneralKeys.SELECTED_FIELD_ID, 0)) { + continue; + } + + ObservationUnitModel[] matchingUnits = database.getObservationUnitsBySearchAttribute( + field.getExp_id(), inputPlotId); + + if (matchingUnits.length > 0) { + studyObj = field; + inputPlotId = matchingUnits[0].getObservation_unit_db_id(); // Update inputPlotId to the matched observation unit + found = true; + break; } } - if (found && studyObj.getExp_name() != null && studyObj.getExp_id() != -1) { + // If not found by search attribute in any field, try direct plot_id matching + if (!found) { + ObservationUnitModel[] models = database.getAllObservationUnits(); + for (ObservationUnitModel m : models) { + if (m.getObservation_unit_db_id().equals(inputPlotId)) { + FieldObject study = database.getFieldObject(m.getStudy_id()); + if (study != null && study.getExp_name() != null) { + studyObj = study; + found = true; + break; + } + } + } + } + if (found && studyObj != null && studyObj.getExp_name() != null && studyObj.getExp_id() != -1) { int studyId = studyObj.getExp_id(); String fieldName = studyObj.getExp_alias(); String msg = getString(R.string.act_collect_barcode_search_exists_in_other_field, fieldName); - SnackbarUtils.showNavigateSnack(getLayoutInflater(), findViewById(R.id.traitHolder), msg, R.id.toolbarBottom,8000, null, + SnackbarUtils.showNavigateSnack(getLayoutInflater(), findViewById(R.id.traitHolder), msg, R.id.toolbarBottom, 8000, null, (v) -> switchField(studyId, null)); - } else { - soundHelper.playError(); - Utils.makeToast(getApplicationContext(), getString(R.string.main_toolbar_moveto_no_match)); - } } } diff --git a/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java b/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java index a1d6b1405..6dcb0a7c0 100644 --- a/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java +++ b/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java @@ -898,9 +898,9 @@ public void updateSearchAttribute(int studyId, String newSearchAttribute) { StudyDao.Companion.updateSearchAttribute(studyId, newSearchAttribute); } - public String getSearchAttribute(int studyId) { + public ObservationUnitModel[] getObservationUnitsBySearchAttribute(int studyId, String searchValue) { open(); - return StudyDao.Companion.getSearchAttribute(studyId); + return ObservationUnitDao.Companion.getBySearchAttribute(studyId, searchValue); } public void updateImages(List images) { diff --git a/app/src/main/java/com/fieldbook/tracker/database/dao/ObservationUnitDao.kt b/app/src/main/java/com/fieldbook/tracker/database/dao/ObservationUnitDao.kt index 1e58049a6..8d2309fbd 100644 --- a/app/src/main/java/com/fieldbook/tracker/database/dao/ObservationUnitDao.kt +++ b/app/src/main/java/com/fieldbook/tracker/database/dao/ObservationUnitDao.kt @@ -2,6 +2,7 @@ package com.fieldbook.tracker.database.dao import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import android.util.Log import com.fieldbook.tracker.database.Migrator.ObservationUnit import com.fieldbook.tracker.database.Migrator.Study import com.fieldbook.tracker.database.models.ObservationUnitModel @@ -59,6 +60,44 @@ class ObservationUnitDao { } } + /** + * Find observation units matching a search attribute value + * @param studyId The study ID to search within + * @param searchValue The value to search for + * @return Array of matching observation unit models, or empty array if none found + */ + fun getBySearchAttribute(studyId: Int, searchValue: String): Array = withDatabase { db -> + + Log.d("Field Book", "getBySearchAttribute called with studyId: $studyId, searchValue: $searchValue") + + val query = """ + SELECT ou.* + FROM ${ObservationUnit.tableName} ou + JOIN observation_units_values ouv ON + ou.${ObservationUnit.PK} = ouv.observation_unit_id + AND ouv.study_id = ? + AND ouv.observation_unit_value_name = ? + AND ouv.observation_unit_attribute_db_id IN ( + SELECT internal_id_observation_unit_attribute + FROM observation_units_attributes + WHERE observation_unit_attribute_name = ( + SELECT observation_unit_search_attribute + FROM ${Study.tableName} + WHERE ${Study.PK} = ? + ) + ) + """ + + db.rawQuery(query, arrayOf( + studyId.toString(), + searchValue, + studyId.toString() + )) + .toTable() + .map { ObservationUnitModel(it) } + .toTypedArray() + } ?: emptyArray() + fun getAll(eid: Int): Array = withDatabase { db -> arrayOf(*db.query(ObservationUnit.tableName, diff --git a/app/src/main/java/com/fieldbook/tracker/database/dao/ObservationUnitPropertyDao.kt b/app/src/main/java/com/fieldbook/tracker/database/dao/ObservationUnitPropertyDao.kt index 68b7da0b6..edbc1ac25 100644 --- a/app/src/main/java/com/fieldbook/tracker/database/dao/ObservationUnitPropertyDao.kt +++ b/app/src/main/java/com/fieldbook/tracker/database/dao/ObservationUnitPropertyDao.kt @@ -412,7 +412,7 @@ class ObservationUnitPropertyDao { $orderByClause """.trimIndent() - Log.d("getSortedObsUnitData", "Executing dynamic query: $query") +// Log.d("getSortedObsUnitData", "Executing dynamic query: $query") db.rawQuery(query, null) } diff --git a/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt b/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt index 4030eebd9..bb5f19b06 100644 --- a/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt +++ b/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt @@ -73,16 +73,6 @@ class StudyDao { ) } - fun getSearchAttribute(studyId: Int): String? = withDatabase { db -> - val query = "SELECT observation_unit_search_attribute FROM ${Study.tableName} WHERE ${Study.PK} = $studyId" - db.rawQuery(query, null).use { cursor -> - if (cursor.moveToFirst()) { - return@withDatabase cursor.getString(0) - } - null - } - } - private fun fixPlotAttributes(db: SQLiteDatabase) { db.rawQuery("PRAGMA foreign_keys=OFF;", null).close() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index acf1213f1..c68c631b2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -206,6 +206,8 @@ Change Search Attribute Invalid Selection + Found %1$d matching plots. Showing the first one. + Barcode scan failed or was cancelled From 7c439ac30d6caaab66e6c7db2752185902d00c4a Mon Sep 17 00:00:00 2001 From: bellerbrock Date: Thu, 27 Feb 2025 16:46:05 -0500 Subject: [PATCH 08/11] better cross field barcode matching --- .../tracker/activities/CollectActivity.java | 248 +++++++++--------- 1 file changed, 131 insertions(+), 117 deletions(-) diff --git a/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java b/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java index 939ac32c0..6b23be6cb 100644 --- a/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java +++ b/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java @@ -941,90 +941,51 @@ public boolean moveToSearch( } } - // if (command.equals("barcode")) { - // int rangeSize = plotIndices.length; - // int currentFieldId = preferences.getInt(GeneralKeys.SELECTED_FIELD_ID, 0); - - // String searchAttribute = database.getSearchAttribute(currentFieldId); - // boolean isSearchAttrEmpty = searchAttribute == null || searchAttribute.isEmpty(); - - // for (int j = 1; j <= rangeSize; j++) { - // rangeBox.setRangeByIndex(j - 1); - // RangeObject ro = rangeBox.getCRange(); - - // // Match against search attribute first (if available) - // if (!isSearchAttrEmpty) { - // String[] attributeValue = database.getDropDownRange(searchAttribute, ro.plot_id); - // String searchValue = attributeValue != null && attributeValue.length > 0 ? attributeValue[0] : ""; - - // if (searchValue.equals(data)) { - // moveToResultCore(j); - // return true; - // } - // } - - // // Fall back to plot_id check - // if (ro.plot_id.equals(data)) { - // moveToResultCore(j); - // return true; - // } - // } - // } - if (command.equals("barcode")) { int currentFieldId = preferences.getInt(GeneralKeys.SELECTED_FIELD_ID, 0); - Log.d(TAG, "Barcode search in current field: " + currentFieldId + ", searching for: " + data); - + Log.d("Field Book", "Barcode search in current field: " + currentFieldId + ", searching for: " + data); + ObservationUnitModel[] matchingUnits = database.getObservationUnitsBySearchAttribute( - currentFieldId, data); - Log.d(TAG, "Search attribute results: " + matchingUnits.length + " units found"); - - // if (matchingUnits.length > 0) { - // // If found by search attribute, move to that observation unit - // String matchingObsUnitId = matchingUnits[0].getObservation_unit_db_id(); - // rangeBox.setAllRangeID(); - // int[] rangeID = rangeBox.getRangeID(); - // if (moveToSearch("id", rangeID, null, null, matchingObsUnitId, -1)) { - // return true; - // } - // } + currentFieldId, data); + Log.d("Field Book", "Search attribute results: " + matchingUnits.length + " units found"); + if (matchingUnits.length > 0) { // If found by search attribute, move to that observation unit String matchingObsUnitId = matchingUnits[0].getObservation_unit_db_id(); - Log.d(TAG, "Found match by search attribute. Unit ID: " + matchingObsUnitId); - - // If multiple matches were found, show a toast notification + Log.d("Field Book", "Found match by search attribute. Unit ID: " + matchingObsUnitId); + + // If multiple matches found, show notification if (matchingUnits.length > 1) { Utils.makeToast(this, getString(R.string.search_multiple_matches_found, matchingUnits.length)); } - + for (int j = 1; j <= plotIndices.length; j++) { rangeBox.setRangeByIndex(j - 1); RangeObject ro = rangeBox.getCRange(); - Log.d(TAG, "Checking against plot_id: " + ro.plot_id); - + if (ro.plot_id.equals(matchingObsUnitId)) { moveToResultCore(j); return true; } } - Log.d(TAG, "Couldn't find matching plot in range box despite database match"); } - + // Fallback: check if the barcode directly matches a plot_id - Log.d(TAG, "Falling back to direct plot_id matching"); + Log.d("Field Book", "Falling back to direct plot_id matching"); for (int j = 1; j <= plotIndices.length; j++) { rangeBox.setRangeByIndex(j - 1); RangeObject ro = rangeBox.getCRange(); - + if (ro.plot_id.equals(data)) { - Log.d(TAG, "Direct match found at index: " + j); + Log.d("Field Book", "Direct match found at index: " + j); moveToResultCore(j); return true; } } - - return false; + + // If we didn't find it in the current field, try the cross-field search + Log.d("Field Book", "Not found in current field, trying other fields"); + return performCrossFallbackSearch(data); } if (!command.equals("quickgoto") && !command.equals("barcode")) @@ -1033,6 +994,95 @@ public boolean moveToSearch( return false; } + /** + * Searches for a barcode across all fields when not found in the current field. + * @param searchValue The barcode or search value to find + * @return true if found in another field, false otherwise + */ + private boolean performCrossFallbackSearch(String searchValue) { + Log.d("Field Book", "Starting cross-field fallback search for: " + searchValue); + + boolean found = false; + FieldObject studyObj = null; + + // Store search value in inputPlotId for use in the fallback + inputPlotId = searchValue; + + int currentFieldId = preferences.getInt(GeneralKeys.SELECTED_FIELD_ID, 0); + Log.d("Field Book", "Current field ID: " + currentFieldId); + + // Check all other fields by search attribute + ArrayList allFields = database.getAllFieldObjects(); + Log.d("Field Book", "Searching across " + allFields.size() + " fields"); + + for (FieldObject field : allFields) { + // Skip the current field + if (field.getExp_id() == currentFieldId) { + continue; + } + + Log.d("Field Book", "Checking field: " + field.getExp_id() + " (" + field.getExp_name() + ")"); + + ObservationUnitModel[] matchingUnits = database.getObservationUnitsBySearchAttribute( + field.getExp_id(), searchValue); + + Log.d("Field Book", "Found " + matchingUnits.length + " matches in field " + field.getExp_id()); + + if (matchingUnits.length > 0) { + studyObj = field; + String oldPlotId = inputPlotId; + inputPlotId = matchingUnits[0].getObservation_unit_db_id(); + Log.d("Field Book", "Match found! Field: " + field.getExp_name() + + ", unit ID updated from " + oldPlotId + " to " + inputPlotId); + found = true; + break; + } + } + + // If not found by search attribute in any field, try direct plot_id matching + if (!found) { + Log.d("Field Book", "No matches by search attribute, trying direct ID match"); + + ObservationUnitModel[] models = database.getAllObservationUnits(); + + for (ObservationUnitModel m : models) { + if (m.getObservation_unit_db_id().equals(searchValue)) { + FieldObject study = database.getFieldObject(m.getStudy_id()); + if (study != null && study.getExp_name() != null) { + studyObj = study; + found = true; + Log.d("Field Book", "Direct match found in study: " + study.getExp_name()); + break; + } + } + } + } + + // Handle the result of the search + if (found && studyObj != null && studyObj.getExp_name() != null && studyObj.getExp_id() != -1) { + int studyId = studyObj.getExp_id(); + String fieldName = studyObj.getExp_alias(); + + // Save the matching observation unit ID from the matched unit, not the search value + final String matchedObsUnitId = inputPlotId; // This should be the one set earlier from matchingUnits[0] + + Log.d("Field Book", "Showing navigation prompt to field: " + fieldName + " and plot ID: " + matchedObsUnitId); + + String msg = getString(R.string.act_collect_barcode_search_exists_in_other_field, fieldName); + + SnackbarUtils.showNavigateSnack(getLayoutInflater(), findViewById(R.id.traitHolder), + msg, R.id.toolbarBottom, 8000, null, + (v) -> switchField(studyId, matchedObsUnitId)); + + return true; + } else { + Log.d("Field Book", "No match found in any field"); + soundHelper.playError(); + Utils.makeToast(getApplicationContext(), getString(R.string.main_toolbar_moveto_no_match)); + return false; + } + } + private void moveToResultCore(int j) { rangeBox.setPaging(j); @@ -2078,78 +2128,42 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { } break; case BARCODE_SEARCH_CODE: - if(resultCode == RESULT_OK) { + if (resultCode == RESULT_OK) { + Log.d("Field Book", "Barcode scan successful"); - if (geoNavHelper.getSnackbar() != null) geoNavHelper.getSnackbar().dismiss(); - String barcodeValue; + if (geoNavHelper.getSnackbar() != null) { + geoNavHelper.getSnackbar().dismiss(); + } - if(mlkitEnabled) { + String barcodeValue; + if (mlkitEnabled) { barcodeValue = data.getStringExtra("barcode"); - } - else { + } else { IntentResult plotSearchResult = IntentIntegrator.parseActivityResult(resultCode, data); barcodeValue = plotSearchResult.getContents(); } - rangeBox.setAllRangeID(); - int[] rangeID = rangeBox.getRangeID(); - boolean success = moveToSearch("barcode", rangeID, null, null, barcodeValue, -1); - - //play success or error sound if the plotId was not found - if (success) { - soundHelper.playCelebrate(); - } else { - boolean found = false; - FieldObject studyObj = null; - - // Check all other fields by search attribute - ArrayList allFields = database.getAllFieldObjects(); - for (FieldObject field : allFields) { - - // Skip the current field - if (field.getExp_id() == preferences.getInt(GeneralKeys.SELECTED_FIELD_ID, 0)) { - continue; - } - - ObservationUnitModel[] matchingUnits = database.getObservationUnitsBySearchAttribute( - field.getExp_id(), inputPlotId); + if (barcodeValue != null && !barcodeValue.isEmpty()) { + Log.d("Field Book", "Scanned barcode: " + barcodeValue); - if (matchingUnits.length > 0) { - studyObj = field; - inputPlotId = matchingUnits[0].getObservation_unit_db_id(); // Update inputPlotId to the matched observation unit - found = true; - break; - } - } - - // If not found by search attribute in any field, try direct plot_id matching - if (!found) { - ObservationUnitModel[] models = database.getAllObservationUnits(); - for (ObservationUnitModel m : models) { - if (m.getObservation_unit_db_id().equals(inputPlotId)) { - FieldObject study = database.getFieldObject(m.getStudy_id()); - if (study != null && study.getExp_name() != null) { - studyObj = study; - found = true; - break; - } - } - } - } + // Set inputPlotId globally to ensure it's available everywhere + inputPlotId = barcodeValue; - if (found && studyObj != null && studyObj.getExp_name() != null && studyObj.getExp_id() != -1) { - int studyId = studyObj.getExp_id(); - String fieldName = studyObj.getExp_alias(); + rangeBox.setAllRangeID(); + int[] rangeID = rangeBox.getRangeID(); - String msg = getString(R.string.act_collect_barcode_search_exists_in_other_field, fieldName); + boolean success = moveToSearch("barcode", rangeID, null, null, barcodeValue, -1); - SnackbarUtils.showNavigateSnack(getLayoutInflater(), findViewById(R.id.traitHolder), msg, R.id.toolbarBottom, 8000, null, - (v) -> switchField(studyId, null)); - } else { - soundHelper.playError(); - Utils.makeToast(getApplicationContext(), getString(R.string.main_toolbar_moveto_no_match)); - } + // If success is true, moveToSearch found the barcode, either in current field + // or via the fallback in another field. Success sound happens in moveToSearch + // if needed. No additional action required here. + } else { + Log.d("Field Book", "Barcode scan returned empty result"); + soundHelper.playError(); + Utils.makeToast(getApplicationContext(), getString(R.string.main_toolbar_moveto_no_match)); } + } else { + Log.d("Field Book", "Barcode scan cancelled or failed"); } break; case BARCODE_COLLECT_CODE: From 21cd521b7127af03f1c226177a1de964efd41620 Mon Sep 17 00:00:00 2001 From: bellerbrock Date: Mon, 3 Mar 2025 13:41:02 -0500 Subject: [PATCH 09/11] add apply to all checkbox --- .../tracker/activities/FieldDetailFragment.kt | 43 ++++++++++++++++--- .../tracker/database/DataHelper.java | 5 +++ .../tracker/database/dao/StudyDao.kt | 39 +++++++++++++++++ .../tracker/dialogs/AttributeChooserDialog.kt | 23 +++++++++- app/src/main/res/values/strings.xml | 4 ++ 5 files changed, 107 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt b/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt index 1c9e903ed..8a63f46ee 100644 --- a/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt +++ b/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt @@ -452,14 +452,45 @@ class FieldDetailFragment : Fragment(), FieldSyncController { } private fun showChangeSearchAttributeDialog(field: FieldObject) { - val dialog = AttributeChooserDialog(showTraits = false, showOther = false, uniqueOnly = true).apply { - setOnAttributeSelectedListener(object : AttributeChooserDialog.OnAttributeSelectedListener { - override fun onAttributeSelected(label: String) { + val dialog = AttributeChooserDialog( + showTraits = false, + showOther = false, + uniqueOnly = true, + showApplyAllCheckbox = true + ) + + dialog.setOnAttributeSelectedListener(object : AttributeChooserDialog.OnAttributeSelectedListener { + override fun onAttributeSelected(label: String) { + // Get the apply to all state + val applyToAll = dialog.getApplyToAllState() + + if (applyToAll) { + // Update search attribute for all fields with this attribute + val count = database.updateSearchAttributeForAllFields(label) + Toast.makeText( + context, + getString(R.string.search_attribute_updated_all, count), + Toast.LENGTH_SHORT + ).show() + } else { + // Update only the current field database.updateSearchAttribute(field.exp_id, label) - loadFieldDetails() + Toast.makeText( + context, + getString(R.string.search_attribute_updated), + Toast.LENGTH_SHORT + ).show() } - }) - } + + loadFieldDetails() + + // If apply to all was selected, refresh the parent activity's field list + if (applyToAll) { + (activity as? FieldAdapterController)?.queryAndLoadFields() + } + } + }) + dialog.show(parentFragmentManager, AttributeChooserDialog.TAG) } diff --git a/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java b/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java index 6dcb0a7c0..80a964c6c 100644 --- a/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java +++ b/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java @@ -897,6 +897,11 @@ public void updateSearchAttribute(int studyId, String newSearchAttribute) { open(); StudyDao.Companion.updateSearchAttribute(studyId, newSearchAttribute); } + + public int updateSearchAttributeForAllFields(String newSearchAttribute) { + open(); + return StudyDao.Companion.updateSearchAttributeForAllFields(newSearchAttribute); + } public ObservationUnitModel[] getObservationUnitsBySearchAttribute(int studyId, String searchValue) { open(); diff --git a/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt b/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt index bb5f19b06..cc2da921f 100644 --- a/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt +++ b/app/src/main/java/com/fieldbook/tracker/database/dao/StudyDao.kt @@ -73,6 +73,45 @@ class StudyDao { ) } + /** + * Updates the observation unit search attribute for all studies that have this attribute + * @return The number of studies that were updated + */ + fun updateSearchAttributeForAllFields(newSearchAttribute: String): Int = withDatabase { db -> + // First check which studies have this attribute + val studiesWithAttribute = mutableListOf() + + val query = """ + SELECT DISTINCT study_id + FROM observation_units_attributes + WHERE observation_unit_attribute_name = ? + """ + + Log.d("StudyDao", "Finding studies with attribute: $newSearchAttribute") + + db.rawQuery(query, arrayOf(newSearchAttribute)).use { cursor -> + while (cursor.moveToNext()) { + cursor.getInt(0).let { studyId -> + studiesWithAttribute.add(studyId) + Log.d("StudyDao", "Found study with matching attribute: $studyId") + } + } + } + + // Now update each study that has this attribute + var updatedCount = 0 + if (studiesWithAttribute.isNotEmpty()) { + for (studyId in studiesWithAttribute) { + // Fix: Add null safety with the Elvis operator (?:) + val result = updateSearchAttribute(studyId, newSearchAttribute) + if ((result ?: 0) > 0) updatedCount++ + } + } + + Log.d("StudyDao", "Updated search attribute for $updatedCount studies") + updatedCount + } ?: 0 + private fun fixPlotAttributes(db: SQLiteDatabase) { db.rawQuery("PRAGMA foreign_keys=OFF;", null).close() diff --git a/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt b/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt index 8f923275c..f283fb444 100644 --- a/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt +++ b/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt @@ -4,6 +4,8 @@ import android.app.AlertDialog import android.app.Dialog import android.os.Bundle import android.util.Log +import android.widget.CheckBox +import android.widget.LinearLayout import android.widget.ProgressBar import androidx.fragment.app.DialogFragment import androidx.preference.PreferenceManager @@ -26,7 +28,8 @@ import com.google.android.material.tabs.TabLayout open class AttributeChooserDialog( private val showTraits: Boolean = true, private val showOther: Boolean = true, - private val uniqueOnly: Boolean = false + private val uniqueOnly: Boolean = false, + private val showApplyAllCheckbox: Boolean = false ) : DialogFragment(), AttributeAdapter.AttributeAdapterController { companion object { @@ -41,6 +44,10 @@ open class AttributeChooserDialog( private lateinit var recyclerView: RecyclerView private lateinit var progressBar: ProgressBar + private var applyAllCheckbox: CheckBox? = null + private var lastApplyToAllState: Boolean = false + fun getApplyToAllState(): Boolean = lastApplyToAllState + private var attributes = arrayOf() private var traits = arrayOf() private var other = arrayOf() @@ -62,6 +69,19 @@ open class AttributeChooserDialog( tabLayout.getTabAt(1)?.view?.visibility = if (showTraits) TabLayout.VISIBLE else TabLayout.GONE tabLayout.getTabAt(2)?.view?.visibility = if (showOther) TabLayout.VISIBLE else TabLayout.GONE + // Add checkbox if requested - add it directly to the root LinearLayout + if (showApplyAllCheckbox) { + val checkBox = CheckBox(requireContext()).apply { + text = getString(R.string.apply_to_all_fields) + setPadding(50, 20, 50, 20) + } + + // The view is already a LinearLayout, so we can just add the checkbox to it + (view as? LinearLayout)?.addView(checkBox) + + applyAllCheckbox = checkBox + } + val dialog = AlertDialog.Builder(requireActivity(), R.style.AppAlertDialog) .setView(view) .setNegativeButton(android.R.string.cancel, null) @@ -191,6 +211,7 @@ open class AttributeChooserDialog( } override fun onAttributeClicked(label: String, position: Int) { + lastApplyToAllState = applyAllCheckbox?.isChecked ?: false onAttributeSelectedListener?.onAttributeSelected(label) ?: run { Log.w(TAG, "No OnAttributeSelectedListener set for AttributeChooserDialog.") } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c68c631b2..4cc5921d3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -208,6 +208,10 @@ Invalid Selection Found %1$d matching plots. Showing the first one. Barcode scan failed or was cancelled + Apply choice to all eligible fields + Choose search attribute + Search attribute updated + Search attribute updated for %1$d field(s) From a410a3df253c2ccc4d87541e74803e23eac97b01 Mon Sep 17 00:00:00 2001 From: bellerbrock Date: Tue, 4 Mar 2025 11:25:31 -0500 Subject: [PATCH 10/11] additional refinements --- .../tracker/activities/FieldDetailFragment.kt | 2 ++ .../tracker/dialogs/AttributeChooserDialog.kt | 31 +++++++++++-------- .../res/layout/dialog_collect_att_chooser.xml | 10 ++++++ app/src/main/res/values/strings.xml | 11 +++---- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt b/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt index 8a63f46ee..ccabbafcc 100644 --- a/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt +++ b/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt @@ -452,6 +452,8 @@ class FieldDetailFragment : Fragment(), FieldSyncController { } private fun showChangeSearchAttributeDialog(field: FieldObject) { + (activity as? FieldEditorActivity)?.setActiveField(field.exp_id) + val dialog = AttributeChooserDialog( showTraits = false, showOther = false, diff --git a/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt b/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt index f283fb444..c8d3a2ad5 100644 --- a/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt +++ b/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt @@ -4,8 +4,8 @@ import android.app.AlertDialog import android.app.Dialog import android.os.Bundle import android.util.Log +import android.view.View import android.widget.CheckBox -import android.widget.LinearLayout import android.widget.ProgressBar import androidx.fragment.app.DialogFragment import androidx.preference.PreferenceManager @@ -62,6 +62,12 @@ open class AttributeChooserDialog( recyclerView = view.findViewById(R.id.dialog_collect_att_chooser_lv) progressBar = view.findViewById(R.id.dialog_collect_att_chooser_pb) + // Initialize checkbox if needed + if (showApplyAllCheckbox) { + applyAllCheckbox = view.findViewById(R.id.dialog_collect_att_chooser_checkbox) + applyAllCheckbox?.visibility = View.VISIBLE + } + recyclerView.layoutManager = LinearLayoutManager(requireActivity()) recyclerView.adapter = AttributeAdapter(this, null) @@ -69,21 +75,19 @@ open class AttributeChooserDialog( tabLayout.getTabAt(1)?.view?.visibility = if (showTraits) TabLayout.VISIBLE else TabLayout.GONE tabLayout.getTabAt(2)?.view?.visibility = if (showOther) TabLayout.VISIBLE else TabLayout.GONE - // Add checkbox if requested - add it directly to the root LinearLayout - if (showApplyAllCheckbox) { - val checkBox = CheckBox(requireContext()).apply { - text = getString(R.string.apply_to_all_fields) - setPadding(50, 20, 50, 20) - } - - // The view is already a LinearLayout, so we can just add the checkbox to it - (view as? LinearLayout)?.addView(checkBox) - - applyAllCheckbox = checkBox - } + // val dialog = AlertDialog.Builder(requireActivity(), R.style.AppAlertDialog) + // .setView(view) + // .setNegativeButton(android.R.string.cancel, null) + // .setCancelable(true) + // .create() + + // if (uniqueOnly) { + // createdDialog.setTitle(getString(R.string.search_attribute_dialog_title)) + // } val dialog = AlertDialog.Builder(requireActivity(), R.style.AppAlertDialog) .setView(view) + .apply { if (uniqueOnly) setTitle(R.string.search_attribute_dialog_title) } .setNegativeButton(android.R.string.cancel, null) .setCancelable(true) .create() @@ -94,6 +98,7 @@ open class AttributeChooserDialog( BackgroundUiTask.execute( backgroundBlock = { if (uniqueOnly) { + tabLayout.getTabAt(0)?.text = getString(R.string.dialog_att_chooser_unique_attributes) val prefs = PreferenceManager.getDefaultSharedPreferences(requireActivity()) val activeFieldId = prefs.getInt(GeneralKeys.SELECTED_FIELD_ID, -1) val activity = requireActivity() as FieldEditorActivity diff --git a/app/src/main/res/layout/dialog_collect_att_chooser.xml b/app/src/main/res/layout/dialog_collect_att_chooser.xml index c0db5d791..db2255c2c 100644 --- a/app/src/main/res/layout/dialog_collect_att_chooser.xml +++ b/app/src/main/res/layout/dialog_collect_att_chooser.xml @@ -46,5 +46,15 @@ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:listitem="@layout/list_item_infobar" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4cc5921d3..418789a9e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -204,14 +204,12 @@ import name display name - Change Search Attribute - Invalid Selection - Found %1$d matching plots. Showing the first one. - Barcode scan failed or was cancelled - Apply choice to all eligible fields - Choose search attribute + Set Search ID + Unique Attributes + Apply choice to all eligible fields Search attribute updated Search attribute updated for %1$d field(s) + Found %1$d matching plots. Showing the first one. @@ -352,7 +350,6 @@ Next entry with no data - Unique Attributes Attributes Traits Other From e85a9a6e3dbf7a286c799cfdafd1da97a15c4fb2 Mon Sep 17 00:00:00 2001 From: bellerbrock Date: Tue, 4 Mar 2025 13:14:46 -0500 Subject: [PATCH 11/11] move attribute dialog customiairtons to own class --- .../tracker/activities/FieldDetailFragment.kt | 73 ++++++--- .../tracker/dialogs/AttributeChooserDialog.kt | 109 ++++++------- .../dialogs/SearchAttributeChooserDialog.kt | 148 ++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 249 insertions(+), 82 deletions(-) create mode 100644 app/src/main/java/com/fieldbook/tracker/dialogs/SearchAttributeChooserDialog.kt diff --git a/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt b/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt index ccabbafcc..3864fbadb 100644 --- a/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt +++ b/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt @@ -29,7 +29,7 @@ import com.fieldbook.tracker.adapters.FieldDetailAdapter import com.fieldbook.tracker.adapters.FieldDetailItem import com.fieldbook.tracker.brapi.service.BrAPIService import com.fieldbook.tracker.database.DataHelper -import com.fieldbook.tracker.dialogs.AttributeChooserDialog +import com.fieldbook.tracker.dialogs.SearchAttributeChooserDialog import com.fieldbook.tracker.dialogs.BrapiSyncObsDialog import com.fieldbook.tracker.interfaces.FieldAdapterController import com.fieldbook.tracker.interfaces.FieldSortController @@ -451,26 +451,62 @@ class FieldDetailFragment : Fragment(), FieldSyncController { dialog.show() } + // private fun showChangeSearchAttributeDialog(field: FieldObject) { + // (activity as? FieldEditorActivity)?.setActiveField(field.exp_id) + + // val dialog = AttributeChooserDialog( + // showTraits = false, + // showOther = false, + // uniqueOnly = true, + // showApplyAllCheckbox = true + // ) + + // dialog.setOnAttributeSelectedListener(object : AttributeChooserDialog.OnAttributeSelectedListener { + // override fun onAttributeSelected(label: String) { + // // Get the apply to all state + // val applyToAll = dialog.getApplyToAllState() + + // if (applyToAll) { + // // Update search attribute for all fields with this attribute + // val count = database.updateSearchAttributeForAllFields(label) + // Toast.makeText( + // context, + // getString(R.string.search_attribute_updated_all, count), + // Toast.LENGTH_SHORT + // ).show() + // } else { + // // Update only the current field + // database.updateSearchAttribute(field.exp_id, label) + // Toast.makeText( + // context, + // getString(R.string.search_attribute_updated), + // Toast.LENGTH_SHORT + // ).show() + // } + + // loadFieldDetails() + + // // If apply to all was selected, refresh the parent activity's field list + // if (applyToAll) { + // (activity as? FieldAdapterController)?.queryAndLoadFields() + // } + // } + // }) + + // dialog.show(parentFragmentManager, AttributeChooserDialog.TAG) + // } + private fun showChangeSearchAttributeDialog(field: FieldObject) { (activity as? FieldEditorActivity)?.setActiveField(field.exp_id) - val dialog = AttributeChooserDialog( - showTraits = false, - showOther = false, - uniqueOnly = true, - showApplyAllCheckbox = true - ) - - dialog.setOnAttributeSelectedListener(object : AttributeChooserDialog.OnAttributeSelectedListener { - override fun onAttributeSelected(label: String) { - // Get the apply to all state - val applyToAll = dialog.getApplyToAllState() - + val dialog = SearchAttributeChooserDialog() + dialog.setOnSearchAttributeSelectedListener(object : SearchAttributeChooserDialog.OnSearchAttributeSelectedListener { + override fun onSearchAttributeSelected(label: String, applyToAll: Boolean) { if (applyToAll) { // Update search attribute for all fields with this attribute val count = database.updateSearchAttributeForAllFields(label) Toast.makeText( - context, + context, getString(R.string.search_attribute_updated_all, count), Toast.LENGTH_SHORT ).show() @@ -478,22 +514,21 @@ class FieldDetailFragment : Fragment(), FieldSyncController { // Update only the current field database.updateSearchAttribute(field.exp_id, label) Toast.makeText( - context, + context, getString(R.string.search_attribute_updated), Toast.LENGTH_SHORT ).show() } - + loadFieldDetails() - + // If apply to all was selected, refresh the parent activity's field list if (applyToAll) { (activity as? FieldAdapterController)?.queryAndLoadFields() } } }) - - dialog.show(parentFragmentManager, AttributeChooserDialog.TAG) + dialog.show(parentFragmentManager, SearchAttributeChooserDialog.TAG) } /** diff --git a/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt b/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt index c8d3a2ad5..f7cd4ece7 100644 --- a/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt +++ b/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt @@ -4,7 +4,6 @@ import android.app.AlertDialog import android.app.Dialog import android.os.Bundle import android.util.Log -import android.view.View import android.widget.CheckBox import android.widget.ProgressBar import androidx.fragment.app.DialogFragment @@ -13,7 +12,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.fieldbook.tracker.R import com.fieldbook.tracker.activities.CollectActivity -import com.fieldbook.tracker.activities.FieldEditorActivity import com.fieldbook.tracker.adapters.AttributeAdapter import com.fieldbook.tracker.objects.TraitObject import com.fieldbook.tracker.preferences.GeneralKeys @@ -23,50 +21,60 @@ import com.google.android.material.tabs.TabLayout /** * A tab layout with tabs: attributes, traits, and other. * Each tab will load data into a recycler view that lets user choose infobar prefixes. + * + * Added 'showSystemAttributes' that will toggle the adding of the field name attribute to the list of attributes (and any future system attributes). */ open class AttributeChooserDialog( private val showTraits: Boolean = true, private val showOther: Boolean = true, - private val uniqueOnly: Boolean = false, - private val showApplyAllCheckbox: Boolean = false + private val showSystemAttributes: Boolean = true ) : DialogFragment(), AttributeAdapter.AttributeAdapterController { companion object { const val TAG = "AttributeChooserDialog" + + private const val ARG_TITLE = "title" + + /** Factory method to create a new dialog with an infoBarPosition argument. */ + fun newInstance(titleResId: Int): AttributeChooserDialog { + val args = Bundle().apply { + putInt(ARG_TITLE, titleResId) + } + return AttributeChooserDialog().apply { + arguments = args + } + } } interface OnAttributeSelectedListener { fun onAttributeSelected(label: String) } - private lateinit var tabLayout: TabLayout - private lateinit var recyclerView: RecyclerView - private lateinit var progressBar: ProgressBar - - private var applyAllCheckbox: CheckBox? = null - private var lastApplyToAllState: Boolean = false - fun getApplyToAllState(): Boolean = lastApplyToAllState + protected lateinit var tabLayout: TabLayout + protected lateinit var recyclerView: RecyclerView + protected lateinit var progressBar: ProgressBar + protected lateinit var applyAllCheckbox: CheckBox - private var attributes = arrayOf() - private var traits = arrayOf() - private var other = arrayOf() - private var onAttributeSelectedListener: OnAttributeSelectedListener? = null + protected var attributes = arrayOf() + protected var traits = arrayOf() + protected var other = arrayOf() + protected var attributeSelectedListener: OnAttributeSelectedListener? = null override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val inflater = requireActivity().layoutInflater val view = inflater.inflate(R.layout.dialog_collect_att_chooser, null) + var titleResId = arguments?.getInt(ARG_TITLE) ?: R.string.dialog_att_chooser_title_default + if (titleResId == 0) titleResId = R.string.dialog_att_chooser_title_default + + val dialogTitle = context?.getString(titleResId) + // Initialize UI elements tabLayout = view.findViewById(R.id.dialog_collect_att_chooser_tl) recyclerView = view.findViewById(R.id.dialog_collect_att_chooser_lv) progressBar = view.findViewById(R.id.dialog_collect_att_chooser_pb) - - // Initialize checkbox if needed - if (showApplyAllCheckbox) { - applyAllCheckbox = view.findViewById(R.id.dialog_collect_att_chooser_checkbox) - applyAllCheckbox?.visibility = View.VISIBLE - } + applyAllCheckbox = view.findViewById(R.id.dialog_collect_att_chooser_checkbox) recyclerView.layoutManager = LinearLayoutManager(requireActivity()) recyclerView.adapter = AttributeAdapter(this, null) @@ -75,48 +83,22 @@ open class AttributeChooserDialog( tabLayout.getTabAt(1)?.view?.visibility = if (showTraits) TabLayout.VISIBLE else TabLayout.GONE tabLayout.getTabAt(2)?.view?.visibility = if (showOther) TabLayout.VISIBLE else TabLayout.GONE - // val dialog = AlertDialog.Builder(requireActivity(), R.style.AppAlertDialog) - // .setView(view) - // .setNegativeButton(android.R.string.cancel, null) - // .setCancelable(true) - // .create() - - // if (uniqueOnly) { - // createdDialog.setTitle(getString(R.string.search_attribute_dialog_title)) - // } - - val dialog = AlertDialog.Builder(requireActivity(), R.style.AppAlertDialog) + val builder = AlertDialog.Builder(requireActivity(), R.style.AppAlertDialog) .setView(view) - .apply { if (uniqueOnly) setTitle(R.string.search_attribute_dialog_title) } .setNegativeButton(android.R.string.cancel, null) .setCancelable(true) - .create() + + dialogTitle?.let { builder.setTitle(it) } + + val dialog = builder.create() // Call loadData after dialog is shown dialog.setOnShowListener { toggleProgressVisibility(true) BackgroundUiTask.execute( - backgroundBlock = { - if (uniqueOnly) { - tabLayout.getTabAt(0)?.text = getString(R.string.dialog_att_chooser_unique_attributes) - val prefs = PreferenceManager.getDefaultSharedPreferences(requireActivity()) - val activeFieldId = prefs.getInt(GeneralKeys.SELECTED_FIELD_ID, -1) - val activity = requireActivity() as FieldEditorActivity - attributes = activity.getDatabase().getPossibleUniqueAttributes(activeFieldId)?.toTypedArray() ?: emptyArray() - } else { - loadData() - } - }, - uiBlock = { - toggleProgressVisibility(false) - if (uniqueOnly) { - loadTab(getString(R.string.dialog_att_chooser_attributes)) - } else { - setupTabLayout() - } - }, - onCanceled = ::setupTabLayout - ) + backgroundBlock = ::loadData, + uiBlock = ::setupTabLayout, + onCanceled = ::setupTabLayout) } return dialog @@ -124,7 +106,7 @@ open class AttributeChooserDialog( } fun setOnAttributeSelectedListener(listener: OnAttributeSelectedListener) { - onAttributeSelectedListener = listener + attributeSelectedListener = listener } private fun loadData() { @@ -134,7 +116,9 @@ open class AttributeChooserDialog( val activity = requireActivity() as CollectActivity attributes = activity.getDatabase().getAllObservationUnitAttributeNames(activity.studyId.toInt()) val attributesList = attributes.toMutableList() - attributesList.add(0, getString(R.string.field_name_attribute)) + if (showSystemAttributes) { + attributesList.add(0, getString(R.string.field_name_attribute)) + } attributes = attributesList.toTypedArray() traits = activity.getDatabase().allTraitObjects.toTypedArray() other = traits.filter { !it.visible }.toTypedArray() @@ -145,7 +129,7 @@ open class AttributeChooserDialog( } } - private fun toggleProgressVisibility(show: Boolean) { + protected fun toggleProgressVisibility(show: Boolean) { progressBar.visibility = if (show) ProgressBar.VISIBLE else ProgressBar.GONE tabLayout.visibility = if (show) TabLayout.GONE else TabLayout.VISIBLE recyclerView.visibility = if (show) RecyclerView.GONE else RecyclerView.VISIBLE @@ -156,7 +140,7 @@ open class AttributeChooserDialog( * select first tab programmatically to load initial data * save the selected tab as preference */ - private fun setupTabLayout() { + protected fun setupTabLayout() { try { @@ -197,7 +181,7 @@ open class AttributeChooserDialog( protected open fun getSelected(): String? = null /** Handles loading data into the recycler view adapter. */ - private fun loadTab(label: String) { + protected fun loadTab(label: String) { val attributesLabel = getString(R.string.dialog_att_chooser_attributes) val traitsLabel = getString(R.string.dialog_att_chooser_traits) @@ -216,11 +200,10 @@ open class AttributeChooserDialog( } override fun onAttributeClicked(label: String, position: Int) { - lastApplyToAllState = applyAllCheckbox?.isChecked ?: false - onAttributeSelectedListener?.onAttributeSelected(label) ?: run { + attributeSelectedListener?.onAttributeSelected(label) ?: run { Log.w(TAG, "No OnAttributeSelectedListener set for AttributeChooserDialog.") } dismiss() } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/fieldbook/tracker/dialogs/SearchAttributeChooserDialog.kt b/app/src/main/java/com/fieldbook/tracker/dialogs/SearchAttributeChooserDialog.kt new file mode 100644 index 000000000..66dcb3d25 --- /dev/null +++ b/app/src/main/java/com/fieldbook/tracker/dialogs/SearchAttributeChooserDialog.kt @@ -0,0 +1,148 @@ +// package com.fieldbook.tracker.dialogs + +// import android.app.AlertDialog +// import android.app.Dialog +// import android.os.Bundle +// import android.view.View +// import android.widget.CheckBox +// import com.fieldbook.tracker.R + +// /** +// * Dialog to choose a search attribute for a field. +// * Extends the AttributeChooserDialog to add a checkbox for applying the selection to all fields. +// */ +// class SearchAttributeChooserDialog : AttributeChooserDialog( +// showTraits = false, +// showOther = false, +// showSystemAttributes = false +// ) { + +// companion object { +// const val TAG = "SearchAttributeChooserDialog" +// } + +// interface OnSearchAttributeSelectedListener { +// fun onSearchAttributeSelected(label: String, applyToAll: Boolean) +// } + +// private var onSearchAttributeSelectedListener: OnSearchAttributeSelectedListener? = null +// private var applyAllCheckbox: CheckBox? = null + +// fun setOnSearchAttributeSelectedListener(listener: OnSearchAttributeSelectedListener) { +// onSearchAttributeSelectedListener = listener +// } + +// override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { +// // Create the base dialog +// val dialog = super.onCreateDialog(savedInstanceState) as AlertDialog + +// // Set the title +// dialog.setTitle(R.string.search_attribute_dialog_title) + +// // Find and show the checkbox +// dialog.findViewById(R.id.dialog_collect_att_chooser_checkbox)?.let { +// applyAllCheckbox = it +// it.visibility = View.VISIBLE +// } + +// return dialog +// } + +// override fun onAttributeClicked(label: String, position: Int) { +// val applyToAll = applyAllCheckbox?.isChecked ?: false +// onSearchAttributeSelectedListener?.onSearchAttributeSelected(label, applyToAll) +// dismiss() +// } +// } + +package com.fieldbook.tracker.dialogs + +import android.app.AlertDialog +import android.app.Dialog +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.CheckBox +import androidx.preference.PreferenceManager +import com.fieldbook.tracker.R +import com.fieldbook.tracker.activities.FieldEditorActivity +import com.fieldbook.tracker.preferences.GeneralKeys +import com.fieldbook.tracker.utilities.BackgroundUiTask + +/** + * Dialog to choose a search attribute for a field. + * Extends the AttributeChooserDialog to add a checkbox for applying the selection to all fields. + */ +class SearchAttributeChooserDialog : AttributeChooserDialog( + showTraits = false, + showOther = false, + showSystemAttributes = false +) { + + companion object { + const val TAG = "SearchAttributeDialog" + } + + interface OnSearchAttributeSelectedListener { + fun onSearchAttributeSelected(label: String, applyToAll: Boolean) + } + + private var onSearchAttributeSelectedListener: OnSearchAttributeSelectedListener? = null + + fun setOnSearchAttributeSelectedListener(listener: OnSearchAttributeSelectedListener) { + onSearchAttributeSelectedListener = listener + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + // Create the base dialog + val dialog = super.onCreateDialog(savedInstanceState) as AlertDialog + + // Set the title and show checkbox + dialog.setTitle(R.string.search_attribute_dialog_title) + applyAllCheckbox.visibility = View.VISIBLE + + // Override the loading process with our own + dialog.setOnShowListener { + toggleProgressVisibility(true) + BackgroundUiTask.execute( + backgroundBlock = { loadUniqueAttributes() }, + uiBlock = { + toggleProgressVisibility(false) + loadTab(getString(R.string.dialog_att_chooser_attributes)) + }, + onCanceled = { + toggleProgressVisibility(false) + setupTabLayout() + } + ) + } + + return dialog + } + + // Load unique attributes instead of regular attributes + private fun loadUniqueAttributes() { + try { + val prefs = PreferenceManager.getDefaultSharedPreferences(requireActivity()) + val activeFieldId = prefs.getInt(GeneralKeys.SELECTED_FIELD_ID, -1) + val activity = requireActivity() as FieldEditorActivity + + // Get unique attributes from database + attributes = activity.getDatabase().getPossibleUniqueAttributes(activeFieldId)?.toTypedArray() ?: emptyArray() + + // Update the first tab's text + tabLayout.post { + tabLayout.getTabAt(0)?.text = getString(R.string.dialog_att_chooser_unique_attributes) + } + } catch (e: Exception) { + Log.d(TAG, "Error loading unique attributes: ${e.message}") + e.printStackTrace() + } + } + + override fun onAttributeClicked(label: String, position: Int) { + val applyToAll = applyAllCheckbox?.isChecked ?: false + onSearchAttributeSelectedListener?.onSearchAttributeSelected(label, applyToAll) + dismiss() + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 418789a9e..1402009bf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -204,6 +204,7 @@ import name display name + Choose an attribute Set Search ID Unique Attributes Apply choice to all eligible fields