diff --git a/app/build.gradle b/app/build.gradle
index f65f2b563..f8b312572 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -226,7 +226,10 @@ dependencies {
implementation "androidx.exifinterface:exifinterface:1.3.6"
//camerax
- def camerax_version = "1.3.3"
+ def camerax_version = "1.4.1"
+
+ implementation "androidx.camera:camera-effects:$camerax_version"
+
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:$camerax_version"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c94b9a1c6..1122bae4e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -206,6 +206,10 @@
android:scheme="fieldbook" />
+
= listOf()
+ private var traitId: String? = null
+
companion object {
val TAG = CameraActivity::class.simpleName
const val EXTRA_TITLE = "title"
-
+ const val EXTRA_TRAIT_ID = "trait_id"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ //get trait id from extras
+ traitId = intent.getStringExtra(EXTRA_TRAIT_ID)
+
setContentView(R.layout.activity_camera)
previewView = findViewById(R.id.act_camera_pv)
@@ -117,7 +124,9 @@ class CameraActivity : ThemedActivity() {
val resolution = getSupportedResolutionByPreferences()
cameraXFacade.bindPreview(
- previewView, resolution
+ previewView, resolution,
+ traitId,
+ Handler(Looper.getMainLooper())
) { camera, executor, capture ->
setupCaptureUi(camera, executor, capture)
@@ -141,8 +150,10 @@ class CameraActivity : ThemedActivity() {
object : ImageCapture.OnImageSavedCallback {
override fun onError(error: ImageCaptureException) {}
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
- setResult(RESULT_OK)
- finish()
+ runOnUiThread {
+ setResult(RESULT_OK)
+ finish()
+ }
}
})
}
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 4ffa4fe27..345029ba4 100644
--- a/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java
+++ b/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java
@@ -55,6 +55,7 @@
import com.fieldbook.tracker.dialogs.GeoNavCollectDialog;
import com.fieldbook.tracker.dialogs.ObservationMetadataFragment;
import com.fieldbook.tracker.dialogs.SearchDialog;
+import com.fieldbook.tracker.fragments.CropImageFragment;
import com.fieldbook.tracker.interfaces.FieldSwitcher;
import com.fieldbook.tracker.location.GPSTracker;
import com.fieldbook.tracker.objects.FieldObject;
@@ -117,6 +118,7 @@
import org.phenoapps.utils.TextToSpeechHelper;
import org.threeten.bp.OffsetDateTime;
+import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
@@ -153,6 +155,7 @@ public class CollectActivity extends ThemedActivity
SensorHelper.RelativeRotationListener {
public static final int REQUEST_FILE_EXPLORER_CODE = 1;
+ public static final int REQUEST_CROP_IMAGE_CODE = 101;
public static final int BARCODE_COLLECT_CODE = 99;
public static final int BARCODE_SEARCH_CODE = 98;
@@ -483,7 +486,7 @@ public String getStudyId() {
}
public String getObservationUnit() {
- return getCRange().plot_id;
+ return getCRange().uniqueId;
}
public String getPerson() {
@@ -842,11 +845,11 @@ public void initWidgets(final boolean rangeSuppress) {
traitBox.initTraitDetails();
+ String currentSortOrder = preferences.getString(GeneralKeys.TRAITS_LIST_SORT_ORDER, "position");
// trait is unique, format is not
- String[] traits = database.getVisibleTrait();
+ String[] traits = database.getVisibleTrait(currentSortOrder);
if (traits != null) {
traitBox.initTraitType(traits, rangeSuppress);
-
}
}
@@ -881,7 +884,7 @@ public boolean moveToSearch(
RangeObject ro = rangeBox.getCRange();
//issue #634 fix for now to check the search query by plot_id which should be the unique id
- if (Objects.equals(ro.plot_id, searchUnique)) {
+ if (Objects.equals(ro.uniqueId, searchUnique)) {
moveToResultCore(j);
return true;
}
@@ -893,7 +896,7 @@ public boolean moveToSearch(
for (int j = 1; j <= plotIndices.length; j++) {
rangeBox.setRangeByIndex(j - 1);
- if (rangeBox.getCRange().range.equals(range) & rangeBox.getCRange().plot.equals(plot)) {
+ if (rangeBox.getCRange().primaryId.equals(range) & rangeBox.getCRange().secondaryId.equals(plot)) {
moveToResultCore(j);
return true;
}
@@ -905,7 +908,7 @@ public boolean moveToSearch(
for (int j = 1; j <= plotIndices.length; j++) {
rangeBox.setRangeByIndex(j - 1);
- if (rangeBox.getCRange().plot.equals(data)) {
+ if (rangeBox.getCRange().secondaryId.equals(data)) {
moveToResultCore(j);
return true;
}
@@ -917,7 +920,7 @@ public boolean moveToSearch(
for (int j = 1; j <= plotIndices.length; j++) {
rangeBox.setRangeByIndex(j - 1);
- if (rangeBox.getCRange().range.equals(data)) {
+ if (rangeBox.getCRange().primaryId.equals(data)) {
moveToResultCore(j);
return true;
}
@@ -930,7 +933,7 @@ public boolean moveToSearch(
for (int j = 1; j <= rangeSize; j++) {
rangeBox.setRangeByIndex(j - 1);
- if (rangeBox.getCRange().plot_id.equals(data)) {
+ if (rangeBox.getCRange().uniqueId.equals(data)) {
if (trait == -1) {
moveToResultCore(j);
@@ -1186,8 +1189,9 @@ public void navigateToTrait(String trait) {
if (trait != null) {
+ String currentSortOrder = preferences.getString(GeneralKeys.TRAITS_LIST_SORT_ORDER, "position");
//get all traits, filter the preference trait and check it's visibility
- String[] traits = database.getVisibleTrait();
+ String[] traits = database.getVisibleTrait(currentSortOrder);
try {
@@ -1453,7 +1457,7 @@ public boolean onOptionsItemSelected(MenuItem item) {
collectDataTapTargetView(R.id.traitLeft, getString(R.string.tutorial_main_traits_title), getString(R.string.tutorial_main_traits_description), 60),
collectDataTapTargetView(R.id.traitTypeTv, getString(R.string.tutorial_main_traitlist_title), getString(R.string.tutorial_main_traitlist_description), 80),
collectDataTapTargetView(R.id.rangeLeft, getString(R.string.tutorial_main_entries_title), getString(R.string.tutorial_main_entries_description), 60),
- collectDataTapTargetView(R.id.valuesPlotRangeHolder, getString(R.string.tutorial_main_navinfo_title), getString(R.string.tutorial_main_navinfo_description), 60),
+ collectDataTapTargetView(R.id.namesHolderLayout, getString(R.string.tutorial_main_navinfo_title), getString(R.string.tutorial_main_navinfo_description), 60),
collectDataTapTargetView(R.id.traitHolder, getString(R.string.tutorial_main_datacollect_title), getString(R.string.tutorial_main_datacollect_description), 200),
collectDataTapTargetView(R.id.missingValue, getString(R.string.tutorial_main_na_title), getString(R.string.tutorial_main_na_description), 60),
collectDataTapTargetView(R.id.deleteValue, getString(R.string.tutorial_main_delete_title), getString(R.string.tutorial_main_delete_description), 60)
@@ -2115,6 +2119,13 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
} else triggerTts(fail);
break;
+ case REQUEST_CROP_IMAGE_CODE:
+ if (resultCode == RESULT_OK) {
+ File f = new File(getContext().getCacheDir(), AbstractCameraTrait.TEMPORARY_IMAGE_NAME);
+ Uri uri = Uri.fromFile(f);
+ startCropActivity(getCurrentTrait().getId(), uri);
+ }
+ break;
}
}
@@ -2281,7 +2292,8 @@ public boolean existsTrait(final int plotId) {
*/
@Override
public int existsAllTraits(final int traitIndex, final int plotId) {
- final ArrayList traits = database.getVisibleTraitObjects();
+ String sortOrder = preferences.getString(GeneralKeys.TRAITS_LIST_SORT_ORDER, "position");
+ final ArrayList traits = database.getVisibleTraitObjects(sortOrder);
for (int i = 0; i < traits.size(); i++) {
if (i != traitIndex
&& !database.getTraitExists(plotId, traits.get(i).getId())) return i;
@@ -2292,7 +2304,8 @@ public int existsAllTraits(final int traitIndex, final int plotId) {
@NonNull
@Override
public List getNonExistingTraits(final int plotId) {
- final ArrayList traits = database.getVisibleTraitObjects();
+ String sortOrder = preferences.getString(GeneralKeys.TRAITS_LIST_SORT_ORDER, "position");
+ final ArrayList traits = database.getVisibleTraitObjects(sortOrder);
final ArrayList indices = new ArrayList<>();
for (int i = 0; i < traits.size(); i++) {
if (!database.getTraitExists(plotId, traits.get(i).getId()))
@@ -2882,4 +2895,50 @@ private void sendCrashReport(Exception e) {
}
}
+
+ /**
+ * a function that starts the crop activity and sends it required intent data
+ */
+ public void startCropActivity(String traitId, Uri uri) {
+ try {
+ Intent intent = new Intent(this, CropImageActivity.class);
+ intent.putExtra(CropImageFragment.EXTRA_TRAIT_ID, Integer.parseInt(traitId));
+ intent.putExtra(CropImageFragment.EXTRA_IMAGE_URI, uri.toString());
+ cameraXFacade.unbind();
+ startActivity(intent);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void requestAndCropImage() {
+ try {
+ Intent intent = new Intent(this, CameraActivity.class);
+ intent.putExtra(CropImageFragment.EXTRA_TRAIT_ID, Integer.parseInt(getCurrentTrait().getId()));
+ cameraXFacade.unbind();
+ startActivityForResult(intent, REQUEST_CROP_IMAGE_CODE);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * shows an alert dialog that asks user to define a crop region
+ */
+ public void showCropDialog(String traitId, Uri uri) {
+ try {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.AppAlertDialog);
+ builder.setTitle(R.string.dialog_crop_title);
+ builder.setMessage(R.string.dialog_crop_message);
+ builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
+ startCropActivity(traitId, uri);
+ });
+ builder.setNegativeButton(android.R.string.no, (dialog, which) -> {
+ dialog.dismiss();
+ });
+ builder.create().show();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/fieldbook/tracker/activities/ConfigActivity.java b/app/src/main/java/com/fieldbook/tracker/activities/ConfigActivity.java
index 589a3fc23..aefedf869 100644
--- a/app/src/main/java/com/fieldbook/tracker/activities/ConfigActivity.java
+++ b/app/src/main/java/com/fieldbook/tracker/activities/ConfigActivity.java
@@ -378,7 +378,8 @@ private void loadScreen() {
*/
private int checkTraitsExist() {
- String[] traits = database.getVisibleTrait();
+ String currentSortOrder = preferences.getString(GeneralKeys.TRAITS_LIST_SORT_ORDER, "position");
+ String[] traits = database.getVisibleTrait(currentSortOrder);
if (!preferences.getBoolean(GeneralKeys.IMPORT_FIELD_FINISHED, false)
|| preferences.getInt(GeneralKeys.SELECTED_FIELD_ID, -1) == -1) {
diff --git a/app/src/main/java/com/fieldbook/tracker/activities/CropImageActivity.kt b/app/src/main/java/com/fieldbook/tracker/activities/CropImageActivity.kt
new file mode 100644
index 000000000..18ef32860
--- /dev/null
+++ b/app/src/main/java/com/fieldbook/tracker/activities/CropImageActivity.kt
@@ -0,0 +1,34 @@
+package com.fieldbook.tracker.activities
+
+import android.os.Bundle
+import androidx.core.os.bundleOf
+import androidx.fragment.app.add
+import androidx.fragment.app.commit
+import com.fieldbook.tracker.R
+import com.fieldbook.tracker.fragments.CropImageFragment
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class CropImageActivity: ThemedActivity() {
+
+ companion object {
+ const val TAG = "CropImageActivity"
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_crop_image)
+ if (savedInstanceState == null) {
+
+ val bundle = bundleOf(
+ CropImageFragment.EXTRA_TRAIT_ID to intent.getIntExtra(CropImageFragment.EXTRA_TRAIT_ID, -1),
+ CropImageFragment.EXTRA_IMAGE_URI to intent.getStringExtra(CropImageFragment.EXTRA_IMAGE_URI),
+ )
+
+ supportFragmentManager.commit {
+ setReorderingAllowed(true)
+ add(R.id.fragment_container_view, args = bundle)
+ }
+ }
+ }
+}
\ No newline at end of file
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..b3ce2f907 100644
--- a/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt
+++ b/app/src/main/java/com/fieldbook/tracker/activities/FieldDetailFragment.kt
@@ -456,7 +456,11 @@ class FieldDetailFragment : Fragment(), FieldSyncController {
}
fun checkTraitsExist(): Int {
- val traits = database.getVisibleTrait()
+
+ val currentSortOrder: String =
+ preferences.getString(GeneralKeys.TRAITS_LIST_SORT_ORDER, "position") ?: ""
+
+ val traits = database.getVisibleTrait(currentSortOrder)
return when {
traits.isEmpty() -> {
diff --git a/app/src/main/java/com/fieldbook/tracker/activities/FieldEditorActivity.java b/app/src/main/java/com/fieldbook/tracker/activities/FieldEditorActivity.java
index 2f80770b0..e81621e96 100644
--- a/app/src/main/java/com/fieldbook/tracker/activities/FieldEditorActivity.java
+++ b/app/src/main/java/com/fieldbook/tracker/activities/FieldEditorActivity.java
@@ -109,9 +109,7 @@ public class FieldEditorActivity extends ThemedActivity
private static final Handler mHandler = new Handler();
private FieldFileObject.FieldFileBase fieldFile;
private final int PERMISSIONS_REQUEST_STORAGE = 998;
- Spinner unique;
- Spinner primary;
- Spinner secondary;
+ private Spinner unique;
private Menu systemMenu;
private GPSTracker mGpsTracker;
private ActionMode actionMode;
@@ -135,9 +133,7 @@ public void run() {
new ImportRunnableTask(FieldEditorActivity.this,
fieldFile,
unique.getSelectedItemPosition(),
- unique.getSelectedItem().toString(),
- primary.getSelectedItem().toString(),
- secondary.getSelectedItem().toString()).execute(0);
+ unique.getSelectedItem().toString()).execute(0);
}
};
@@ -941,15 +937,11 @@ private void loadFile(FieldFileObject.FieldFileBase fieldFile) {
private void importDialog(String[] columns) {
LayoutInflater inflater = this.getLayoutInflater();
- View layout = inflater.inflate(R.layout.dialog_import, null);
+ View layout = inflater.inflate(R.layout.dialog_field_file_import, null);
unique = layout.findViewById(R.id.uniqueSpin);
- primary = layout.findViewById(R.id.primarySpin);
- secondary = layout.findViewById(R.id.secondarySpin);
setSpinner(unique, columns, GeneralKeys.UNIQUE_NAME);
- setSpinner(primary, columns, GeneralKeys.PRIMARY_NAME);
- setSpinner(secondary, columns, GeneralKeys.SECONDARY_NAME);
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.AppAlertDialog);
builder.setTitle(R.string.fields_new_dialog_title)
@@ -957,9 +949,7 @@ private void importDialog(String[] columns) {
.setView(layout);
builder.setPositiveButton(getString(R.string.dialog_import), (dialogInterface, i) -> {
- if (checkImportColumnNames()) {
- mHandler.post(importRunnable);
- }
+ mHandler.post(importRunnable);
});
builder.show();
@@ -973,20 +963,6 @@ private void setSpinner(Spinner spinner, String[] data, String pref) {
spinner.setSelection(spinnerPosition);
}
- // Validate that column choices are different from one another
- private boolean checkImportColumnNames() {
- final String uniqueS = unique.getSelectedItem().toString();
- final String primaryS = primary.getSelectedItem().toString();
- final String secondaryS = secondary.getSelectedItem().toString();
-
- if (uniqueS.equals(primaryS) || uniqueS.equals(secondaryS) || primaryS.equals(secondaryS)) {
- Utils.makeToast(getApplicationContext(), getString(R.string.import_error_column_choice));
- return false;
- }
-
- return true;
- }
-
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
diff --git a/app/src/main/java/com/fieldbook/tracker/activities/SearchActivity.java b/app/src/main/java/com/fieldbook/tracker/activities/SearchActivity.java
index 5d8102230..b9189204f 100644
--- a/app/src/main/java/com/fieldbook/tracker/activities/SearchActivity.java
+++ b/app/src/main/java/com/fieldbook/tracker/activities/SearchActivity.java
@@ -334,8 +334,9 @@ public void addRow(String text) {
if (col != null) {
rangeUntil = col.length;
+ String currentSortOrder = preferences.getString(GeneralKeys.TRAITS_LIST_SORT_ORDER, "position");
ArrayAdapter adapter2 = new ArrayAdapter(SearchActivity.this, R.layout.custom_spinner_layout,
- concat(col, database.getVisibleTrait()));
+ concat(col, database.getVisibleTrait(currentSortOrder)));
adapter2.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
c.setAdapter(adapter2);
diff --git a/app/src/main/java/com/fieldbook/tracker/activities/SummaryFragment.kt b/app/src/main/java/com/fieldbook/tracker/activities/SummaryFragment.kt
index 142362215..eef5f52eb 100644
--- a/app/src/main/java/com/fieldbook/tracker/activities/SummaryFragment.kt
+++ b/app/src/main/java/com/fieldbook/tracker/activities/SummaryFragment.kt
@@ -313,7 +313,8 @@ class SummaryFragment : Fragment(), SummaryAdapter.SummaryController {
this?.let { collector ->
- if (attribute in database.visibleTrait) {
+ val sortOrder = collector.preferences.getString(GeneralKeys.TRAITS_LIST_SORT_ORDER, "position")
+ if (attribute in database.getVisibleTrait(sortOrder)) {
collector.navigateToTrait(attribute)
diff --git a/app/src/main/java/com/fieldbook/tracker/activities/TraitEditorActivity.java b/app/src/main/java/com/fieldbook/tracker/activities/TraitEditorActivity.java
index 3bd4400fe..c17bbe1f9 100644
--- a/app/src/main/java/com/fieldbook/tracker/activities/TraitEditorActivity.java
+++ b/app/src/main/java/com/fieldbook/tracker/activities/TraitEditorActivity.java
@@ -120,6 +120,8 @@ public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.
int to = target.getBindingAdapterPosition();
try {
+ preferences.edit().putString(GeneralKeys.TRAITS_LIST_SORT_ORDER, "position").apply();
+ queryAndLoadTraits();
adapter.moveItem(from, to);
} catch (IndexOutOfBoundsException iobe) {
iobe.printStackTrace();
@@ -399,6 +401,9 @@ private void checkShowDeleteDialog() {
Toast.makeText(this, R.string.act_trait_editor_no_traits_exist, Toast.LENGTH_SHORT).show();
} else {
showDeleteTraitDialog((dialog, which) -> {
+ for (TraitObject t : traits) {
+ preferences.edit().remove(GeneralKeys.getCropCoordinatesKey(Integer.parseInt(t.getId()))).apply();
+ }
database.deleteTraitsTable();
queryAndLoadTraits();
dialog.dismiss();
@@ -586,6 +591,10 @@ private void showFileDialog() {
if (!traits.isEmpty()) {
+ for (TraitObject t : traits) {
+ preferences.edit().remove(GeneralKeys.getCropCoordinatesKey(Integer.parseInt(t.getId()))).apply();
+ }
+
showDeleteTraitDialog((dialog, which) -> {
database.deleteTraitsTable();
@@ -643,9 +652,10 @@ private void checkTraitExportDialog() {
private void showTraitSortDialog() {
Map sortOptions = new LinkedHashMap<>();
- final String defaultSortOrder = "internal_id_observation_variable";
+ final String defaultSortOrder = "position";
String currentSortOrder = preferences.getString(GeneralKeys.TRAITS_LIST_SORT_ORDER, defaultSortOrder);
+ sortOptions.put(getString(R.string.traits_sort_default), "position");
sortOptions.put(getString(R.string.traits_sort_name), "observation_variable_name");
sortOptions.put(getString(R.string.traits_sort_format), "observation_variable_field_book_format");
sortOptions.put(getString(R.string.traits_sort_import_order), "internal_id_observation_variable");
@@ -924,6 +934,8 @@ private void deleteTrait(TraitObject trait) {
builder.setPositiveButton(getString(R.string.dialog_yes), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
+ preferences.edit().remove(GeneralKeys.getCropCoordinatesKey(Integer.parseInt(trait.getId()))).apply();
+
getDatabase().deleteTrait(trait.getId());
queryAndLoadTraits();
diff --git a/app/src/main/java/com/fieldbook/tracker/activities/brapi/io/BrapiStudyImportActivity.kt b/app/src/main/java/com/fieldbook/tracker/activities/brapi/io/BrapiStudyImportActivity.kt
index 9d1123ea8..4771328eb 100644
--- a/app/src/main/java/com/fieldbook/tracker/activities/brapi/io/BrapiStudyImportActivity.kt
+++ b/app/src/main/java/com/fieldbook/tracker/activities/brapi/io/BrapiStudyImportActivity.kt
@@ -105,8 +105,6 @@ class BrapiStudyImportActivity : ThemedActivity(), CoroutineScope by MainScope()
private var selectedLevel: Int = -1
private var selectedSort: Int = -1
- private var selectedPrimary: Int = -1
- private var selectedSecondary: Int = -1
private var attributesTable: HashMap>>? = null
@@ -208,7 +206,7 @@ class BrapiStudyImportActivity : ThemedActivity(), CoroutineScope by MainScope()
}
enum class Tab {
- LEVELS, PRIMARY_ORDER, SECONDARY_ORDER, SORT
+ LEVELS, SORT
}
private fun loadTabLayout(studyDbIds: List) {
@@ -220,8 +218,6 @@ class BrapiStudyImportActivity : ThemedActivity(), CoroutineScope by MainScope()
when (tab?.position) {
Tab.LEVELS.ordinal -> setLevelListOptions()
Tab.SORT.ordinal -> setSortListOptions()
- Tab.PRIMARY_ORDER.ordinal -> setPrimaryOrderListOptions()
- Tab.SECONDARY_ORDER.ordinal -> setSecondaryOrderListOptions()
}
listView.visibility = View.VISIBLE
@@ -314,66 +310,8 @@ class BrapiStudyImportActivity : ThemedActivity(), CoroutineScope by MainScope()
}
}
- private fun setPrimaryOrderListOptions() {
-
- listView.visibility = View.VISIBLE
-
- val attributes = getAttributeKeys()
-
- listView.adapter = ArrayAdapter(
- this,
- android.R.layout.simple_list_item_single_choice,
- attributes
- )
-
- listView.setItemChecked(selectedPrimary, true)
-
- listView.smoothScrollToPosition(selectedPrimary)
-
- listView.setOnItemClickListener { _, _, position, _ ->
-
- selectedPrimary = if (selectedPrimary == position) {
-
- listView.setItemChecked(selectedPrimary, false)
-
- -1
-
- } else position
- }
- }
-
- private fun setSecondaryOrderListOptions() {
-
- listView.visibility = View.VISIBLE
-
- val attributes = getAttributeKeys()
-
- listView.adapter = ArrayAdapter(
- this,
- android.R.layout.simple_list_item_single_choice,
- attributes
- )
-
- listView.setItemChecked(selectedSecondary, true)
-
- listView.smoothScrollToPosition(selectedSecondary)
-
- listView.setOnItemClickListener { _, _, position, _ ->
-
- selectedSecondary = if (selectedSecondary == position) {
-
- listView.setItemChecked(selectedSecondary, false)
-
- -1
-
- } else position
- }
- }
-
private fun setDefaultAttributeIdentifiers() {
- val attributes = getAttributeKeys()
-
val levels = existingLevels()
if (selectedLevel == -1) {
@@ -383,26 +321,6 @@ class BrapiStudyImportActivity : ThemedActivity(), CoroutineScope by MainScope()
0
}
}
-
- if (selectedPrimary == -1) {
- selectedPrimary = if (attributes.contains("Row")) {
- attributes.indexOf("Row")
- } else if (attributes.contains("Block")) {
- attributes.indexOf("Block")
- } else {
- 0
- }
- }
-
- if (selectedSecondary == -1) {
- selectedSecondary = if (attributes.contains("Column")) {
- attributes.indexOf("Column")
- } else if (attributes.contains("Rep")) {
- attributes.indexOf("Rep")
- } else {
- 0
- }
- }
}
private fun getAttributes(studyDbId: String): Map> {
@@ -643,16 +561,27 @@ class BrapiStudyImportActivity : ThemedActivity(), CoroutineScope by MainScope()
}
val allAttributes = getAttributeKeys()
- val primaryId = allAttributes[selectedPrimary]
- val secondaryId = allAttributes[selectedSecondary]
val sortOrder = if (selectedSort == -1) "" else allAttributes[selectedSort]
studyDbIds.forEach { id ->
- studies.firstOrNull { it.studyDbId == id }?.let {
+ try {
+
+ studies.firstOrNull { it.studyDbId == id }?.let {
- saveStudy(it, level, primaryId, secondaryId, sortOrder)
+ saveStudy(it, level, sortOrder)
+
+ }
+ } catch (e: Exception) {
+
+ Log.e(TAG, "Failed to save study", e)
+
+ runOnUiThread {
+
+ Toast.makeText(this@BrapiStudyImportActivity, getString(R.string.failed_to_save_study), Toast.LENGTH_SHORT).show()
+
+ }
}
}
@@ -666,8 +595,6 @@ class BrapiStudyImportActivity : ThemedActivity(), CoroutineScope by MainScope()
private fun saveStudy(
study: BrAPIStudy,
level: BrapiObservationLevel,
- primaryId: String,
- secondaryId: String,
sortId: String
) {
@@ -693,58 +620,47 @@ class BrapiStudyImportActivity : ThemedActivity(), CoroutineScope by MainScope()
val attributes = (studyAttributes.values.flatMap { it.keys } + geoCoordinateColumnName).distinct()
- if (listOf(primaryId, secondaryId, sortId).filter { it.isNotEmpty() }.all { it in attributes }) {
+ details.attributes = attributes
- details.attributes = attributes
+ val unitAttributes = ArrayList>()
+ units.forEach { unit ->
- val unitAttributes = ArrayList>()
- units.forEach { unit ->
+ val row = ArrayList()
- val row = ArrayList()
-
- attributes.forEach { attr ->
- if (attr != geoCoordinateColumnName) {
- row.add(studyAttributes[unit.observationUnitDbId]?.get(attr) ?: "")
- }
+ attributes.forEach { attr ->
+ if (attr != geoCoordinateColumnName) {
+ row.add(studyAttributes[unit.observationUnitDbId]?.get(attr) ?: "")
}
+ }
- //add geo json as json string
- if (geoCoordinateColumnName in attributes) {
- unit.observationUnitPosition?.geoCoordinates?.let { coordString ->
- try {
- row.add(JSON().serialize(coordString))
- } catch (e: Exception) {
- Log.e(TAG, "Failed to serialize geo coordinates", e)
- }
+ //add geo json as json string
+ if (geoCoordinateColumnName in attributes) {
+ unit.observationUnitPosition?.geoCoordinates?.let { coordString ->
+ try {
+ row.add(JSON().serialize(coordString))
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to serialize geo coordinates", e)
}
}
-
- unitAttributes.add(row)
-
}
- details.values = mutableListOf()
- details.values.addAll(unitAttributes)
+ unitAttributes.add(row)
- brapiService.saveStudyDetails(
- details,
- level,
- primaryId,
- secondaryId,
- sortId,
- )
- } else {
-
- runOnUiThread {
-
- Toast.makeText(this, getString(R.string.failed_to_save_study), Toast.LENGTH_SHORT).show()
- setResult(Activity.RESULT_CANCELED)
- finish()
- }
}
+
+ details.values = mutableListOf()
+ details.values.addAll(unitAttributes)
+
+ //primary/secondary no longer required
+ brapiService.saveStudyDetails(
+ details,
+ level,
+ "",
+ "",
+ sortId,
+ )
}
}
-
}
private suspend fun fetchObservationLevels(programDbId: String) = coroutineScope {
diff --git a/app/src/main/java/com/fieldbook/tracker/adapters/TraitsStatusAdapter.kt b/app/src/main/java/com/fieldbook/tracker/adapters/TraitsStatusAdapter.kt
index 74d66ffa8..1d6919fe3 100644
--- a/app/src/main/java/com/fieldbook/tracker/adapters/TraitsStatusAdapter.kt
+++ b/app/src/main/java/com/fieldbook/tracker/adapters/TraitsStatusAdapter.kt
@@ -106,7 +106,8 @@ class TraitsStatusAdapter(private val traitBoxView: TraitBoxView) :
// Only update if the new selection is different
if (currentSelection != newSelection) {
currentSelection = newSelection
- notifyDataSetChanged() // Notify adapter to refresh all items
+ notifyItemChanged(currentSelection)
+ notifyItemChanged(newSelection)
}
}
@@ -118,7 +119,7 @@ class TraitsStatusAdapter(private val traitBoxView: TraitBoxView) :
this[currentSelection].hasObservation = value
}
submitList(updatedList)
- notifyDataSetChanged()
+ notifyItemChanged(currentSelection)
}
}
@@ -128,11 +129,11 @@ class TraitsStatusAdapter(private val traitBoxView: TraitBoxView) :
class DiffCallback : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: TraitBoxItemModel, newItem: TraitBoxItemModel): Boolean {
- return oldItem == newItem
+ return oldItem.trait == newItem.trait
}
override fun areContentsTheSame(oldItem: TraitBoxItemModel, newItem: TraitBoxItemModel): Boolean {
- return oldItem == newItem
+ return oldItem.hasObservation == newItem.hasObservation
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/fieldbook/tracker/async/ImportRunnableTask.java b/app/src/main/java/com/fieldbook/tracker/async/ImportRunnableTask.java
index 1645a20f4..14000595e 100644
--- a/app/src/main/java/com/fieldbook/tracker/async/ImportRunnableTask.java
+++ b/app/src/main/java/com/fieldbook/tracker/async/ImportRunnableTask.java
@@ -27,7 +27,7 @@ public class ImportRunnableTask extends AsyncTask {
WeakReference mContext;
FieldAdapterController controller;
FieldFileObject.FieldFileBase mFieldFile;
- String unique, primary, secondary;
+ String unique;
int idColPosition;
SharedPreferences preferences;
@@ -37,7 +37,7 @@ public class ImportRunnableTask extends AsyncTask {
boolean containsDuplicates = false;
public ImportRunnableTask(Context context, FieldFileObject.FieldFileBase fieldFile,
- int idColPosition, String unique, String primary, String secondary) {
+ int idColPosition, String unique) {
mFieldFile = fieldFile;
@@ -51,8 +51,6 @@ public ImportRunnableTask(Context context, FieldFileObject.FieldFileBase fieldFi
this.idColPosition = idColPosition;
this.unique = unique;
- this.primary = primary;
- this.secondary = secondary;
}
@Override
@@ -116,26 +114,21 @@ protected Integer doInBackground(Integer... params) {
if (columns[i].equals(unique)) {
uniqueIndex = i;
- } else if (columns[i].equals(primary)) {
- primaryIndex = i;
- } else if (columns[i].equals(secondary)) {
- secondaryIndex = i;
}
+
} else containsDuplicates = true;
}
}
FieldObject f = mFieldFile.createFieldObject();
f.setUnique_id(unique);
- f.setPrimary_id(primary);
- f.setSecondary_id(secondary);
controller.getDatabase().beginTransaction();
studyId = controller.getDatabase().createField(f, nonEmptyColumns, false);
//start iterating over all the rows of the csv file only if we found the u/p/s indices
- if (uniqueIndex > -1 && primaryIndex > -1 && secondaryIndex > -1) {
+ if (uniqueIndex > -1) {
int line = 0;
@@ -149,11 +142,10 @@ protected Integer doInBackground(Integer... params) {
int rowSize = data.length;
//ensure next check won't cause an AIOB
- if (rowSize > uniqueIndex && rowSize > primaryIndex && rowSize > secondaryIndex) {
+ if (rowSize > uniqueIndex) {
//check that all u/p/s strings are not empty
- if (!data[uniqueIndex].isEmpty() && !data[primaryIndex].isEmpty()
- && !data[secondaryIndex].isEmpty()) {
+ if (!data[uniqueIndex].isEmpty()) {
ArrayList nonEmptyData = new ArrayList<>();
for (int j = 0; j < data.length; j++) {
@@ -162,7 +154,7 @@ protected Integer doInBackground(Integer... params) {
}
}
- controller.getDatabase().createFieldData(studyId, nonEmptyColumns, nonEmptyData);
+ controller.getDatabase().createFieldData(studyId, nonEmptyColumns, nonEmptyData, false);
}
}
diff --git a/app/src/main/java/com/fieldbook/tracker/brapi/service/BrAPIServiceV1.java b/app/src/main/java/com/fieldbook/tracker/brapi/service/BrAPIServiceV1.java
index 91be0ae79..e0ec34ac1 100644
--- a/app/src/main/java/com/fieldbook/tracker/brapi/service/BrAPIServiceV1.java
+++ b/app/src/main/java/com/fieldbook/tracker/brapi/service/BrAPIServiceV1.java
@@ -1203,7 +1203,7 @@ public BrapiControllerResponse saveStudyDetails(BrapiStudyDetails studyDetails,
System.out.println("Size of study details: "+studyDetails.getValues().size());
for (List dataRow : studyDetails.getValues()) {
- dataHelper.createFieldData(expId, studyDetails.getAttributes(), dataRow);
+ dataHelper.createFieldData(expId, studyDetails.getAttributes(), dataRow, true);
}
// Insert the traits already associated with this study
diff --git a/app/src/main/java/com/fieldbook/tracker/brapi/service/BrAPIServiceV2.java b/app/src/main/java/com/fieldbook/tracker/brapi/service/BrAPIServiceV2.java
index f01f973ac..e95c8a4b6 100644
--- a/app/src/main/java/com/fieldbook/tracker/brapi/service/BrAPIServiceV2.java
+++ b/app/src/main/java/com/fieldbook/tracker/brapi/service/BrAPIServiceV2.java
@@ -1600,7 +1600,7 @@ public BrapiControllerResponse saveStudyDetails(BrapiStudyDetails studyDetails,
System.out.println("Size of study details: " + studyDetails.getValues().size());
for (List dataRow : studyDetails.getValues()) {
- dataHelper.createFieldData(expId, studyDetails.getAttributes(), dataRow);
+ dataHelper.createFieldData(expId, studyDetails.getAttributes(), dataRow, true);
Log.d("BrAPIServiceV2", "Saving: Attributes: " + studyDetails.getAttributes());
Log.d("BrAPIServiceV2", "Saving: dataRow: " + dataRow);
}
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..dd63d56a0 100644
--- a/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java
+++ b/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java
@@ -1052,11 +1052,11 @@ public Cursor getExportTableData(int fieldId, ArrayList traits) {
/**
* Used by the application to return all traits which are visible
*/
- public String[] getVisibleTrait() {
+ public String[] getVisibleTrait(String sortOrder) {
open();
- return VisibleObservationVariableDao.Companion.getVisibleTrait();
+ return VisibleObservationVariableDao.Companion.getVisibleTrait(sortOrder);
// String[] data = null;
//
@@ -1083,11 +1083,11 @@ public String[] getVisibleTrait() {
// return data;
}
- public ArrayList getVisibleTraitObjects() {
+ public ArrayList getVisibleTraitObjects(String sortOrder) {
open();
- return VisibleObservationVariableDao.Companion.getVisibleTraitObjects();
+ return VisibleObservationVariableDao.Companion.getVisibleTraitObjects(sortOrder);
}
/**
@@ -1314,7 +1314,7 @@ public FieldObject getFieldObject(Integer studyId) {
return StudyDao.Companion.getFieldObject(
studyId,
- preferences.getString(GeneralKeys.TRAITS_LIST_SORT_ORDER, "internal_id_observation_variable")
+ preferences.getString(GeneralKeys.TRAITS_LIST_SORT_ORDER, "position")
);
// Cursor cursor = db.query(EXP_INDEX, new String[]{"exp_id", "exp_name", "unique_id", "primary_id",
@@ -1352,7 +1352,7 @@ public ArrayList getAllTraitObjects() {
open();
return ObservationVariableDao.Companion.getAllTraitObjects(
- preferences.getString(GeneralKeys.TRAITS_LIST_SORT_ORDER, "internal_id_observation_variable")
+ preferences.getString(GeneralKeys.TRAITS_LIST_SORT_ORDER, "position")
);
// ArrayList list = new ArrayList<>();
@@ -2104,12 +2104,13 @@ public void updateTraitPosition(String id, int realPosition) {
*/
public long editTraits(String traitDbId, String trait, String format, String defaultValue,
String minimum, String maximum, String details, String categories,
- Boolean closeKeyboardOnOpen) {
+ Boolean closeKeyboardOnOpen,
+ Boolean cropImage) {
open();
return ObservationVariableDao.Companion.editTraits(traitDbId, trait, format, defaultValue,
- minimum, maximum, details, categories, closeKeyboardOnOpen);
+ minimum, maximum, details, categories, closeKeyboardOnOpen, cropImage);
// try {
// ContentValues c = new ContentValues();
// c.put("trait", trait);
@@ -2147,7 +2148,7 @@ public long updateTrait(TraitObject trait) {
return ObservationVariableDao.Companion.editTraits(trait.getId(), trait.getName(),
trait.getFormat(), trait.getDefaultValue(), trait.getMinimum(), trait.getMaximum(),
- trait.getDetails(), trait.getCategories(), trait.getCloseKeyboardOnOpen());
+ trait.getDetails(), trait.getCategories(), trait.getCloseKeyboardOnOpen(), trait.getCropImage());
}
public boolean checkUnique(HashMap values) {
@@ -2351,11 +2352,11 @@ public int createField(FieldObject e, List columns, Boolean fromBrapi) {
// return (int) exp_id;
}
- public void createFieldData(int studyId, List columns, List data) {
+ public void createFieldData(int studyId, List columns, List data, Boolean isBrapi) {
open();
- StudyDao.Companion.createFieldData(studyId, columns, data);
+ StudyDao.Companion.createFieldData(studyId, columns, data, isBrapi);
// // get unique_id, primary_id, secondary_id names from exp_id
// Cursor cursor = db.rawQuery("SELECT exp_id.unique_id, exp_id.primary_id, exp_id.secondary_id from exp_id where exp_id.exp_id = " + exp_id, null);
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..7a65c6b83 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
@@ -60,21 +60,20 @@ class ObservationUnitPropertyDao {
RangeObject().apply {
val model = db.query(sObservationUnitPropertyViewName,
- select = arrayOf(firstName, secondName, uniqueName, "id"),
where = "id = ?",
whereArgs = arrayOf(id.toString())).toFirst()
- range = model[firstName].toString()
+ primaryId = model[firstName].toString()
- plot = model[secondName].toString()
+ secondaryId = model[secondName].toString()
- plot_id = model[uniqueName].toString()
+ uniqueId = model[uniqueName].toString()
}
} ?: RangeObject().apply {
- range = ""
- plot = ""
- plot_id = ""
+ primaryId = ""
+ secondaryId = ""
+ uniqueId = ""
}
/**
diff --git a/app/src/main/java/com/fieldbook/tracker/database/dao/ObservationVariableDao.kt b/app/src/main/java/com/fieldbook/tracker/database/dao/ObservationVariableDao.kt
index bb7f5afb7..1f2e91abb 100644
--- a/app/src/main/java/com/fieldbook/tracker/database/dao/ObservationVariableDao.kt
+++ b/app/src/main/java/com/fieldbook/tracker/database/dao/ObservationVariableDao.kt
@@ -214,15 +214,12 @@ class ObservationVariableDao {
)
}
- fun getAllTraitObjects(sortOrder: String = "internal_id_observation_variable"): ArrayList = withDatabase { db ->
+ fun getAllTraitObjects(sortOrder: String = "position"): ArrayList = withDatabase { db ->
val traits = ArrayList()
- // Sort ascending except for visibility, for visibility sort desc to have visible traits first
- val orderDirection = if (sortOrder == "visible") "DESC" else "ASC"
-
val query = """
SELECT * FROM ${ObservationVariable.tableName}
- ORDER BY $sortOrder COLLATE NOCASE $orderDirection
+ ORDER BY ${if (sortOrder == "visible") "position" else sortOrder} COLLATE NOCASE ASC
"""
db.rawQuery(query, null).use { cursor ->
@@ -243,6 +240,7 @@ class ObservationVariableDao {
minimum = ""
categories = ""
closeKeyboardOnOpen = false
+ cropImage = false
val values = ObservationVariableValueDao.getVariableValues(id.toInt())
values?.forEach { value ->
@@ -252,17 +250,27 @@ class ObservationVariableDao {
"validValuesMax" -> maximum = value["observation_variable_attribute_value"] as? String ?: ""
"category" -> categories = value["observation_variable_attribute_value"] as? String ?: ""
"closeKeyboardOnOpen" -> closeKeyboardOnOpen = (value["observation_variable_attribute_value"] as? String ?: "false").toBoolean()
+ "cropImage" -> cropImage = (value["observation_variable_attribute_value"] as? String ?: "false").toBoolean()
}
}
}
traits.add(trait)
}
}
- ArrayList(traits)
+
+ if (sortOrder == "visible") {
+ val visibleTraits = traits.filter { it.visible }
+ val invisibleTraits = traits.filter { !it.visible }
+
+ ArrayList(visibleTraits.sortedBy { it.realPosition } + ArrayList(invisibleTraits.sortedBy { it.realPosition }))
+
+ } else {
+ ArrayList(traits)
+ }
} ?: ArrayList()
// Overload for Java compatibility
- fun getAllTraitObjects(): ArrayList = getAllTraitObjects("internal_id_observation_variable")
+ fun getAllTraitObjects(): ArrayList = getAllTraitObjects("position")
// fun getAllTraitObjects(sortOrder: String): ArrayList = withDatabase { db ->
@@ -350,6 +358,7 @@ class ObservationVariableDao {
Log.d("ObservationVariableDao", "maximum: ${t.maximum.orEmpty()}")
Log.d("ObservationVariableDao", "categories: ${t.categories.orEmpty()}")
Log.d("ObservationVariableDao", "closeKeyboardOnOpen: ${t.closeKeyboardOnOpen ?: "false"}")
+ Log.d("ObservationVariableDao", "cropImage: ${t.cropImage ?: "false"}")
val varRowId = db.insert(ObservationVariable.tableName, null, contentValues)
@@ -359,6 +368,7 @@ class ObservationVariableDao {
t.maximum.orEmpty(),
t.categories.orEmpty(),
(t.closeKeyboardOnOpen ?: "false").toString(),
+ (t.cropImage ?: "false").toString(),
varRowId.toString()
)
Log.d("ObservationVariableDao", "Trait ${t.name} inserted successfully with row ID: $varRowId")
@@ -390,7 +400,8 @@ class ObservationVariableDao {
//TODO need to edit min/max/category obs. var. val/attrs
fun editTraits(id: String, trait: String, format: String, defaultValue: String,
minimum: String, maximum: String, details: String, categories: String,
- closeKeyboardOnOpen: Boolean): Long = withDatabase { db ->
+ closeKeyboardOnOpen: Boolean,
+ cropImage: Boolean): Long = withDatabase { db ->
val rowid = db.update(ObservationVariable.tableName, ContentValues().apply {
put("observation_variable_name", trait)
@@ -415,7 +426,10 @@ class ObservationVariableDao {
ObservationVariableValueDao.update(id, attrId.toString(), categories)
}
"closeKeyboardOnOpen" -> {
- ObservationVariableValueDao.insertCloseKeyboard(closeKeyboardOnOpen.toString(), id)
+ ObservationVariableValueDao.insertAttributeValue(it, closeKeyboardOnOpen.toString(), id)
+ }
+ "cropImage" -> {
+ ObservationVariableValueDao.insertAttributeValue(it, cropImage.toString(), id)
}
}
}
diff --git a/app/src/main/java/com/fieldbook/tracker/database/dao/ObservationVariableValueDao.kt b/app/src/main/java/com/fieldbook/tracker/database/dao/ObservationVariableValueDao.kt
index 21eec24c7..096883c6f 100644
--- a/app/src/main/java/com/fieldbook/tracker/database/dao/ObservationVariableValueDao.kt
+++ b/app/src/main/java/com/fieldbook/tracker/database/dao/ObservationVariableValueDao.kt
@@ -27,38 +27,32 @@ class ObservationVariableValueDao {
}
- fun insertCloseKeyboard(closeKeyboardOnOpen: String, id: String) = withDatabase { db ->
+ fun insertAttributeValue(attrName: String, value: String, id: String) = withDatabase { db ->
- val attrId = ObservationVariableAttributeDao.getAttributeIdByName("closeKeyboardOnOpen")
+ val attrId = ObservationVariableAttributeDao.getAttributeIdByName(attrName)
db.insert(ObservationVariableValue.tableName, null, contentValuesOf(
ObservationVariable.FK to id,
Migrator.ObservationVariableAttribute.FK to attrId,
- "observation_variable_attribute_value" to closeKeyboardOnOpen
+ "observation_variable_attribute_value" to value
))
}
- fun insert(min: String, max: String, categories: String, closeKeyboardOnOpen: String, id: String) = withDatabase { db ->
+ fun insert(min: String, max: String, categories: String, closeKeyboardOnOpen: String, cropImage: String, id: String) = withDatabase { db ->
//iterate through mapping of the old columns that are now attr/vals
mapOf(
"validValuesMin" to min,
"validValuesMax" to max,
"category" to categories,
- "closeKeyboardOnOpen" to closeKeyboardOnOpen
+ "closeKeyboardOnOpen" to closeKeyboardOnOpen,
+ "cropImage" to cropImage
).asSequence().forEach { attrValue ->
- val attrId = ObservationVariableAttributeDao.getAttributeIdByName(attrValue.key)
+ insertAttributeValue(attrValue.key, attrValue.value, id)
- db.insert(ObservationVariableValue.tableName, null, contentValuesOf(
-
- ObservationVariable.FK to id,
- Migrator.ObservationVariableAttribute.FK to attrId,
- "observation_variable_attribute_value" to attrValue.value
-
- ))
}
}
}
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 5122c2574..fff87c9a1 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
@@ -202,7 +202,7 @@ class StudyDao {
(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
FROM ${Study.tableName} AS Studies
- ORDER BY $sortOrder COLLATE NOCASE ${if (isDateSort) "DESC" else "ASC"}
+ ORDER BY ${if (sortOrder == "visible") "position" else sortOrder} COLLATE NOCASE ${if (isDateSort) "DESC" else "ASC"}
"""
db.rawQuery(query, null).use { cursor ->
while (cursor.moveToNext()) {
@@ -218,7 +218,7 @@ class StudyDao {
} ?: ArrayList()
- fun getFieldObject(exp_id: Int, sortOrder: String = "internal_id_observation_variable"): FieldObject? = withDatabase { db ->
+ fun getFieldObject(exp_id: Int, sortOrder: String = "position"): FieldObject? = withDatabase { db ->
val query = """
SELECT
${Study.PK},
@@ -253,7 +253,7 @@ class StudyDao {
}
map.toFieldObject().apply {
// Set the trait details
- this.setTraitDetails(getTraitDetailsForStudy(exp_id, sortOrder))
+ this.traitDetails = getTraitDetailsForStudy(exp_id, sortOrder)
}
} else {
null
@@ -275,13 +275,10 @@ class StudyDao {
* )
*/
- fun getTraitDetailsForStudy(studyId: Int, sortOrder: String = "internal_id_observation_variable"): List {
+ fun getTraitDetailsForStudy(studyId: Int, sortOrder: String = "position"): List {
return withDatabase { db ->
val traitDetails = mutableListOf()
- // Sort ascending except for visibility, for visibility sort desc to have visible traits first
- val orderDirection = if (sortOrder == "visible") "DESC" else "ASC"
-
val cursor = db.rawQuery("""
SELECT o.observation_variable_name, o.observation_variable_field_book_format, COUNT(*) as count, GROUP_CONCAT(o.value, '|') as observations,
(SELECT COUNT(DISTINCT observation_unit_id) FROM observations WHERE study_id = ? AND observation_variable_name = o.observation_variable_name) AS distinct_obs_units,
@@ -294,7 +291,7 @@ class StudyDao {
JOIN observation_variables ov ON o.observation_variable_db_id = ov.internal_id_observation_variable
WHERE o.study_id = ? AND o.observation_variable_db_id > 0
GROUP BY o.observation_variable_name, o.observation_variable_field_book_format
- ORDER BY ov.$sortOrder COLLATE NOCASE $orderDirection
+ ORDER BY ov.${if (sortOrder == "visible") "position" else sortOrder} COLLATE NOCASE ASC
""", arrayOf(studyId.toString(), studyId.toString(), studyId.toString()))
if (cursor.moveToFirst()) {
@@ -400,17 +397,15 @@ class StudyDao {
/**
* This function should always be called within a transaction.
*/
- fun createFieldData(studyId: Int, columns: List, data: List) = withDatabase { db ->
+ fun createFieldData(studyId: Int, columns: List, data: List, isBrapi: Boolean = false) = withDatabase { db ->
val names = getNames(studyId)!!
- //TODO: indexOf can return -1 which leads to array out of bounds exception
//input data corresponds to original database column names
val uniqueIndex = columns.indexOf(names.unique)
val primaryIndex = columns.indexOf(names.primary)
val secondaryIndex = columns.indexOf(names.secondary)
- //TODO remove when we handle primary/secondary ids better
//check if data size matches the columns size, on mismatch fill with dummy data
//mainly fixes issues with BrAPI when xtype/ytype and row/col values are not given
val actualData = if (data.size != columns.size) {
@@ -429,12 +424,10 @@ class StudyDao {
geoCoordinates = data[geoCoordinatesIndex]
}
}
-
+
val rowid = db.insert(ObservationUnit.tableName, null, contentValuesOf(
Study.FK to studyId,
"observation_unit_db_id" to actualData[uniqueIndex],
- "primary_id" to if (primaryIndex < 0) "NA" else actualData[primaryIndex],
- "secondary_id" to if (secondaryIndex < 0) "NA" else actualData[secondaryIndex],
"geo_coordinates" to geoCoordinates
))
@@ -452,28 +445,31 @@ class StudyDao {
}
}
- if (primaryIndex < 0) {
+ if (isBrapi) {
- val attrId = ObservationUnitAttributeDao.getIdByName("Row")
+ if (primaryIndex < 0) {
- db.insert(ObservationUnitValue.tableName, null, contentValuesOf(
- Study.FK to studyId,
- ObservationUnit.FK to rowid,
- ObservationUnitAttribute.FK to attrId,
- "observation_unit_value_name" to "NA"
- ))
- }
+ val attrId = ObservationUnitAttributeDao.getIdByName("Row")
+
+ db.insert(ObservationUnitValue.tableName, null, contentValuesOf(
+ Study.FK to studyId,
+ ObservationUnit.FK to rowid,
+ ObservationUnitAttribute.FK to attrId,
+ "observation_unit_value_name" to "NA"
+ ))
+ }
- if (secondaryIndex < 0) {
+ if (secondaryIndex < 0) {
- val attrId = ObservationUnitAttributeDao.getIdByName("Column")
+ val attrId = ObservationUnitAttributeDao.getIdByName("Column")
- db.insert(ObservationUnitValue.tableName, null, contentValuesOf(
- Study.FK to studyId,
- ObservationUnit.FK to rowid,
- ObservationUnitAttribute.FK to attrId,
- "observation_unit_value_name" to "NA"
- ))
+ db.insert(ObservationUnitValue.tableName, null, contentValuesOf(
+ Study.FK to studyId,
+ ObservationUnit.FK to rowid,
+ ObservationUnitAttribute.FK to attrId,
+ "observation_unit_value_name" to "NA"
+ ))
+ }
}
}
diff --git a/app/src/main/java/com/fieldbook/tracker/database/dao/VisibleObservationVariableDao.kt b/app/src/main/java/com/fieldbook/tracker/database/dao/VisibleObservationVariableDao.kt
index f52101523..e0b937ea5 100644
--- a/app/src/main/java/com/fieldbook/tracker/database/dao/VisibleObservationVariableDao.kt
+++ b/app/src/main/java/com/fieldbook/tracker/database/dao/VisibleObservationVariableDao.kt
@@ -7,24 +7,27 @@ import com.fieldbook.tracker.database.query
import com.fieldbook.tracker.database.toTable
import com.fieldbook.tracker.database.withDatabase
import com.fieldbook.tracker.objects.TraitObject
+import com.fieldbook.tracker.preferences.GeneralKeys
class VisibleObservationVariableDao {
companion object {
- fun getVisibleTrait(): Array = withDatabase { db ->
+ fun getVisibleTrait(sortOrder: String = "position"): Array = withDatabase { db ->
- db.query(sVisibleObservationVariableViewName,
- select = arrayOf("observation_variable_name"),
- orderBy = "position").toTable().map { it ->
- it["observation_variable_name"].toString()
- }.toTypedArray()
+ db.query(sVisibleObservationVariableViewName)
+ .toTable()
+ .sortedBy { (it[if (sortOrder == "visible") "position" else sortOrder] as? String ?: "position").lowercase() }
+ .map { it["observation_variable_name"] as? String ?: ""}
+ .toTypedArray()
} ?: emptyArray()
- fun getVisibleTraitObjects(): ArrayList = withDatabase { db ->
- val rows = db.query(sVisibleObservationVariableViewName, orderBy = "position").toTable()
+ fun getVisibleTraitObjects(sortOrder: String = "position"): ArrayList = withDatabase { db ->
+ val rows = db.query(sVisibleObservationVariableViewName)
+ .toTable()
+ .sortedBy { (it[if (sortOrder == "visible") "position" else sortOrder] as? String ?: "position").lowercase() }
val variables: ArrayList = ArrayList()
@@ -84,8 +87,10 @@ class VisibleObservationVariableDao {
"closeKeyboardOnOpen" -> closeKeyboardOnOpen =
(it["observation_variable_attribute_value"] as? String ?: "false").toBoolean()
- }
+ "cropImage" -> cropImage =
+ (it["observation_variable_attribute_value"] as? String ?: "false").toBoolean()
+ }
}
}
}
diff --git a/app/src/main/java/com/fieldbook/tracker/database/models/ObservationUnitModel.kt b/app/src/main/java/com/fieldbook/tracker/database/models/ObservationUnitModel.kt
index 8f0b9e9a7..a5f0ae5a5 100644
--- a/app/src/main/java/com/fieldbook/tracker/database/models/ObservationUnitModel.kt
+++ b/app/src/main/java/com/fieldbook/tracker/database/models/ObservationUnitModel.kt
@@ -13,8 +13,8 @@ data class ObservationUnitModel(val map: Row) {
val internal_id_observation_unit: Int by map //comp. pk 1
val study_id: Int by map //fk to studies table
val observation_unit_db_id: String by map //unique id
- val primary_id: String by map
- val secondary_id: String by map
+ var primary_id: String? = (map["primary_id"] as? String) ?: ""
+ var secondary_id: String? = (map["secondary_id"] as? String) ?: ""
var geo_coordinates: String? = (map["geo_coordinates"] as? String) ?: "" //blob?
val additionalInfo: String? by map //blob, can be replaced with value/attr query?
val germplasmDbId: String? by map //brapId ?
diff --git a/app/src/main/java/com/fieldbook/tracker/devices/camera/CanonApi.kt b/app/src/main/java/com/fieldbook/tracker/devices/camera/CanonApi.kt
index ccd9c4c03..422fe8f1c 100644
--- a/app/src/main/java/com/fieldbook/tracker/devices/camera/CanonApi.kt
+++ b/app/src/main/java/com/fieldbook/tracker/devices/camera/CanonApi.kt
@@ -433,7 +433,7 @@ class CanonApi @Inject constructor(@ActivityContext private val context: Context
saveState = if (offset == 0) AbstractCameraTrait.SaveState.NEW else AbstractCameraTrait.SaveState.SAVING,
offset = offset ?: 0)
- Log.d(TAG, "Getting offset: ${offset ?: 0} for ${unit.plot_id}")
+ Log.d(TAG, "Getting offset: ${offset ?: 0} for ${unit.uniqueId}")
requestGetImage(session, handle, unit, (offset ?: 0) + length, saveTime)
}
@@ -517,7 +517,7 @@ class CanonApi @Inject constructor(@ActivityContext private val context: Context
lastSavedTime?.let { time ->
- log("Found new image $handleInteger for ${unit.plot_id}")
+ log("Found new image $handleInteger for ${unit.uniqueId}")
//requestUiLock(true, session, storageId, unit)
diff --git a/app/src/main/java/com/fieldbook/tracker/devices/camera/GoProApi.kt b/app/src/main/java/com/fieldbook/tracker/devices/camera/GoProApi.kt
index ab281e4ad..0ed2d4581 100644
--- a/app/src/main/java/com/fieldbook/tracker/devices/camera/GoProApi.kt
+++ b/app/src/main/java/com/fieldbook/tracker/devices/camera/GoProApi.kt
@@ -2,8 +2,6 @@ package com.fieldbook.tracker.devices.camera
import android.bluetooth.BluetoothDevice
import android.content.Context
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Log
import androidx.media3.common.Player
@@ -124,7 +122,7 @@ class GoProApi @Inject constructor(
}
Player.STATE_READY -> {
- Log.d(TAG, "Player Ready ${if (range.isNotEmpty()) range[0].range.plot_id else ""}")
+ Log.d(TAG, "Player Ready ${if (range.isNotEmpty()) range[0].range.uniqueId else ""}")
callbacks?.onStreamReady()
}
@@ -440,7 +438,7 @@ class GoProApi @Inject constructor(
*/
private fun requestFileUrl(url: String, model: ImageRequestData) {
- Log.d(TAG, "Image request: $url for model: ${model.range.plot_id}")
+ Log.d(TAG, "Image request: $url for model: ${model.range.uniqueId}")
//stop stream first, on fail or success start stream again:
val requestImage: Request = Request.Builder()
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 046ed22eb..9e16e4417 100644
--- a/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt
+++ b/app/src/main/java/com/fieldbook/tracker/dialogs/AttributeChooserDialog.kt
@@ -20,12 +20,32 @@ 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 : DialogFragment(), AttributeAdapter.AttributeAdapterController {
+open class AttributeChooserDialog(
+ private val showTraits: Boolean = true,
+ private val showOther: Boolean = true,
+ 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)
}
@@ -43,6 +63,11 @@ open class AttributeChooserDialog : DialogFragment(), AttributeAdapter.Attribute
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)
@@ -51,11 +76,18 @@ open class AttributeChooserDialog : DialogFragment(), AttributeAdapter.Attribute
recyclerView.layoutManager = LinearLayoutManager(requireActivity())
recyclerView.adapter = AttributeAdapter(this, null)
- val dialog = AlertDialog.Builder(requireActivity(), R.style.AppAlertDialog)
+ //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 builder = AlertDialog.Builder(requireActivity(), R.style.AppAlertDialog)
.setView(view)
.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 {
@@ -81,7 +113,9 @@ open class AttributeChooserDialog : DialogFragment(), AttributeAdapter.Attribute
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()
diff --git a/app/src/main/java/com/fieldbook/tracker/dialogs/FieldCreatorDialogFragment.kt b/app/src/main/java/com/fieldbook/tracker/dialogs/FieldCreatorDialogFragment.kt
index 6736dd6a3..3d2131ad7 100644
--- a/app/src/main/java/com/fieldbook/tracker/dialogs/FieldCreatorDialogFragment.kt
+++ b/app/src/main/java/com/fieldbook/tracker/dialogs/FieldCreatorDialogFragment.kt
@@ -490,7 +490,7 @@ class FieldCreatorDialogFragment(private val activity: ThemedActivity) :
val col = j.toString()
val index = k.toString()
- helper.createFieldData(studyDbId, fieldColumns, listOf(row, col, index, uuid))
+ helper.createFieldData(studyDbId, fieldColumns, listOf(row, col, index, uuid), false)
updatePlotInsertText(row, col, index)
diff --git a/app/src/main/java/com/fieldbook/tracker/dialogs/NewTraitDialog.kt b/app/src/main/java/com/fieldbook/tracker/dialogs/NewTraitDialog.kt
index 133d6a467..9232d0711 100644
--- a/app/src/main/java/com/fieldbook/tracker/dialogs/NewTraitDialog.kt
+++ b/app/src/main/java/com/fieldbook/tracker/dialogs/NewTraitDialog.kt
@@ -493,7 +493,8 @@ class NewTraitDialog(
traitObject.maximum,
traitObject.details,
traitObject.categories,
- traitObject.closeKeyboardOnOpen
+ traitObject.closeKeyboardOnOpen,
+ traitObject.cropImage
)
}
diff --git a/app/src/main/java/com/fieldbook/tracker/dialogs/QuickGotoDialog.kt b/app/src/main/java/com/fieldbook/tracker/dialogs/QuickGotoDialog.kt
new file mode 100644
index 000000000..12c39fef3
--- /dev/null
+++ b/app/src/main/java/com/fieldbook/tracker/dialogs/QuickGotoDialog.kt
@@ -0,0 +1,88 @@
+package com.fieldbook.tracker.dialogs
+
+import android.app.AlertDialog
+import android.app.Dialog
+import android.os.Bundle
+import android.text.InputType
+import android.widget.EditText
+import android.widget.ImageButton
+import androidx.fragment.app.DialogFragment
+import com.fieldbook.tracker.R
+import com.fieldbook.tracker.interfaces.CollectRangeController
+import com.fieldbook.tracker.preferences.GeneralKeys
+import org.phenoapps.utils.SoftKeyboardUtil
+
+/**
+ * Dialog to quickly jump to a specific range of data in the CollectActivity.
+ */
+class QuickGotoDialog(
+ private val controller: CollectRangeController,
+ private val primaryClicked: Boolean = true,
+ private val callback: (String, String) -> Unit): DialogFragment() {
+
+ companion object {
+ const val TAG = "QuickGotoDialog"
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val inflater = requireActivity().layoutInflater
+ val view = inflater.inflate(R.layout.dialog_quick_goto, null)
+
+ val prefs = controller.getPreferences()
+
+ //get primary/secondary from preferences and set hints of the edit texts
+ val primary = prefs.getString(GeneralKeys.PRIMARY_NAME, "")
+ val secondary = prefs.getString(GeneralKeys.SECONDARY_NAME, "")
+
+ val primaryEt = view.findViewById(R.id.quick_goto_primary_et)
+ val secondaryEt = view.findViewById(R.id.quick_goto_secondary_et)
+
+ val swapButton = view.findViewById(R.id.quick_goto_swap_btn)
+
+ primaryEt.hint = primary
+ secondaryEt.hint = secondary
+
+ val dialog = AlertDialog.Builder(requireContext())
+ .setTitle(R.string.dialog_quick_goto_title)
+ .setView(view)
+ .setPositiveButton(R.string.dialog_quick_goto_go) { _, _ ->
+ val primaryId = primaryEt.text.toString()
+ val secondaryId = secondaryEt.text.toString()
+ callback(primaryId, secondaryId)
+ }
+ .setNegativeButton(android.R.string.cancel) { _, _ -> }
+ .create()
+
+ dialog.setOnShowListener {
+
+ with(if (primaryClicked) primaryEt else secondaryEt) {
+ this.requestFocus()
+ SoftKeyboardUtil.Companion.showKeyboard(context, this)
+ }
+ }
+
+ swapButton.setOnClickListener {
+
+ toggleInputType(primaryEt)
+ toggleInputType(secondaryEt)
+
+
+ swapButton.setImageResource(if (secondaryEt.inputType == InputType.TYPE_CLASS_NUMBER) {
+ R.drawable.ic_trait_text
+ } else {
+ R.drawable.ic_trait_numeric
+ })
+ }
+
+ return dialog
+ }
+
+ private fun toggleInputType(editText: EditText) {
+
+ editText.inputType = if (editText.inputType == InputType.TYPE_CLASS_NUMBER) {
+ InputType.TYPE_CLASS_TEXT
+ } else {
+ InputType.TYPE_CLASS_NUMBER
+ }
+ }
+}
diff --git a/app/src/main/java/com/fieldbook/tracker/fragments/CropImageFragment.kt b/app/src/main/java/com/fieldbook/tracker/fragments/CropImageFragment.kt
new file mode 100644
index 000000000..d51d87387
--- /dev/null
+++ b/app/src/main/java/com/fieldbook/tracker/fragments/CropImageFragment.kt
@@ -0,0 +1,92 @@
+package com.fieldbook.tracker.fragments
+
+import android.content.SharedPreferences
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.Fragment
+import com.fieldbook.tracker.R
+import com.fieldbook.tracker.preferences.GeneralKeys
+import com.fieldbook.tracker.utilities.BitmapLoader
+import com.fieldbook.tracker.views.CropImageView
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+/**
+ * Controller code for handling user input for cropping an image.
+ */
+@AndroidEntryPoint
+class CropImageFragment: Fragment(R.layout.crop_image_fragment), CoroutineScope by MainScope() {
+
+ companion object {
+ const val TAG = "CropImageFragment"
+ const val EXTRA_TRAIT_ID = "traitId"
+ const val EXTRA_IMAGE_URI = "imageUri"
+ }
+
+ @Inject
+ lateinit var prefs: SharedPreferences
+
+ private var cropImageView: CropImageView? = null
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ val traitId = requireArguments().getInt(EXTRA_TRAIT_ID)
+ val imageUri = requireArguments().getString(EXTRA_IMAGE_URI) ?: ""
+ cropImageView = view.findViewById(R.id.crop_image_view)
+ setupCropImageView(traitId, imageUri)
+ }
+
+ private fun setupCropImageView(traitId: Int, imageUri: String) {
+
+ cropImageView?.cropImageHandler = object: CropImageView.CropImageHandler {
+
+ override fun onCropImageSaved(rectCoordinates: String) {
+
+ //save the coordinate text to preferences, make the key relative to the input trait id
+ prefs.edit().putString(GeneralKeys.getCropCoordinatesKey(traitId), rectCoordinates).apply()
+
+ launch(Dispatchers.IO) {
+
+ val uri = Uri.parse(imageUri)
+
+ val croppedBmp = BitmapLoader.cropBitmap(context, uri, rectCoordinates)
+
+ //save cropped bmp to uri
+ context?.contentResolver?.openOutputStream(uri)?.use { output ->
+ croppedBmp.compress(Bitmap.CompressFormat.JPEG, 80, output)
+ }
+
+ withContext(Dispatchers.Main) {
+
+ //finish from the crop activity
+ activity?.finish()
+ }
+ }
+ }
+
+ override fun getCropCoordinates(): String {
+
+ return prefs.getString(GeneralKeys.getCropCoordinatesKey(traitId), "") ?: ""
+ }
+
+ override fun getImageUri() = imageUri
+ }
+
+ cropImageView?.post {
+ cropImageView?.initialize()
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ cancel()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/fieldbook/tracker/location/GPSTracker.java b/app/src/main/java/com/fieldbook/tracker/location/GPSTracker.java
index 3c6dee685..4cfcfe28f 100644
--- a/app/src/main/java/com/fieldbook/tracker/location/GPSTracker.java
+++ b/app/src/main/java/com/fieldbook/tracker/location/GPSTracker.java
@@ -70,34 +70,40 @@ private Location getLastLocation(long minDistance, long minTime) {
// First get location from Network Provider
if (isNetworkEnabled) {
- locationManager.requestLocationUpdates(
- LocationManager.NETWORK_PROVIDER,
- minTime,
- minDistance, this);
- Log.d("Network", "Network");
+
if (locationManager != null) {
+
+ locationManager.requestLocationUpdates(
+ LocationManager.NETWORK_PROVIDER,
+ minTime,
+ minDistance, this);
+
location = locationManager
.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
if (location != null) {
latitude = location.getLatitude();
longitude = location.getLongitude();
+ onLocationChanged(location);
}
}
// if GPS Enabled get lat/long using GPS Services
if (isGPSEnabled) {
if (location == null) {
- locationManager.requestLocationUpdates(
- LocationManager.GPS_PROVIDER,
- minTime,
- minDistance, this);
- Log.d("GPS Enabled", "GPS Enabled");
if (locationManager != null) {
+
+ locationManager.requestLocationUpdates(
+ LocationManager.GPS_PROVIDER,
+ minTime,
+ minDistance, this);
+
location = locationManager
.getLastKnownLocation(LocationManager.GPS_PROVIDER);
+
if (location != null) {
latitude = location.getLatitude();
longitude = location.getLongitude();
+ onLocationChanged(location);
}
}
}
@@ -106,7 +112,9 @@ private Location getLastLocation(long minDistance, long minTime) {
}
} catch (Exception e) {
+
e.printStackTrace();
+
}
return location;
diff --git a/app/src/main/java/com/fieldbook/tracker/objects/RangeObject.java b/app/src/main/java/com/fieldbook/tracker/objects/RangeObject.java
index c24723b9e..9aa92164c 100644
--- a/app/src/main/java/com/fieldbook/tracker/objects/RangeObject.java
+++ b/app/src/main/java/com/fieldbook/tracker/objects/RangeObject.java
@@ -4,7 +4,7 @@
* Simple wrapper class for range data
*/
public class RangeObject {
- public String range;
- public String plot;
- public String plot_id;
+ public String primaryId;
+ public String secondaryId;
+ public String uniqueId;
}
diff --git a/app/src/main/java/com/fieldbook/tracker/objects/TraitObject.java b/app/src/main/java/com/fieldbook/tracker/objects/TraitObject.java
index 8e4dd5f24..16b50730d 100644
--- a/app/src/main/java/com/fieldbook/tracker/objects/TraitObject.java
+++ b/app/src/main/java/com/fieldbook/tracker/objects/TraitObject.java
@@ -31,6 +31,7 @@ public class TraitObject {
private String traitDataSource;
private String additionalInfo;
private Boolean closeKeyboardOnOpen = false;
+ private Boolean cropImage = false;
/**
* This is a BMS specific field. This will be populated when traits are imported from
@@ -189,12 +190,12 @@ public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TraitObject that = (TraitObject) o;
- return realPosition == that.realPosition && Objects.equals(name, that.name) && Objects.equals(format, that.format) && Objects.equals(defaultValue, that.defaultValue) && Objects.equals(minimum, that.minimum) && Objects.equals(maximum, that.maximum) && Objects.equals(details, that.details) && Objects.equals(categories, that.categories) && Objects.equals(id, that.id) && Objects.equals(visible, that.visible) && Objects.equals(externalDbId, that.externalDbId) && Objects.equals(traitDataSource, that.traitDataSource) && Objects.equals(additionalInfo, that.additionalInfo) && Objects.equals(observationLevelNames, that.observationLevelNames) && Objects.equals(closeKeyboardOnOpen, that.closeKeyboardOnOpen);
+ return realPosition == that.realPosition && Objects.equals(name, that.name) && Objects.equals(format, that.format) && Objects.equals(defaultValue, that.defaultValue) && Objects.equals(minimum, that.minimum) && Objects.equals(maximum, that.maximum) && Objects.equals(details, that.details) && Objects.equals(categories, that.categories) && Objects.equals(id, that.id) && Objects.equals(visible, that.visible) && Objects.equals(externalDbId, that.externalDbId) && Objects.equals(traitDataSource, that.traitDataSource) && Objects.equals(additionalInfo, that.additionalInfo) && Objects.equals(observationLevelNames, that.observationLevelNames) && Objects.equals(closeKeyboardOnOpen, that.closeKeyboardOnOpen) && Objects.equals(cropImage, that.cropImage);
}
@Override
public int hashCode() {
- return Objects.hash(name, format, defaultValue, minimum, maximum, details, categories, realPosition, id, visible, externalDbId, traitDataSource, additionalInfo, observationLevelNames, closeKeyboardOnOpen);
+ return Objects.hash(name, format, defaultValue, minimum, maximum, details, categories, realPosition, id, visible, externalDbId, traitDataSource, additionalInfo, observationLevelNames, closeKeyboardOnOpen, cropImage);
}
public TraitObject clone() {
@@ -215,6 +216,7 @@ public TraitObject clone() {
t.setAdditionalInfo(this.additionalInfo);
t.setObservationLevelNames(this.observationLevelNames);
t.setCloseKeyboardOnOpen(this.closeKeyboardOnOpen);
+ t.setCropImage(this.cropImage);
return t;
}
@@ -226,4 +228,12 @@ public Boolean getCloseKeyboardOnOpen() {
public void setCloseKeyboardOnOpen(Boolean closeKeyboardOnOpen) {
this.closeKeyboardOnOpen = closeKeyboardOnOpen;
}
+
+ public Boolean getCropImage() {
+ return cropImage;
+ }
+
+ public void setCropImage(Boolean cropImage) {
+ this.cropImage = cropImage;
+ }
}
\ No newline at end of file
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..a92b495d2 100644
--- a/app/src/main/java/com/fieldbook/tracker/preferences/GeneralKeys.java
+++ b/app/src/main/java/com/fieldbook/tracker/preferences/GeneralKeys.java
@@ -41,7 +41,6 @@ public class GeneralKeys {
// General
public static final String TUTORIAL_MODE = "Tips";
public static final String NEXT_ENTRY_NO_DATA = "NextEmptyPlot";
- public static final String QUICK_GOTO = "QuickGoTo";
public static final String MOVE_TO_UNIQUE_ID = "com.fieldbook.tracker.MOVE_TO_UNIQUE_ID";
public static final String DATAGRID_SETTING = "DataGrid";
public static final String HIDE_ENTRIES_WITH_DATA = "com.fieldbook.tracker.HIDE_ENTRIES";
@@ -278,6 +277,16 @@ public class GeneralKeys {
@NotNull
public static final String RESET_PREFERENCES = "RESET_PREFERENCES";
+ /**
+ * Function that returns the key for the crop coordinates of a trait
+ * @param traitId --the internal db id of the trait
+ * @return key used in preferences to obtain the (tl, tr, br, bl) coordinates used for cropping images
+ */
+ @NotNull
+ public static String getCropCoordinatesKey(int traitId) {
+ return "com.fieldbook.tracker.crop_coordinates." + traitId;
+ }
+
private GeneralKeys() {
}
diff --git a/app/src/main/java/com/fieldbook/tracker/traits/AbstractCameraTrait.kt b/app/src/main/java/com/fieldbook/tracker/traits/AbstractCameraTrait.kt
index b28685a1b..b192857ec 100644
--- a/app/src/main/java/com/fieldbook/tracker/traits/AbstractCameraTrait.kt
+++ b/app/src/main/java/com/fieldbook/tracker/traits/AbstractCameraTrait.kt
@@ -39,6 +39,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import org.threeten.bp.OffsetDateTime
import java.io.File
@@ -164,6 +165,16 @@ abstract class AbstractCameraTrait :
}
}
+ protected fun isCropRequired() = (currentTrait?.cropImage ?: false)
+
+ protected fun isCropExist() = (preferences.getString(GeneralKeys.getCropCoordinatesKey(currentTrait?.id?.toInt() ?: -1), "") ?: "").isNotEmpty()
+
+ protected fun requestCropDefinition(traitId: String, imageUri: Uri) {
+
+ (context as CollectActivity).showCropDialog(traitId, imageUri)
+
+ }
+
protected fun saveJpegToStorage(
format: String,
data: ByteArray,
@@ -262,7 +273,7 @@ abstract class AbstractCameraTrait :
(controller.getContext() as CollectActivity).person,
timestamp,
database.getStudyById(studyId),
- database.getObservationUnitById(currentRange.plot_id),
+ database.getObservationUnitById(currentRange.uniqueId),
database.getObservationVariableById(currentTrait.id),
file.uri,
controller.getRotationRelativeToDevice()
@@ -278,7 +289,7 @@ abstract class AbstractCameraTrait :
saver: (Uri) -> Unit
) {
- val plot = obsUnit.plot_id
+ val plot = obsUnit.uniqueId
val studyId = collectActivity.studyId
val person = (activity as? CollectActivity)?.person
val location = (activity as? CollectActivity)?.locationByPreferences
@@ -318,7 +329,7 @@ abstract class AbstractCameraTrait :
writeExif(file, studyId, saveTime)
- notifyItemInserted()
+ notifyItemInserted(file.uri)
}
}
@@ -338,7 +349,7 @@ abstract class AbstractCameraTrait :
writeExif(file, studyId, saveTime)
- notifyItemInserted()
+ notifyItemInserted(file.uri)
} else {
@@ -352,17 +363,52 @@ abstract class AbstractCameraTrait :
}
}
- private fun notifyItemInserted() {
+ private fun notifyItemInserted(uri: Uri) {
ui.launch {
- loadAdapterItems()
+ if (isCropRequired()) {
+
+ if (isCropExist()) {
+
+ //get bitmap from uri, create new bitmap from preference roi and update uri to database
+ val cropRect = preferences.getString(GeneralKeys.getCropCoordinatesKey(currentTrait.id.toInt()), "") ?: ""
+
+ withContext(Dispatchers.IO) {
+
+ //crop bmp
+ val croppedBmp = BitmapLoader.cropBitmap(context, uri, cropRect)
+
+ //save cropped bmp to uri
+ context.contentResolver.openOutputStream(uri)?.use { output ->
+ croppedBmp.compress(Bitmap.CompressFormat.JPEG, 80, output)
+ }
+
+ withContext(Dispatchers.Main) {
+ loadItems()
+ }
+ }
+
+ } else {
+
+ requestCropDefinition(currentTrait.id, uri)
+ }
+
+ } else {
- // update trait status as observation was saved
- (context as CollectActivity).updateCurrentTraitStatus(true)
+ loadItems()
+ }
}
}
+ private fun loadItems() {
+
+ loadAdapterItems()
+
+ // update trait status as observation was saved
+ (context as CollectActivity).updateCurrentTraitStatus(true)
+ }
+
private fun getSelectedImage(): ImageAdapter.Model? {
//check if its external or no preview
@@ -574,7 +620,7 @@ abstract class AbstractCameraTrait :
private fun insertNa() {
database.insertObservation(
- currentRange.plot_id,
+ currentRange.uniqueId,
currentTrait.id,
currentTrait.format,
"NA",
@@ -599,7 +645,7 @@ abstract class AbstractCameraTrait :
val studyId = prefs.getInt(GeneralKeys.SELECTED_FIELD_ID, 0).toString()
- val plot = currentRange.plot_id
+ val plot = currentRange.uniqueId
val traitDbId = currentTrait.id
diff --git a/app/src/main/java/com/fieldbook/tracker/traits/AudioTraitLayout.java b/app/src/main/java/com/fieldbook/tracker/traits/AudioTraitLayout.java
index 900d7ea88..d6201f064 100644
--- a/app/src/main/java/com/fieldbook/tracker/traits/AudioTraitLayout.java
+++ b/app/src/main/java/com/fieldbook/tracker/traits/AudioTraitLayout.java
@@ -2,16 +2,20 @@
import android.app.Activity;
import android.content.Context;
+import android.media.MediaMetadataRetriever;
import android.media.MediaPlayer;
-import android.media.MediaRecorder;
import android.net.Uri;
import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;
+import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
+import androidx.cardview.widget.CardView;
import androidx.documentfile.provider.DocumentFile;
import com.fieldbook.tracker.R;
@@ -20,16 +24,24 @@
import com.fieldbook.tracker.utilities.FieldAudioHelper;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
public class AudioTraitLayout extends BaseTraitLayout {
static public String type = "audio";
-
- private MediaRecorder mediaRecorder;
private MediaPlayer mediaPlayer;
private Uri recordingLocation;
private FloatingActionButton controlButton;
private ButtonState buttonState;
- private TextView audioRecordingText;
+
+ private CardView audioInfoCard;
+ private LinearLayout fileMetadataLayout;
+ private TextView fileNameText;
+ private TextView fileTimestamp;
+ private TextView fileDuration;
+ private TextView fileSize;
public AudioTraitLayout(Context context) {
super(context);
@@ -45,7 +57,13 @@ public AudioTraitLayout(Context context, AttributeSet attrs, int defStyleAttr) {
@Override
public void setNaTraitsText() {
- audioRecordingText.setText("NA");
+ // delete previously saved audio if it exists
+ deleteRecording();
+ recordingLocation = null;
+ mediaPlayer = null;
+ refreshButtonState();
+
+ setNAAudioInfoCard();
}
@Override
@@ -60,17 +78,24 @@ public int layoutId() {
@Override
public void init(Activity act) {
- audioRecordingText = act.findViewById(R.id.audioRecordingText);
+ audioInfoCard = act.findViewById(R.id.audio_info_card);
+ fileMetadataLayout = act.findViewById(R.id.file_metadata_layout);
+ fileNameText = act.findViewById(R.id.file_name_text);
+ fileTimestamp = act.findViewById(R.id.file_timestamp);
+ fileDuration = act.findViewById(R.id.file_duration);
+ fileSize = act.findViewById(R.id.file_size);
+
buttonState = ButtonState.WAITING_FOR_RECORDING;
controlButton = act.findViewById(R.id.record);
controlButton.setOnClickListener(new AudioTraitOnClickListener());
controlButton.requestFocus();
- audioRecordingText.setOnLongClickListener( view -> {
+ audioInfoCard.setOnLongClickListener( view -> {
+ ((CollectActivity) getContext()).showObservationMetadataDialog();
// handle the long click when some audio was saved
- if(audioRecordingText.getText().toString().equals(getContext().getString(R.string.trait_layout_data_stored))){
- ((CollectActivity) getContext()).showObservationMetadataDialog();
- }
+// if(fileNameText.getText().toString().equals(getContext().getString(R.string.trait_layout_data_stored))){
+// ((CollectActivity) getContext()).showObservationMetadataDialog();
+// }
return true;
});
}
@@ -81,14 +106,14 @@ public void afterLoadExists(CollectActivity act, String value) {
if (value != null && value.equals("NA")) {
buttonState = ButtonState.WAITING_FOR_RECORDING;
controlButton.setImageResource(buttonState.getImageId());
- audioRecordingText.setText("NA");
+ setNAAudioInfoCard();
} else {
DocumentFile file = DocumentFile.fromSingleUri(getContext(), Uri.parse(value));
if (file != null && file.exists()) {
this.recordingLocation = file.getUri();
buttonState = ButtonState.WAITING_FOR_PLAYBACK;
controlButton.setImageResource(buttonState.getImageId());
- audioRecordingText.setText(getContext().getString(R.string.trait_layout_data_stored));
+ setAudioInfoCard(file);
} else {
deleteTraitListener();
}
@@ -100,7 +125,7 @@ public void afterLoadNotExists(CollectActivity act) {
super.afterLoadNotExists(act);
buttonState = ButtonState.WAITING_FOR_RECORDING;
controlButton.setImageResource(buttonState.getImageId());
- audioRecordingText.setText("");
+ hideAudioInfoCard();
}
@Override
@@ -122,13 +147,13 @@ public void refreshLayout(Boolean onNew) {
private void refreshButtonState() {
ObservationModel model = getCurrentObservation();
- if (model != null && !model.getValue().isEmpty()) {
+ if (model != null && !model.getValue().isEmpty() && !model.getValue().equals("NA")) {
buttonState = ButtonState.WAITING_FOR_PLAYBACK;
controlButton.setImageResource(buttonState.getImageId());
- } else {
+ } else { // for NA, change the button state to WAITING_FOR_RECORDING
buttonState = ButtonState.WAITING_FOR_RECORDING;
controlButton.setImageResource(buttonState.getImageId());
- audioRecordingText.setText("");
+ fileNameText.setText("");
getCollectInputView().setText("");
}
}
@@ -140,8 +165,8 @@ public void deleteTraitListener() {
super.deleteTraitListener();
recordingLocation = null;
mediaPlayer = null;
- mediaRecorder = null;
refreshButtonState();
+ hideAudioInfoCard();
}
// Delete recording
@@ -155,6 +180,67 @@ private void deleteRecording() {
}
}
+ private void setNAAudioInfoCard() {
+ audioInfoCard.setVisibility(View.VISIBLE);
+ fileMetadataLayout.setVisibility(View.GONE);
+
+ fileNameText.setText("NA");
+ fileNameText.setGravity(Gravity.CENTER);
+ }
+
+ private void setAudioInfoCard(DocumentFile file) {
+ audioInfoCard.setVisibility(View.VISIBLE);
+ fileMetadataLayout.setVisibility(View.VISIBLE);
+
+ fileNameText.setGravity(Gravity.START);
+ fileNameText.setText(getContext().getString(R.string.trait_audio_placeholder_filename));
+
+ fileTimestamp.setText(formatDateTime(file.lastModified()));
+ fileSize.setText(getFileSize(file.length()));
+ fileDuration.setText(getAudioDuration(file.getUri()));
+ }
+
+ private void hideAudioInfoCard() {
+ audioInfoCard.setVisibility(View.GONE);
+ }
+
+ private String getAudioDuration(Uri uri) {
+ try (MediaMetadataRetriever retriever = new MediaMetadataRetriever()) {
+ retriever.setDataSource(getContext(), uri);
+ String time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
+
+ if (time == null) {
+ Log.w("AudioTraitLayout", "Could not extract file duration");
+ return "00:00";
+ }
+
+
+ long timeInMillis = Long.parseLong(time);
+
+ // ceil any fraction of a second to the next second
+ long totalSeconds = (long) Math.ceil(timeInMillis / 1000.0);
+ long minutes = totalSeconds / 60;
+ long seconds = totalSeconds % 60;
+
+ return String.format("%02d:%02d", minutes, seconds);
+ } catch (Exception e) {
+ Log.e("AudioTraitLayout", "Error getting file duration", e);
+ return "00:00";
+ }
+ }
+
+ private String getFileSize(long bytes) {
+ if (bytes < 1024) return bytes + " B";
+ int exponent = (int) (Math.log(bytes) / Math.log(1024));
+ String prefix = "KMGTPE".charAt(exponent-1) + "";
+ // bytes / 1024^(exponent)
+ return String.format("%.2f %sB", bytes / Math.pow(1024, exponent), prefix);
+ }
+
+ private String formatDateTime(long timestamp) {
+ return new SimpleDateFormat("MMM d, yyyy | h:mm a", Locale.getDefault()).format(new Date(timestamp));
+ }
+
public boolean isAudioRecording(){
return buttonState == ButtonState.RECORDING;
}
@@ -188,7 +274,7 @@ public class AudioTraitOnClickListener implements OnClickListener {
@Override
public void onClick(View view) {
- ((CollectActivity) getContext()).setNewTraits(getDatabase().getUserDetail(getCurrentRange().plot_id));
+ ((CollectActivity) getContext()).setNewTraits(getDatabase().getUserDetail(getCurrentRange().uniqueId));
boolean enableNavigation = true;
switch (buttonState) {
@@ -264,7 +350,7 @@ private void stopPlayback() {
private void startRecording() {
try {
removeTrait(getCurrentTrait());
- audioRecordingText.setText("");
+ hideAudioInfoCard();
fieldAudioHelper.startRecording(false);
} catch (Exception e) {
e.printStackTrace();
@@ -274,9 +360,18 @@ private void startRecording() {
private void stopRecording() {
try {
fieldAudioHelper.stopRecording();
- updateObservation(getCurrentTrait(), fieldAudioHelper.getRecordingLocation().toString());
- audioRecordingText.setText(getContext().getString(R.string.trait_layout_data_stored));
- getCollectInputView().setText(fieldAudioHelper.getRecordingLocation().toString());
+ Uri audioUri = fieldAudioHelper.getRecordingLocation();
+ if (audioUri != null) {
+ updateObservation(getCurrentTrait(), audioUri.toString());
+ getCollectInputView().setText(audioUri.toString());
+
+ // update recordingLocation
+ DocumentFile file = DocumentFile.fromSingleUri(getContext(), audioUri);
+ if (file != null && file.exists()) {
+ AudioTraitLayout.this.recordingLocation = file.getUri();
+ setAudioInfoCard(file);
+ }
+ }
} catch (Exception e) {
e.printStackTrace();
}
diff --git a/app/src/main/java/com/fieldbook/tracker/traits/BaseTraitLayout.java b/app/src/main/java/com/fieldbook/tracker/traits/BaseTraitLayout.java
index 5bbba7886..1ada0c81d 100644
--- a/app/src/main/java/com/fieldbook/tracker/traits/BaseTraitLayout.java
+++ b/app/src/main/java/com/fieldbook/tracker/traits/BaseTraitLayout.java
@@ -182,6 +182,8 @@ private void checkDefaultValue() {
}
public void afterLoadExists(CollectActivity act, @Nullable String value) {
+ getCollectInputView().markObservationSaved();
+ getCollectInputView().setTextColor(Color.parseColor(getDisplayColor()));
//lock data if frozen or locked state
isLocked = act.isFrozen() || act.isLocked();
}
@@ -311,6 +313,8 @@ protected void toggleVisibility(int visibility) {
*/
public void updateObservation(TraitObject trait, String value) {
((CollectActivity) getContext()).updateObservation(trait, value, null);
+
+ setCurrentValueAsEdited();
}
public void removeTrait(TraitObject trait) {
@@ -338,4 +342,9 @@ protected ObservationModel getCurrentObservation() {
}
protected DataHelper getDatabase() { return controller.getDatabase(); }
+
+ protected void setCurrentValueAsEdited() {
+ getCollectInputView().markObservationEdited();
+ getCollectInputView().setTextColor(getTextColor());
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/fieldbook/tracker/traits/CategoricalTraitLayout.java b/app/src/main/java/com/fieldbook/tracker/traits/CategoricalTraitLayout.java
index c0307a478..45a5429ac 100644
--- a/app/src/main/java/com/fieldbook/tracker/traits/CategoricalTraitLayout.java
+++ b/app/src/main/java/com/fieldbook/tracker/traits/CategoricalTraitLayout.java
@@ -131,8 +131,6 @@ public void afterLoadExists(CollectActivity act, @Nullable String value) {
}
- getCollectInputView().setTextColor(Color.parseColor(getDisplayColor()));
-
}
}
diff --git a/app/src/main/java/com/fieldbook/tracker/traits/DateTraitLayout.java b/app/src/main/java/com/fieldbook/tracker/traits/DateTraitLayout.java
index d2a76c1f6..58cdf5b8c 100644
--- a/app/src/main/java/com/fieldbook/tracker/traits/DateTraitLayout.java
+++ b/app/src/main/java/com/fieldbook/tracker/traits/DateTraitLayout.java
@@ -6,6 +6,7 @@
import android.util.AttributeSet;
import android.util.Log;
import android.widget.ImageButton;
+import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -32,6 +33,7 @@ public class DateTraitLayout extends BaseTraitLayout {
FloatingActionButton addDayBtn;
FloatingActionButton minusDayBtn;
FloatingActionButton saveDayBtn;
+ private TextView datePreviewText;
private String date;
private boolean isBlocked = true; //tracks when multi measures can be navigated
private boolean isFirstLoad = true;
@@ -51,8 +53,6 @@ public DateTraitLayout(Context context, AttributeSet attrs, int defStyleAttr) {
@Override
public void setNaTraitsText() {
getCollectInputView().setText("NA");
- //issue 413 apply the date saved preference color to NA values
- forceDataSavedColor();
}
@Override
@@ -86,6 +86,9 @@ public void init(Activity act) {
addDayBtn = act.findViewById(R.id.addDateBtn);
minusDayBtn = act.findViewById(R.id.minusDateBtn);
saveDayBtn = act.findViewById(R.id.enterBtn);
+ datePreviewText = act.findViewById(R.id.datePreviewText);
+
+ updatePreviewDate(calendar);
ImageButton calendarVisibilityBtn = act.findViewById(R.id.trait_date_calendar_visibility_btn);
@@ -102,10 +105,9 @@ public void init(Activity act) {
calendar.set(y, m, d);
- updateViewDate(calendar);
+ updatePreviewDate(calendar);
- //this saves the date, so update text to display color
- forceDataSavedColor();
+ updateViewDate(calendar);
String rep = ((CollectActivity) getContext()).getRep();
@@ -141,9 +143,9 @@ public void init(Activity act) {
// Add day
calendar.add(Calendar.DATE, 1);
- updateViewDate(calendar);
+ updatePreviewDate(calendar);
- isBlocked = true;
+ isBlocked = false;
});
// Minus day
@@ -163,9 +165,9 @@ public void init(Activity act) {
//Subtract day, rewrite date
calendar.add(Calendar.DATE, -1);
- updateViewDate(calendar);
+ updatePreviewDate(calendar);
- isBlocked = true;
+ isBlocked = false;
});
// Saving date data
@@ -182,18 +184,14 @@ public void init(Activity act) {
e.printStackTrace();
}
- if (!getCollectInputView().getText().equals("NA")) { //issue 413, don't update NA when save button is pressed
- if (getPrefs().getBoolean(GeneralKeys.USE_DAY_OF_YEAR, false)) {
- updateObservation(getCurrentTrait(), String.valueOf(calendar.get(Calendar.DAY_OF_YEAR)));
- } else {
- updateObservation(getCurrentTrait(), dateFormat.format(calendar.getTime()));
- }
- }
+ String previewText = datePreviewText.getText().toString();
+ getCollectInputView().setText(previewText);
- parseDateAndView();
-
- // Change the text color accordingly
- forceDataSavedColor();
+ if (getPrefs().getBoolean(GeneralKeys.USE_DAY_OF_YEAR, false)) {
+ updateObservation(getCurrentTrait(), String.valueOf(calendar.get(Calendar.DAY_OF_YEAR)));
+ } else {
+ updateObservation(getCurrentTrait(), dateFormat.format(calendar.getTime()));
+ }
isBlocked = false;
});
@@ -208,6 +206,13 @@ private String getTtsFromCalendar(Calendar calendar) {
return month + " " + day;
}
+ private void updatePreviewDate(Calendar calendar) {
+ date = dateFormat.format(calendar.getTime());
+ log();
+ setDatePreviewText(getMonthForInt(calendar.get(Calendar.MONTH)),
+ String.format(Locale.getDefault(),"%02d", calendar.get(Calendar.DAY_OF_MONTH)));
+ }
+
private void updateViewDate(Calendar calendar) {
date = dateFormat.format(calendar.getTime());
@@ -221,14 +226,8 @@ private void updateViewDate(Calendar calendar) {
.putString(GeneralKeys.CALENDAR_LAST_SAVED_DATE, yearText + "-" + monthText + "-" + dayOfMonth)
.apply();
- getCollectInputView().setText(getMonthForInt(calendar.get(Calendar.MONTH)) + " " + dayOfMonth);
-
- // Change text color
- if (getNewTraits().containsKey(getCurrentTrait().getName())) {
- getCollectInputView().setTextColor(getValueAlteredColor());
- } else {
- getCollectInputView().setTextColor(getTextColor());
- }
+ setDateText(getMonthForInt(calendar.get(Calendar.MONTH)),
+ String.format(Locale.getDefault(),"%02d", calendar.get(Calendar.DAY_OF_MONTH)));
}
private void log() {
@@ -353,19 +352,12 @@ public void afterLoadExists(CollectActivity act, @Nullable String value) {
public void afterLoadNotExists(CollectActivity act) {
super.afterLoadNotExists(act);
- getCollectInputView().setTextColor(Color.BLACK);
-
//if data does not exist, use the current date as a default value
final Calendar c = Calendar.getInstance();
- date = dateFormat.format(c.getTime());
- log();
-
- parseDateAndView();
+ updatePreviewDate(c);
isBlocked = true;
-
- setDateText(getMonthForInt(c.get(Calendar.MONTH)), String.format(Locale.getDefault(), "%02d", c.get(Calendar.DAY_OF_MONTH)) );
}
@Override
@@ -380,15 +372,8 @@ public void deleteTraitListener() {
parseDateAndView();
- } else {
-
- final Calendar c = Calendar.getInstance();
- date = dateFormat.format(c.getTime());
-
- getCollectInputView().setTextColor(getTextColor());
-
- //This is used to persist moving between months
- setDateText(getMonthForInt(c.get(Calendar.MONTH)), String.format(Locale.getDefault(), "%02d", c.get(Calendar.DAY_OF_MONTH)) );
+ } else { // clear the editText
+ getCollectInputView().setText("");
}
}
@@ -407,6 +392,10 @@ public String getMonthForInt(int m) {
return month;
}
+ private void setDatePreviewText(String month, String day) {
+ datePreviewText.setText(month + " " + day);
+ }
+
private void setDateText(String month, String day) {
getCollectInputView().setText(month + " " + day);
}
diff --git a/app/src/main/java/com/fieldbook/tracker/traits/GNSSTraitLayout.kt b/app/src/main/java/com/fieldbook/tracker/traits/GNSSTraitLayout.kt
index a542e6e73..7a606aedf 100644
--- a/app/src/main/java/com/fieldbook/tracker/traits/GNSSTraitLayout.kt
+++ b/app/src/main/java/com/fieldbook/tracker/traits/GNSSTraitLayout.kt
@@ -239,7 +239,7 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener {
val studyDbId = (context as CollectActivity).studyId
val units = database.getAllObservationUnits(studyDbId.toInt())
- .filter { it.observation_unit_db_id == currentRange.plot_id }
+ .filter { it.observation_unit_db_id == currentRange.uniqueId }
if (units.isNotEmpty()) {
@@ -374,7 +374,7 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener {
val newLng = longitude.toDouble()
val units = database.getAllObservationUnits(studyDbId.toInt())
- .filter { it.observation_unit_db_id == currentRange.plot_id }
+ .filter { it.observation_unit_db_id == currentRange.uniqueId }
if (units.isNotEmpty()) {
@@ -892,14 +892,14 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener {
val studyDbId = prefs.getInt(GeneralKeys.SELECTED_FIELD_ID, 0).toString()
val observation =
- database.getObservation(studyDbId, currentRange.plot_id, currentTrait.id, rep)
+ database.getObservation(studyDbId, currentRange.uniqueId, currentTrait.id, rep)
if (observation != null) {
- database.deleteTrait(studyDbId, currentRange.plot_id, currentTrait.id, rep)
+ database.deleteTrait(studyDbId, currentRange.uniqueId, currentTrait.id, rep)
val units = controller.getDatabase().getAllObservationUnits(studyDbId.toInt())
- .filter { it.observation_unit_db_id == currentRange.plot_id }
+ .filter { it.observation_unit_db_id == currentRange.uniqueId }
if (units.isNotEmpty()) {
units.first().let { unit ->
controller.getDatabase().updateObservationUnit(unit, "")
diff --git a/app/src/main/java/com/fieldbook/tracker/traits/GoProTraitLayout.kt b/app/src/main/java/com/fieldbook/tracker/traits/GoProTraitLayout.kt
index afa3d67e4..a8fd09d89 100644
--- a/app/src/main/java/com/fieldbook/tracker/traits/GoProTraitLayout.kt
+++ b/app/src/main/java/com/fieldbook/tracker/traits/GoProTraitLayout.kt
@@ -99,7 +99,7 @@ class GoProTraitLayout :
override fun loadLayout() {
super.loadLayout()
setup()
- currentPlotId = currentRange.plot_id
+ currentPlotId = currentRange.uniqueId
}
private fun initializeConnectButton() {
@@ -223,7 +223,7 @@ class GoProTraitLayout :
private fun connect() {
- controller.advisor().withNearby { adapter ->
+ controller.advisor().withNearby { adapter: BluetoothAdapter ->
if (!adapter.isEnabled) {
diff --git a/app/src/main/java/com/fieldbook/tracker/traits/LabelPrintTraitLayout.java b/app/src/main/java/com/fieldbook/tracker/traits/LabelPrintTraitLayout.java
index 2121a35fe..b91f7f5cd 100644
--- a/app/src/main/java/com/fieldbook/tracker/traits/LabelPrintTraitLayout.java
+++ b/app/src/main/java/com/fieldbook/tracker/traits/LabelPrintTraitLayout.java
@@ -515,7 +515,7 @@ public String getValueFromSpinner(Spinner spinner, String[] options) {
} else {
int pos = spinner.getSelectedItemPosition();
if (pos < options.length) {
- String[] v = getDatabase().getDropDownRange(options[pos], getCurrentRange().plot_id);
+ String[] v = getDatabase().getDropDownRange(options[pos], getCurrentRange().uniqueId);
if (v.length != 0) {
value = v[0];
}
diff --git a/app/src/main/java/com/fieldbook/tracker/traits/LayoutCollections.java b/app/src/main/java/com/fieldbook/tracker/traits/LayoutCollections.java
index 72d53dd5b..9675d607d 100644
--- a/app/src/main/java/com/fieldbook/tracker/traits/LayoutCollections.java
+++ b/app/src/main/java/com/fieldbook/tracker/traits/LayoutCollections.java
@@ -1,9 +1,6 @@
package com.fieldbook.tracker.traits;
import android.app.Activity;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
import java.util.ArrayList;
@@ -61,6 +58,7 @@ public void deleteTraitListener(String format) {
public void setNaTraitsText(String format) {
getTraitLayout(format).setNaTraitsText();
+ getTraitLayout(format).setCurrentValueAsEdited();
}
// Deprecated - simpler to disable/enable data collection using lockOverlay instead
diff --git a/app/src/main/java/com/fieldbook/tracker/traits/MultiCatTraitLayout.java b/app/src/main/java/com/fieldbook/tracker/traits/MultiCatTraitLayout.java
index 49158d725..c37da5287 100644
--- a/app/src/main/java/com/fieldbook/tracker/traits/MultiCatTraitLayout.java
+++ b/app/src/main/java/com/fieldbook/tracker/traits/MultiCatTraitLayout.java
@@ -2,7 +2,6 @@
import android.app.Activity;
import android.content.Context;
-import android.graphics.Color;
import android.util.AttributeSet;
import android.view.ViewTreeObserver;
import android.widget.Button;
@@ -214,7 +213,6 @@ private void loadScale() {
refreshList();
refreshCategoryText();
- getCollectInputView().setTextColor(Color.parseColor(getDisplayColor()));
}
diff --git a/app/src/main/java/com/fieldbook/tracker/traits/PercentTraitLayout.java b/app/src/main/java/com/fieldbook/tracker/traits/PercentTraitLayout.java
index dfa657f3f..651dc951d 100644
--- a/app/src/main/java/com/fieldbook/tracker/traits/PercentTraitLayout.java
+++ b/app/src/main/java/com/fieldbook/tracker/traits/PercentTraitLayout.java
@@ -68,7 +68,7 @@ public void init(Activity act) {
seekListener = new SeekBar.OnSeekBarChangeListener() {
- public void onProgressChanged(SeekBar sb, int progress, boolean arg2) {
+ public void onProgressChanged(SeekBar sb, int progress, boolean fromUser) {
int minimum = 0;
if (getCurrentTrait() != null) {
try {
@@ -80,7 +80,13 @@ public void onProgressChanged(SeekBar sb, int progress, boolean arg2) {
if (sb.getProgress() < minimum)
sb.setProgress(minimum);
- setCurrentValueText(sb.getProgress(), Color.parseColor(getDisplayColor()));
+ getCollectInputView().setText(sb.getProgress() + "%");
+
+ // check if the change was from user interaction.
+ // useful when navigating across repeated values
+ if (fromUser) {
+ setCurrentValueAsEdited();
+ }
}
public void onStartTrackingTouch(SeekBar sb) {
@@ -156,16 +162,12 @@ public void afterLoadExists(CollectActivity act, String value) {
String maxString = getCurrentTrait().getMaximum();
setSeekBarMax(maxString);
- int textColor = value.equals(getDefaultValue()) ? Color.BLACK : Color.parseColor(getDisplayColor());
- setCurrentValueText(value, textColor);
-
seekBar.setOnSeekBarChangeListener(null);
setSeekBarProgress(value);
seekBar.setOnSeekBarChangeListener(seekListener);
} else if (value != null && value.equals("NA")) {
getCollectInputView().setText("NA");
- getCollectInputView().setTextColor(Color.parseColor(getDisplayColor()));
seekBar.setProgress(0);
}
}
@@ -180,7 +182,8 @@ public void afterLoadNotExists(CollectActivity act) {
@Override
public void afterLoadDefault(CollectActivity act) {
super.afterLoadDefault(act);
- updateLoadBarValue(getDefaultValue());
+ getCollectInputView().setText(getDefaultValue() + "%");
+ updateLoadBar();
}
@Override
@@ -215,11 +218,6 @@ private void updateLoadBar() {
seekBar.setOnSeekBarChangeListener(seekListener);
}
- private void updateLoadBarValue(String value) {
- setCurrentValueText(value, Color.BLACK);
- updateLoadBar();
- }
-
private String getDefaultValue() {
String defaultValue = "0";
if (getCurrentTrait().getDefaultValue() != null
@@ -229,18 +227,6 @@ private String getDefaultValue() {
return defaultValue;
}
- private void setCurrentValueText(int value, int color) {
- setCurrentValueText(String.valueOf(value), color);
- }
-
- private void setCurrentValueText(String value, int color) {
- getCollectInputView().setTextColor(color);
- if (value.isEmpty())
- getCollectInputView().setText(value);
- else
- getCollectInputView().setText(value + "%");
- }
-
@Override
public void deleteTraitListener() {
removeTrait(getCurrentTrait());
@@ -248,11 +234,15 @@ public void deleteTraitListener() {
ObservationModel model = getCurrentObservation();
seekBar.setOnSeekBarChangeListener(null);
if (model != null) {
- setCurrentValueText(model.getValue(), Color.BLACK);
- setSeekBarProgress(model.getValue());
+ getCollectInputView().setText(model.getValue() + "%");
+ if (model.getValue().equals("NA")) {
+ seekBar.setProgress(0);
+ } else {
+ setSeekBarProgress(model.getValue());
+ }
} else {
String defaultValue = getDefaultValue();
- setCurrentValueText(defaultValue, Color.BLACK);
+ getCollectInputView().setText("");
setSeekBarProgress(defaultValue);
}
diff --git a/app/src/main/java/com/fieldbook/tracker/traits/PhotoTraitLayout.kt b/app/src/main/java/com/fieldbook/tracker/traits/PhotoTraitLayout.kt
index 1607cb22b..cc3546807 100644
--- a/app/src/main/java/com/fieldbook/tracker/traits/PhotoTraitLayout.kt
+++ b/app/src/main/java/com/fieldbook/tracker/traits/PhotoTraitLayout.kt
@@ -146,7 +146,9 @@ class PhotoTraitLayout : CameraTrait {
setupCaptureUi(camera, executor, capture)
}
- } catch (_: IllegalArgumentException) {
+ } catch (e: IllegalArgumentException) {
+
+ e.printStackTrace()
}
}
@@ -191,14 +193,17 @@ class PhotoTraitLayout : CameraTrait {
controller.getCameraXFacade().bindPreview(
previewViewHolder?.previewView,
- resolution
+ resolution,
+ currentTrait.id,
+ Handler(Looper.getMainLooper())
) { camera, executor, capture ->
setupCaptureUi(camera, executor, capture)
}
- } catch (_: IllegalArgumentException) {
+ } catch (e: IllegalArgumentException) {
+ e.printStackTrace()
}
}
@@ -249,6 +254,8 @@ class PhotoTraitLayout : CameraTrait {
} else {
+ controller.getCameraXFacade().unbind()
+
previewViewHolder = null
loadAdapterItems()
@@ -325,7 +332,6 @@ class PhotoTraitLayout : CameraTrait {
}
}
-
/**
* When button is pressed, create a cached image and switch to the camera intent.
* CollectActivity will receive REQUEST_IMAGE_CAPTURE and call this layout's makeImage() method.
@@ -354,7 +360,10 @@ class PhotoTraitLayout : CameraTrait {
}
private fun launchCameraX() {
+ controller.getCameraXFacade().unbind()
val intent = Intent(context, CameraActivity::class.java)
+ //set current trait id to set crop region
+ intent.putExtra(CameraActivity.EXTRA_TRAIT_ID, currentTrait.id)
activity?.startActivityForResult(intent, PICTURE_REQUEST_CODE)
}
diff --git a/app/src/main/java/com/fieldbook/tracker/traits/TextTraitLayout.kt b/app/src/main/java/com/fieldbook/tracker/traits/TextTraitLayout.kt
index fa6fe2b01..a2ec65c12 100644
--- a/app/src/main/java/com/fieldbook/tracker/traits/TextTraitLayout.kt
+++ b/app/src/main/java/com/fieldbook/tracker/traits/TextTraitLayout.kt
@@ -3,6 +3,7 @@ package com.fieldbook.tracker.traits
import android.app.Activity
import android.content.Context
import android.graphics.Color
+import android.graphics.Typeface
import android.text.Editable
import android.text.TextWatcher
import android.util.AttributeSet
@@ -53,6 +54,8 @@ class TextTraitLayout : BaseTraitLayout {
collectInputView.text = value
updateObservation(currentTrait, value)
+
+ observationEditedStyle()
}
}
@@ -69,6 +72,7 @@ class TextTraitLayout : BaseTraitLayout {
override fun setNaTraitsText() {
inputEditText?.setText("NA")
+ observationEditedStyle()
}
override fun type(): String {
@@ -241,4 +245,10 @@ class TextTraitLayout : BaseTraitLayout {
super.refreshLock()
loadLayout()
}
+
+ private fun observationEditedStyle() {
+ inputEditText?.setTypeface(null, Typeface.ITALIC)
+
+ inputEditText?.setTextColor(textColor)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/fieldbook/tracker/traits/formats/BasePhotoFormat.kt b/app/src/main/java/com/fieldbook/tracker/traits/formats/BasePhotoFormat.kt
index 110b8aa90..c3f358d1e 100644
--- a/app/src/main/java/com/fieldbook/tracker/traits/formats/BasePhotoFormat.kt
+++ b/app/src/main/java/com/fieldbook/tracker/traits/formats/BasePhotoFormat.kt
@@ -3,6 +3,7 @@ package com.fieldbook.tracker.traits.formats
import android.content.Context
import android.view.View
import com.fieldbook.tracker.R
+import com.fieldbook.tracker.traits.formats.parameters.CropImageParameter
import com.fieldbook.tracker.traits.formats.parameters.DetailsParameter
import com.fieldbook.tracker.traits.formats.parameters.NameParameter
import com.fieldbook.tracker.traits.formats.presenters.UriPresenter
@@ -26,4 +27,5 @@ open class BasePhotoFormat(
stringNameAux = stringNameAux,
NameParameter(),
DetailsParameter(),
+ CropImageParameter()
), ValuePresenter by UriPresenter()
\ No newline at end of file
diff --git a/app/src/main/java/com/fieldbook/tracker/traits/formats/parameters/CropImageParameter.kt b/app/src/main/java/com/fieldbook/tracker/traits/formats/parameters/CropImageParameter.kt
new file mode 100644
index 000000000..65c0c2239
--- /dev/null
+++ b/app/src/main/java/com/fieldbook/tracker/traits/formats/parameters/CropImageParameter.kt
@@ -0,0 +1,55 @@
+package com.fieldbook.tracker.traits.formats.parameters
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ToggleButton
+import com.fieldbook.tracker.R
+import com.fieldbook.tracker.database.DataHelper
+import com.fieldbook.tracker.objects.TraitObject
+import com.fieldbook.tracker.traits.formats.ValidationResult
+
+class CropImageParameter(private val initialDefaultValue: Boolean? = null) :
+ BaseFormatParameter(
+ nameStringResourceId = R.string.traits_crop_image_parameter,
+ defaultLayoutId = R.layout.list_item_trait_parameter_default_toggle_value,
+ parameter = Parameters.CROP_IMAGE
+ ) {
+
+ override fun createViewHolder(
+ parent: ViewGroup,
+ ): BaseFormatParameter.ViewHolder {
+ val v = LayoutInflater.from(parent.context)
+ .inflate(R.layout.list_item_trait_parameter_default_toggle_value, parent, false)
+ return ViewHolder(v)
+ }
+
+ inner class ViewHolder(itemView: View) : BaseFormatParameter.ViewHolder(itemView) {
+
+ val defaultValueToggle =
+ itemView.findViewById(R.id.dialog_new_trait_default_toggle_btn).also {
+ initialDefaultValue?.let { value ->
+ it.isChecked = value
+ }
+ }
+
+ override fun merge(traitObject: TraitObject) = traitObject.apply {
+ cropImage = defaultValueToggle.isChecked
+ }
+
+ override fun load(traitObject: TraitObject?): Boolean {
+ try {
+ defaultValueToggle.isChecked = traitObject?.cropImage == true
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+ override fun validate(
+ database: DataHelper,
+ initialTraitObject: TraitObject?
+ ) = ValidationResult()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/fieldbook/tracker/traits/formats/parameters/Parameters.kt b/app/src/main/java/com/fieldbook/tracker/traits/formats/parameters/Parameters.kt
index 727a0ada5..bc06cf704 100644
--- a/app/src/main/java/com/fieldbook/tracker/traits/formats/parameters/Parameters.kt
+++ b/app/src/main/java/com/fieldbook/tracker/traits/formats/parameters/Parameters.kt
@@ -2,7 +2,7 @@ package com.fieldbook.tracker.traits.formats.parameters
enum class Parameters {
- FORMAT, NAME, DEFAULT_VALUE, DETAILS, MAXIMUM, MINIMUM, CATEGORIES, CAMERA, CLOSE_KEYBOARD;
+ FORMAT, NAME, DEFAULT_VALUE, DETAILS, MAXIMUM, MINIMUM, CATEGORIES, CAMERA, CLOSE_KEYBOARD, CROP_IMAGE;
companion object {
@@ -10,7 +10,8 @@ enum class Parameters {
"validValuesMin",
"validValuesMax",
"category",
- "closeKeyboardOnOpen"
+ "closeKeyboardOnOpen",
+ "cropImage",
)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/fieldbook/tracker/utilities/BitmapLoader.kt b/app/src/main/java/com/fieldbook/tracker/utilities/BitmapLoader.kt
index 33342f84f..099b6670e 100644
--- a/app/src/main/java/com/fieldbook/tracker/utilities/BitmapLoader.kt
+++ b/app/src/main/java/com/fieldbook/tracker/utilities/BitmapLoader.kt
@@ -8,6 +8,8 @@ import android.graphics.Point
import android.net.Uri
import android.provider.DocumentsContract
import com.fieldbook.tracker.R
+import kotlin.math.max
+import kotlin.math.min
/**
* Uses functions from android documentation:
@@ -90,5 +92,44 @@ class BitmapLoader {
} ?: Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
}
}
+
+ fun cropBitmap(context: Context?, imageUri: Uri, rectCoordinates: String): Bitmap {
+
+ val (tlx, tly, blx, bly) = rectCoordinates.split(",").map { it.toFloat() }
+
+ //load bmp from uri
+ var bmp = BitmapFactory.decodeStream(context?.contentResolver?.openInputStream(imageUri))
+
+ val (h, w) = if (bmp.width > bmp.height) (bmp.width to bmp.height) else (bmp.height to bmp.width)
+
+ if (h != bmp.height) {
+ //rotate the bitmap 90
+ val matrix = android.graphics.Matrix()
+ matrix.postRotate(90f)
+ bmp = Bitmap.createBitmap(bmp, 0, 0, bmp.width, bmp.height, matrix, true)
+ }
+
+ //convert normalized coordinates to relative image coordinates
+ val rtlx = min(max((tlx * w).toInt(), 0), w)
+ val rtly = min(max((tly * h).toInt(), 0), h)
+ val rblx = min(max((blx * w).toInt(), 0), w)
+ val rbly = min(max((bly * h).toInt(), 0), h)
+
+ var iw = max(0, min(w, rblx - rtlx))
+ var ih = max(0, min(h, rbly - rtly))
+
+ if (iw + rtlx > w) {
+ iw = w
+ }
+
+ if (ih + rtly > h) {
+ ih = h
+ }
+
+ //crop bmp
+ val croppedBmp = Bitmap.createBitmap(bmp, rtlx, rtly, iw, ih)
+
+ return croppedBmp
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/fieldbook/tracker/utilities/CameraXFacade.kt b/app/src/main/java/com/fieldbook/tracker/utilities/CameraXFacade.kt
index 60eb9e017..5dd6de031 100644
--- a/app/src/main/java/com/fieldbook/tracker/utilities/CameraXFacade.kt
+++ b/app/src/main/java/com/fieldbook/tracker/utilities/CameraXFacade.kt
@@ -1,26 +1,43 @@
package com.fieldbook.tracker.utilities
import android.content.Context
-import android.content.res.Configuration
+import android.graphics.Color
import android.graphics.ImageFormat
+import android.graphics.Matrix
+import android.graphics.PorterDuff
+import android.graphics.Rect
+import android.graphics.RectF
+import android.graphics.Region
import android.hardware.camera2.CameraCharacteristics
+import android.os.Build
+import android.os.Handler
import android.util.Log
import android.util.Size
+import android.util.TypedValue
import androidx.annotation.OptIn
import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
import androidx.camera.core.AspectRatio
import androidx.camera.core.Camera
+import androidx.camera.core.CameraEffect.PREVIEW
+import androidx.camera.core.CameraEffect.IMAGE_CAPTURE
+import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
+import androidx.camera.core.UseCaseGroup
import androidx.camera.core.resolutionselector.AspectRatioStrategy
import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.core.resolutionselector.ResolutionStrategy
+import androidx.camera.effects.OverlayEffect
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
+import com.fieldbook.tracker.R
+import com.fieldbook.tracker.activities.ThemedActivity
+import com.fieldbook.tracker.preferences.GeneralKeys
+import com.fieldbook.tracker.views.CropImageView
import dagger.hilt.android.qualifiers.ActivityContext
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@@ -50,6 +67,7 @@ class CameraXFacade @Inject constructor(@ActivityContext private val context: Co
}
fun await(context: Context, onBindReady: () -> Unit) {
+ cameraXInstance.get().unbind()
cameraXInstance.addListener({
onBindReady()
}, ContextCompat.getMainExecutor(context))
@@ -141,9 +159,13 @@ class CameraXFacade @Inject constructor(@ActivityContext private val context: Co
}
}
+ private var cropEffect: OverlayEffect? = null
+
fun bindPreview(
previewView: PreviewView?,
targetResolution: Size?,
+ traitId: String? = null,
+ handler: Handler,
onBind: (Camera, ExecutorService, ImageCapture) -> Unit
) {
@@ -174,13 +196,98 @@ class CameraXFacade @Inject constructor(@ActivityContext private val context: Co
val p = prevBuilder.build()
- p.setSurfaceProvider(previewView?.surfaceProvider)
+ p.surfaceProvider = previewView?.surfaceProvider
+
+ //close previous crop effect or else EGL leak
+ cropEffect?.close()
+
+ //create an overlay effect to draw a rectangle on the preview
+ cropEffect = OverlayEffect(VIDEO_CAPTURE or PREVIEW or IMAGE_CAPTURE, 0, handler) { e ->
+ Log.e(TAG, "OverlayEffect error: $e")
+ }
+
+ //parse crop from prefs
+ val cropCoordinates = (context as? ThemedActivity)?.prefs?.getString(GeneralKeys.getCropCoordinatesKey(
+ traitId?.toInt() ?: -1), "")
+
+ //overlay effect gives a frame that has an 'overlayCanvas'
+ cropEffect?.setOnDrawListener { frame ->
+ try {
+ //check that coordinates are correct, parse and draw the rect
+ if (!cropCoordinates.isNullOrEmpty() && cropCoordinates != CropImageView.DEFAULT_CROP_COORDINATES) {
+ //convert the camera sensor coordinates to local preview view coordinates
+ val sensorToUi = previewView!!.sensorToViewTransform
+ if (sensorToUi != null) {
+ val sensorToEffect = frame.sensorToBufferTransform
+ val uiToSensor = Matrix()
+ sensorToUi.invert(uiToSensor)
+ uiToSensor.postConcat(sensorToEffect)
+
+ //get canvas and clear color, apply affine transformation
+ val canvas = frame.overlayCanvas
+ canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
+ canvas.setMatrix(uiToSensor)
+
+ val rect = CropImageView.parseRectCoordinates(cropCoordinates)
+ //check if rect is null or matches the default
+ if (rect != null) {
+ //draw a rectangle on the left of the canvas
+ //converts normalized coordinates to previewView-relative
+ val left = rect.left * previewView.width
+ val top = (rect.top * previewView.height) - 8f
+ val right = rect.right * previewView.width
+ val bottom = (rect.bottom * previewView.height) + 8f
+
+ //newer APIs allow clipping a rect in two commands, earlier is a little more complex
+ val clipRect = RectF(left, top, right, bottom)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ canvas.clipOutRect(clipRect)
+ } else {
+ canvas.clipRect(clipRect, Region.Op.DIFFERENCE)
+ }
+
+ val typedValue = TypedValue()
+ context.theme?.resolveAttribute(
+ R.attr.fb_inverse_crop_region_color,
+ typedValue,
+ true
+ )
+
+ canvas.drawARGB(
+ Color.alpha(typedValue.data),
+ Color.red(typedValue.data),
+ Color.green(typedValue.data),
+ Color.blue(typedValue.data)
+ )
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ canvas.clipRect(
+ Rect(0, 0, previewView.width, previewView.height),
+ Region.Op.REPLACE
+ )
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ //skip invalid region exceptions
+ if ("Region.Op" !in e.message.orEmpty())
+ e.printStackTrace()
+ }
+
+ true
+ }
+
+ val useCaseGroup = UseCaseGroup.Builder()
+ .addUseCase(p)
+ .addUseCase(imageCapture)
+ .addEffect(cropEffect!!)
+ .build()
val camera = cameraXInstance.get().bindToLifecycle(
context as LifecycleOwner,
frontSelector,
- p,
- imageCapture
+ useCaseGroup
)
Log.d(TAG, "Camera lifecycle bound: ${camera.cameraInfo}")
diff --git a/app/src/main/java/com/fieldbook/tracker/utilities/FieldAudioHelper.kt b/app/src/main/java/com/fieldbook/tracker/utilities/FieldAudioHelper.kt
index 74174ef8b..b0d2c7961 100644
--- a/app/src/main/java/com/fieldbook/tracker/utilities/FieldAudioHelper.kt
+++ b/app/src/main/java/com/fieldbook/tracker/utilities/FieldAudioHelper.kt
@@ -129,7 +129,7 @@ class FieldAudioHelper @Inject constructor(@ActivityContext private val context:
paths.add(fullGeoNavFile)
paths.add(traitsDocumentFile)
- val mGeneratedName = "field_audio_log" + context.cRange.plot_id + "_" + fieldAlias + " " + timeStamp.format(c.time) + ".zip"
+ val mGeneratedName = "field_audio_log" + context.cRange.uniqueId + "_" + fieldAlias + " " + timeStamp.format(c.time) + ".zip"
val exportDir = getDirectory(context, R.string.dir_field_export)
val zipFile = exportDir?.createFile("*/*", mGeneratedName)
@@ -207,10 +207,10 @@ class FieldAudioHelper @Inject constructor(@ActivityContext private val context:
val mGeneratedName: String
val fieldAlias = preferences.getString(GeneralKeys.FIELD_FILE, "")
mGeneratedName = try {
- if (isFieldAudio) "field_audio_" + (context as CollectActivity).cRange.plot_id + "_" + fieldAlias + " " + timeStamp.format(
+ if (isFieldAudio) "field_audio_" + (context as CollectActivity).cRange.uniqueId + "_" + fieldAlias + " " + timeStamp.format(
c.time
)
- else (context as CollectActivity).cRange.plot_id + " " + timeStamp.format(c.time)
+ else (context as CollectActivity).cRange.uniqueId + " " + timeStamp.format(c.time)
} catch (e: Exception) {
"error " + timeStamp.format(c.time)
}
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..4472d5bd0 100644
--- a/app/src/main/java/com/fieldbook/tracker/utilities/FieldSwitchImpl.kt
+++ b/app/src/main/java/com/fieldbook/tracker/utilities/FieldSwitchImpl.kt
@@ -1,6 +1,7 @@
package com.fieldbook.tracker.utilities
import android.content.Context
+import android.util.Log
import androidx.preference.PreferenceManager
import com.fieldbook.tracker.database.DataHelper
import com.fieldbook.tracker.interfaces.FieldSwitcher
@@ -15,6 +16,12 @@ import javax.inject.Inject
*/
class FieldSwitchImpl @Inject constructor(@ActivityContext private val context: Context): FieldSwitcher {
+ companion object {
+ private const val TAG = "FieldSwitchImpl"
+ private val POSSIBLE_COLUMN_IDS = listOf("col", "column", "column_id", "range")
+ private val POSSIBLE_ROW_IDS = listOf("row", "row_id")
+ }
+
@Inject
lateinit var database: DataHelper
@@ -36,14 +43,59 @@ class FieldSwitchImpl @Inject constructor(@ActivityContext private val context:
database.switchField(field.exp_id)
+ //get all entry props from field
+ val entryProps = database.getAllObservationUnitAttributeNames(field.exp_id).toMutableList()
+
+ //remove unique id as a choice for the initial primary/secondary ids
+ val uniqueId = field.unique_id
+ entryProps.remove(uniqueId)
+
+ //attempt to automatically select based on previous selections
+ val primaryName = preferences.getString(GeneralKeys.PRIMARY_NAME, "")
+ val secondaryName = preferences.getString(GeneralKeys.SECONDARY_NAME, "")
+
+ //add some basic logic to match row/col or block/rep if it exists, otherwise just use the first two
+ val hasPrimary = entryProps.indexOfFirst { it.equals(primaryName, true) }
+ val hasRow = entryProps.indexOfFirst { it.lowercase() in POSSIBLE_ROW_IDS }
+ val hasRange = entryProps.indexOfFirst { it.equals("range", true) }
+ val hasBlock = entryProps.indexOfFirst { it.equals("block", true) }
+
+ val primary = if (field.primary_id == "null" || field.primary_id == null || field.primary_id.isEmpty()) {
+ if (hasPrimary != -1) {
+ entryProps.removeAt(hasPrimary)
+ } else if (hasRow != -1) {
+ entryProps.removeAt(hasRow)
+ } else if (hasRange != -1) {
+ entryProps.removeAt(hasRange)
+ } else if (hasBlock != -1) {
+ entryProps.removeAt(hasBlock)
+ } else if (entryProps.isNotEmpty()) entryProps.removeAt(0) else ""
+ } else field.primary_id
+
+ val hasSecondary = entryProps.indexOfFirst { it.equals(secondaryName, true) }
+ val hasCol = entryProps.indexOfFirst { it.lowercase() in POSSIBLE_COLUMN_IDS }
+ val hasPlot = entryProps.indexOfFirst { it.equals("plot", true) }
+
+ val secondary = if (field.secondary_id == "null" || field.secondary_id == null || field.secondary_id.isEmpty()) {
+ if (hasSecondary != -1) {
+ entryProps.removeAt(hasSecondary)
+ } else if (hasCol != -1) {
+ entryProps.removeAt(hasCol)
+ } else if (hasPlot != -1) {
+ entryProps.removeAt(hasPlot)
+ } else if (entryProps.isNotEmpty()) entryProps.removeAt(0) else ""
+ } else field.secondary_id
+
+ Log.d(TAG, "Field Switched: ${field.exp_id}\tUnique: $uniqueId\tPrimary: $primary\tSecondary: $secondary")
+
//clear field selection after updates
preferences.edit().putInt(GeneralKeys.SELECTED_FIELD_ID, field.exp_id)
.putString(GeneralKeys.FIELD_FILE, field.exp_name)
.putString(GeneralKeys.FIELD_ALIAS, field.exp_alias)
.putString(GeneralKeys.FIELD_OBS_LEVEL, field.observation_level)
.putString(GeneralKeys.UNIQUE_NAME, field.unique_id)
- .putString(GeneralKeys.PRIMARY_NAME, field.primary_id)
- .putString(GeneralKeys.SECONDARY_NAME, field.secondary_id)
+ .putString(GeneralKeys.PRIMARY_NAME, primary)
+ .putString(GeneralKeys.SECONDARY_NAME, secondary)
.putBoolean(GeneralKeys.IMPORT_FIELD_FINISHED, true)
.putString(GeneralKeys.LAST_PLOT, null).apply()
diff --git a/app/src/main/java/com/fieldbook/tracker/utilities/GeoNavHelper.kt b/app/src/main/java/com/fieldbook/tracker/utilities/GeoNavHelper.kt
index bc472b369..ff753b3f5 100644
--- a/app/src/main/java/com/fieldbook/tracker/utilities/GeoNavHelper.kt
+++ b/app/src/main/java/com/fieldbook/tracker/utilities/GeoNavHelper.kt
@@ -22,6 +22,7 @@ import android.os.Message
import android.util.Log
import android.util.TypedValue
import android.view.View
+import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
@@ -526,7 +527,17 @@ class GeoNavHelper @Inject constructor(private val controller: CollectController
//add all units that have non null coordinates.
for (model in units) {
if (model.geo_coordinates != null && model.geo_coordinates!!.isNotEmpty()) {
- coordinates.add(model)
+ val entryKeys = controller.getDatabase().getRange(
+ preferences.getString(GeneralKeys.PRIMARY_NAME, null),
+ preferences.getString(GeneralKeys.SECONDARY_NAME, null),
+ model.observation_unit_db_id,
+ model.internal_id_observation_unit,
+ )
+ //set currently chosen primary/secondary ids to model
+ coordinates.add(model.also {
+ it.primary_id = entryKeys.primaryId
+ it.secondary_id = entryKeys.secondaryId
+ })
}
}
@@ -556,7 +567,7 @@ class GeoNavHelper @Inject constructor(private val controller: CollectController
val id = first.observation_unit_db_id
with((controller.getContext() as CollectActivity)) {
- if (id != getRangeBox().cRange.plot_id && id != lastPlotIdNav) {
+ if (id != getRangeBox().cRange.uniqueId && id != lastPlotIdNav) {
lastPlotIdNav = id
runOnUiThread {
if (preferences.getBoolean(GeneralKeys.GEONAV_AUTO, false)) {
@@ -572,7 +583,6 @@ class GeoNavHelper @Inject constructor(private val controller: CollectController
findViewById(R.id.toolbarBottom),
id, Snackbar.LENGTH_INDEFINITE
)
- val snackLayout = mGeoNavSnackbar?.view as SnackbarLayout
val snackView: View =
layoutInflater.inflate(
R.layout.geonav_snackbar_layout,
@@ -609,13 +619,15 @@ class GeoNavHelper @Inject constructor(private val controller: CollectController
params.bottomToTop = R.id.toolbarBottom
snackView.layoutParams = params
- snackLayout.addView(snackView)
- snackLayout.setPadding(0, 0, 0, 0)
+ (mGeoNavSnackbar?.view as? FrameLayout)?.addView(snackView)
+ (mGeoNavSnackbar?.view as? FrameLayout)?.setPadding(0, 0, 0, 0)
+
val tv =
snackView.findViewById(R.id.geonav_snackbar_tv)
- var popupHeader =
+ val popupHeader =
preferences.getString(GeneralKeys.GEONAV_POPUP_DISPLAY, "plot_id")
+
tv.text = getPopupInfo(id, popupHeader ?: "plot_id")
// if the value saved in GEONAV_POPUP_DISPLAY was disabled in traits
@@ -627,17 +639,12 @@ class GeoNavHelper @Inject constructor(private val controller: CollectController
preferenceChangeListener
)
-// if (tv != null) {
-// tv.text = id
-// }
-
val btn =
snackView.findViewById(R.id.geonav_snackbar_btn)
btn?.setOnClickListener { v: View? ->
mGeoNavSnackbar?.dismiss()
lastPlotIdNav = null
- println(snackView.height)
//when navigate button is pressed use rangeBox to go to the plot id
moveToSearch(
"id",
diff --git a/app/src/main/java/com/fieldbook/tracker/views/CameraTraitSettingsView.kt b/app/src/main/java/com/fieldbook/tracker/views/CameraTraitSettingsView.kt
index 01d64b2d0..a9628c97e 100644
--- a/app/src/main/java/com/fieldbook/tracker/views/CameraTraitSettingsView.kt
+++ b/app/src/main/java/com/fieldbook/tracker/views/CameraTraitSettingsView.kt
@@ -4,6 +4,7 @@ import android.content.Context
import android.util.AttributeSet
import android.util.Size
import android.view.View
+import android.widget.Button
import android.widget.CheckBox
import android.widget.FrameLayout
import android.widget.RadioButton
@@ -12,6 +13,7 @@ import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.preference.PreferenceManager
import com.fieldbook.tracker.R
+import com.fieldbook.tracker.activities.CollectActivity
import com.fieldbook.tracker.preferences.GeneralKeys
/**
@@ -29,6 +31,7 @@ open class CameraTraitSettingsView: ConstraintLayout {
protected val resolutionGroup: RadioGroup
protected val resolutionTitle: TextView
protected val resolutionFrameLayout: FrameLayout
+ protected val cropButton: Button
private var lastCameraId: Int? = null
private var lastPreview: Boolean? = null
@@ -42,6 +45,12 @@ open class CameraTraitSettingsView: ConstraintLayout {
resolutionGroup = view.findViewById(R.id.view_trait_photo_settings_resolution_rg)
resolutionTitle = view.findViewById(R.id.view_trait_photo_settings_resolution_tv)
resolutionFrameLayout = view.findViewById(R.id.view_trait_photo_settings_resolution_fl)
+
+ cropButton = view.findViewById(R.id.view_trait_photo_settings_crop_btn)
+ // Set the visibility of the crop button to GONE if the trait does not support cropping
+ (context as CollectActivity).currentTrait.cropImage?.let {
+ cropButton.visibility = if (it) View.VISIBLE else View.GONE
+ }
}
constructor(ctx: Context, supportedResolutions: List) : super(ctx) {
@@ -107,7 +116,16 @@ open class CameraTraitSettingsView: ConstraintLayout {
private fun setup() {
setupSystemCheckBox()
+ setupCropButton()
+ }
+
+ private fun setupCropButton() {
+ cropButton.setOnClickListener {
+
+ (context as CollectActivity).requestAndCropImage()
+
+ }
}
private fun setupSettingsModeBasedOnPreference(checkedRadioButtonId: Int) {
diff --git a/app/src/main/java/com/fieldbook/tracker/views/CollectInputView.kt b/app/src/main/java/com/fieldbook/tracker/views/CollectInputView.kt
index 863b01aa3..c815936df 100644
--- a/app/src/main/java/com/fieldbook/tracker/views/CollectInputView.kt
+++ b/app/src/main/java/com/fieldbook/tracker/views/CollectInputView.kt
@@ -1,6 +1,7 @@
package com.fieldbook.tracker.views
import android.content.Context
+import android.graphics.Typeface
import android.util.AttributeSet
import android.widget.EditText
import android.widget.TextView
@@ -22,6 +23,8 @@ class CollectInputView(context: Context, attributeSet: AttributeSet) : Constrain
var hasData: Boolean = false
+ private var isObservationSaved: Boolean = false
+
private val originalEditText: EditText
val repeatView: RepeatedValuesView
@@ -51,6 +54,7 @@ class CollectInputView(context: Context, attributeSet: AttributeSet) : Constrain
} else {
text = ""
+ markObservationEdited()
}
}
@@ -59,6 +63,8 @@ class CollectInputView(context: Context, attributeSet: AttributeSet) : Constrain
initialize(models)
repeatView.prepareModeNonEmpty()
+
+ markObservationSaved()
}
fun initialize(models: List) {
@@ -70,6 +76,7 @@ class CollectInputView(context: Context, attributeSet: AttributeSet) : Constrain
} else {
text = models.minByOrNull { it.rep.toInt() }?.value ?: ""
+ markObservationEdited()
}
}
@@ -138,4 +145,26 @@ class CollectInputView(context: Context, attributeSet: AttributeSet) : Constrain
fun resetInitialIndex() {
forceInitialRep = -1
}
+
+ fun markObservationEdited() {
+ isObservationSaved = false
+ updateCurrentValueETStyle()
+ }
+
+ /**
+ * Mark current input as saved and update styling
+ */
+ fun markObservationSaved() {
+ isObservationSaved = true
+ updateCurrentValueETStyle()
+ }
+
+ /**
+ * Updates the text style based on saved/edited state
+ */
+ private fun updateCurrentValueETStyle() {
+ val style = if (isObservationSaved) Typeface.BOLD else Typeface.ITALIC
+ editText.setTypeface(null, style)
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/fieldbook/tracker/views/CropImageView.kt b/app/src/main/java/com/fieldbook/tracker/views/CropImageView.kt
new file mode 100644
index 000000000..8e45e99a9
--- /dev/null
+++ b/app/src/main/java/com/fieldbook/tracker/views/CropImageView.kt
@@ -0,0 +1,624 @@
+package com.fieldbook.tracker.views
+
+import android.annotation.SuppressLint
+import android.content.ClipData
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Canvas
+import android.graphics.RectF
+import android.net.Uri
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.inputmethod.EditorInfo
+import android.widget.EditText
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.RelativeLayout
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.marginBottom
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import com.fieldbook.tracker.R
+import com.fieldbook.tracker.utilities.VibrateUtil
+import com.google.android.material.button.MaterialButton
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.util.Locale
+import javax.inject.Inject
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.round
+import kotlin.math.sqrt
+
+/**
+ * Custom view to crop an image using handles.
+ * The view contains a bitmap image, and five handles to crop the image.
+ * The handles are draggable, and the image is cropped to the rectangle defined by the handles.
+ * The fifth handle is a middle handle that adjusts the location of the entire rect.
+ * The view also contains buttons to save, reset, copy, and expand the crop rectangle.
+ */
+@AndroidEntryPoint
+class CropImageView : ConstraintLayout {
+
+ companion object {
+ const val TAG = "CropImageView"
+ const val MIN_DISTANCE_BETWEEN_HANDLES = 32
+ const val DEFAULT_CROP_COORDINATES = "0.00, 0.00, 1.00, 1.00"
+ fun parseRectCoordinates(rectCoordinates: String): RectF? {
+
+ if (rectCoordinates.isNotBlank()) {
+ val rect = RectF()
+ val values = rectCoordinates.split(",")
+ if (values.size == 4) {
+ try {
+ rect.left = values[0].toFloat()
+ rect.top = values[1].toFloat()
+ rect.right = values[2].toFloat()
+ rect.bottom = values[3].toFloat()
+
+ return rect
+
+ } catch (e: Exception) {
+
+ e.printStackTrace()
+
+ }
+ }
+ }
+
+ return null
+ }
+ }
+
+ interface CropImageHandler {
+ fun onCropImageSaved(rectCoordinates: String)
+ fun getImageUri(): String
+ fun getCropCoordinates(): String
+ }
+
+ @Inject
+ lateinit var vibrateUtil: VibrateUtil
+
+ //interface reference
+ var cropImageHandler: CropImageHandler? = null
+
+ private var cropHandleTop: ImageView? = null
+ private var cropHandleBottom: ImageView? = null
+ private var cropHandleStart: ImageView? = null
+ private var cropHandleEnd: ImageView? = null
+ private var cropHandleMid: ImageView? = null
+
+ //declare buttons
+ private var saveButton: MaterialButton? = null
+ private var resetButton: ImageButton? = null
+ private var copyButton: ImageButton? = null
+ private var expandButton: ImageButton? = null
+
+ private var relativeLayout: RelativeLayout? = null
+
+ //declare edit text
+ private var editText: EditText? = null
+
+ private var imageView: OverlayImageView? = null
+
+ //size of the handle image view, set in dimens.xml
+ private val handleSize: Int
+
+ //global handle that tracks which handle was last clicked
+ private var handle: ImageView? = null
+
+ //bitmap that holds the image to crop
+ private var bitmap: Bitmap? =
+ null // = BitmapFactory.decodeStream(resources.openRawResource(R.raw.chip))
+
+ init {
+
+ //allows child views to run onDraw function on invalidation
+ setWillNotDraw(false)
+
+ //half the size of the handle, to center on lines
+ handleSize = resources.getDimensionPixelSize(R.dimen.crop_handle_size)
+
+ val view = inflate(context, R.layout.view_crop_image, this)
+
+ //get the handles
+ cropHandleTop = view.findViewById(R.id.crop_top_handle)
+ cropHandleBottom = view.findViewById(R.id.crop_bottom_handle)
+ cropHandleStart = view.findViewById(R.id.crop_start_handle)
+ cropHandleEnd = view.findViewById(R.id.crop_end_handle)
+ cropHandleMid = view.findViewById(R.id.crop_mid_handle)
+
+ //initialize buttons
+ saveButton = view.findViewById(R.id.crop_save_btn)
+ resetButton = view.findViewById(R.id.crop_reset_btn)
+ copyButton = view.findViewById(R.id.crop_copy_btn)
+ expandButton = view.findViewById(R.id.crop_expand_btn)
+
+ //initialize edit text
+ editText = view.findViewById(R.id.crop_image_tv)
+
+ //initialize image view
+ imageView = view.findViewById(R.id.crop_image_iv)
+
+ //initialize relative layout
+ relativeLayout = view.findViewById(R.id.crop_image_rl)
+
+ initExpandButton()
+ initEditText()
+ initResetButton()
+ initCopyButton()
+ initSaveButton()
+ init()
+ }
+
+ constructor(context: Context) : super(context)
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+ constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
+ context,
+ attrs,
+ defStyleAttr
+ )
+
+ private fun distance(x1: Float, y1: Float, x2: Float, y2: Float): Float {
+ return sqrt((x2 - x1).toDouble() * (x2 - x1) + (y2 - y1).toDouble() * (y2 - y1)).toFloat()
+ }
+
+ fun initialize() {
+
+ try {
+
+ //set the crop coordinates from the handler
+ val rectCoordinates = cropImageHandler?.getCropCoordinates()
+ val imageUri = Uri.parse(cropImageHandler?.getImageUri())
+
+ editText?.setText(rectCoordinates)
+
+ //load, rotate and scale the image using a background coroutine
+ findViewTreeLifecycleOwner()?.lifecycleScope?.launch {
+
+ withContext(Dispatchers.IO) {
+
+ //set the image uri from the handler
+ BitmapFactory.decodeStream(context.contentResolver.openInputStream(imageUri))?.let { bmp ->
+
+ bitmap = bmp
+
+ //rotate bitmap if portrait
+ if (bmp.width > bmp.height) {
+ val matrix = android.graphics.Matrix()
+ matrix.postRotate(90f)
+ bitmap = Bitmap.createBitmap(
+ bmp,
+ 0,
+ 0,
+ bmp.width,
+ bmp.height,
+ matrix,
+ true
+ )
+ }
+
+ withContext(Dispatchers.Main) {
+
+ invalidate()
+
+ submitCoordinatesToUi()
+ }
+ }
+ }
+ }
+
+ } catch (e: Exception) {
+
+ e.printStackTrace()
+
+ }
+ }
+
+ /**
+ * Expand rect coordinates to the size of the image
+ */
+ private fun initExpandButton() {
+
+ expandButton?.setOnClickListener {
+
+ VibrateUtil(context).vibrate(1L)
+ VibrateUtil(context).vibrate(1L)
+
+ imageView?.let { iv ->
+
+ //reset handles to default positions
+ cropHandleTop?.let { top ->
+ top.x = iv.x + iv.width / 2 - handleSize / 2
+ top.y = iv.y - handleSize / 2
+ }
+
+ cropHandleBottom?.let { bot ->
+ bot.x = iv.x + iv.width / 2 - handleSize / 2
+ bot.y = iv.y + iv.height - handleSize / 2
+ }
+
+ cropHandleStart?.let { start ->
+ start.x = iv.x - handleSize / 2
+ start.y = iv.y + iv.height / 2 - handleSize / 2
+ }
+
+ cropHandleEnd?.let { end ->
+ end.x = iv.x + iv.width - handleSize / 2
+ end.y = iv.y + iv.height / 2 - handleSize / 2
+ }
+ }
+
+ invalidate()
+ }
+ }
+
+ private fun submitCoordinatesToUi() {
+ val input = editText!!.text.toString()
+ if (input.isNotBlank()) {
+ val values = input.split(",")
+ if (values.size == 4) {
+ try {
+ val topLeftX = values[0].toFloat()
+ val topLeftY = values[1].toFloat()
+ val bottomRightX = values[2].toFloat()
+ val bottomRightY = values[3].toFloat()
+
+ imageView?.let { iv ->
+ cropHandleTop?.y =
+ iv.y + topLeftY * iv.height - handleSize / 2
+ cropHandleBottom?.y =
+ iv.y + bottomRightY * iv.height - handleSize / 2
+ cropHandleStart?.x =
+ iv.x + topLeftX * iv.width - handleSize / 2
+ cropHandleEnd?.x =
+ iv.x + bottomRightX * iv.width - handleSize / 2
+ }
+
+ editText?.clearFocus()
+
+ invalidate()
+
+ } catch (e: Exception) {
+
+ e.printStackTrace()
+
+ }
+ }
+ }
+ }
+
+ private fun initEditText() {
+
+ //detect action ime done on keyboard
+ editText?.setOnEditorActionListener { _, actionId, _ ->
+ //on action done/ok
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ submitCoordinatesToUi()
+ }
+ true
+ }
+ }
+
+ /**
+ * Save the coordinates to preferences.
+ */
+ private fun initSaveButton() {
+
+ saveButton?.setOnClickListener {
+
+ vibrateUtil.vibrate(1L)
+ vibrateUtil.vibrate(1L)
+
+ try {
+ cropImageHandler?.onCropImageSaved(editText?.text.toString())
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+
+ /**
+ * Copies the coordinates to user clipboard.
+ */
+ private fun initCopyButton() {
+
+ copyButton?.setOnClickListener {
+
+ vibrateUtil.vibrate(1L)
+ vibrateUtil.vibrate(1L)
+
+ try {
+ //start a text share chooser
+ val text = editText?.text.toString()
+ val clipboard =
+ context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
+ val clip = ClipData.newPlainText(context.getString(R.string.crop_image_clip_data_label), text)
+ clipboard.setPrimaryClip(clip)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+
+ /**
+ * Resets the rect coordinates to what is saved currently.
+ */
+ private fun initResetButton() {
+
+ resetButton?.setOnClickListener {
+
+ vibrateUtil.vibrate(1L)
+ vibrateUtil.vibrate(1L)
+
+ //reset handles to default positions in preferences
+ editText?.setText(cropImageHandler?.getCropCoordinates())
+
+ submitCoordinatesToUi()
+
+ invalidate()
+ }
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ private fun init() {
+
+ /**
+ * pass touch events to parent, mid has slightly less elevation to give others priority
+ */
+ cropHandleTop?.setOnTouchListener { _, _ ->
+ handle = cropHandleTop
+ false
+ }
+
+ cropHandleBottom?.setOnTouchListener { _, _ ->
+ handle = cropHandleBottom
+ false
+ }
+
+ cropHandleStart?.setOnTouchListener { _, _ ->
+ handle = cropHandleStart
+ false
+ }
+
+ cropHandleEnd?.setOnTouchListener { _, _ ->
+ handle = cropHandleEnd
+ false
+ }
+
+ cropHandleMid?.setOnTouchListener { _, _ ->
+ handle = cropHandleMid
+ false
+ }
+
+ /**
+ * Handle touch events over the entire view, ignore some touch events that are outside the image
+ */
+ //set touch area to the size of the crop image view
+ setOnTouchListener { _, event ->
+
+ imageView?.let { iv ->
+
+ //return early if views are null
+ if (cropHandleBottom == null
+ || cropHandleTop == null
+ || cropHandleStart == null
+ || cropHandleEnd == null
+ || cropHandleMid == null) return@setOnTouchListener true
+
+ //don't allow touch events below the image
+ if (event.y > iv.y + iv.height + iv.marginBottom / 2) {
+ return@setOnTouchListener true
+ }
+
+ //detect the closest handle to the touch event on action down
+ if (event.action == MotionEvent.ACTION_DOWN) {
+
+ vibrateUtil.vibrate(1L)
+
+ //find handle that the event is closest to
+ val topDistance = distance(event.x, event.y, cropHandleTop!!.x, cropHandleTop!!.y)
+ val bottomDistance =
+ distance(event.x, event.y, cropHandleBottom!!.x, cropHandleBottom!!.y)
+ val startDistance =
+ distance(event.x, event.y, cropHandleStart!!.x, cropHandleStart!!.y)
+ val endDistance = distance(event.x, event.y, cropHandleEnd!!.x, cropHandleEnd!!.y)
+ val midDistance = distance(event.x, event.y, cropHandleMid!!.x, cropHandleMid!!.y)
+
+ if (topDistance <= bottomDistance && topDistance <= startDistance && topDistance <= endDistance) {
+ handle = cropHandleTop
+ }
+
+ if (bottomDistance <= topDistance && bottomDistance <= startDistance && bottomDistance <= endDistance) {
+ handle = cropHandleBottom
+ }
+
+ if (startDistance <= topDistance && startDistance <= bottomDistance && startDistance <= endDistance) {
+ handle = cropHandleStart
+ }
+
+ if (endDistance <= topDistance && endDistance <= bottomDistance && endDistance <= startDistance) {
+ handle = cropHandleEnd
+ }
+
+ if (midDistance <= topDistance && midDistance <= bottomDistance && midDistance <= startDistance && midDistance <= endDistance) {
+ handle = cropHandleMid
+ }
+ }
+
+ //when touch drags, drag the handle that was closest on touch, but keep handle within bounds of image
+ //middle handle drags the entire crop rectangle
+ if (event.action == MotionEvent.ACTION_MOVE) {
+
+ handle?.let { h ->
+
+ if (h == cropHandleTop) {
+ h.y = event.y
+ if (h.y + handleSize / 2 < iv.y) {
+ h.y = iv.y - handleSize / 2
+ }
+ } else if (h == cropHandleBottom) {
+ h.y = event.y
+ if (h.y + handleSize / 2 > iv.height + iv.y) {
+ h.y = iv.height + iv.y - handleSize / 2
+ }
+ } else if (h == cropHandleStart) {
+ h.x = event.x
+ if (h.x < iv.x) {
+ h.x = iv.x - handleSize / 2
+ }
+ } else if (h == cropHandleEnd) {
+ h.x = event.x
+ if (h.x + handleSize / 2 > iv.width + iv.x) {
+ h.x = iv.width + iv.x - handleSize / 2
+ }
+ } else {
+ //middle handle position is updated, other handles are updated in onDraw
+ val deltaX = event.x - h.x
+ val deltaY = event.y - h.y
+
+ for (hand in setOf(
+ cropHandleTop,
+ cropHandleBottom,
+ cropHandleStart,
+ cropHandleEnd,
+ cropHandleMid
+ )) {
+ hand?.x = hand!!.x + deltaX
+ hand.y += deltaY
+ }
+ }
+
+ invalidate()
+ }
+
+ }
+ }
+
+ true
+ }
+ }
+
+ private fun updateEditTextValue() {
+
+ try {
+ val topLeftX = (cropHandleStart!!.x - imageView!!.x + handleSize / 2) / imageView!!.width
+ val topLeftY = (cropHandleTop!!.y - imageView!!.y + handleSize / 2) / imageView!!.height
+ val bottomRightX = (cropHandleEnd!!.x + handleSize / 2 - imageView!!.x) / imageView!!.width
+ val bottomRightY =
+ (cropHandleBottom!!.y - imageView!!.y + handleSize / 2) / imageView!!.height
+
+ editText?.setText(
+ String.format(
+ Locale.getDefault(),
+ "%.2f, %.2f, %.2f, %.2f",
+ topLeftX,
+ topLeftY,
+ bottomRightX,
+ bottomRightY
+ )
+ )
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+
+ try {
+
+ if (bitmap == null) {
+ return
+ }
+
+ //update handles to be within the bounds of the image
+ cropHandleTop?.y = round(
+ max(
+ imageView!!.y - handleSize / 2,
+ min(cropHandleTop!!.y, imageView!!.height.toFloat() + imageView!!.y)
+ )
+ )
+ cropHandleBottom?.y = round(
+ max(
+ imageView!!.y,
+ min(cropHandleBottom!!.y, imageView!!.height.toFloat() + imageView!!.y)
+ )
+ )
+ cropHandleStart?.x = round(
+ max(
+ imageView!!.x - handleSize / 2,
+ min(cropHandleStart!!.x, imageView!!.width.toFloat() + imageView!!.x)
+ )
+ )
+ cropHandleEnd?.x = round(
+ max(
+ imageView!!.x,
+ min(cropHandleEnd!!.x, imageView!!.width.toFloat() + imageView!!.x)
+ )
+ )
+ cropHandleMid?.x = round(
+ max(
+ imageView!!.x,
+ min(cropHandleMid!!.x, imageView!!.width.toFloat() + imageView!!.x)
+ )
+ )
+ cropHandleMid?.y = round(
+ max(
+ imageView!!.y,
+ min(cropHandleMid!!.y, imageView!!.height.toFloat() + imageView!!.y)
+ )
+ )
+
+ //update handles to not cross each other
+ if (cropHandleTop!!.y > cropHandleBottom!!.y - MIN_DISTANCE_BETWEEN_HANDLES) {
+ cropHandleTop!!.y = round(cropHandleBottom!!.y - MIN_DISTANCE_BETWEEN_HANDLES)
+ }
+
+ if (cropHandleStart!!.x > cropHandleEnd!!.x - MIN_DISTANCE_BETWEEN_HANDLES) {
+ cropHandleStart!!.x = round(cropHandleEnd!!.x - MIN_DISTANCE_BETWEEN_HANDLES)
+ }
+
+ if (cropHandleBottom!!.y < cropHandleTop!!.y + MIN_DISTANCE_BETWEEN_HANDLES) {
+ cropHandleBottom!!.y = round(cropHandleTop!!.y + MIN_DISTANCE_BETWEEN_HANDLES)
+ }
+
+ if (cropHandleEnd!!.x < cropHandleStart!!.x + MIN_DISTANCE_BETWEEN_HANDLES) {
+ cropHandleEnd!!.x = round(cropHandleStart!!.x + MIN_DISTANCE_BETWEEN_HANDLES)
+ }
+
+ //get midpoints
+ val midX = round((cropHandleStart!!.x + cropHandleEnd!!.x) / 2)
+ val midY = round((cropHandleTop!!.y + cropHandleBottom!!.y) / 2)
+
+ //set points to be middle of rect lines
+ cropHandleTop!!.x = midX
+ cropHandleBottom!!.x = midX
+ cropHandleStart!!.y = midY
+ cropHandleEnd!!.y = midY
+
+ //update mid handle to the mid point
+ cropHandleMid!!.x = midX
+ cropHandleMid!!.y = midY
+
+ //draw a rectangle between the two handles, notice relative to the imageView using OverlayImageView
+ //otherwise canvas draws underneath the image
+ //also add offset so rectangle intersects middle of handles
+ imageView?.drawRectangle(
+ bitmap!!, imageView!!.x, imageView!!.y, imageView!!.width, imageView!!.height,
+ cropHandleStart!!.x + handleSize / 2,
+ cropHandleTop!!.y + handleSize / 2,
+ cropHandleEnd!!.x + handleSize / 2,
+ cropHandleBottom!!.y + handleSize / 2
+ )
+
+ updateEditTextValue()
+
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/fieldbook/tracker/views/OverlayImageView.kt b/app/src/main/java/com/fieldbook/tracker/views/OverlayImageView.kt
new file mode 100644
index 000000000..706fa886d
--- /dev/null
+++ b/app/src/main/java/com/fieldbook/tracker/views/OverlayImageView.kt
@@ -0,0 +1,90 @@
+package com.fieldbook.tracker.views
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.graphics.Rect
+import android.os.Build
+import android.util.AttributeSet
+import androidx.annotation.RequiresApi
+import com.fieldbook.tracker.R
+import androidx.appcompat.widget.AppCompatImageView
+
+/**
+ * An ImageView wrapper class that draws a rectangle around an image, leaving the outside semi-transparent.
+ */
+class OverlayImageView: AppCompatImageView {
+
+ private var topX: Float = 0f
+ private var topY: Float = 0f
+ private var bottomX: Float = 0f
+ private var bottomY: Float = 0f
+ private var bitmap: Bitmap? = null
+ private var parentX: Float = 0f
+ private var parentY: Float = 0f
+ private var parentWidth: Int = 0
+ private var parentHeight: Int = 0
+ private var parentRect: Rect? = null
+
+ private val rectPaint = Paint().also { paint ->
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ paint.color = context.getColor(R.color.main_primary)
+ } else {
+ paint.color = Color.BLACK
+ }
+ //create a dashed path
+ paint.style = Paint.Style.STROKE
+ paint.strokeWidth = 15f
+ }
+
+ private val paint = Paint()
+ private val porterDuffXfermode = PorterDuffXfermode(PorterDuff.Mode.OVERLAY)
+
+ constructor(context: Context) : super(context)
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+ constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
+
+ //draw rect relative to this image view
+ fun drawRectangle(bitmap: Bitmap, parentX: Float, parentY: Float, parentWidth: Int, parentHeight: Int,
+ topX: Float, topY: Float, bottomX: Float, bottomY: Float) {
+ this.topX = topX - x
+ this.topY = topY - y
+ this.bottomX = bottomX - x
+ this.bottomY = bottomY - y
+ this.bitmap = bitmap
+ this.parentX = parentX
+ this.parentY = parentY
+ this.parentWidth = parentWidth
+ this.parentHeight = parentHeight
+ this.parentRect = Rect(0, 0, parentWidth, parentHeight)
+
+ invalidate()
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+
+ bitmap?.let { bmp ->
+
+ parentRect?.let { parentRect ->
+
+ //set alpha to half and draw the full bitmap scaled to the parent
+ paint.alpha = 128
+ canvas.drawBitmap(bmp, null, parentRect, paint)
+
+ //enable porter duff overlay, draw rect with full alpha and then disable
+ paint.alpha = 255
+ paint.xfermode = porterDuffXfermode
+ canvas.drawRect(topX, topY, bottomX, bottomY, paint)
+ paint.xfermode = null
+
+ //draw the crop border
+ canvas.drawRect(topX, topY, bottomX, bottomY, rectPaint)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/fieldbook/tracker/views/RangeBoxView.kt b/app/src/main/java/com/fieldbook/tracker/views/RangeBoxView.kt
index 4881c4202..70f5eaba4 100644
--- a/app/src/main/java/com/fieldbook/tracker/views/RangeBoxView.kt
+++ b/app/src/main/java/com/fieldbook/tracker/views/RangeBoxView.kt
@@ -1,6 +1,5 @@
package com.fieldbook.tracker.views
-import android.app.Service
import android.content.Context
import android.content.SharedPreferences
import android.database.Cursor
@@ -9,19 +8,17 @@ import android.text.Editable
import android.text.TextWatcher
import android.util.AttributeSet
import android.util.Log
-import android.view.KeyEvent
import android.view.MotionEvent
-import android.view.View
import android.view.View.OnTouchListener
-import android.view.inputmethod.EditorInfo
-import android.view.inputmethod.InputMethodManager
-import android.widget.EditText
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
import com.fieldbook.tracker.R
+import com.fieldbook.tracker.activities.CollectActivity
+import com.fieldbook.tracker.dialogs.AttributeChooserDialog
+import com.fieldbook.tracker.dialogs.QuickGotoDialog
import com.fieldbook.tracker.interfaces.CollectRangeController
import com.fieldbook.tracker.objects.RangeObject
import com.fieldbook.tracker.objects.TraitObject
@@ -33,6 +30,8 @@ class RangeBoxView : ConstraintLayout {
companion object {
const val TAG = "RangeBoxView"
+ //truncate to three characters plus the colon
+ const val TRUNCATE_LENGTH = 3 + 1
}
private var controller: CollectRangeController
@@ -43,16 +42,12 @@ class RangeBoxView : ConstraintLayout {
var cRange: RangeObject
private var lastRange: String
- private var rangeName: TextView
- private var plotName: TextView
+ private var primaryNameTv: TextView
+ private var secondaryNameTv: TextView
- //edit text used for quick goto feature range = primary id
- private var rangeEt: EditText
+ var primaryIdTv: TextView
+ var secondaryIdTv: TextView
- //edit text used for quick goto feature plot = secondary id
- private var plotEt: EditText
- private var tvRange: TextView
- private var tvPlot: TextView
private var rangeLeft: ImageView
private var rangeRight: ImageView
@@ -66,14 +61,6 @@ class RangeBoxView : ConstraintLayout {
private var rangeEdited = false
private var plotEdited = false
- /**
- * unique plot names used in range queries
- * query and save them once during initialization
- */
- private var firstName: String
- private var secondName: String
- private var uniqueName: String
-
private var delay = 100
private var count = 1
@@ -85,25 +72,20 @@ class RangeBoxView : ConstraintLayout {
this.rangeLeft = v.findViewById(R.id.rangeLeft)
this.rangeRight = v.findViewById(R.id.rangeRight)
- this.tvRange = v.findViewById(R.id.tvRange)
- this.tvPlot = v.findViewById(R.id.tvPlot)
- this.plotEt = v.findViewById(R.id.plot)
- this.rangeEt = v.findViewById(R.id.range)
- this.rangeName = v.findViewById(R.id.rangeName)
- this.plotName = v.findViewById(R.id.plotName)
+ this.primaryIdTv = v.findViewById(R.id.primaryIdTv)
+ this.secondaryIdTv = v.findViewById(R.id.secondaryIdTv)
+ this.primaryNameTv = v.findViewById(R.id.primaryNameTv)
+ this.secondaryNameTv = v.findViewById(R.id.secondaryNameTv)
this.plotsProgressBar = v.findViewById(R.id.plotsProgressBar)
this.controller = context as CollectRangeController
rangeID = this.controller.getDatabase().allRangeID
cRange = RangeObject()
- cRange.plot = ""
- cRange.plot_id = ""
- cRange.range = ""
+ cRange.secondaryId = ""
+ cRange.uniqueId = ""
+ cRange.primaryId = ""
lastRange = ""
- firstName = controller.getPreferences().getString(GeneralKeys.PRIMARY_NAME, "") ?: ""
- secondName = controller.getPreferences().getString(GeneralKeys.SECONDARY_NAME, "") ?: ""
- uniqueName = controller.getPreferences().getString(GeneralKeys.UNIQUE_NAME, "") ?: ""
}
constructor(ctx: Context) : super(ctx)
@@ -123,6 +105,18 @@ class RangeBoxView : ConstraintLayout {
defStyleRes
)
+ private fun getPrimaryName(): String {
+ return controller.getPreferences().getString(GeneralKeys.PRIMARY_NAME, "") ?: ""
+ }
+
+ private fun getSecondaryName(): String {
+ return controller.getPreferences().getString(GeneralKeys.SECONDARY_NAME, "") ?: ""
+ }
+
+ private fun getUniqueName(): String {
+ return controller.getPreferences().getString(GeneralKeys.UNIQUE_NAME, "") ?: ""
+ }
+
fun toggleNavigation(toggle: Boolean) {
rangeLeft.isEnabled = toggle
rangeRight.isEnabled = toggle
@@ -145,11 +139,11 @@ class RangeBoxView : ConstraintLayout {
}
fun getPlotID(): String? {
- return cRange.plot_id
+ return cRange.uniqueId
}
fun isEmpty(): Boolean {
- return cRange.plot_id.isEmpty()
+ return cRange.uniqueId.isEmpty()
}
fun connectTraitBox(traitBoxView: TraitBoxView) {
@@ -166,118 +160,120 @@ class RangeBoxView : ConstraintLayout {
// Go to next range
rangeRight.setOnClickListener { moveEntryRight() }
- rangeEt.setOnEditorActionListener(createOnEditorListener(rangeEt, "range"))
- plotEt.setOnEditorActionListener(createOnEditorListener(plotEt, "plot"))
- rangeEt.setOnTouchListener { _, _ ->
- rangeEt.isCursorVisible = true
- false
- }
+ setName()
- plotEt.setOnTouchListener { _, _ ->
- plotEt.isCursorVisible = true
- false
- }
- setName(10)
- rangeName.setOnTouchListener { _, _ ->
- Utils.makeToast(
- context,
- controller.getPreferences().getString(
- GeneralKeys.PRIMARY_NAME,
- context.getString(R.string.search_results_dialog_range)
- )
+ val attributeChooserDialog = AttributeChooserDialog(
+ showTraits = false,
+ showOther = false,
+ showSystemAttributes = false
+ )
+
+ primaryNameTv.setOnClickListener {
+ attributeChooserDialog.setOnAttributeSelectedListener(object :
+ AttributeChooserDialog.OnAttributeSelectedListener {
+ override fun onAttributeSelected(label: String) {
+ //update preference primary name
+ controller.getPreferences().edit().putString(GeneralKeys.PRIMARY_NAME, label).apply()
+ setName()
+ refresh()
+ }
+ })
+ attributeChooserDialog.show(
+ (controller.getContext() as CollectActivity).supportFragmentManager,
+ "attributeChooserDialog"
)
- false
}
- //TODO https://stackoverflow.com/questions/47107105/android-button-has-setontouchlistener-called-on-it-but-does-not-override-perform
- plotName.setOnTouchListener { v: View, _: MotionEvent? ->
- Utils.makeToast(
- context,
- controller.getPreferences().getString(
- GeneralKeys.SECONDARY_NAME,
- context.getString(R.string.search_results_dialog_range)
- )
+ secondaryNameTv.setOnClickListener {
+ attributeChooserDialog.setOnAttributeSelectedListener(object :
+ AttributeChooserDialog.OnAttributeSelectedListener {
+ override fun onAttributeSelected(label: String) {
+ //update preference primary name
+ controller.getPreferences().edit().putString(GeneralKeys.SECONDARY_NAME, label).apply()
+ setName()
+ refresh()
+ }
+ })
+ attributeChooserDialog.show(
+ (controller.getContext() as CollectActivity).supportFragmentManager,
+ "attributeChooserDialog"
)
- v.performClick()
+ }
+
+ primaryIdTv.setOnClickListener {
+ showQuickGoToDialog()
+ }
+
+ secondaryIdTv.setOnClickListener {
+ showQuickGoToDialog(primaryClicked = false)
}
}
- private fun repeatUpdate() {
+ /**
+ * Builds and shows an alert dialog with two edit text fields for the primary/secondary ids
+ */
+ private fun showQuickGoToDialog(primaryClicked: Boolean = true) {
- controller.getTraitBox().setNewTraits(getPlotID())
+ val dialog = QuickGotoDialog(controller, primaryClicked) { primaryId, secondaryId ->
- }
+ quickGoToNavigateFromDialog(primaryId, secondaryId)
+ }
+
+ dialog.show((context as CollectActivity).supportFragmentManager, "quickGotoDialog")
- private fun truncate(s: String, maxLen: Int): String {
- return if (s.length > maxLen) s.substring(0, maxLen - 1) + ":" else s
}
- /**
- * This listener is used in the QuickGoto feature.
- * This listens to the primary/secondary edit text's in the rangebox.
- * When the soft keyboard enter key action is pressed (IME_ACTION_DONE)
- * this will use the moveToSearch function.
- * First it will search for both primary/secondary ids if they have both been changed.
- * If one has not been changed or a plot is not found for both terms then it defaults to
- * a search with whatever was changed last.
- * @param edit the edit text to assign this listener to
- * @param searchType the type used in moveToSearch, either plot or range
- */
- private fun createOnEditorListener(
- edit: EditText,
- searchType: String
- ): TextView.OnEditorActionListener {
- return object : TextView.OnEditorActionListener {
- override fun onEditorAction(view: TextView, actionId: Int, event: KeyEvent?): Boolean {
- // do not do bit check on event, crashes keyboard
- if (actionId == EditorInfo.IME_ACTION_DONE) {
- try {
-
- //if both quick goto et's have been changed, attempt a search with them
- if (rangeEdited && plotEdited) {
-
- //if the search fails back-down to the original search
- if (!controller.moveToSearch(
- "quickgoto", rangeID,
- rangeEt.text.toString(),
- plotEt.text.toString(), null, -1
- )
- ) {
- controller.moveToSearch(
- searchType,
- rangeID,
- null,
- null,
- view.text.toString(),
- -1
- )
- }
- } else { //original search if only one has changed
- controller.moveToSearch(
- searchType,
- rangeID,
- null,
- null,
- view.text.toString(),
- -1
- )
- }
+ private fun quickGoToNavigateFromDialog(primaryId: String, secondaryId: String) {
- //reset the changed flags
- rangeEdited = false
- plotEdited = false
- val imm: InputMethodManager =
- context.getSystemService(Service.INPUT_METHOD_SERVICE) as InputMethodManager
- imm.hideSoftInputFromWindow(edit.windowToken, 0)
- } catch (ignore: Exception) {
- }
- return true
+ try {
+
+ when {
+
+ primaryId.isNotBlank() && secondaryId.isNotBlank() -> {
+ controller.moveToSearch(
+ "quickgoto", rangeID,
+ primaryId,
+ secondaryId, null, -1
+ )
}
- return false
+
+ primaryId.isNotBlank() && secondaryId.isBlank() -> {
+ controller.moveToSearch(
+ "range", rangeID,
+ primaryId,
+ null, primaryId, -1
+ )
+ }
+
+ primaryId.isBlank() && secondaryId.isNotBlank() -> {
+ controller.moveToSearch(
+ "plot", rangeID,
+ null,
+ secondaryId, secondaryId, -1
+ )
+ }
+
+ else -> return
+
}
+
+ } catch (e: Exception) {
+
+ Log.e(TAG, "Error in quickGoToNavigateFromDialog: $e")
+
}
}
+ private fun repeatUpdate() {
+
+ controller.getTraitBox().setNewTraits(getPlotID())
+
+ }
+
+ private fun truncate(s: String, maxLen: Int): String {
+ return if (s.length > maxLen) s.substring(0, maxLen - 1) + ":" else s
+ }
+
private fun createRunnable(directionStr: String): Runnable {
return object : Runnable {
override fun run() {
@@ -366,10 +362,10 @@ class RangeBoxView : ConstraintLayout {
// Refresh onscreen controls
updateCurrentRange(rangeID[paging - 1])
saveLastPlot()
- if (cRange.plot_id.isEmpty()) return
+ if (cRange.uniqueId.isEmpty()) return
if (controller.getPreferences().getBoolean(GeneralKeys.PRIMARY_SOUND, false)) {
- if (cRange.range != lastRange && lastRange != "") {
- lastRange = cRange.range
+ if (cRange.primaryId != lastRange && lastRange != "") {
+ lastRange = cRange.primaryId
controller.getSoundHelper().playPlonk()
}
}
@@ -388,11 +384,15 @@ class RangeBoxView : ConstraintLayout {
*/
private fun updateCurrentRange(id: Int) {
- if (firstName.isNotEmpty() && secondName.isNotEmpty() && uniqueName.isNotEmpty()) {
+ val primaryId = getPrimaryName()
+ val secondaryId = getSecondaryName()
+ val uniqueId = getUniqueName()
+
+ if (primaryId.isNotEmpty() && secondaryId.isNotEmpty() && uniqueId.isNotEmpty()) {
try {
- cRange = controller.getDatabase().getRange(firstName, secondName, uniqueName, id)
+ cRange = controller.getDatabase().getRange(primaryId, secondaryId, uniqueId, id)
// RangeID is a sorted list of obs unit ids for the current field.
// Set bar maximum to number of obs units in the field
@@ -420,20 +420,14 @@ class RangeBoxView : ConstraintLayout {
}
fun reload() {
-
- firstName = controller.getPreferences().getString(GeneralKeys.PRIMARY_NAME, "") ?: ""
- secondName = controller.getPreferences().getString(GeneralKeys.SECONDARY_NAME, "") ?: ""
- uniqueName = controller.getPreferences().getString(GeneralKeys.UNIQUE_NAME, "") ?: ""
-
- switchVisibility(controller.getPreferences().getBoolean(GeneralKeys.QUICK_GOTO, false))
- setName(8)
+ setName()
paging = 1
setAllRangeID()
if (rangeID.isNotEmpty()) {
updateCurrentRange(rangeID[0])
- lastRange = cRange.range
+ lastRange = cRange.primaryId
display()
- controller.getTraitBox().setNewTraits(cRange.plot_id)
+ controller.getTraitBox().setNewTraits(cRange.uniqueId)
} else { //if no fields, print a message and finish with result canceled
Utils.makeToast(context, context.getString(R.string.act_collect_no_plots))
controller.cancelAndFinish()
@@ -445,8 +439,8 @@ class RangeBoxView : ConstraintLayout {
updateCurrentRange(rangeID[paging - 1])
display()
if (controller.getPreferences().getBoolean(GeneralKeys.PRIMARY_SOUND, false)) {
- if (cRange.range != lastRange && lastRange != "") {
- lastRange = cRange.range
+ if (cRange.primaryId != lastRange && lastRange != "") {
+ lastRange = cRange.primaryId
controller.getSoundHelper().playPlonk()
}
}
@@ -454,12 +448,8 @@ class RangeBoxView : ConstraintLayout {
// Updates the data shown in the dropdown
fun display() {
- rangeEt.setText(cRange.range)
- plotEt.setText(cRange.plot)
- rangeEt.isCursorVisible = false
- plotEt.isCursorVisible = false
- tvRange.text = cRange.range
- tvPlot.text = cRange.plot
+ primaryIdTv.text = cRange.primaryId
+ secondaryIdTv.text = cRange.secondaryId
}
fun rightClick() {
@@ -468,7 +458,7 @@ class RangeBoxView : ConstraintLayout {
fun saveLastPlot() {
val ed: SharedPreferences.Editor = controller.getPreferences().edit()
- ed.putString(GeneralKeys.LAST_PLOT, cRange.plot_id)
+ ed.putString(GeneralKeys.LAST_PLOT, cRange.uniqueId)
ed.apply()
}
@@ -482,25 +472,7 @@ class RangeBoxView : ConstraintLayout {
}
}
- private fun switchVisibility(textview: Boolean) {
- if (textview) {
- tvRange.visibility = GONE
- tvPlot.visibility = GONE
- rangeEt.visibility = VISIBLE
- plotEt.visibility = VISIBLE
-
- //when the et's are visible create text watchers to listen for changes
- rangeEt.addTextChangedListener(createTextWatcher("range"))
- plotEt.addTextChangedListener(createTextWatcher("plot"))
- } else {
- tvRange.visibility = VISIBLE
- tvPlot.visibility = VISIBLE
- rangeEt.visibility = GONE
- plotEt.visibility = GONE
- }
- }
-
- fun setName(maxLen: Int) {
+ fun setName() {
val primaryName = controller.getPreferences().getString(
GeneralKeys.PRIMARY_NAME,
context.getString(R.string.search_results_dialog_range)
@@ -509,8 +481,8 @@ class RangeBoxView : ConstraintLayout {
GeneralKeys.SECONDARY_NAME,
context.getString(R.string.search_results_dialog_plot)
) + ":"
- rangeName.text = truncate(primaryName, maxLen)
- plotName.text = truncate(secondaryName, maxLen)
+ this.primaryNameTv.text = truncate(primaryName, TRUNCATE_LENGTH)
+ this.secondaryNameTv.text = truncate(secondaryName, TRUNCATE_LENGTH)
}
fun setAllRangeID() {
@@ -528,11 +500,9 @@ class RangeBoxView : ConstraintLayout {
}
fun setLastRange() {
- lastRange = cRange.range
+ lastRange = cRange.primaryId
}
- ///// paging /////
-
///// paging /////
fun moveEntryLeft() {
if (!controller.validateData(controller.getCurrentObservation()?.value)) {
@@ -609,7 +579,8 @@ class RangeBoxView : ConstraintLayout {
moveToNextUncollectedObs(pos, step, arrayListOf(currentTraitObj))
}
2 -> {
- val visibleTraits = ArrayList(controller.getDatabase().visibleTraitObjects.filterNotNull())
+ val sortOrder = controller.getPreferences().getString(GeneralKeys.TRAITS_LIST_SORT_ORDER, "position")
+ val visibleTraits = ArrayList(controller.getDatabase().getVisibleTraitObjects(sortOrder).filterNotNull())
moveToNextUncollectedObs(pos, step, visibleTraits)
}
else -> moveSimply(pos, step)
diff --git a/app/src/main/java/com/fieldbook/tracker/views/TraitBoxView.kt b/app/src/main/java/com/fieldbook/tracker/views/TraitBoxView.kt
index 79d2aa58e..f488f661d 100644
--- a/app/src/main/java/com/fieldbook/tracker/views/TraitBoxView.kt
+++ b/app/src/main/java/com/fieldbook/tracker/views/TraitBoxView.kt
@@ -112,6 +112,13 @@ class TraitBoxView : ConstraintLayout {
// Go to next trait
traitRight.setOnClickListener { moveTrait("right") }
+
+ traitsStatusBarRv?.adapter = TraitsStatusAdapter(this)
+ traitsStatusBarRv?.layoutManager = object : LinearLayoutManager(context, HORIZONTAL, false) {
+ override fun canScrollHorizontally(): Boolean {
+ return false
+ }
+ }
}
fun initTraitDetails() {
@@ -133,13 +140,6 @@ class TraitBoxView : ConstraintLayout {
this.visibleTraitsList = visibleTraits
this.rangeSuppress = rangeSuppress
- traitsStatusBarRv?.adapter = TraitsStatusAdapter(this)
- traitsStatusBarRv?.layoutManager = object : LinearLayoutManager(context, HORIZONTAL, false) {
- override fun canScrollHorizontally(): Boolean {
- return false
- }
- }
-
// recyclerView?.viewTreeObserver?.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
// override fun onGlobalLayout() {
// recyclerView?.viewTreeObserver?.removeOnGlobalLayoutListener(this)
@@ -160,32 +160,40 @@ class TraitBoxView : ConstraintLayout {
// Display dialog or menu for trait selection
showTraitPickerDialog(visibleTraits)
}
+
+ updateTraitsStatusBar()
+
}
fun getRecyclerView(): RecyclerView? {
return traitsStatusBarRv
}
+ private var previousSelection = 0
+
fun loadLayout(rangeSuppress: Boolean) {
val traitPosition = getSelectedItemPosition()
setSelection(traitPosition)
+ traitsStatusBarRv?.adapter?.notifyItemChanged(previousSelection)
+ traitsStatusBarRv?.adapter?.notifyItemChanged(traitPosition)
+
+ previousSelection = traitPosition
+
// This updates the in memory hashmap from database
currentTrait = controller.getDatabase().getDetail(
traitTypeTv.text
.toString()
)
- updateTraitsStatusBar()
-
// Update last used trait so it is preserved when entry moves
controller.getPreferences().edit().putString(GeneralKeys.LAST_USED_TRAIT,traitTypeTv.text.toString()).apply()
traitTypeTv.text = currentTrait?.name
-
val imm =
context.getSystemService(Service.INPUT_METHOD_SERVICE) as InputMethodManager
+
if (currentTrait?.format != "text") {
try {
@@ -193,7 +201,9 @@ class TraitBoxView : ConstraintLayout {
} catch (ignore: Exception) {
}
}
- traitDetails.text = currentTrait?.details
+
+ traitDetails.text = currentTrait?.details ?: ""
+
if (!rangeSuppress or (currentTrait?.format != "numeric")) {
if (controller.getInputView().visibility == VISIBLE) {
controller.getInputView().visibility = GONE
@@ -221,8 +231,6 @@ class TraitBoxView : ConstraintLayout {
controller.getInputView().visibility = VISIBLE
controller.getInputView().isEnabled = true
}
-
- updateTraitsStatusBar()
}
private fun showTraitPickerDialog(visibleTraits: Array?) {
@@ -254,7 +262,9 @@ class TraitBoxView : ConstraintLayout {
}
private fun updateTraitsStatusBar() {
- val visibleTraits: Array = controller.getDatabase().getVisibleTrait()
+
+ val currentSortOrder = controller.getPreferences().getString(GeneralKeys.TRAITS_LIST_SORT_ORDER, "position")
+ val visibleTraits: Array = controller.getDatabase().getVisibleTrait(currentSortOrder)
// images saved are not stored in newTraits hashMap
// get the data for current plot_id again
@@ -267,6 +277,7 @@ class TraitBoxView : ConstraintLayout {
traitsValue.containsKey(trait)
)
}
+
(traitsStatusBarRv?.adapter as TraitsStatusAdapter).submitList(traitBoxItemModels)
// the recyclerView height was 0 initially, so calculate the icon size again
@@ -278,8 +289,6 @@ class TraitBoxView : ConstraintLayout {
}
}
}
- (traitsStatusBarRv?.adapter as TraitsStatusAdapter).notifyDataSetChanged()
-
}
fun getNewTraits(): Map {
diff --git a/app/src/main/res/drawable/content_copy.xml b/app/src/main/res/drawable/content_copy.xml
new file mode 100644
index 000000000..ebd6fb78e
--- /dev/null
+++ b/app/src/main/res/drawable/content_copy.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/crop_handle.xml b/app/src/main/res/drawable/crop_handle.xml
new file mode 100644
index 000000000..29cadbe39
--- /dev/null
+++ b/app/src/main/res/drawable/crop_handle.xml
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/undo.xml b/app/src/main/res/drawable/undo.xml
new file mode 100644
index 000000000..04755d9ce
--- /dev/null
+++ b/app/src/main/res/drawable/undo.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/undo_variant.xml b/app/src/main/res/drawable/undo_variant.xml
new file mode 100644
index 000000000..615ee01f2
--- /dev/null
+++ b/app/src/main/res/drawable/undo_variant.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_crop_image.xml b/app/src/main/res/layout/activity_crop_image.xml
new file mode 100644
index 000000000..b6ed0ae4a
--- /dev/null
+++ b/app/src/main/res/layout/activity_crop_image.xml
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_study_importer.xml b/app/src/main/res/layout/activity_study_importer.xml
index da9b347fe..e3e9cb8cf 100644
--- a/app/src/main/res/layout/activity_study_importer.xml
+++ b/app/src/main/res/layout/activity_study_importer.xml
@@ -50,16 +50,6 @@
android:layout_height="wrap_content"
android:text="@string/level"/>
-
-
-
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_field_file_import.xml b/app/src/main/res/layout/dialog_field_file_import.xml
new file mode 100644
index 000000000..410fe4806
--- /dev/null
+++ b/app/src/main/res/layout/dialog_field_file_import.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_import.xml b/app/src/main/res/layout/dialog_import.xml
deleted file mode 100644
index adbf504ca..000000000
--- a/app/src/main/res/layout/dialog_import.xml
+++ /dev/null
@@ -1,79 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/dialog_quick_goto.xml b/app/src/main/res/layout/dialog_quick_goto.xml
new file mode 100644
index 000000000..e9019742b
--- /dev/null
+++ b/app/src/main/res/layout/dialog_quick_goto.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_item_image.xml b/app/src/main/res/layout/list_item_image.xml
index dac7a5010..04ce80e02 100644
--- a/app/src/main/res/layout/list_item_image.xml
+++ b/app/src/main/res/layout/list_item_image.xml
@@ -5,7 +5,8 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_margin="4dp"
android:layout_width="wrap_content"
- android:layout_height="wrap_content">
+ android:layout_height="wrap_content"
+ android:background="@drawable/cell">
@@ -27,9 +29,10 @@
android:id="@+id/list_item_study_title_tv"
style="@style/TextViewStyle.Bold.Title"
android:layout_width="0dp"
- android:layout_height="match_parent"
+ android:layout_height="0dp"
android:layout_margin="16dp"
- app:layout_constraintBottom_toBottomOf="parent"
+ android:gravity="center_vertical"
+ app:layout_constraintBottom_toTopOf="@id/list_item_study_units_chip"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/list_item_study_pb"
@@ -46,7 +49,7 @@
android:checkable="false"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/list_item_study_title_tv"
+ app:layout_constraintTop_toBottomOf="@id/list_item_study_pb"
tools:text="108"/>
-
+ android:layout_marginHorizontal="16dp"
+ android:layout_marginVertical="8dp">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+ android:text="Preview Date"
+ android:textSize="@dimen/text_size_xlarge"
+ android:textStyle="bold" />
-
+
+
+
+
+
diff --git a/app/src/main/res/layout/view_crop_image.xml b/app/src/main/res/layout/view_crop_image.xml
new file mode 100644
index 000000000..e18874185
--- /dev/null
+++ b/app/src/main/res/layout/view_crop_image.xml
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/view_range_box.xml b/app/src/main/res/layout/view_range_box.xml
index 73587d208..6306e70f2 100644
--- a/app/src/main/res/layout/view_range_box.xml
+++ b/app/src/main/res/layout/view_range_box.xml
@@ -1,18 +1,21 @@
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+ android:textStyle="bold"
+ tools:text="row:" />
+ android:textStyle="bold"
+ tools:text="col:" />
-
-
-
-
+ tools:text="1" />
+ tools:text="13RPN00005" />
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_trait_photo_settings.xml b/app/src/main/res/layout/view_trait_photo_settings.xml
index bdbbc09d3..a96309b1a 100644
--- a/app/src/main/res/layout/view_trait_photo_settings.xml
+++ b/app/src/main/res/layout/view_trait_photo_settings.xml
@@ -61,7 +61,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/view_trait_photo_settings_resolution_tv"
- app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/view_trait_photo_settings_crop_btn"
app:layout_constraintVertical_bias="0">
+
+
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index d48cff943..87a4c8f65 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -314,4 +314,6 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 90ac80000..1bd9576cd 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -37,6 +37,7 @@
#47b65d
#00a771
#009683
+ #80101010
@color/BLACK
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 6a6851434..8da4b7128 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -1,6 +1,9 @@
+
+ 32dp
+
150dp
115dp
@@ -9,6 +12,10 @@
230dp
300dp
+
+ 340dp
+ 480dp
+
40dp
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
index ae9f85665..ded836d2a 100644
--- a/app/src/main/res/values/ids.xml
+++ b/app/src/main/res/values/ids.xml
@@ -45,9 +45,9 @@
-
+
-
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6d886a00c..ba02f4dae 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -881,6 +881,10 @@
Trait level audio is already recording. Stop that first.
Trait level audio is playing. Stop that first.
Error creating zip file
+ Recorded Audio File
+ Timestamp:
+ Duration:
+ File Size:
Enable Individual Field Pages
Selecting a field in the fields list opens a detail page
@@ -1392,6 +1396,19 @@
Invalid categorical data was entered.
Failed to load data
Press OK to send an anonymous diagnostic report to help improve the app.
+ Choose an attribute
+ Quick GoTo
+ Go
+ Primary ID
+ Secondary ID
+ running person
Failed fetching observation levels
+ Default
+ Crop
+ Setup Crop Region
+ Define a crop region that will be applied to all future pictures for this trait.
+ Set Crop Region
+ Field Book crop region data
+ toggle input type
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index c2c4547f6..ee7a40480 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -129,6 +129,8 @@
- @color/main_heatmap_color_max
- ?fb_heatmap_color_medium
+
+ - @color/main_inverse_crop_region_color
diff --git a/app/src/main/res/values/styles_widgets.xml b/app/src/main/res/values/styles_widgets.xml
index 200e1c5b0..92696c81b 100644
--- a/app/src/main/res/values/styles_widgets.xml
+++ b/app/src/main/res/values/styles_widgets.xml
@@ -48,6 +48,12 @@
- ?attr/fb_body_text_size
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/preferences_appearance.xml b/app/src/main/res/xml/preferences_appearance.xml
index 3c286cbfc..f67a2d512 100644
--- a/app/src/main/res/xml/preferences_appearance.xml
+++ b/app/src/main/res/xml/preferences_appearance.xml
@@ -55,12 +55,5 @@
android:summary="@string/preferences_appearance_infobar_hide_prefix_description"
android:title="@string/preferences_appearance_infobar_hide_prefix" />
-
-
\ No newline at end of file