From 1db8082ee848901fae726d819e7a2f9860a1fadf Mon Sep 17 00:00:00 2001 From: Prasad Kamath Date: Fri, 8 Sep 2023 18:34:52 -0400 Subject: [PATCH] Merge v5.5 --- .github/workflows/github-release.yml | 40 +- .github/workflows/pr-prerelease.yml | 1 + .gitignore | 3 - app/build.gradle | 15 +- app/proguard-rules.txt | 2 + app/src/main/AndroidManifest.xml | 2 +- app/src/main/assets/database/sample.db | Bin 208896 -> 208896 bytes .../assets/field_import/training_sample.csv | 2 +- .../tracker/activities/AboutActivity.java | 85 +- .../tracker/activities/CollectActivity.java | 190 ++- .../tracker/activities/ConfigActivity.java | 20 +- .../activities/FieldEditorActivity.java | 78 +- .../tracker/activities/SearchActivity.java | 19 +- .../activities/TraitEditorActivity.java | 50 +- .../tracker/brapi/BrapiLoadDialog.java | 6 + .../tracker/brapi/service/BrAPIServiceV2.java | 16 + .../tracker/database/DataHelper.java | 8 + .../tracker/dialogs/GeoNavCollectDialog.kt | 99 ++ .../tracker/dialogs/NewTraitDialog.java | 37 +- .../tracker/interfaces/CollectController.kt | 9 +- .../tracker/location/GPSTracker.java | 4 + .../tracker/location/gnss/ConnectThread.kt | 4 +- .../tracker/location/gnss/NmeaParser.kt | 24 +- .../tracker/objects/TraitObject.java | 16 + .../preferences/BrapiPreferencesFragment.java | 6 +- .../DatabasePreferencesFragment.java | 41 +- .../tracker/preferences/GeneralKeys.java | 18 +- .../GeneralPreferencesFragment.java | 48 + .../preferences/LanguagePreferenceFragment.kt | 6 +- .../tracker/storage/StorageDefinerFragment.kt | 1 + .../tracker/traits/DateTraitLayout.java | 20 +- .../tracker/traits/GNSSTraitLayout.kt | 349 +++- .../tracker/traits/GoProTraitLayout.kt | 2 +- .../tracker/traits/UsbCameraTraitLayout.kt | 53 + .../{objects => utilities}/GeoNavHelper.kt | 202 ++- .../tracker/utilities/GeodeticUtils.kt | 25 +- .../{objects => utilities}/GoProWrapper.kt | 10 +- .../tracker/utilities/SnackbarUtils.java | 10 +- .../tracker/utilities/TapTargetUtil.kt | 77 +- .../VerifyPersonHelper.kt | 2 +- .../tracker/utilities/VibrateUtil.kt | 22 + .../fieldbook/tracker/views/RangeBoxView.kt | 4 - .../tracker/views/RepeatedValuesView.kt | 12 +- .../main/res/drawable/book_open_variant.xml | 1 + .../ic_pref_general_root_directory.xml | 3 +- app/src/main/res/drawable/ic_sv.png | Bin 0 -> 1296 bytes app/src/main/res/drawable/ic_vi.png | Bin 0 -> 2494 bytes app/src/main/res/drawable/minus.xml | 9 + .../main/res/layout/dialog_geonav_collect.xml | 60 + app/src/main/res/layout/dialog_info.xml | 2 +- app/src/main/res/layout/dialog_sort.xml | 84 - .../res/layout/geonav_snackbar_layout.xml | 2 +- app/src/main/res/layout/list_item_config.xml | 2 +- app/src/main/res/layout/list_item_summary.xml | 3 +- app/src/main/res/layout/list_item_trait.xml | 8 +- app/src/main/res/layout/trait_gnss.xml | 52 +- app/src/main/res/layout/trait_usb_camera.xml | 30 +- app/src/main/res/menu/menu_main.xml | 9 +- app/src/main/res/raw/changelog.xml | 9 +- .../{values-am => values-am-rET}/strings.xml | 250 ++- app/src/main/res/values-ar-rSA/strings.xml | 419 +++++ app/src/main/res/values-ar/strings.xml | 112 -- .../{values-bn => values-bn-rBD}/strings.xml | 283 ++-- .../{values-de => values-de-rDE}/strings.xml | 244 ++- .../{values-es => values-es-rMX}/strings.xml | 474 +++--- app/src/main/res/values-fr-rFR/strings.xml | 406 +++++ app/src/main/res/values-fr/strings.xml | 112 -- .../{values-hi => values-hi-rIN}/strings.xml | 250 ++- .../{values-it => values-it-rIT}/strings.xml | 476 +++--- .../{values-ja => values-ja-rJP}/strings.xml | 472 +++--- app/src/main/res/values-om-rET/strings.xml | 250 ++- app/src/main/res/values-pa-rIN/strings.xml | 77 + app/src/main/res/values-pt-rBR/strings.xml | 249 ++- .../{values-ru => values-ru-rRU}/strings.xml | 250 ++- app/src/main/res/values-sv-rSE/strings.xml | 311 ++++ app/src/main/res/values-vi-rVN/strings.xml | 418 +++++ app/src/main/res/values-zh-rCN/strings.xml | 476 +++--- app/src/main/res/values/array.xml | 21 + app/src/main/res/values/colors.xml | 2 +- app/src/main/res/values/directories.xml | 24 +- app/src/main/res/values/strings.xml | 1407 +++++++++-------- .../main/res/values/strings_trait_gnss.xml | 10 + app/src/main/res/values/styles.xml | 4 +- app/src/main/res/xml/locales_config.xml | 24 +- app/src/main/res/xml/preferences.xml | 2 +- app/src/main/res/xml/preferences_brapi.xml | 6 +- app/src/main/res/xml/preferences_general.xml | 75 +- app/src/main/res/xml/preferences_language.xml | 32 +- app/src/main/res/xml/preferences_sounds.xml | 2 +- docs/source/_static/images/training_field.png | Bin 0 -> 109570 bytes docs/source/index.rst | 1 + docs/source/training-resources.rst | 12 + settings.gradle | 15 + version.properties | 4 +- 94 files changed, 6101 insertions(+), 2981 deletions(-) create mode 100644 app/proguard-rules.txt create mode 100644 app/src/main/java/com/fieldbook/tracker/dialogs/GeoNavCollectDialog.kt rename app/src/main/java/com/fieldbook/tracker/{objects => utilities}/GeoNavHelper.kt (78%) rename app/src/main/java/com/fieldbook/tracker/{objects => utilities}/GoProWrapper.kt (93%) rename app/src/main/java/com/fieldbook/tracker/{objects => utilities}/VerifyPersonHelper.kt (99%) create mode 100644 app/src/main/java/com/fieldbook/tracker/utilities/VibrateUtil.kt create mode 100644 app/src/main/res/drawable/book_open_variant.xml create mode 100644 app/src/main/res/drawable/ic_sv.png create mode 100644 app/src/main/res/drawable/ic_vi.png create mode 100644 app/src/main/res/drawable/minus.xml create mode 100644 app/src/main/res/layout/dialog_geonav_collect.xml delete mode 100644 app/src/main/res/layout/dialog_sort.xml rename app/src/main/res/{values-am => values-am-rET}/strings.xml (73%) create mode 100644 app/src/main/res/values-ar-rSA/strings.xml delete mode 100644 app/src/main/res/values-ar/strings.xml rename app/src/main/res/{values-bn => values-bn-rBD}/strings.xml (84%) rename app/src/main/res/{values-de => values-de-rDE}/strings.xml (71%) rename app/src/main/res/{values-es => values-es-rMX}/strings.xml (92%) create mode 100644 app/src/main/res/values-fr-rFR/strings.xml delete mode 100644 app/src/main/res/values-fr/strings.xml rename app/src/main/res/{values-hi => values-hi-rIN}/strings.xml (74%) rename app/src/main/res/{values-it => values-it-rIT}/strings.xml (92%) rename app/src/main/res/{values-ja => values-ja-rJP}/strings.xml (92%) create mode 100644 app/src/main/res/values-pa-rIN/strings.xml rename app/src/main/res/{values-ru => values-ru-rRU}/strings.xml (74%) create mode 100644 app/src/main/res/values-sv-rSE/strings.xml create mode 100644 app/src/main/res/values-vi-rVN/strings.xml create mode 100644 app/src/main/res/values/strings_trait_gnss.xml create mode 100644 docs/source/_static/images/training_field.png create mode 100644 docs/source/training-resources.rst diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index 13fa12513..b5d24fd6b 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -1,16 +1,46 @@ on: + schedule: + - cron: "0 20 * * *" workflow_dispatch: - push: - branches: - - main - paths: - - 'app/**' name: do-github-release jobs: + + check-app-changes: + runs-on: ubuntu-latest + outputs: + app_changed: ${{ steps.check_app_changes.outputs.app_changed }} + steps: + - name: Checkout repo + uses: actions/checkout@v2 + with: + token: ${{secrets.ACTIONS_PAT}} + fetch-depth: 0 + + - name: Check if app directory changed + id: check_app_changes + run: | + LAST_RELEASE_TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) + echo "LAST_RELEASE_TAG was $LAST_RELEASE_TAG" + LAST_RELEASE_COMMIT=$(git rev-list -n 1 $LAST_RELEASE_TAG) + echo "LAST_RELEASE_COMMIT was $LAST_RELEASE_COMMIT" + + changed_files=$(git diff-tree --no-commit-id --name-only $LAST_RELEASE_COMMIT $GITHUB_SHA | grep '^app' || echo "none") + echo "Changed app files: $changed_files" + + if [ "$changed_files" != "none" ]; then + echo "App directory has changed since the last release." + echo "app_changed=true" >> "$GITHUB_OUTPUT" + else + echo "App directory hasn't changed since the last release." + echo "app_changed=false" >> "$GITHUB_OUTPUT" + fi + build-and-release: runs-on: ubuntu-latest + needs: check-app-changes + if: ${{ needs.check-app-changes.outputs.app_changed == 'true' }} steps: - name: Checkout repo diff --git a/.github/workflows/pr-prerelease.yml b/.github/workflows/pr-prerelease.yml index ba38f1db2..9e32e7a4c 100644 --- a/.github/workflows/pr-prerelease.yml +++ b/.github/workflows/pr-prerelease.yml @@ -9,6 +9,7 @@ name: do-pr-prerelease jobs: build-and-upload: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index 57f8fa09f..60d4e3a74 100644 --- a/.gitignore +++ b/.gitignore @@ -24,9 +24,6 @@ gradlew.bat # Local configuration file (sdk path, etc) local.properties -# Proguard folder generated by Eclipse -proguard/ - # Log Files *.log diff --git a/app/build.gradle b/app/build.gradle index e54e232a4..dd1ff3685 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,10 +21,6 @@ private Integer makeVersionCode() { } private String makeVersionName() { - if (ext.buildNumber) { - return "${ext.majorVersion}.${ext.minorVersion}.${ext.patchVersion}.${ext.buildNumber}" - } - return "${ext.majorVersion}.${ext.minorVersion}.${ext.patchVersion}" } @@ -72,17 +68,16 @@ android { buildTypes { release { - shrinkResources true - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' - // signingConfig signingConfigs.playStoreConfig //Add your own signing config + minifyEnabled false + debuggable false + //signingConfig signingConfigs.playStoreConfig //Add your own signing config } debug { clean debuggable true manifestPlaceholders = [crashlyticsCollectionEnabled: "false"] - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + //proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } @@ -146,7 +141,7 @@ dependencies { implementation 'com.google.zxing:core:3.3.3' - implementation('com.github.phenoapps:phenolib:v0.9.48') + implementation('com.github.phenoapps:phenolib:v0.9.49') implementation 'com.google.android.exoplayer:exoplayer:2.17.0' implementation 'com.arthenica:ffmpeg-kit-min:5.1.LTS' diff --git a/app/proguard-rules.txt b/app/proguard-rules.txt new file mode 100644 index 000000000..4160b1e89 --- /dev/null +++ b/app/proguard-rules.txt @@ -0,0 +1,2 @@ +-keep public class com.fieldbook.tracker.** + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2880dc211..229199d73 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ - + diff --git a/app/src/main/assets/database/sample.db b/app/src/main/assets/database/sample.db index dca11b3248659ddc642ea85f6e90652ff796791d..553cf7a5e114c86df01d6b50715a38e610e8bd12 100644 GIT binary patch delta 101 zcmZp8z|-)6XM!}N{X`jOR(l4$aIcLibM58%ycl?RpEB@2;4kF&<=5k9=6k_+if=Vv zAfGZHJMYuYjDkmbCqMN~6=q;yU}xsIr-(y3;fy__%Uu@;K!8k F5CEOo9m)Uz diff --git a/app/src/main/assets/field_import/training_sample.csv b/app/src/main/assets/field_import/training_sample.csv index af936a87d..b554f3d32 100644 --- a/app/src/main/assets/field_import/training_sample.csv +++ b/app/src/main/assets/field_import/training_sample.csv @@ -1,4 +1,4 @@ -plot_id,row,column,plot,replicate,germplasm_name,pedigree +plot_id,row,column,plot,replicate,germplasm,pedigree 23TRN010_0101,1,1,0101,1,Barrie,"BW429, CPSR1//CNR17/3/NI98133/SALWIN" 23TRN010_0102,1,2,0102,1,Oahe,Ransom/SD96240-3-1 23TRN010_0103,1,3,0103,1,Jet,MS Chancellor/SD004072 diff --git a/app/src/main/java/com/fieldbook/tracker/activities/AboutActivity.java b/app/src/main/java/com/fieldbook/tracker/activities/AboutActivity.java index 465a9e04d..ff2c4f5ea 100644 --- a/app/src/main/java/com/fieldbook/tracker/activities/AboutActivity.java +++ b/app/src/main/java/com/fieldbook/tracker/activities/AboutActivity.java @@ -24,9 +24,6 @@ import com.fieldbook.tracker.BuildConfig; import com.fieldbook.tracker.R; import com.fieldbook.tracker.preferences.GeneralKeys; -import com.michaelflisar.changelog.ChangelogBuilder; -import com.michaelflisar.changelog.classes.ImportanceChangelogSorter; -import com.michaelflisar.changelog.internal.ChangelogDialogFragment; import com.mikepenz.aboutlibraries.LibsBuilder; import java.io.BufferedReader; @@ -81,16 +78,11 @@ public MaterialAboutList getMaterialAboutList(@NonNull Context c) { appCardBuilder.addItem(updateCheckItem); - appCardBuilder.addItem(new MaterialAboutActionItem.Builder() - .text(getString(R.string.changelog_title)) - .icon(R.drawable.ic_about_changelog) - .setOnClickAction(new MaterialAboutItemOnClickAction() { - @Override - public void onClick() { - showChangelog(false, false); - } - }) - .build()); + appCardBuilder.addItem(ConvenienceBuilder.createWebsiteActionItem(c, + getResources().getDrawable(R.drawable.book_open_variant), + getString(R.string.about_manual_title), + false, + Uri.parse("https://docs.fieldbook.phenoapps.org/en/latest/field-book.html"))); appCardBuilder.addItem(ConvenienceBuilder.createRateActionItem(c, getResources().getDrawable(R.drawable.ic_about_rate), @@ -98,12 +90,6 @@ public void onClick() { null )); - appCardBuilder.addItem(new MaterialAboutActionItem.Builder() - .text(R.string.about_help_translate_title) - .icon(R.drawable.ic_about_help_translate) - .setOnClickAction(ConvenienceBuilder.createWebsiteOnClickAction(c, Uri.parse("https://osij6hx.oneskyapp.com/collaboration/project?id=28259"))) - .build()); - MaterialAboutCard.Builder authorCardBuilder = new MaterialAboutCard.Builder(); authorCardBuilder.title(getString(R.string.about_project_lead_title)); @@ -120,32 +106,22 @@ public void onClick() { getString(R.string.about_developer_trife_email), "Field Book Question")); - authorCardBuilder.addItem(ConvenienceBuilder.createWebsiteActionItem(c, - getResources().getDrawable(R.drawable.ic_about_website), - "PhenoApps.org", - false, - Uri.parse("http://phenoapps.org/"))); - MaterialAboutCard.Builder contributorsCardBuilder = new MaterialAboutCard.Builder(); - contributorsCardBuilder.title(getString(R.string.about_contributors_title)); + contributorsCardBuilder.title(getString(R.string.about_support_title)); + contributorsCardBuilder.addItem(ConvenienceBuilder.createWebsiteActionItem(c, getResources().getDrawable(R.drawable.ic_about_contributors), - getString(R.string.about_contributors_developers_title), + getString(R.string.about_contributors_title), false, - Uri.parse("https://github.com/PhenoApps/Field-Book/graphs/contributors"))); + Uri.parse("https://github.com/PhenoApps/Field-Book#-contributors"))); - contributorsCardBuilder.addItem(new MaterialAboutActionItem.Builder() - .text(getString(R.string.about_translators_title)) - .subText(getString(R.string.about_translators_text)) - .icon(R.drawable.ic_about_translators) - .build()); + contributorsCardBuilder.addItem(ConvenienceBuilder.createWebsiteActionItem(c, + getResources().getDrawable(R.drawable.ic_about_funding), + getString(R.string.about_contributors_funding_title), + false, + Uri.parse("https://github.com/PhenoApps/Field-Book#-funding"))); - contributorsCardBuilder.addItem(new MaterialAboutActionItem.Builder() - .text(getString(R.string.about_contributors_funding_title)) - .subText(getString(R.string.about_contributors_funding_text)) - .icon(R.drawable.ic_about_funding) - .build()); MaterialAboutCard.Builder technicalCardBuilder = new MaterialAboutCard.Builder(); technicalCardBuilder.title(getString(R.string.about_technical_title)); @@ -171,7 +147,7 @@ public void onClick() { final int libStyleId = styleId; technicalCardBuilder.addItem(new MaterialAboutActionItem.Builder() - .text(R.string.libraries_title) + .text(R.string.about_libraries_title) .icon(R.drawable.ic_about_libraries) .setOnClickAction(new MaterialAboutItemOnClickAction() { @Override @@ -179,7 +155,7 @@ public void onClick() { new LibsBuilder() .withActivityTheme(libStyleId) .withAutoDetect(true) - .withActivityTitle(getString(R.string.libraries_title)) + .withActivityTitle(getString(R.string.about_libraries_title)) .withLicenseShown(true) .withVersionShown(true) .start(getApplicationContext()); @@ -190,23 +166,18 @@ public void onClick() { MaterialAboutCard.Builder otherAppsCardBuilder = new MaterialAboutCard.Builder(); otherAppsCardBuilder.title(getString(R.string.about_title_other_apps)); + otherAppsCardBuilder.addItem(ConvenienceBuilder.createWebsiteActionItem(c, + getResources().getDrawable(R.drawable.ic_about_website), + "PhenoApps.org", + false, + Uri.parse("http://phenoapps.org/"))); + otherAppsCardBuilder.addItem(new MaterialAboutActionItem.Builder() .text("Coordinate") .icon(R.drawable.other_ic_coordinate) .setOnClickAction(openAppOrStore("org.wheatgenetics.coordinate", c)) .build()); - otherAppsCardBuilder.addItem(new MaterialAboutActionItem.Builder() - .text("Inventory") - .icon(R.drawable.other_ic_inventory) - .setOnClickAction(openAppOrStore("org.wheatgenetics.inventory", c)) - .build()); - - otherAppsCardBuilder.addItem(new MaterialAboutActionItem.Builder() - .text("Verify") - .icon(R.drawable.other_ic_verify) - .build()); - otherAppsCardBuilder.addItem(new MaterialAboutActionItem.Builder() .text("Intercross") .icon(R.drawable.other_ic_intercross) @@ -335,18 +306,6 @@ private boolean isNewerVersion(String currentVersion, String latestVersion) { return false; } - private void showChangelog(Boolean managedShow, Boolean rateButton) { - ChangelogDialogFragment builder = new ChangelogBuilder() - .withUseBulletList(true) // true if you want to show bullets before each changelog row, false otherwise - .withManagedShowOnStart(managedShow) // library will take care to show activity/dialog only if the changelog has new infos and will only show this new infos - .withRateButton(rateButton) // enable this to show a "rate app" button in the dialog => clicking it will open the play store; the parent activity or target fragment can also implement IChangelogRateHandler to handle the button click - .withSummary(false, true) // enable this to show a summary and a "show more" button, the second paramter describes if releases without summary items should be shown expanded or not - .withTitle(getString(R.string.changelog_title)) // provide a custom title if desired, default one is "Changelog " - .withOkButtonLabel(getString(android.R.string.ok)) // provide a custom ok button text if desired, default one is "OK" - .withSorter(new ImportanceChangelogSorter()) - .buildAndShowDialog(this, false); // second parameter defines, if the dialog has a dark or light theme - } - @Override protected CharSequence getActivityTitle() { return getString(R.string.mal_title_about); 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 f6344dde5..5653527ad 100644 --- a/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java +++ b/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java @@ -8,6 +8,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; +import android.location.Location; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -45,29 +46,33 @@ import com.fieldbook.tracker.database.DataHelper; import com.fieldbook.tracker.database.models.ObservationModel; import com.fieldbook.tracker.database.models.ObservationUnitModel; +import com.fieldbook.tracker.dialogs.GeoNavCollectDialog; import com.fieldbook.tracker.interfaces.FieldSwitcher; +import com.fieldbook.tracker.location.GPSTracker; import com.fieldbook.tracker.objects.FieldObject; -import com.fieldbook.tracker.objects.GeoNavHelper; import com.fieldbook.tracker.objects.InfoBarModel; -import com.fieldbook.tracker.objects.GoProWrapper; import com.fieldbook.tracker.objects.RangeObject; import com.fieldbook.tracker.objects.TraitObject; -import com.fieldbook.tracker.objects.VerifyPersonHelper; import com.fieldbook.tracker.preferences.GeneralKeys; import com.fieldbook.tracker.traits.BaseTraitLayout; import com.fieldbook.tracker.traits.CategoricalTraitLayout; +import com.fieldbook.tracker.traits.GNSSTraitLayout; import com.fieldbook.tracker.traits.GoProTraitLayout; import com.fieldbook.tracker.traits.LayoutCollections; import com.fieldbook.tracker.traits.PhotoTraitLayout; import com.fieldbook.tracker.utilities.CategoryJsonUtil; import com.fieldbook.tracker.utilities.FieldSwitchImpl; +import com.fieldbook.tracker.utilities.GeoNavHelper; import com.fieldbook.tracker.utilities.GnssThreadHelper; +import com.fieldbook.tracker.utilities.GoProWrapper; import com.fieldbook.tracker.utilities.InfoBarHelper; import com.fieldbook.tracker.utilities.LocationCollectorUtil; import com.fieldbook.tracker.utilities.SnackbarUtils; import com.fieldbook.tracker.utilities.SoundHelperImpl; import com.fieldbook.tracker.utilities.TapTargetUtil; import com.fieldbook.tracker.utilities.Utils; +import com.fieldbook.tracker.utilities.VerifyPersonHelper; +import com.fieldbook.tracker.utilities.VibrateUtil; import com.fieldbook.tracker.views.CollectInputView; import com.fieldbook.tracker.views.RangeBoxView; import com.fieldbook.tracker.views.TraitBoxView; @@ -114,7 +119,8 @@ public class CollectActivity extends ThemedActivity com.fieldbook.tracker.interfaces.CollectRangeController, com.fieldbook.tracker.interfaces.CollectTraitController, InfoBarAdapter.InfoBarController, - GoProTraitLayout.GoProCollector { + GoProTraitLayout.GoProCollector, + GPSTracker.GPSTrackerListener { public static final int REQUEST_FILE_EXPLORER_CODE = 1; public static final int BARCODE_COLLECT_CODE = 99; @@ -122,6 +128,9 @@ public class CollectActivity extends ThemedActivity private GeoNavHelper geoNavHelper; + @Inject + VibrateUtil vibrator; + @Inject GnssThreadHelper gnssThreadHelper; @@ -144,6 +153,8 @@ public class CollectActivity extends ThemedActivity @Inject GoProWrapper goProWrapper; + private GPSTracker gps; + public static boolean searchReload; public static String searchRange; public static String searchPlot; @@ -217,6 +228,12 @@ public class CollectActivity extends ThemedActivity private AlertDialog dialogMultiMeasureDelete; private AlertDialog dialogMultiMeasureConfirmDelete; + /** + * GeoNav dialog + */ + private androidx.appcompat.app.AlertDialog dialogGeoNav; + private androidx.appcompat.app.AlertDialog dialogPrecisionLoss; + public void triggerTts(String text) { if (ep.getBoolean(GeneralKeys.TTS_LANGUAGE_ENABLED, false)) { ttsHelper.speak(text); @@ -226,6 +243,8 @@ public void triggerTts(String text) { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + gps = new GPSTracker(this, this, 0, 10000); + guiThread.start(); myGuiHandler = new Handler(guiThread.getLooper()) { @Override @@ -565,7 +584,7 @@ private void initToolbars() { barcodeInput.setOnClickListener(v -> { triggerTts(barcodeTts); new IntentIntegrator(CollectActivity.this) - .setPrompt(getString(R.string.main_barcode_text)) + .setPrompt(getString(R.string.barcode_scanner_text)) .setBeepEnabled(false) .setRequestCode(BARCODE_COLLECT_CODE) .initiateScan(); @@ -815,6 +834,8 @@ public void onPause() { geoNavHelper.stopGeoNav(); + gnssThreadHelper.stop(); + //save the last used trait if (traitBox.getCurrentTrait() != null) ep.edit().putString(GeneralKeys.LAST_USED_TRAIT, traitBox.getCurrentTrait().getTrait()).apply(); @@ -871,9 +892,8 @@ public void onResume() { // Update menu item visibility if (systemMenu != null) { systemMenu.findItem(R.id.help).setVisible(ep.getBoolean(GeneralKeys.TIPS, false)); - systemMenu.findItem(R.id.jumpToPlot).setVisible(ep.getBoolean(GeneralKeys.UNIQUE_TEXT, false)); systemMenu.findItem(R.id.nextEmptyPlot).setVisible(!ep.getString(GeneralKeys.HIDE_ENTRIES_WITH_DATA_TOOLBAR, "1").equals("1")); - systemMenu.findItem(R.id.barcodeScan).setVisible(ep.getBoolean(GeneralKeys.UNIQUE_CAMERA, false)); + systemMenu.findItem(R.id.jumpToPlot).setVisible(!ep.getString(GeneralKeys.MOVE_TO_UNIQUE_ID, "1").equals("1")); systemMenu.findItem(R.id.datagrid).setVisible(ep.getBoolean(GeneralKeys.DATAGRID_SETTING, false)); } @@ -924,10 +944,7 @@ public void onResume() { //setup logger whenever activity resumes geoNavHelper.setupGeoNavLogger(); - secureBluetooth.withNearby((adapter) -> { - geoNavHelper.startGeoNav(); - return null; - }); + startGeoNav(); } verifyPersonHelper.checkLastOpened(); @@ -945,6 +962,17 @@ public void onResume() { refreshLock(); } + private void startGeoNav() { + try { + secureBluetooth.withNearby((adapter) -> { + geoNavHelper.startGeoNav(); + return null; + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + /** * LAST_USED_TRAIT is a preference saved in CollectActivity.onPause * @@ -1118,9 +1146,8 @@ public boolean onCreateOptionsMenu(Menu menu) { systemMenu = menu; systemMenu.findItem(R.id.help).setVisible(ep.getBoolean(GeneralKeys.TIPS, false)); - systemMenu.findItem(R.id.jumpToPlot).setVisible(ep.getBoolean(GeneralKeys.UNIQUE_TEXT, false)); systemMenu.findItem(R.id.nextEmptyPlot).setVisible(!ep.getString(GeneralKeys.HIDE_ENTRIES_WITH_DATA_TOOLBAR, "1").equals("1")); - systemMenu.findItem(R.id.barcodeScan).setVisible(ep.getBoolean(GeneralKeys.UNIQUE_CAMERA, false)); + systemMenu.findItem(R.id.jumpToPlot).setVisible(!ep.getString(GeneralKeys.MOVE_TO_UNIQUE_ID, "1").equals("1")); systemMenu.findItem(R.id.datagrid).setVisible(ep.getBoolean(GeneralKeys.DATAGRID_SETTING, false)); //toggle repeated values indicator @@ -1147,8 +1174,8 @@ public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); } - private TapTarget collectDataTapTargetView(int id, String title, String desc, int color, int targetRadius) { - return TapTargetUtil.Companion.getTapTargetSettingsView(this, findViewById(id), color, targetRadius, title, desc); + private TapTarget collectDataTapTargetView(int id, String title, String desc, int targetRadius) { + return TapTargetUtil.Companion.getTapTargetSettingsView(this, findViewById(id), title, desc, targetRadius); } @Override @@ -1161,7 +1188,6 @@ public boolean onOptionsItemSelected(MenuItem item) { final int resourcesId = R.id.resources; final int nextEmptyPlotId = R.id.nextEmptyPlot; final int jumpToPlotId = R.id.jumpToPlot; - final int barcodeScanId = R.id.barcodeScan; final int dataGridId = R.id.datagrid; final int lockDataId = R.id.lockData; final int summaryId = R.id.summary; @@ -1169,26 +1195,26 @@ public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case helpId: TapTargetSequence sequence = new TapTargetSequence(this) - .targets(collectDataTapTargetView(R.id.act_collect_infobar_rv, getString(R.string.tutorial_main_infobars_title), getString(R.string.tutorial_main_infobars_description), R.color.main_primary_dark,200), - collectDataTapTargetView(R.id.traitLeft, getString(R.string.tutorial_main_traits_title), getString(R.string.tutorial_main_traits_description), R.color.main_primary_dark,60), - collectDataTapTargetView(R.id.traitType, getString(R.string.tutorial_main_traitlist_title), getString(R.string.tutorial_main_traitlist_description), R.color.main_primary_dark,80), - collectDataTapTargetView(R.id.rangeLeft, getString(R.string.tutorial_main_entries_title), getString(R.string.tutorial_main_entries_description), R.color.main_primary_dark,60), - collectDataTapTargetView(R.id.valuesPlotRangeHolder, getString(R.string.tutorial_main_navinfo_title), getString(R.string.tutorial_main_navinfo_description), R.color.main_primary_dark,60), - collectDataTapTargetView(R.id.traitHolder, getString(R.string.tutorial_main_datacollect_title), getString(R.string.tutorial_main_datacollect_description), R.color.main_primary_dark,200), - collectDataTapTargetView(R.id.missingValue, getString(R.string.tutorial_main_na_title), getString(R.string.tutorial_main_na_description), R.color.main_primary,60), - collectDataTapTargetView(R.id.deleteValue, getString(R.string.tutorial_main_delete_title), getString(R.string.tutorial_main_delete_description), R.color.main_primary,60) + .targets(collectDataTapTargetView(R.id.act_collect_infobar_rv, getString(R.string.tutorial_main_infobars_title), getString(R.string.tutorial_main_infobars_description),200), + collectDataTapTargetView(R.id.traitLeft, getString(R.string.tutorial_main_traits_title), getString(R.string.tutorial_main_traits_description),60), + collectDataTapTargetView(R.id.traitType, 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.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) ); if (systemMenu.findItem(R.id.search).isVisible()) { - sequence.target(collectDataTapTargetView(R.id.search, getString(R.string.tutorial_main_search_title), getString(R.string.tutorial_main_search_description), R.color.main_primary_dark,60)); + sequence.target(collectDataTapTargetView(R.id.search, getString(R.string.tutorial_main_search_title), getString(R.string.tutorial_main_search_description),60)); } if (systemMenu.findItem(R.id.resources).isVisible()) { - sequence.target(collectDataTapTargetView(R.id.resources, getString(R.string.tutorial_main_resources_title), getString(R.string.tutorial_main_resources_description), R.color.main_primary_dark,60)); + sequence.target(collectDataTapTargetView(R.id.resources, getString(R.string.tutorial_main_resources_title), getString(R.string.tutorial_main_resources_description),60)); } if (systemMenu.findItem(R.id.summary).isVisible()) { - sequence.target(collectDataTapTargetView(R.id.summary, getString(R.string.tutorial_main_summary_title), getString(R.string.tutorial_main_summary_description), R.color.main_primary_dark,60)); + sequence.target(collectDataTapTargetView(R.id.summary, getString(R.string.tutorial_main_summary_title), getString(R.string.tutorial_main_summary_description),60)); } if (systemMenu.findItem(R.id.lockData).isVisible()) { - sequence.target(collectDataTapTargetView(R.id.lockData, getString(R.string.tutorial_main_lockdata_title), getString(R.string.tutorial_main_lockdata_description), R.color.main_primary_dark,60)); + sequence.target(collectDataTapTargetView(R.id.lockData, getString(R.string.tutorial_main_lockdata_title), getString(R.string.tutorial_main_lockdata_description),60)); } sequence.start(); @@ -1215,14 +1241,16 @@ public boolean onOptionsItemSelected(MenuItem item) { break; case jumpToPlotId: - moveToPlotID(); - break; - case barcodeScanId: - new IntentIntegrator(this) - .setPrompt(getString(R.string.main_barcode_text)) - .setBeepEnabled(false) - .setRequestCode(BARCODE_SEARCH_CODE) - .initiateScan(); + String moveToUniqueIdValue = ep.getString(GeneralKeys.MOVE_TO_UNIQUE_ID, ""); + if (moveToUniqueIdValue.equals("2")) { + moveToPlotID(); + } else if (moveToUniqueIdValue.equals("3")) { + new IntentIntegrator(this) + .setPrompt(getString(R.string.barcode_scanner_text)) + .setBeepEnabled(false) + .setRequestCode(BARCODE_SEARCH_CODE) + .initiateScan(); + } break; case summaryId: showSummary(); @@ -1253,21 +1281,18 @@ public boolean onOptionsItemSelected(MenuItem item) { Log.d(GEOTAG, "Menu item clicked."); - geoNavHelper.setMGeoNavActivated(!geoNavHelper.getMGeoNavActivated()); - MenuItem navItem = systemMenu.findItem(R.id.action_act_collect_geonav_sw); - if (geoNavHelper.getMGeoNavActivated()) { + dialogGeoNav = new GeoNavCollectDialog(this).create(); - navItem.setIcon(R.drawable.ic_explore_black_24dp); + if (!dialogGeoNav.isShowing()) { - mPrefs.edit().putBoolean(GeneralKeys.GEONAV_AUTO, true).apply(); - - } - else { - - navItem.setIcon(R.drawable.ic_explore_off_black_24dp); - - mPrefs.edit().putBoolean(GeneralKeys.GEONAV_AUTO, false).apply(); + if (getWindow().isActive()) { + try { + dialogGeoNav.show(); + } catch (Exception e) { + e.printStackTrace(); + } + } } return true; @@ -1514,7 +1539,7 @@ public void onClick(DialogInterface dialog, int which) { @Override public void onClick(DialogInterface dialogInterface, int i) { new IntentIntegrator(CollectActivity.this) - .setPrompt(getString(R.string.main_barcode_text)) + .setPrompt(getString(R.string.barcode_scanner_text)) .setBeepEnabled(false) .setRequestCode(BARCODE_SEARCH_CODE) .initiateScan(); @@ -1702,7 +1727,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { String msg = getString(R.string.act_collect_barcode_search_exists_in_other_field, fieldName); - SnackbarUtils.showNavigateSnack(getLayoutInflater(), findViewById(R.id.traitHolder), msg, 8000, null, + SnackbarUtils.showNavigateSnack(getLayoutInflater(), findViewById(R.id.traitHolder), msg, R.id.toolbarBottom,8000, null, (v) -> switchField(studyId, null)); } else { @@ -2146,5 +2171,70 @@ public String queryForLabelValue( } } } + + @NonNull + @Override + public VibrateUtil getVibrator() { + return vibrator; + } + + @NonNull + @Override + public Context getContext() { + return this; + } + + public void showLocationPrecisionLossDialog() { + + if (getWindow().isActive()) { + + try { + + if (dialogPrecisionLoss != null) { + dialogPrecisionLoss.dismiss(); + } + + dialogPrecisionLoss = new androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle(getString(R.string.dialog_geonav_precision_loss_title)) + .setMessage(getString(R.string.dialog_geonav_precision_loss_msg)) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + dialog.dismiss(); + }) + .show(); + + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @NonNull + @Override + public GPSTracker getGps() { + return gps; + } + + @Override + public void onLocationChanged(@NonNull Location location) { + + TraitObject trait = getCurrentTrait(); + + if (trait != null) { + + if (trait.getFormat().equals("gnss")) { + + ((GNSSTraitLayout) traitLayouts.getTraitLayout("gnss")) + .onLocationChanged(location); + + } + } + } + @Override + public Location getLocation() { + + if (gps == null) return null; + + return gps.getLocation(0, 0); + } } \ 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 1595fb340..b6f4bfce3 100644 --- a/app/src/main/java/com/fieldbook/tracker/activities/ConfigActivity.java +++ b/app/src/main/java/com/fieldbook/tracker/activities/ConfigActivity.java @@ -335,7 +335,7 @@ private void loadScreen() { barcodeSearchFab = findViewById(R.id.act_config_search_fab); barcodeSearchFab.setOnClickListener(v -> { new IntentIntegrator(this) - .setPrompt(getString(R.string.main_barcode_text)) + .setPrompt(getString(R.string.barcode_scanner_text)) .setBeepEnabled(false) .setRequestCode(REQUEST_BARCODE) .initiateScan(); @@ -853,20 +853,22 @@ protected void onActivityResult(int requestCode, int resultCode, @Nullable Inten @AfterPermissionGranted(PERMISSIONS_REQUEST_EXPORT_DATA) private void exportPermission() { String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE}; - if (EasyPermissions.hasPermissions(this, perms)) { - showSaveDialog(); - } else { - EasyPermissions.requestPermissions(this, getString(R.string.permission_rationale_storage_export), - PERMISSIONS_REQUEST_EXPORT_DATA, perms); - } + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + if (EasyPermissions.hasPermissions(this, perms)) { + showSaveDialog(); + } else { + EasyPermissions.requestPermissions(this, getString(R.string.permission_rationale_storage_export), + PERMISSIONS_REQUEST_EXPORT_DATA, perms); + } + } else showSaveDialog(); } @AfterPermissionGranted(PERMISSIONS_REQUEST_TRAIT_DATA) public void collectDataFilePermission() { - String[] perms = {Manifest.permission.RECORD_AUDIO, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA}; + String[] perms = {Manifest.permission.VIBRATE, Manifest.permission.RECORD_AUDIO, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA}; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - perms = new String[] {Manifest.permission.RECORD_AUDIO, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.CAMERA}; + perms = new String[] {Manifest.permission.VIBRATE, Manifest.permission.RECORD_AUDIO, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.CAMERA}; } if (EasyPermissions.hasPermissions(this, perms)) { 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 cf29b54cf..2638843e9 100644 --- a/app/src/main/java/com/fieldbook/tracker/activities/FieldEditorActivity.java +++ b/app/src/main/java/com/fieldbook/tracker/activities/FieldEditorActivity.java @@ -11,6 +11,7 @@ import android.location.Location; import android.media.MediaScannerConnection; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.provider.OpenableColumns; @@ -52,11 +53,11 @@ import com.fieldbook.tracker.preferences.GeneralKeys; import com.fieldbook.tracker.utilities.DocumentTreeUtil; import com.fieldbook.tracker.utilities.FieldSwitchImpl; +import com.fieldbook.tracker.utilities.SnackbarUtils; import com.fieldbook.tracker.utilities.TapTargetUtil; import com.fieldbook.tracker.utilities.Utils; import com.getkeepsafe.taptargetview.TapTarget; import com.getkeepsafe.taptargetview.TapTargetSequence; -import com.google.android.material.snackbar.Snackbar; import org.phenoapps.utils.BaseDocumentTreeUtil; @@ -177,7 +178,6 @@ public void onCreate(Bundle savedInstanceState) { fieldList = findViewById(R.id.myList); mAdapter = new FieldAdapter(thisActivity, database.getAllFieldObjects(), fieldSwitcher); fieldList.setAdapter(mAdapter); - } private void showFileDialog() { @@ -270,15 +270,16 @@ public void loadCloud() { @AfterPermissionGranted(PERMISSIONS_REQUEST_STORAGE) public void loadLocalPermission() { - String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE}; - if (EasyPermissions.hasPermissions(this, perms)) { - loadLocal(); - } else { - // Do not have permissions, request them now - EasyPermissions.requestPermissions(this, getString(R.string.permission_rationale_storage_import), - PERMISSIONS_REQUEST_STORAGE, perms); - } - + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE}; + if (EasyPermissions.hasPermissions(this, perms)) { + loadLocal(); + } else { + // Do not have permissions, request them now + EasyPermissions.requestPermissions(this, getString(R.string.permission_rationale_storage_import), + PERMISSIONS_REQUEST_STORAGE, perms); + } + } else loadLocal(); } @Override @@ -303,8 +304,8 @@ private TapTarget fieldsTapTargetRect(Rect item, String title, String desc) { return TapTargetUtil.Companion.getTapTargetSettingsRect(this, item, title, desc); } - private TapTarget fieldsTapTargetMenu(int id, String title, String desc) { - return TapTargetUtil.Companion.getTapTargetSettingsView(this, findViewById(id), title, desc); + private TapTarget fieldsTapTargetMenu(int id, String title, String desc, int targetRadius) { + return TapTargetUtil.Companion.getTapTargetSettingsView(this, findViewById(id), title, desc, targetRadius); } //TODO @@ -318,8 +319,8 @@ public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.help: TapTargetSequence sequence = new TapTargetSequence(this) - .targets(fieldsTapTargetMenu(R.id.importField, getString(R.string.tutorial_fields_add_title), getString(R.string.tutorial_fields_add_description)), - fieldsTapTargetMenu(R.id.importField, getString(R.string.tutorial_fields_add_title), getString(R.string.tutorial_fields_file_description)) + .targets(fieldsTapTargetMenu(R.id.importField, getString(R.string.tutorial_fields_add_title), getString(R.string.tutorial_fields_add_description), 60), + fieldsTapTargetMenu(R.id.importField, getString(R.string.tutorial_fields_add_title), getString(R.string.tutorial_fields_file_description), 60) ); if (fieldExists()) { @@ -436,32 +437,39 @@ private void selectPlotByDistance() { int studyId = model.getStudy_id(); + FieldObject study = database.getFieldObject(studyId); + + String studyName = study.getExp_name(); + if (studyId == ep.getInt(GeneralKeys.SELECTED_FIELD_ID, -1)) { - Snackbar.make(findViewById(R.id.field_editor_parent_linear_layout), + SnackbarUtils.showNavigateSnack(getLayoutInflater(), + findViewById(R.id.main_content), getString(R.string.activity_field_editor_switch_field_same), - Snackbar.LENGTH_LONG).show(); + null, + 8000, null, null + ); +// Snackbar.make(findViewById(R.id.field_editor_parent_linear_layout), +// Snackbar.LENGTH_LONG).show(); } else { - Snackbar mySnackbar = Snackbar.make(findViewById(R.id.field_editor_parent_linear_layout), - getString(R.string.activity_field_editor_switch_field, String.valueOf(studyId)), - Snackbar.LENGTH_LONG); - - mySnackbar.setAction(R.string.activity_field_editor_switch_field_action, (view) -> { - - int count = mAdapter.getCount(); - - for (int i = 0; i < count; i++) { - FieldObject field = mAdapter.getItem(i); - if (field.getExp_id() == studyId) { - mAdapter.getView(i, null, null).performClick(); - } - } - - }); - - mySnackbar.show(); + SnackbarUtils.showNavigateSnack( + getLayoutInflater(), + findViewById(R.id.main_content), + getString(R.string.activity_field_editor_switch_field, studyName), + null, + 8000, + null, (v) -> { + int count = mAdapter.getCount(); + + for (int i = 0; i < count; i++) { + FieldObject field = mAdapter.getItem(i); + if (field.getExp_id() == studyId) { + mAdapter.getView(i, null, null).performClick(); + } + } + }); } } 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 6b42467cc..58b50b931 100644 --- a/app/src/main/java/com/fieldbook/tracker/activities/SearchActivity.java +++ b/app/src/main/java/com/fieldbook/tracker/activities/SearchActivity.java @@ -94,6 +94,12 @@ public void onCreate(Bundle savedInstanceState) { start.setOnClickListener(new OnClickListener() { public void onClick(View arg0) { + Spinner c = parent.getChildAt(0).findViewById(R.id.columns); + Spinner s = parent.getChildAt(0).findViewById(R.id.like); + SharedPreferences.Editor ed = ep.edit(); + ed.putInt(GeneralKeys.SEARCH_COLUMN_DEFAULT, c.getSelectedItemPosition()); + ed.putInt(GeneralKeys.SEARCH_LIKE_DEFAULT, s.getSelectedItemPosition()); + ed.apply(); try { // Create the sql query based on user selection @@ -109,8 +115,8 @@ public void onClick(View arg0) { EditText t = child.findViewById(R.id.searchText); - Spinner c = child.findViewById(R.id.columns); - Spinner s = child.findViewById(R.id.like); + c = child.findViewById(R.id.columns); + s = child.findViewById(R.id.like); String value = ""; String prefix; @@ -341,6 +347,15 @@ public void addRow(String text) { parent.addView(v); } + int columnDefault = ep.getInt(GeneralKeys.SEARCH_COLUMN_DEFAULT, 0); + int likeDefault = ep.getInt(GeneralKeys.SEARCH_LIKE_DEFAULT, 0); + if (columnDefault < c.getCount()) { + c.setSelection(columnDefault); + } else { + // Set to first column if the default is not present. + c.setSelection(0); + } + s.setSelection(likeDefault); } @Override 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 d2fad0206..e2380761f 100644 --- a/app/src/main/java/com/fieldbook/tracker/activities/TraitEditorActivity.java +++ b/app/src/main/java/com/fieldbook/tracker/activities/TraitEditorActivity.java @@ -14,6 +14,7 @@ import android.content.SharedPreferences.Editor; import android.graphics.Rect; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.util.Log; @@ -354,8 +355,8 @@ private TapTarget traitsTapTargetRect(Rect item, String title, String desc) { return TapTargetUtil.Companion.getTapTargetSettingsRect(this, item, title, desc); } - private TapTarget traitsTapTargetMenu(int id, String title, String desc) { - return TapTargetUtil.Companion.getTapTargetSettingsView(this, findViewById(id), title, desc); + private TapTarget traitsTapTargetMenu(int id, String title, String desc, int targetRadius) { + return TapTargetUtil.Companion.getTapTargetSettingsView(this, findViewById(id), title, desc, targetRadius); } @Override @@ -364,7 +365,7 @@ public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.help: TapTargetSequence sequence = new TapTargetSequence(this) - .targets(traitsTapTargetMenu(R.id.addTrait, getString(R.string.tutorial_traits_add_title), getString(R.string.tutorial_traits_add_description)) + .targets(traitsTapTargetMenu(R.id.addTrait, getString(R.string.tutorial_traits_add_title), getString(R.string.tutorial_traits_add_description), 60) //Todo add overflow menu action ); @@ -489,31 +490,40 @@ public void onClick(DialogInterface dialog, int which) { @AfterPermissionGranted(PERMISSIONS_REQUEST_STORAGE_IMPORT) public void loadTraitFilePermission() { - String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE}; - if (EasyPermissions.hasPermissions(this, perms)) { - if (ep.getBoolean(GeneralKeys.TRAITS_EXPORTED, false)) { - showFileDialog(); + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE}; + if (EasyPermissions.hasPermissions(this, perms)) { + if (ep.getBoolean(GeneralKeys.TRAITS_EXPORTED, false)) { + showFileDialog(); + } else { + checkTraitExportDialog(); + } } else { - checkTraitExportDialog(); + // Do not have permissions, request them now + EasyPermissions.requestPermissions(this, getString(R.string.permission_rationale_storage_import), + PERMISSIONS_REQUEST_STORAGE_IMPORT, perms); } + } else if (ep.getBoolean(GeneralKeys.TRAITS_EXPORTED, false)) { + showFileDialog(); } else { - // Do not have permissions, request them now - EasyPermissions.requestPermissions(this, getString(R.string.permission_rationale_storage_import), - PERMISSIONS_REQUEST_STORAGE_IMPORT, perms); + checkTraitExportDialog(); } - } @AfterPermissionGranted(PERMISSIONS_REQUEST_STORAGE_EXPORT) public void exportTraitFilePermission() { - String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE}; - if (EasyPermissions.hasPermissions(this, perms)) { - showExportDialog(); - } else { - // Do not have permissions, request them now - EasyPermissions.requestPermissions(this, getString(R.string.permission_rationale_storage_export), - PERMISSIONS_REQUEST_STORAGE_EXPORT, perms); - } + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE}; + if (EasyPermissions.hasPermissions(this, perms)) { + showExportDialog(); + } else { + // Do not have permissions, request them now + EasyPermissions.requestPermissions(this, getString(R.string.permission_rationale_storage_export), + PERMISSIONS_REQUEST_STORAGE_EXPORT, perms); + } + } else showExportDialog(); } private void showImportDialog() { diff --git a/app/src/main/java/com/fieldbook/tracker/brapi/BrapiLoadDialog.java b/app/src/main/java/com/fieldbook/tracker/brapi/BrapiLoadDialog.java index e523f9a73..400716640 100644 --- a/app/src/main/java/com/fieldbook/tracker/brapi/BrapiLoadDialog.java +++ b/app/src/main/java/com/fieldbook/tracker/brapi/BrapiLoadDialog.java @@ -203,6 +203,12 @@ public Void apply(final BrapiStudyDetails study) { @Override public void run() { BrapiStudyDetails.merge(studyDetails, study); + + // This is BMS specific. Remove the traits that are not part of the selected Observation Level. + // To ensure that only relevant traits are included in the imported study/field. + studyDetails.getTraits().removeIf(t -> t.getObservationLevelNames() != null && + t.getObservationLevelNames().stream().noneMatch(s -> s.equalsIgnoreCase(selectedObservationLevel.getObservationLevelName()))); + loadStudy(); // Check if user should save yet traitLoadStatus = true; 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 07332d29a..f7e31f2b4 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 @@ -23,6 +23,9 @@ import com.fieldbook.tracker.utilities.CategoryJsonUtil; import com.fieldbook.tracker.utilities.FailureFunction; import com.fieldbook.tracker.utilities.SuccessFunction; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.reflect.TypeToken; import org.brapi.client.v2.BrAPIClient; import org.brapi.client.v2.model.exceptions.ApiException; @@ -68,6 +71,7 @@ import org.json.JSONException; import org.json.JSONObject; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -79,6 +83,8 @@ public class BrAPIServiceV2 extends AbstractBrAPIService implements BrAPIService { + private static final String ADDITIONAL_INFO_OBSERVATION_LEVEL_NAMES = "observationLevelNames"; + //used to identify field book db id in external references private final String fieldBookReferenceSource = "Field Book Upload"; @@ -1017,6 +1023,16 @@ private Pair, Integer> mapTraits(List + Type listType = new TypeToken>() {}.getType(); + trait.setObservationLevelNames(new Gson().fromJson(observationVariableNames, listType)); + } + // Set some config variables in fieldbook trait.setVisible(true); trait.setRealPosition(0); 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 b1339ceb3..776174633 100644 --- a/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java +++ b/app/src/main/java/com/fieldbook/tracker/database/DataHelper.java @@ -221,6 +221,14 @@ public void updateTraitVisibility(String trait, boolean val) { // String.valueOf(val), trait}); } + public void updateObservationUnit(ObservationUnitModel model, String geoCoordinates) { + + open(); + + ObservationUnitDao.Companion.updateObservationUnit(model, geoCoordinates); + + } + public ObservationUnitModel[] getAllObservationUnits() { open(); diff --git a/app/src/main/java/com/fieldbook/tracker/dialogs/GeoNavCollectDialog.kt b/app/src/main/java/com/fieldbook/tracker/dialogs/GeoNavCollectDialog.kt new file mode 100644 index 000000000..f34f9c625 --- /dev/null +++ b/app/src/main/java/com/fieldbook/tracker/dialogs/GeoNavCollectDialog.kt @@ -0,0 +1,99 @@ +package com.fieldbook.tracker.dialogs + +import android.content.Context +import android.view.LayoutInflater +import android.widget.CheckBox +import android.widget.Spinner +import androidx.appcompat.app.AlertDialog +import com.fieldbook.tracker.R +import com.fieldbook.tracker.activities.CollectActivity +import com.fieldbook.tracker.preferences.GeneralKeys +import com.fieldbook.tracker.utilities.Utils + +class GeoNavCollectDialog(private val activity: CollectActivity) : + AlertDialog.Builder(activity, R.style.AppAlertDialog) { + + private val prefs by lazy { + context.getSharedPreferences(GeneralKeys.SHARED_PREF_FILE_NAME, Context.MODE_PRIVATE) + } + + private var auto + get() = prefs.getBoolean(GeneralKeys.GEONAV_AUTO, false) + set(value) { + prefs.edit().putBoolean(GeneralKeys.GEONAV_AUTO, value).apply() + } + + private var audioOnDrop + get() = prefs.getBoolean(GeneralKeys.GEONAV_CONFIG_AUDIO_ON_DROP, false) + set(value) { + prefs.edit().putBoolean(GeneralKeys.GEONAV_CONFIG_AUDIO_ON_DROP, value).apply() + } + + private var degreeOfPrecision + get() = prefs.getString(GeneralKeys.GEONAV_CONFIG_DEGREE_PRECISION, "Any") + set(value) { + prefs.edit().putString(GeneralKeys.GEONAV_CONFIG_DEGREE_PRECISION, value).apply() + } + + private var autoNavigateCb: CheckBox? = null + private var audioOnDropCb: CheckBox? = null + private var degreeOfPrecisionSp: Spinner? = null + + private val view by lazy { + LayoutInflater.from(context).inflate(R.layout.dialog_geonav_collect, null, false) + } + + init { + setView(R.layout.dialog_geonav_collect) + } + + override fun setView(layoutResId: Int): AlertDialog.Builder { + + setTitle(R.string.dialog_geonav_collect_title) + + autoNavigateCb = view.findViewById(R.id.dialog_geonav_collect_auto_navigate) + audioOnDropCb = view.findViewById(R.id.dialog_geonav_collect_notify_on_precision_loss) + degreeOfPrecisionSp = view.findViewById(R.id.dialog_geonav_collect_precision_threshold) + + loadPreferencesIntoUi() + + setNeutralButton(context.getString(R.string.dialog_geonav_collect_neutral_reconnect)) { dialog, which -> + Utils.makeToast(context, context.getString(R.string.dialog_geonav_collect_reset_start_toast_message)) + activity.getGeoNavHelper().stopGeoNav() + activity.getGeoNavHelper().startGeoNav() + Utils.makeToast(context, context.getString(R.string.dialog_geonav_collect_reset_end_toast_message)) + dialog.dismiss() + } + + setNegativeButton(android.R.string.cancel) { dialog, which -> + dialog.dismiss() + } + + setPositiveButton(android.R.string.ok) { dialog, which -> + saveUiToPreferences() + dialog.dismiss() + } + + return super.setView(view) + } + + private fun loadPreferencesIntoUi() { + + autoNavigateCb?.isChecked = auto + audioOnDropCb?.isChecked = audioOnDrop + degreeOfPrecisionSp?.setSelection( + when (degreeOfPrecision) { + "GPS" -> 1 + "RTK" -> 2 + "Float RTK" -> 3 + else -> 0 + } + ) + } + + private fun saveUiToPreferences() { + auto = autoNavigateCb?.isChecked ?: false + audioOnDrop = audioOnDropCb?.isChecked ?: false + degreeOfPrecision = degreeOfPrecisionSp?.selectedItem.toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fieldbook/tracker/dialogs/NewTraitDialog.java b/app/src/main/java/com/fieldbook/tracker/dialogs/NewTraitDialog.java index 2ccdc6820..3aedfd548 100644 --- a/app/src/main/java/com/fieldbook/tracker/dialogs/NewTraitDialog.java +++ b/app/src/main/java/com/fieldbook/tracker/dialogs/NewTraitDialog.java @@ -152,23 +152,26 @@ public void onClick(DialogInterface dialogInterface, int i) { addCategoryButton.setOnClickListener((v) -> { String value = categoryValueEt.getText().toString(); - if (!value.isEmpty()) { + addCategory(value); + categoryValueEt.setText(""); + }); + } - ArrayList values = new ArrayList<>(); - for (BrAPIScaleValidValuesCategories s : catList) { - values.add(s.getValue()); - } + private void addCategory(String value) { + if (!value.isEmpty()) { + ArrayList values = new ArrayList<>(); + for (BrAPIScaleValidValuesCategories s : catList) { + values.add(s.getValue()); + } - if (!values.contains(value)) { - BrAPIScaleValidValuesCategories scale = new BrAPIScaleValidValuesCategories(); - scale.setLabel(value); - scale.setValue(value); - catList.add(scale); - categoryValueEt.setText(""); - updateCatAdapter(); - } + if (!values.contains(value)) { + BrAPIScaleValidValuesCategories scale = new BrAPIScaleValidValuesCategories(); + scale.setLabel(value); + scale.setValue(value); + catList.add(scale); + updateCatAdapter(); } - }); + } } private void updateCatAdapter() { @@ -368,7 +371,11 @@ private TraitObject createTraitObjectByDialogItems(int pos) { t.setMinimum(minimum.getText().toString()); t.setMaximum(maximum.getText().toString()); t.setDetails(details.getText().toString()); - //t.setCategories(categoryLabelEt.getText().toString()); + String finalCat = categoryValueEt.getText().toString(); + if (!finalCat.isEmpty()) { + addCategory(finalCat); + categoryValueEt.setText(""); + } try { diff --git a/app/src/main/java/com/fieldbook/tracker/interfaces/CollectController.kt b/app/src/main/java/com/fieldbook/tracker/interfaces/CollectController.kt index 4abce039b..93a9e94bf 100644 --- a/app/src/main/java/com/fieldbook/tracker/interfaces/CollectController.kt +++ b/app/src/main/java/com/fieldbook/tracker/interfaces/CollectController.kt @@ -1,20 +1,27 @@ package com.fieldbook.tracker.interfaces import android.content.Context +import android.location.Location import android.os.Handler -import com.fieldbook.tracker.objects.GeoNavHelper +import com.fieldbook.tracker.location.GPSTracker +import com.fieldbook.tracker.utilities.GeoNavHelper import com.fieldbook.tracker.utilities.GnssThreadHelper import com.fieldbook.tracker.utilities.SoundHelperImpl +import com.fieldbook.tracker.utilities.VibrateUtil import com.fieldbook.tracker.views.CollectInputView import com.fieldbook.tracker.views.RangeBoxView import com.fieldbook.tracker.views.TraitBoxView import org.phenoapps.security.SecureBluetoothActivityImpl interface CollectController: FieldController { + fun getContext(): Context + fun getGps(): GPSTracker + fun getLocation(): Location? fun getRangeBox(): RangeBoxView fun getTraitBox(): TraitBoxView fun getInputView(): CollectInputView fun getSoundHelper(): SoundHelperImpl + fun getVibrator(): VibrateUtil fun resetGeoNavMessages() fun getGeoNavHelper(): GeoNavHelper fun getSecurityChecker(): SecureBluetoothActivityImpl 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 73cf0a08f..3c6dee685 100644 --- a/app/src/main/java/com/fieldbook/tracker/location/GPSTracker.java +++ b/app/src/main/java/com/fieldbook/tracker/location/GPSTracker.java @@ -112,6 +112,10 @@ private Location getLastLocation(long minDistance, long minTime) { return location; } + public Location getLastLocation() { + return location; + } + public Location getLocation() { return getLastLocation(MIN_DISTANCE_CHANGE_FOR_UPDATES, MIN_TIME_BW_UPDATES); } diff --git a/app/src/main/java/com/fieldbook/tracker/location/gnss/ConnectThread.kt b/app/src/main/java/com/fieldbook/tracker/location/gnss/ConnectThread.kt index 6f6c7a04f..a52fd0a30 100644 --- a/app/src/main/java/com/fieldbook/tracker/location/gnss/ConnectThread.kt +++ b/app/src/main/java/com/fieldbook/tracker/location/gnss/ConnectThread.kt @@ -5,7 +5,7 @@ import android.bluetooth.BluetoothSocket import android.os.Handler import android.util.Log import java.io.IOException -import java.util.* +import java.util.UUID class ConnectThread(device: BluetoothDevice, private val handler: Handler) : Thread() { @@ -71,8 +71,6 @@ class ConnectThread(device: BluetoothDevice, private val handler: Handler) : Thr mmSocket?.close() - this.handler.removeCallbacksAndMessages(null) - } catch (e: IOException) { Log.e(TAG, "Could not close the client socket", e) diff --git a/app/src/main/java/com/fieldbook/tracker/location/gnss/NmeaParser.kt b/app/src/main/java/com/fieldbook/tracker/location/gnss/NmeaParser.kt index 08650073e..693236920 100644 --- a/app/src/main/java/com/fieldbook/tracker/location/gnss/NmeaParser.kt +++ b/app/src/main/java/com/fieldbook/tracker/location/gnss/NmeaParser.kt @@ -3,7 +3,6 @@ package com.fieldbook.tracker.location.gnss import java.math.BigDecimal import java.math.RoundingMode import java.text.ParseException -import java.util.* /** * NMEA 0183 parser. @@ -225,4 +224,27 @@ class NmeaParser { else -> "invalid" } } + + /** + * For now just display GPS, RTK, Float RTK, or invalid + */ + fun getSimpleFix() = when (fix) { + "GPS", "DGPS", "PPS" -> "GPS" + "RTK" -> "RTK" + "invalid" -> "invalid" + else -> "Float RTK" + } + + fun compareFix(fix: String, precisionThresh: String): Boolean { + + if (precisionThresh == "Any") return true + + if (precisionThresh == "Float RTK" && fix == "Float RTK") return true + + if (precisionThresh == "RTK" && (fix == "RTK" || fix == "Float RTK")) return true + + if (precisionThresh == "GPS" && (fix == "GPS" || fix == "RTK" || fix == "Float RTK")) return true + + return false + } } 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 189208d99..3268febde 100644 --- a/app/src/main/java/com/fieldbook/tracker/objects/TraitObject.java +++ b/app/src/main/java/com/fieldbook/tracker/objects/TraitObject.java @@ -4,6 +4,8 @@ import androidx.annotation.NonNull; +import java.util.List; + /** * Simple wrapper class for trait data */ @@ -22,6 +24,12 @@ public class TraitObject { private String traitDataSource; private String additionalInfo; + /** + * This is a BMS specific field. This will be populated when traits are imported from + * the BMS implementation of Brapi 2.0 GET /variables. + */ + private List observationLevelNames; + public String getTrait() { return trait; } @@ -126,6 +134,14 @@ public void setAdditionalInfo(String additionalInfo) { this.additionalInfo = additionalInfo; } + public List getObservationLevelNames() { + return observationLevelNames; + } + + public void setObservationLevelNames(List observationLevelNames) { + this.observationLevelNames = observationLevelNames; + } + public boolean isValidValue(final String s) { // this code is not perfect. // I think that it is necessary to check diff --git a/app/src/main/java/com/fieldbook/tracker/preferences/BrapiPreferencesFragment.java b/app/src/main/java/com/fieldbook/tracker/preferences/BrapiPreferencesFragment.java index 8a4ed0300..115b55810 100644 --- a/app/src/main/java/com/fieldbook/tracker/preferences/BrapiPreferencesFragment.java +++ b/app/src/main/java/com/fieldbook/tracker/preferences/BrapiPreferencesFragment.java @@ -141,7 +141,7 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { brapiServerBarcode.setOnPreferenceClickListener(preference -> { new IntentIntegrator(getActivity()) - .setPrompt(getString(R.string.main_barcode_text)) + .setPrompt(getString(R.string.barcode_scanner_text)) .setBeepEnabled(true) .setRequestCode(IntentIntegrator.REQUEST_CODE) .initiateScan(); @@ -263,14 +263,14 @@ public void onDisplayPreferenceDialog(Preference preference) { //change request code for brapi url vs oidc url if (preference.getKey().equals(brapiURLPreference.getKey())) { new IntentIntegrator(getActivity()) - .setPrompt(getString(R.string.main_barcode_text)) + .setPrompt(getString(R.string.barcode_scanner_text)) .setBeepEnabled(false) .setRequestCode(REQUEST_BARCODE_SCAN_BASE_URL) .initiateScan(); } else { prefMgr.getSharedPreferences().edit().putBoolean(GeneralKeys.BRAPI_EXPLICIT_OIDC_URL, true).apply(); new IntentIntegrator(getActivity()) - .setPrompt(getString(R.string.main_barcode_text)) + .setPrompt(getString(R.string.barcode_scanner_text)) .setBeepEnabled(false) .setRequestCode(REQUEST_BARCODE_SCAN_OIDC_URL) .initiateScan(); diff --git a/app/src/main/java/com/fieldbook/tracker/preferences/DatabasePreferencesFragment.java b/app/src/main/java/com/fieldbook/tracker/preferences/DatabasePreferencesFragment.java index 6bc5d989e..eb26f5407 100644 --- a/app/src/main/java/com/fieldbook/tracker/preferences/DatabasePreferencesFragment.java +++ b/app/src/main/java/com/fieldbook/tracker/preferences/DatabasePreferencesFragment.java @@ -10,6 +10,7 @@ import android.content.SharedPreferences; import android.net.Uri; import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.text.Html; @@ -467,27 +468,33 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { @AfterPermissionGranted(PERMISSIONS_REQUEST_DATABASE_EXPORT) public void exportDatabaseFilePermission() { - String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE}; - if (EasyPermissions.hasPermissions(getContext(), perms)) { - showDatabaseExportDialog(); - } else { - // Do not have permissions, request them now - EasyPermissions.requestPermissions(this, getString(R.string.permission_rationale_storage_export), - PERMISSIONS_REQUEST_DATABASE_EXPORT, perms); - } - } - @AfterPermissionGranted(PERMISSIONS_REQUEST_DATABASE_IMPORT) - public void importDatabaseFilePermission() { - String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE}; - if (getContext() != null) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE}; if (EasyPermissions.hasPermissions(getContext(), perms)) { - showDatabaseImportDialog(); + showDatabaseExportDialog(); } else { // Do not have permissions, request them now - EasyPermissions.requestPermissions(this, getString(R.string.permission_rationale_storage_import), - PERMISSIONS_REQUEST_DATABASE_IMPORT, perms); + EasyPermissions.requestPermissions(this, getString(R.string.permission_rationale_storage_export), + PERMISSIONS_REQUEST_DATABASE_EXPORT, perms); } - } + } else showDatabaseExportDialog(); + } + + @AfterPermissionGranted(PERMISSIONS_REQUEST_DATABASE_IMPORT) + public void importDatabaseFilePermission() { + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE}; + if (getContext() != null) { + if (EasyPermissions.hasPermissions(getContext(), perms)) { + showDatabaseImportDialog(); + } else { + // Do not have permissions, request them now + EasyPermissions.requestPermissions(this, getString(R.string.permission_rationale_storage_import), + PERMISSIONS_REQUEST_DATABASE_IMPORT, perms); + } + } + } else showDatabaseImportDialog(); } } \ 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 592629c32..46bf4ae61 100644 --- a/app/src/main/java/com/fieldbook/tracker/preferences/GeneralKeys.java +++ b/app/src/main/java/com/fieldbook/tracker/preferences/GeneralKeys.java @@ -37,8 +37,7 @@ public class GeneralKeys { 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 UNIQUE_CAMERA = "BarcodeScan"; - public static final String UNIQUE_TEXT = "JumpToPlot"; + 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"; public static final String HIDE_ENTRIES_WITH_DATA_TOOLBAR = "com.fieldbook.tracker.HIDE_ENTRIES_WITH_DATA_TOOLBAR"; @@ -89,11 +88,22 @@ public class GeneralKeys { public static final String GEONAV_PARAMETER_D1 = GEONAV_PREFIX + "parameters.trapezoid.D1"; public static final String GEONAV_PARAMETER_D2 = GEONAV_PREFIX + "parameters.trapezoid.D2"; public static final String GEONAV_SEARCH_METHOD = GEONAV_PREFIX + "SEARCH_METHOD"; + + // GeoNav Configuration Preferences + public static final String GEONAV_CONFIG_AUDIO_ON_DROP = GEONAV_PREFIX + "AUDIO_ON_DROP"; + + public static final String GEONAV_CONFIG_DEGREE_PRECISION = GEONAV_PREFIX + "DEGREE_PRECISION"; // @formatter:on // GNSS public static final String GNSS_LAST_PAIRED_DEVICE_NAME = "GNSS_LAST_PAIRED_DEVICE_NAME"; + public static final String GNSS_LAST_CHOSEN_PRECISION = "GNSS_LAST_CHOSEN_PRECISION"; + + public static final String GNSS_WARNED_PRECISION = "GNSS_WARNED_PRECISION"; + + public static final String GNSS_PRECISION_OK_SOUND = "GNSS_PRECISION_OK_SOUND"; + //Beta feature keys public static final String REPEATED_VALUES_PREFERENCE_KEY = "com.tracker.fieldbook.preferences.keys.repeated_values"; @@ -150,6 +160,10 @@ public class GeneralKeys { //preference key to save the last plot during collect activity public static final String LAST_PLOT = "lastplot"; + // collect search activity defaults + public static final String SEARCH_COLUMN_DEFAULT = "SEARCH_COLUMN_DEFAULT"; + public static final String SEARCH_LIKE_DEFAULT = "SEARCH_LIKE_DEFAULT"; + public static final String DATA_LOCK_STATE = "DataLockState"; //export flags diff --git a/app/src/main/java/com/fieldbook/tracker/preferences/GeneralPreferencesFragment.java b/app/src/main/java/com/fieldbook/tracker/preferences/GeneralPreferencesFragment.java index 68287c677..c575d0262 100644 --- a/app/src/main/java/com/fieldbook/tracker/preferences/GeneralPreferencesFragment.java +++ b/app/src/main/java/com/fieldbook/tracker/preferences/GeneralPreferencesFragment.java @@ -64,6 +64,20 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { } + Preference moveToUniqueIdPref = this.findPreference(GeneralKeys.MOVE_TO_UNIQUE_ID); + + if (skipEntriesPref != null) { + + //set preference change listener to change summary when needed + moveToUniqueIdPref.setOnPreferenceChangeListener(this); + + //also initialize the summary whenever the fragment is opened, or else it defaults to "disabled" + String moveMode = prefMgr.getSharedPreferences().getString(GeneralKeys.MOVE_TO_UNIQUE_ID, "1"); + + switchMovePreferenceMode(moveMode, moveToUniqueIdPref); + + } + updateLocationCollectionPreference(); } @@ -103,6 +117,36 @@ private void switchSkipPreferenceMode(String mode, Preference preference) { } } + private void switchMovePreferenceMode(String mode, Preference preference) { + + switch (mode) { + + case "2": { + + preference.setSummary(R.string.move_to_unique_id_text_or_scan_description); + + break; + + } + + case "3": { + + preference.setSummary(R.string.move_to_unique_id_direct_camera_scan_description); + + break; + + } + + default: { + + preference.setSummary(R.string.preferences_general_feature_barcode_text_description); + + break; + + } + } + } + @Override public void onAttach(Context context) { super.onAttach(context); @@ -122,6 +166,10 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { switchSkipPreferenceMode((String) newValue, preference); + } else if (preference.getKey().equals(GeneralKeys.MOVE_TO_UNIQUE_ID)) { + + switchMovePreferenceMode((String) newValue, preference); + } } diff --git a/app/src/main/java/com/fieldbook/tracker/preferences/LanguagePreferenceFragment.kt b/app/src/main/java/com/fieldbook/tracker/preferences/LanguagePreferenceFragment.kt index effbb1f98..852aa2871 100644 --- a/app/src/main/java/com/fieldbook/tracker/preferences/LanguagePreferenceFragment.kt +++ b/app/src/main/java/com/fieldbook/tracker/preferences/LanguagePreferenceFragment.kt @@ -21,9 +21,9 @@ class LanguagePreferenceFragment : PreferenceFragmentCompat(), Preference.OnPref getString(R.string.preferences_appearance_language) for (key in setOf("com.fieldbook.tracker.preference.language.default", - "am", "ar", "bn", "de", "en", "es", - "fr", "hi", "it", "ja", "om-ET", "pt-BR", - "ru", "zh-CN")) { + "am-ET", "ar-SA", "bn-BD", "de-DE", "en-US", "es-MX", + "fr-FR", "hi-IN", "it-IT", "ja-JP", "om-ET", "pt-BR", + "ru-RU", "sv-SE", "vi-VN", "zh-CN")) { findPreference(key)?.onPreferenceClickListener = this } } diff --git a/app/src/main/java/com/fieldbook/tracker/storage/StorageDefinerFragment.kt b/app/src/main/java/com/fieldbook/tracker/storage/StorageDefinerFragment.kt index 5938f54d9..1981bb147 100644 --- a/app/src/main/java/com/fieldbook/tracker/storage/StorageDefinerFragment.kt +++ b/app/src/main/java/com/fieldbook/tracker/storage/StorageDefinerFragment.kt @@ -26,6 +26,7 @@ class StorageDefinerFragment: PhenoLibStorageDefinerFragment() { AssetSample("field_import", "field_sample2.csv") to R.string.dir_field_import, AssetSample("field_import", "field_sample3.csv") to R.string.dir_field_import, AssetSample("field_import", "rtk_sample.csv") to R.string.dir_field_import, + AssetSample("field_import", "training_sample.csv") to R.string.dir_field_import, AssetSample("trait", "trait_sample.trt") to R.string.dir_trait, AssetSample("trait", "severity.txt") to R.string.dir_trait, AssetSample("database", "sample.db") to R.string.dir_database, 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 8440f6112..10abd5c7d 100644 --- a/app/src/main/java/com/fieldbook/tracker/traits/DateTraitLayout.java +++ b/app/src/main/java/com/fieldbook/tracker/traits/DateTraitLayout.java @@ -240,6 +240,7 @@ private void loadSelectedDate() { ObservationModel model = getCurrentObservation(); date = model.getValue(); log(); + //afterLoadExists((CollectActivity) getContext(), date); } catch (Exception e) { e.printStackTrace(); } @@ -272,15 +273,19 @@ public void refreshLayout(Boolean onNew) { if (!block() || isFirstLoad) { isFirstLoad = false; super.refreshLayout(onNew); - loadSelectedDate(); + if (!onNew) { + ObservationModel model = getCurrentObservation(); + if (model != null) { + date = getCurrentObservation().getValue(); + } + refreshDateText(date); + } } else { Utils.makeToast(getContext(), getContext().getString(R.string.view_repeated_values_add_button_fail)); } } - @Override - public void afterLoadExists(CollectActivity act, @Nullable String value) { - super.afterLoadExists(act, value); + private void refreshDateText(String value) { //first check if observation values is observed for this plot and the value is not NA if (value != null && !value.equals("NA")) { @@ -333,6 +338,13 @@ public void afterLoadExists(CollectActivity act, @Nullable String value) { forceDataSavedColor(); } + } + + @Override + public void afterLoadExists(CollectActivity act, @Nullable String value) { + super.afterLoadExists(act, value); + + refreshDateText(value); isBlocked = false; } 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 5e590ceb3..b66605216 100644 --- a/app/src/main/java/com/fieldbook/tracker/traits/GNSSTraitLayout.kt +++ b/app/src/main/java/com/fieldbook/tracker/traits/GNSSTraitLayout.kt @@ -12,12 +12,17 @@ import android.os.Looper import android.os.Message import android.util.AttributeSet import android.view.View +import android.widget.AdapterView +import android.widget.AdapterView.OnItemSelectedListener import android.widget.ImageButton +import android.widget.ProgressBar +import android.widget.Spinner import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SwitchCompat import androidx.constraintlayout.widget.Group +import androidx.core.view.isVisible import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.fieldbook.tracker.R import com.fieldbook.tracker.activities.CollectActivity @@ -34,6 +39,7 @@ import com.fieldbook.tracker.utilities.GeodeticUtils.Companion.truncateFixQualit import com.fieldbook.tracker.utilities.GnssThreadHelper import com.google.android.material.chip.ChipGroup import org.json.JSONObject +import java.util.UUID import kotlin.math.atan2 import kotlin.math.cos import kotlin.math.sin @@ -51,17 +57,26 @@ import kotlin.math.sqrt */ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener { + companion object { + const val CONNECTION_STATUS_INTERVAL = 5000L + } + private var mActivity: Activity? = null //used for communication between threads and ui thread private lateinit var mLocalBroadcastManager: LocalBroadcastManager - private var mGpsTracker: GPSTracker? = null - private var mLastDevice: BluetoothDevice? = null private var mProgressDialog: AlertDialog? = null + private var precision: String? = null + + private var currentFixQuality = false + + //flag to track when collect button is disabled + private var isCollectEnabled = false + private lateinit var chipGroup: ChipGroup private lateinit var averageSwitch: SwitchCompat private lateinit var utcTextView: TextView @@ -71,10 +86,15 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener { private lateinit var latTextView: TextView private lateinit var lngTextView: TextView private lateinit var hdopTextView: TextView + private lateinit var precisionSp: Spinner private lateinit var connectGroup: Group private lateinit var connectButton: ImageButton private lateinit var collectButton: ImageButton private lateinit var disconnectButton: ImageButton + private lateinit var progressBar: ProgressBar + + private var lastUtc = String() + private var currentUtc = String() private val mAverageResponseHandler = Handler(Looper.getMainLooper()) { @@ -93,7 +113,10 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener { constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {} data class AverageInfo(var unit: ObservationUnitModel, var location: Location?, - var points: List>, val latLength: Int, val lngLength: Int) + var points: List>, + val latLength: Int, + val lngLength: Int, + val precision: String) private fun getThreadHelper(): GnssThreadHelper { return controller.getGnssThreadHelper() @@ -138,6 +161,33 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener { return R.layout.trait_gnss } + private val receiver = object : GNSSResponseReceiver() { + override fun onGNSSParsed(parser: NmeaParser) { + + currentUtc = parser.utc + + checkBeforeUpdate(parser.getSimpleFix()) { + + //populate ui + accTextView.text = parser.fix + latTextView.text = truncateFixQuality(parser.latitude) + lngTextView.text = truncateFixQuality(parser.longitude) + utcTextView.text = parser.utc + hdopTextView.text = parser.hdop + + if (parser.satellites.isEmpty()) { + satTextView.text = "${parser.gsv.size}" + } else { + val maxSats = maxOf(parser.satellites.toInt(), parser.gsv.size) + satTextView.text = "${parser.gsv.size}/$maxSats" + } + altTextView.text = truncateFixQuality(parser.altitude) + + resolveUiStatus() + } + } + } + private fun initialize() { mProgressDialog = AlertDialog.Builder(context) @@ -157,29 +207,9 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener { * The parser parameter is a model for the parsed message, and is used to populate the * trait layout UI. */ - mLocalBroadcastManager.registerReceiver( - object : GNSSResponseReceiver() { - override fun onGNSSParsed(parser: NmeaParser) { - - //populate ui - accTextView.text = parser.fix - latTextView.text = truncateFixQuality(parser.latitude, parser.fix) - lngTextView.text = truncateFixQuality(parser.longitude, parser.fix) - utcTextView.text = parser.utc - hdopTextView.text = parser.hdop - - if (parser.satellites.isEmpty()) { - satTextView.text = "${parser.gsv.size}" - } else { - val maxSats = maxOf(parser.satellites.toInt(), parser.gsv.size) - satTextView.text = "${parser.gsv.size}/$maxSats" - } - altTextView.text = truncateFixQuality(parser.altitude, parser.fix) - } + mLocalBroadcastManager.registerReceiver(receiver, filter) - }, - filter - ) + precision = prefs.getString(GeneralKeys.GNSS_LAST_CHOSEN_PRECISION, "Any") ?: "Any" setupChooseBluetoothDevice() @@ -231,6 +261,8 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener { connectGroup = act.findViewById(R.id.gnss_group) collectButton = act.findViewById(R.id.gnss_collect_button) disconnectButton = act.findViewById(R.id.disconnect_button) + precisionSp = act.findViewById(R.id.precisionSpinner) + progressBar = act.findViewById(R.id.trait_gnss_pb) connectButton.requestFocus() @@ -312,7 +344,7 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener { * First the selected studyDbId is found in the preferences, and the static ObservationUnitDao * is used to find the relevant Obs. Unit. and update the row with the NMEA data. */ - private fun submitGnss(latitude: String, longitude: String, elevation: String) { + private fun submitGnss(latitude: String, longitude: String, elevation: String, precision: String) { if (latitude.isNotBlank() && longitude.isNotBlank()) { @@ -321,7 +353,7 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener { //geo json object : elevation (stored in obs. units, used in navigation) //geo json has properties map for additional info val geoJson = GeoJSON(geometry = Geometry(coordinates = arrayOf(latitude, longitude)), - properties = mapOf("altitude" to elevation)) + properties = mapOf("altitude" to elevation, "fix" to precision)) //save fix length to truncate the average later if needed val latLength = latitude.length @@ -350,7 +382,7 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener { //listen for the duration and append lat/lngs to an array val pointsToAverage = arrayListOf>() - val info = AverageInfo(unit, location, pointsToAverage, latLength, lngLength) + val info = AverageInfo(unit, location, pointsToAverage, latLength, lngLength, precision) if (avgDuration > -1L) { if (location != null) { @@ -415,7 +447,7 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener { private fun updateCoordinateObservation(unit: ObservationUnitModel, json: GeoJSON) { - val coordinates = "${json.geometry.coordinates[0]}; ${json.geometry.coordinates[1]}" + val coordinates = "${json.geometry.coordinates[0]}; ${json.geometry.coordinates[1]}; ${json.properties?.get("fix")}" ObservationUnitDao.updateObservationUnit(unit, json.toJson().toString()) @@ -477,7 +509,9 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener { val averageJson = GeoJSON(geometry = Geometry( coordinates = arrayOf(avgPoint.first.toString(), avgPoint.second.toString())), - properties = mapOf("altitude" to (location?.altitude?.toString() ?: ""))) + properties = mapOf( + "altitude" to (location?.altitude?.toString() ?: ""), + "fix" to info.precision)) updateCoordinateObservation(unit, averageJson) } @@ -503,8 +537,6 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener { **/ private fun findPairedDevice() { - clearUi() - //check if the device has bluetooth enabled, if not, request it to be enabled via system action controller.getSecurityChecker().withAdapter { adapter -> @@ -556,11 +588,15 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener { val chosenDevice = pairedDevices.find { it.name == value } if (chosenDevice == null) { - //register the location listener - //update no matter the distance change and every 10s - mGpsTracker = GPSTracker(context, this, 0, 10000) + + controller.getLocation()?.let { loc -> + + onLocationChanged(loc) + + } triggerTts(internal) + } else { val deviceTts = context.getString(R.string.trait_gnss_external_device_tts, chosenDevice.name) @@ -574,18 +610,11 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener { } } - override fun onExit() { - super.onExit() - //if (::mConnectThread.isInitialized) mConnectThread.cancel() - } - /** * Starts connect thread and sets up the UI. */ private fun setupCommunicationsUi(value: BluetoothDevice? = null) { - //BluetoothAdapter.getDefaultAdapter().cancelDiscovery() - if (value != null) { mLastDevice = value if (!getThreadHelper().isAlive) getThreadHelper().start(value, mHandler) @@ -598,18 +627,38 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener { collectButton.setOnClickListener { - val latitude = latTextView.text.toString() - val longitude = lngTextView.text.toString() - val elevation = altTextView.text.toString() + if (!isCollectEnabled) { - submitGnss(latitude, longitude, elevation) + soundWarning() - triggerTts(context.getString(R.string.trait_location_saved_tts)) + } else { + val latitude = latTextView.text.toString() + val longitude = lngTextView.text.toString() + val elevation = altTextView.text.toString() + val precision = accTextView.text.toString() + + val isFloat = try { + precision.toDouble() + true + } catch (e: NumberFormatException) { + false + } + + submitGnss(latitude, longitude, elevation, if (isFloat) "GPS" else precision) + + triggerTts(context.getString(R.string.trait_location_saved_tts)) + + } } //cancel the thread when the disconnect button is pressed disconnectButton.setOnClickListener { + + clearUi() + + collectButton.visibility = View.INVISIBLE + progressBar.visibility = View.INVISIBLE connectButton.visibility = View.VISIBLE connectGroup.visibility = View.GONE @@ -618,19 +667,117 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener { mHandler.removeMessages(GNSSResponseReceiver.MESSAGE_OUTPUT_FAIL) } - if (mGpsTracker != null) { - mGpsTracker = null - } - chipGroup.visibility = View.GONE + prefs.edit().remove(GeneralKeys.GNSS_LAST_PAIRED_DEVICE_NAME).apply() + setupChooseBluetoothDevice() - prefs.edit().remove(GeneralKeys.GNSS_LAST_PAIRED_DEVICE_NAME).apply() } + precisionSp.setSelection( + when (precision) { + "GPS" -> 1 + "RTK" -> 2 + "Float RTK" -> 3 + else -> 0 + } + ) + + Handler(Looper.getMainLooper()).postDelayed({ + + precisionSp.onItemSelectedListener = object : OnItemSelectedListener { + + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { + + val newPrecision = precisionSp.selectedItem.toString() + + if (!NmeaParser().compareFix(precision ?: "Any", newPrecision)) { + + if (newPrecision in setOf("GPS", "Any")) { + + soundOk() + + isCollectEnabled = true + + prefs.edit().remove(GeneralKeys.GNSS_WARNED_PRECISION).apply() + + } else { + + showWarning() + + isCollectEnabled = false + + prefs.edit().remove(GeneralKeys.GNSS_PRECISION_OK_SOUND).apply() + + } + + } else { + + soundOk() + + isCollectEnabled = true + + prefs.edit().remove(GeneralKeys.GNSS_WARNED_PRECISION).apply() + + } + + precision = newPrecision + + prefs.edit().putString(GeneralKeys.GNSS_LAST_CHOSEN_PRECISION, precision).apply() + + } + + override fun onNothingSelected(parent: AdapterView<*>?) {} + + } + }, 1000) + setupAveragingUi() + connectionCheckHandler() + + } + + private fun resolveUiStatus() { + if (!connectButton.isVisible) { + if (utcTextView.text.toString().isBlank()) { + connectGroup.visibility = View.INVISIBLE + progressBar.visibility = View.VISIBLE + disconnectButton.visibility = View.VISIBLE + collectButton.visibility = View.VISIBLE + } else { + connectGroup.visibility = View.VISIBLE + progressBar.visibility = View.INVISIBLE + } + } + } + + private fun connectionCheckHandler() { + + val deviceName = prefs.getString(GeneralKeys.GNSS_LAST_PAIRED_DEVICE_NAME, null) + + if (deviceName != context.getString(R.string.pref_behavior_geonav_internal_gps_choice)) { + + if (currentUtc.isNotBlank() && (currentUtc == lastUtc)) { + + clearUi() + + } + } + + lastUtc = currentUtc + + resolveUiStatus() + + Handler(Looper.getMainLooper()).postDelayed({ + connectionCheckHandler() + }, CONNECTION_STATUS_INTERVAL) } private fun clearUi() { @@ -667,7 +814,7 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener { .filter { it.observation_unit_db_id == currentRange.plot_id } if (units.isNotEmpty()) { units.first().let { unit -> - ObservationUnitDao.updateObservationUnit(unit, "") + controller.getDatabase().updateObservationUnit(unit, "") } } } @@ -711,33 +858,83 @@ class GNSSTraitLayout : BaseTraitLayout, GPSTracker.GPSTrackerListener { */ override fun onLocationChanged(location: Location) { - //LocationManager accuracy is horizontal accuracy in meters - //>=100 use three decimal places - val fixQuality = when (location.accuracy) { - in 0.0..11.0 -> { - "DGPS" //<11 use five - } - in 12.0..50.0 -> { - "internal" //>11 <=50 use four - } - else -> { - "bad" - } + val deviceName = prefs.getString(GeneralKeys.GNSS_LAST_PAIRED_DEVICE_NAME, null) + + if (deviceName == context.getString(R.string.pref_behavior_geonav_internal_gps_choice)) { + + //this will force collect navigation to keep GPS data on screen, + //otherwise the data is only updated when the fix quality is good + checkBeforeUpdate("GPS") { + + currentUtc = UUID.randomUUID().toString() + + latTextView.text = truncateFixQuality(location.latitude.toString()) + lngTextView.text = truncateFixQuality(location.longitude.toString()) + altTextView.text = truncateFixQuality(location.altitude.toString()) + + accTextView.text = location.accuracy.toString() + utcTextView.text = location.time.toString() + + resolveUiStatus() + } + } + } + + private fun soundOk() { + + val soundOkFlag = prefs.getBoolean(GeneralKeys.GNSS_PRECISION_OK_SOUND, false) + + if (!soundOkFlag) { + + controller.getSoundHelper().playCelebrate() + controller.getVibrator().vibrate(1000L) + + prefs.edit().putBoolean(GeneralKeys.GNSS_PRECISION_OK_SOUND, true).apply() } - latTextView.text = truncateFixQuality(location.latitude.toString(), fixQuality) - lngTextView.text = truncateFixQuality(location.longitude.toString(), fixQuality) - altTextView.text = truncateFixQuality(location.altitude.toString(), fixQuality) + } + + private fun soundWarning() { + controller.getSoundHelper().playError() + controller.getVibrator().vibrate(1000L) + } + + private fun showWarning() { + + val soundWarningFlag = prefs.getBoolean(GeneralKeys.GNSS_WARNED_PRECISION, false) - accTextView.text = location.accuracy.toString() - utcTextView.text = location.time.toString() + if (!soundWarningFlag) { + (controller.getContext() as? CollectActivity)?.showLocationPrecisionLossDialog() + + soundWarning() + + prefs.edit().putBoolean(GeneralKeys.GNSS_WARNED_PRECISION, true).apply() + } } - //close the thread when the linear layout is removed -// override fun onDetachedFromWindow() { -// super.onDetachedFromWindow() -// -// getThreadHelper().stop() -// } + private fun checkBeforeUpdate(fix: String, update: () -> Unit) { + + val precisionThresh = prefs.getString(GeneralKeys.GNSS_LAST_CHOSEN_PRECISION, "Any") ?: "Any" + + val isQuality = NmeaParser().compareFix(fix, precisionThresh) + if (isQuality && !currentFixQuality) { + //quality fix is found, play something good + isCollectEnabled = true + soundOk() + currentFixQuality = true + } else if (isQuality) { + //quality is still good + isCollectEnabled = true + } else { + //quality is bad, play something bad + //reset last plotId if quality drops + currentFixQuality = false + showWarning() + isCollectEnabled = false + + } + + update() + } } \ No newline at end of file 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 5fffff621..3e617cadd 100644 --- a/app/src/main/java/com/fieldbook/tracker/traits/GoProTraitLayout.kt +++ b/app/src/main/java/com/fieldbook/tracker/traits/GoProTraitLayout.kt @@ -27,9 +27,9 @@ import com.fieldbook.tracker.R import com.fieldbook.tracker.activities.CollectActivity import com.fieldbook.tracker.adapters.ImageTraitAdapter import com.fieldbook.tracker.database.dao.ObservationDao -import com.fieldbook.tracker.objects.GoProWrapper import com.fieldbook.tracker.preferences.GeneralKeys import com.fieldbook.tracker.utilities.DocumentTreeUtil +import com.fieldbook.tracker.utilities.GoProWrapper import com.google.android.exoplayer2.DefaultLoadControl import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.MediaItem diff --git a/app/src/main/java/com/fieldbook/tracker/traits/UsbCameraTraitLayout.kt b/app/src/main/java/com/fieldbook/tracker/traits/UsbCameraTraitLayout.kt index 931cd4f20..30ff725db 100644 --- a/app/src/main/java/com/fieldbook/tracker/traits/UsbCameraTraitLayout.kt +++ b/app/src/main/java/com/fieldbook/tracker/traits/UsbCameraTraitLayout.kt @@ -62,6 +62,9 @@ class UsbCameraTraitLayout : BaseTraitLayout, ImageAdapter.ImageItemHandler { private var recyclerView: RecyclerView? = null private var previewGroup: Group? = null private var constraintLayout: ConstraintLayout? = null + //zoom buttons + private var zoomInButton: ImageButton? = null + private var zoomOutButton: ImageButton? = null constructor(context: Context?) : super(context) constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) @@ -88,6 +91,8 @@ class UsbCameraTraitLayout : BaseTraitLayout, ImageAdapter.ImageItemHandler { captureBtn = act.findViewById(R.id.usb_camera_fragment_capture_btn) recyclerView = act.findViewById(R.id.usb_camera_fragment_rv) previewGroup = act.findViewById(R.id.usb_camera_fragment_preview_group) + zoomInButton = act.findViewById(R.id.usb_camera_fragment_plus_btn) + zoomOutButton = act.findViewById(R.id.usb_camera_fragment_minus_btn) activity = act @@ -116,6 +121,54 @@ class UsbCameraTraitLayout : BaseTraitLayout, ImageAdapter.ImageItemHandler { } } + textureView?.setOnClickListener { + + mUsbCameraHelper?.setFocus() + + } + + zoomOutButton?.setOnClickListener { + + try { + + val current = mUsbCameraHelper?.getZoom() ?: 1 + + if (current < Int.MAX_VALUE) { + + mUsbCameraHelper?.setZoom(current) + + } + + } catch (e: Exception) { + + e.printStackTrace() + + Log.d(TAG, "Something went wrong with zooming USB Camera.") + + } + } + + zoomInButton?.setOnClickListener { + + try { + + val current = mUsbCameraHelper?.getZoom() ?: 1 + + if (current > 1) { + + mUsbCameraHelper?.setZoom(current - 1) + + } + + } catch (e: Exception) { + + e.printStackTrace() + + Log.d(TAG, "Something went wrong with zooming USB Camera.") + + } + } + registerReconnectListener() connectBtn?.requestFocus() diff --git a/app/src/main/java/com/fieldbook/tracker/objects/GeoNavHelper.kt b/app/src/main/java/com/fieldbook/tracker/utilities/GeoNavHelper.kt similarity index 78% rename from app/src/main/java/com/fieldbook/tracker/objects/GeoNavHelper.kt rename to app/src/main/java/com/fieldbook/tracker/utilities/GeoNavHelper.kt index 562f8eedf..5114e57af 100644 --- a/app/src/main/java/com/fieldbook/tracker/objects/GeoNavHelper.kt +++ b/app/src/main/java/com/fieldbook/tracker/utilities/GeoNavHelper.kt @@ -1,4 +1,4 @@ -package com.fieldbook.tracker.objects +package com.fieldbook.tracker.utilities import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice @@ -30,10 +30,12 @@ import com.fieldbook.tracker.R import com.fieldbook.tracker.activities.CollectActivity import com.fieldbook.tracker.database.DataHelper import com.fieldbook.tracker.database.models.ObservationUnitModel +import com.fieldbook.tracker.interfaces.CollectController import com.fieldbook.tracker.location.GPSTracker import com.fieldbook.tracker.location.gnss.ConnectThread import com.fieldbook.tracker.location.gnss.GNSSResponseReceiver import com.fieldbook.tracker.location.gnss.NmeaParser +import com.fieldbook.tracker.objects.InfoBarModel import com.fieldbook.tracker.preferences.GeneralKeys import com.fieldbook.tracker.utilities.GeodeticUtils.Companion.impactZoneSearch import com.fieldbook.tracker.utilities.GeodeticUtils.Companion.lowPassFilter @@ -43,7 +45,6 @@ import com.fieldbook.tracker.utilities.InfoBarHelper import com.fieldbook.tracker.utilities.Utils import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.SnackbarLayout -import dagger.hilt.android.qualifiers.ActivityContext import org.phenoapps.utils.BaseDocumentTreeUtil.Companion.getDirectory import java.io.IOException import java.io.OutputStreamWriter @@ -52,8 +53,8 @@ import javax.inject.Inject import kotlin.math.pow import kotlin.math.sqrt -class GeoNavHelper @Inject constructor(@ActivityContext private val context: Context, private val infoBarHelper: InfoBarHelper): -SensorEventListener, GPSTracker.GPSTrackerListener{ +class GeoNavHelper @Inject constructor(private val controller: CollectController, private val infoBarHelper: InfoBarHelper): + SensorEventListener, GPSTracker.GPSTrackerListener { /** * GeoNav sensors and variables @@ -65,51 +66,100 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ private var mAzimuth: Double? = null private var mNotWarnedInterference = true + private var currentFixQuality = false + private val mGnssResponseReceiver: GNSSResponseReceiver = object : GNSSResponseReceiver() { override fun onGNSSParsed(parser: NmeaParser) { - val time = parser.utc.toDouble() - - //only update the gps if it is a newly parsed coordinate - if (time > mLastGeoNavTime) { - if (!mFirstLocationFound) { - mFirstLocationFound = true - (context as CollectActivity).getSoundHelper().playCycle() - Utils.makeToast(context, - context.getString(R.string.act_collect_geonav_first_location_found) - ) - } - mLastGeoNavTime = time - val lat = truncateFixQuality(parser.latitude, parser.fix) - val lng = truncateFixQuality(parser.longitude, parser.fix) - var alt = parser.altitude - val altLength = alt.length - alt = alt.substring(0, altLength - 1) //drop the "M" - - //always log external gps updates - writeGeoNavLog( - mGeoNavLogWriter, - "$lat,$lng,$time,null,null,null,null,null,null,null,null,null,null\n" + + checkBeforeUpdate(parser.getSimpleFix()) { + + updateLocationWithGnss(parser) + } + } + } + + private fun updateLocationWithGnss(parser: NmeaParser) { + val time = parser.utc.toDouble() + + //only update the gps if it is a newly parsed coordinate + if (time > mLastGeoNavTime) { + if (!mFirstLocationFound) { + mFirstLocationFound = true + controller.getSoundHelper().playCycle() + Utils.makeToast(controller.getContext(), + controller.getContext().getString(R.string.act_collect_geonav_first_location_found) ) - mExternalLocation = Location("GeoNav Rover") - - //initialize the double values, attempt to parse the strings, if impossible then don't update the coordinate. - var latValue = Double.NaN - var lngValue = Double.NaN - var altValue = Double.NaN - try { - latValue = lat.toDouble() - lngValue = lng.toDouble() - altValue = alt.toDouble() - } catch (nfe: NumberFormatException) { - nfe.printStackTrace() + } + mLastGeoNavTime = time + val fix = parser.getSimpleFix() + val lat = truncateFixQuality(parser.latitude) + val lng = truncateFixQuality(parser.longitude) + var alt = parser.altitude + val altLength = alt.length + alt = alt.substring(0, altLength - 1) //drop the "M" + + //always log external gps updates + writeGeoNavLog( + mGeoNavLogWriter, + "$lat,$lng,$time,null,null,null,null,null,null,$fix,null,null,null,null\n" + ) + mExternalLocation = Location("GeoNav Rover") + + //initialize the double values, attempt to parse the strings, if impossible then don't update the coordinate. + var latValue = Double.NaN + var lngValue = Double.NaN + var altValue = Double.NaN + try { + latValue = lat.toDouble() + lngValue = lng.toDouble() + altValue = alt.toDouble() + } catch (nfe: NumberFormatException) { + nfe.printStackTrace() + } + if (!java.lang.Double.isNaN(latValue) && !java.lang.Double.isNaN(lngValue)) { + mExternalLocation?.time = time.toLong() + mExternalLocation?.latitude = latValue + mExternalLocation?.longitude = lngValue + mExternalLocation?.altitude = altValue + mExternalLocation?.extras?.putString("fix", fix) + } + } + } + + private fun checkBeforeUpdate(fix: String, update: () -> Unit) { + + val precisionThresh = controller.getPreferences().getString(GeneralKeys.GEONAV_CONFIG_DEGREE_PRECISION, "Any") ?: "Any" + val audioOnDrop = controller.getPreferences().getBoolean(GeneralKeys.GEONAV_CONFIG_AUDIO_ON_DROP, false) + + if (precisionThresh != "Any" || audioOnDrop) { + + val isQuality = NmeaParser().compareFix(fix, precisionThresh) + if (isQuality && !currentFixQuality) { + //quality fix is found, play something good + update() + if (audioOnDrop) { + controller.getSoundHelper().playCelebrate() + controller.getVibrator().vibrate(1000L) } - if (!java.lang.Double.isNaN(latValue) && !java.lang.Double.isNaN(lngValue)) { - mExternalLocation?.time = time.toLong() - mExternalLocation?.latitude = latValue - mExternalLocation?.longitude = lngValue - mExternalLocation?.altitude = altValue + currentFixQuality = true + } else if (isQuality && currentFixQuality) { + //quality is still good + update() + } else if (!isQuality && currentFixQuality) { + //quality is bad, play something bad + //reset last plotId if quality drops + lastPlotIdNav = null + currentFixQuality = false + if (audioOnDrop) { + (controller.getContext() as? CollectActivity)?.showLocationPrecisionLossDialog() + controller.getSoundHelper().playError() + controller.getVibrator().vibrate(1000L) } } + + } else { + update() + currentFixQuality = true } } @@ -118,7 +168,7 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ * The parser parameter is a model for the parsed message, and is used to populate the * trait layout UI. */ - private var mLocalBroadcastManager = LocalBroadcastManager.getInstance(context).also { + private var mLocalBroadcastManager = LocalBroadcastManager.getInstance(controller.getContext()).also { val filter = IntentFilter() filter.addAction(GNSSResponseReceiver.ACTION_BROADCAST_GNSS_ROVER) it.registerReceiver(mGnssResponseReceiver, filter) @@ -140,8 +190,8 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ private var averageHandler: Handler? = null private var lastPlotIdNav: String? = null private var mGeoNavSnackbar: Snackbar? = null - private val mPrefs = PreferenceManager.getDefaultSharedPreferences(context) - private val ep = context.getSharedPreferences(GeneralKeys.SHARED_PREF_FILE_NAME, Context.MODE_PRIVATE) + private val mPrefs = PreferenceManager.getDefaultSharedPreferences(controller.getContext()) + private val ep = controller.getContext().getSharedPreferences(GeneralKeys.SHARED_PREF_FILE_NAME, Context.MODE_PRIVATE) private var mGeoNavLogWriter: OutputStreamWriter? = null var initialized: Boolean = false @@ -168,7 +218,12 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ * Starts a timer (with interval defined in the preferences) that runs the IZ algorithm. */ fun startGeoNav() { - val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager? + + if (!initialized) { + recreateThreads() + } + + val sensorManager = controller.getContext().getSystemService(Context.SENSOR_SERVICE) as SensorManager? if (sensorManager != null) { sensorManager.registerListener( this, @@ -196,11 +251,11 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ //find the mac address of the device, if not found then start the internal GPS val address: String = mPrefs.getString(GeneralKeys.PAIRED_DEVICE_ADDRESS, "") ?: "" - val internalGps: String = context.getString(R.string.pref_behavior_geonav_internal_gps_choice) + val internalGps: String = controller.getContext().getString(R.string.pref_behavior_geonav_internal_gps_choice) var internal = true if (address.isEmpty() || address == internalGps) { //update no matter the distance change and every 10s - val mGpsTracker = GPSTracker(context, this, 0, 10000) + val mGpsTracker = GPSTracker(controller.getContext(), this, 0, 10000) } else { getDeviceByAddress(address)?.let { device -> setupCommunicationsUi(device) @@ -212,7 +267,7 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ } else { Toast.makeText( - context, R.string.activity_collect_sensor_manager_failed, + controller.getContext(), R.string.activity_collect_sensor_manager_failed, Toast.LENGTH_SHORT ).show() } @@ -233,7 +288,7 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ writeGeoNavLog( mGeoNavLogWriter, - "start latitude, start longitude, UTC, end latitude, end longitude, azimuth, teslas, bearing, distance, closest, unique id, primary id, secondary id\n" + "start latitude, start longitude, UTC, end latitude, end longitude, azimuth, teslas, bearing, distance, fix, closest, unique id, primary id, secondary id\n" ) } @@ -291,7 +346,7 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ * @param device the paired device that has been chosen in the preferences. */ private fun setupCommunicationsUi(device: BluetoothDevice) { - (context as CollectActivity).getSecurityChecker().withNearby { adapter: BluetoothAdapter -> + controller.getSecurityChecker().withNearby { adapter: BluetoothAdapter -> adapter.cancelDiscovery() mLastDevice = device @@ -339,7 +394,9 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ //user must have a valid pointing direction before attempting the IZ //initialize the start position and fill with external or internal GPS coordinates val start: Location? = if (internal) { - mInternalLocation + mInternalLocation.also { + it?.extras?.putString("fix", "GPS") + } } else { mExternalLocation } @@ -348,7 +405,7 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ val studyId: Int = ep.getInt(GeneralKeys.SELECTED_FIELD_ID, 0) //find all observation units within the field - val units = (context as CollectActivity).getDatabase().getAllObservationUnits(studyId) + val units = controller.getDatabase().getAllObservationUnits(studyId) val coordinates: MutableList = ArrayList() //add all units that have non null coordinates. @@ -359,7 +416,7 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ } //run the algorithm and time how long it takes - if (start != null) { + if (start != null && currentFixQuality) { mAzimuth?.let { azimuth -> @@ -375,11 +432,12 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ //if we received a result then show it to the user, create a button to navigate to the plot if (first != null) { val id = first.observation_unit_db_id - with (context as CollectActivity) { + with ((controller.getContext() as CollectActivity)) { if (id != getRangeBox().cRange.plot_id && id != lastPlotIdNav) { lastPlotIdNav = id runOnUiThread { - if (mPrefs.getBoolean(GeneralKeys.GEONAV_AUTO, false)) { + if (ep.getBoolean(GeneralKeys.GEONAV_AUTO, false)) { + lastPlotIdNav = null moveToSearch("id", getRangeBox().getRangeID(), null, null, id, -1) Toast.makeText( this, @@ -388,7 +446,7 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ ).show() } else { mGeoNavSnackbar = Snackbar.make( - findViewById(R.id.traitHolder), + findViewById(R.id.toolbarBottom), id, Snackbar.LENGTH_INDEFINITE ) val snackLayout = mGeoNavSnackbar?.view as SnackbarLayout @@ -402,6 +460,7 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT ) + params.bottomToTop = R.id.toolbarBottom snackView.layoutParams = params snackLayout.addView(snackView) snackLayout.setPadding(0, 0, 0, 0) @@ -429,6 +488,7 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ //when navigate button is pressed use rangeBox to go to the plot id moveToSearch("id", getRangeBox().getRangeID(), null, null, id, -1) } + mGeoNavSnackbar?.setAnchorView(R.id.toolbarBottom) mGeoNavSnackbar?.setBackgroundTint(Color.TRANSPARENT) mGeoNavSnackbar?.show() } @@ -442,7 +502,7 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ private fun getInfoBarData(id: String, firstInfoBar: String): String { - var database : DataHelper = (context as CollectActivity).getDatabase() + var database : DataHelper = controller.getDatabase() //ensure that the initialLabel is actually a plot attribute @@ -452,7 +512,7 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ //check if the label is an attribute or a trait val isAttribute = attributes.contains(firstInfoBar) - return (context as CollectActivity).queryForLabelValue(id, firstInfoBar, isAttribute) + return controller.queryForLabelValue(id, firstInfoBar, isAttribute) } /** @@ -460,8 +520,7 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ * Simply stops listening to the sensor manager and stops the geonav timer. */ fun stopGeoNav() { - (context.getSystemService(Context.SENSOR_SERVICE) as SensorManager).unregisterListener(this) - mPrefs.edit().putBoolean(GeneralKeys.GEONAV_AUTO, false).apply() //turn off auto nav + (controller.getContext().getSystemService(Context.SENSOR_SERVICE) as SensorManager).unregisterListener(this) mSchedulerHandlerThread.quit() mAverageHandlerThread.quit() mMessageHandlerThread.quit() @@ -477,6 +536,8 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ mLocalBroadcastManager.unregisterReceiver(mGnssResponseReceiver) mConnectThread?.cancel() + + initialized = false } fun resetGeoNavMessages() { @@ -498,8 +559,8 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ fun setupGeoNavLogger() { if (mPrefs.getBoolean(GeneralKeys.GEONAV_LOG, false)) { try { - val resolver: ContentResolver = context.contentResolver - val geoNavFolder = getDirectory(context, R.string.dir_geonav) + val resolver: ContentResolver = controller.getContext().contentResolver + val geoNavFolder = getDirectory(controller.getContext(), R.string.dir_geonav) if (geoNavFolder != null && geoNavFolder.exists()) { val interval = mPrefs.getString(GeneralKeys.UPDATE_INTERVAL, "1") val address = (mPrefs.getString(GeneralKeys.PAIRED_DEVICE_ADDRESS, "") ?: "") @@ -547,7 +608,7 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ if ((mTeslas < 25 || mTeslas > 65) && mNotWarnedInterference) { mNotWarnedInterference = false Toast.makeText( - context, R.string.activity_collect_geomagnetic_noise_detected, + controller.getContext(), R.string.activity_collect_geomagnetic_noise_detected, Toast.LENGTH_SHORT ).show() } @@ -621,13 +682,22 @@ SensorEventListener, GPSTracker.GPSTrackerListener{ } override fun onLocationChanged(location: Location) { + + checkBeforeUpdate("GPS") { + + updateLocationWithInternal(location) + } + } + + private fun updateLocationWithInternal(location: Location) { + mInternalLocation = location //always log location updates writeGeoNavLog( mGeoNavLogWriter, """ - ${location.latitude},${location.longitude},${location.time},null,null,null,null,null,null,null,null,null,null + ${location.latitude},${location.longitude},${location.time},null,null,null,null,null,null,GPS,null,null,null,null """.trimIndent() ) diff --git a/app/src/main/java/com/fieldbook/tracker/utilities/GeodeticUtils.kt b/app/src/main/java/com/fieldbook/tracker/utilities/GeodeticUtils.kt index 2b2283226..e599b8795 100644 --- a/app/src/main/java/com/fieldbook/tracker/utilities/GeodeticUtils.kt +++ b/app/src/main/java/com/fieldbook/tracker/utilities/GeodeticUtils.kt @@ -8,7 +8,11 @@ import math.geom2d.Point2D import math.geom2d.line.Line2D import java.io.IOException import java.io.OutputStreamWriter -import kotlin.math.* +import kotlin.math.asin +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.sin +import kotlin.math.sqrt class GeodeticUtils { @@ -55,6 +59,9 @@ class GeodeticUtils { * note: the bearing can be null if the compass setting is disabled * * (1) and (2) are a bit outdated in terms of column order (look at the headers above for most up to date) + * + * + * Update (8/2/23): "fix" has been added as a header to the log file, it is the tenth item. This can be any value GPS, RTK, or RTK Float */ fun writeGeoNavLog(log: OutputStreamWriter?, line: String) { @@ -73,9 +80,9 @@ class GeodeticUtils { //Represents what we print to the log data class IzString(val startTime: Long, val uniqueId: String, val primaryId: String, val secondaryId: String, val startLat: Double, val startLng: Double, val endLat: Double, val endLng: Double, val azimuth: Double, - val teslas: Double, var bearing: Double?, val distance: Double, var closest: Int) { + val teslas: Double, var bearing: Double?, val distance: Double, var closest: Int, var fix: String) { override fun toString(): String { - return "$startLat,$startLng,$startTime,$endLat,$endLng,$azimuth,$teslas,$bearing,$distance,$closest,\"${uniqueId.escape()}\",\"${primaryId.escape()}\",\"${secondaryId.escape()}\"\n" + return "$startLat,$startLng,$startTime,$endLat,$endLng,$azimuth,$teslas,$bearing,$distance,$fix,$closest,\"${uniqueId.escape()}\",\"${primaryId.escape()}\",\"${secondaryId.escape()}\"\n" } } @@ -128,9 +135,11 @@ class GeodeticUtils { val bearing: Double = angleFromCoordinate(start.latitude, start.longitude, location.latitude, location.longitude) + val fix = start.extras?.getString("fix") ?: "invalid" + val loggedString = IzString(startTime = start.time, uniqueId = coordinate.observation_unit_db_id, primaryId = coordinate.primary_id, secondaryId = coordinate.secondary_id, startLat = start.latitude, startLng = start.longitude, endLat = location.latitude, endLng = location.longitude, azimuth = azimuth, teslas = teslas, bearing = bearing, - distance = distance, closest = NOT_CLOSEST) + distance = distance, closest = NOT_CLOSEST, fix = fix) if (geoNavMethod == "0") { //default distance based method @@ -243,6 +252,12 @@ class GeodeticUtils { location.longitude = geoJson.geometry.coordinates[1].toDouble() + geoJson.properties?.get("fix")?.let { fix -> + + location.extras?.putString("fix", fix) + + } + } catch (e: Exception) { //could be a NPE, number format exception, index out of bounds or json syntax exception, failed = true @@ -385,7 +400,7 @@ class GeodeticUtils { /** * As of issue #477 v5.3, standardize truncation to 7 digits */ - fun truncateFixQuality(x: String, fix: String): String = try { + fun truncateFixQuality(x: String): String = try { val tokens = x.split(".") val head = tokens[0] diff --git a/app/src/main/java/com/fieldbook/tracker/objects/GoProWrapper.kt b/app/src/main/java/com/fieldbook/tracker/utilities/GoProWrapper.kt similarity index 93% rename from app/src/main/java/com/fieldbook/tracker/objects/GoProWrapper.kt rename to app/src/main/java/com/fieldbook/tracker/utilities/GoProWrapper.kt index adc24d2df..4bf3c48cd 100644 --- a/app/src/main/java/com/fieldbook/tracker/objects/GoProWrapper.kt +++ b/app/src/main/java/com/fieldbook/tracker/utilities/GoProWrapper.kt @@ -1,4 +1,4 @@ -package com.fieldbook.tracker.objects +package com.fieldbook.tracker.utilities import android.content.Context import android.graphics.Bitmap @@ -29,8 +29,12 @@ class GoProWrapper @Inject constructor( } fun destroy() { - helper?.onDestroy() - gatt.clear() + try { + helper?.onDestroy() + gatt.clear() + } catch (e: Exception) { + e.printStackTrace() + } } override fun onApRequested() { diff --git a/app/src/main/java/com/fieldbook/tracker/utilities/SnackbarUtils.java b/app/src/main/java/com/fieldbook/tracker/utilities/SnackbarUtils.java index f31a3e795..15e240f69 100644 --- a/app/src/main/java/com/fieldbook/tracker/utilities/SnackbarUtils.java +++ b/app/src/main/java/com/fieldbook/tracker/utilities/SnackbarUtils.java @@ -50,6 +50,7 @@ private static void showSnackbar(@NonNull View view, @NonNull String message, in public static void showNavigateSnack(LayoutInflater inflater, View view, String msg, + @Nullable Integer anchorViewId, int duration, @Nullable Boolean showGeoNavIcon, View.OnClickListener onClickListener) { @@ -69,7 +70,10 @@ public static void showNavigateSnack(LayoutInflater inflater, View view, } ImageButton btn = snackView.findViewById(R.id.geonav_snackbar_btn); - if (btn != null) { + + if (onClickListener == null) { + btn.setVisibility(View.GONE); + } else if (btn != null) { btn.setOnClickListener((v) -> { snackbar.dismiss(); @@ -86,6 +90,10 @@ public static void showNavigateSnack(LayoutInflater inflater, View view, snackbar.setBackgroundTint(Color.TRANSPARENT); + if (anchorViewId != null) { + snackbar.setAnchorView(anchorViewId); + } + snackbar.show(); } } \ No newline at end of file diff --git a/app/src/main/java/com/fieldbook/tracker/utilities/TapTargetUtil.kt b/app/src/main/java/com/fieldbook/tracker/utilities/TapTargetUtil.kt index 32c54f5fa..309365e89 100644 --- a/app/src/main/java/com/fieldbook/tracker/utilities/TapTargetUtil.kt +++ b/app/src/main/java/com/fieldbook/tracker/utilities/TapTargetUtil.kt @@ -3,7 +3,6 @@ package com.fieldbook.tracker.utilities import android.content.Context import android.graphics.Rect import android.graphics.Typeface -import android.util.TypedValue import android.view.View import com.fieldbook.tracker.R import com.getkeepsafe.taptargetview.TapTarget @@ -12,73 +11,35 @@ class TapTargetUtil { companion object { - fun getTapTargetSettingsView(context: Context, view: View, color: Int, targetRadius: Int, title: String, desc: String): TapTarget { - val value = TypedValue() - val outerCircleValue = TypedValue() - context.theme.resolveAttribute(R.attr.fb_color_primary_dark, outerCircleValue, true) - context.theme.resolveAttribute(R.attr.fb_tap_target_color, value, true) - return TapTarget.forView( - view, - title, - desc - ) // All options below are optional - .outerCircleColor(color) // Specify a color for the outer circle - .outerCircleAlpha(0.95f) // Specify the alpha amount for the outer circle - .targetCircleColor(value.data) // Specify a color for the target circle - .titleTextSize(30) // Specify the size (in sp) of the title text - .descriptionTextSize(20) // Specify the size (in sp) of the description text - .descriptionTextColor(value.data) // Specify the color of the description text + fun getTapTargetSettingsView(context: Context, view: View, title: String, desc: String, targetRadius: Int): TapTarget { + val attrs = intArrayOf(R.attr.fb_color_primary_dark) + val ta = context.obtainStyledAttributes(attrs) + val colorPrimaryDark = ta.getColor(0,0) + ta.recycle() + return TapTarget.forView(view, title, desc) + .outerCircleColorInt(colorPrimaryDark) + .outerCircleAlpha(0.95f) + .titleTextSize(30) + .descriptionTextSize(20) .descriptionTypeface(Typeface.DEFAULT_BOLD) - .textColor(value.data) // Specify a color for both the title and description text - .dimColor(value.data) // If set, will dim behind the view with 30% opacity of the given color - .drawShadow(true) // Whether to draw a drop shadow or not - .cancelable(false) // Whether tapping outside the outer circle dismisses the view - .tintTarget(true) // Whether to tint the target view's color - .transparentTarget(true) // Specify whether the target is transparent (displays the content underneath) + .drawShadow(true) + .cancelable(false) + .tintTarget(true) + .transparentTarget(true) .targetRadius(targetRadius) } fun getTapTargetSettingsRect(context: Context, item: Rect, title: String, desc: String): TapTarget { - val value = TypedValue() - val outerCircleValue = TypedValue() - context.theme.resolveAttribute(R.attr.fb_color_primary_dark, outerCircleValue, true) - context.theme.resolveAttribute(R.attr.fb_tap_target_color, value, true) + val attrs = intArrayOf(R.attr.fb_color_primary_dark) + val ta = context.obtainStyledAttributes(attrs) + val colorPrimaryDark = ta.getColor(0,0) + ta.recycle() return TapTarget.forBounds(item, title, desc) // All options below are optional - .outerCircleColor(outerCircleValue.data) // Specify a color for the outer circle - .outerCircleAlpha(0.95f) // Specify the alpha amount for the outer circle - .targetCircleColor(value.data) // Specify a color for the target circle - .titleTextSize(30) // Specify the size (in sp) of the title text - .descriptionTextSize(20) // Specify the size (in sp) of the description text - .descriptionTypeface(Typeface.DEFAULT_BOLD) - .descriptionTextColor(value.data) // Specify the color of the description text - .textColor(value.data) // Specify a color for both the title and description text - .dimColor(value.data) // If set, will dim behind the view with 30% opacity of the given color - .drawShadow(true) // Whether to draw a drop shadow or not - .cancelable(false) // Whether tapping outside the outer circle dismisses the view - .tintTarget(true) // Whether to tint the target view's color - .transparentTarget(true) // Specify whether the target is transparent (displays the content underneath) - .targetRadius(60) - } - - fun getTapTargetSettingsView(context: Context, view: View, title: String, desc: String): TapTarget { - val value = TypedValue() - val outerCircleValue = TypedValue() - context.theme.resolveAttribute(R.attr.fb_color_primary_dark, outerCircleValue, true) - context.theme.resolveAttribute(R.attr.fb_tap_target_color, value, true) - return TapTarget.forView( - view, - title, - desc - ) // All options below are optional - .outerCircleColor(outerCircleValue.data) // Specify a color for the outer circle + .outerCircleColorInt(colorPrimaryDark) // Specify a color for the outer circle .outerCircleAlpha(0.95f) // Specify the alpha amount for the outer circle - .targetCircleColor(value.data) // Specify a color for the target circle .titleTextSize(30) // Specify the size (in sp) of the title text .descriptionTextSize(20) // Specify the size (in sp) of the description text - .descriptionTextColor(value.data) // Specify the color of the description text .descriptionTypeface(Typeface.DEFAULT_BOLD) - .textColor(value.data) // Specify a color for both the title and description text - .dimColor(value.data) // If set, will dim behind the view with 30% opacity of the given color .drawShadow(true) // Whether to draw a drop shadow or not .cancelable(false) // Whether tapping outside the outer circle dismisses the view .tintTarget(true) // Whether to tint the target view's color diff --git a/app/src/main/java/com/fieldbook/tracker/objects/VerifyPersonHelper.kt b/app/src/main/java/com/fieldbook/tracker/utilities/VerifyPersonHelper.kt similarity index 99% rename from app/src/main/java/com/fieldbook/tracker/objects/VerifyPersonHelper.kt rename to app/src/main/java/com/fieldbook/tracker/utilities/VerifyPersonHelper.kt index c863c6f11..d6465ca0d 100644 --- a/app/src/main/java/com/fieldbook/tracker/objects/VerifyPersonHelper.kt +++ b/app/src/main/java/com/fieldbook/tracker/utilities/VerifyPersonHelper.kt @@ -1,4 +1,4 @@ -package com.fieldbook.tracker.objects +package com.fieldbook.tracker.utilities import android.content.Context import android.content.DialogInterface diff --git a/app/src/main/java/com/fieldbook/tracker/utilities/VibrateUtil.kt b/app/src/main/java/com/fieldbook/tracker/utilities/VibrateUtil.kt new file mode 100644 index 000000000..ece0339cc --- /dev/null +++ b/app/src/main/java/com/fieldbook/tracker/utilities/VibrateUtil.kt @@ -0,0 +1,22 @@ +package com.fieldbook.tracker.utilities + +import android.content.Context +import android.os.Build +import android.os.VibrationEffect +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class VibrateUtil @Inject constructor(@ApplicationContext context: Context) { + + private val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as android.os.Vibrator + + fun vibrate(duration: Long) { + if (vibrator.hasVibrator()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + vibrator.vibrate(VibrationEffect.createWaveform(longArrayOf(1L, 5L, 8L, 5L, 1L), -1)) + } else { + vibrator.vibrate(duration) + } + } + } +} \ 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 54c4b98c0..c0c591e2f 100644 --- a/app/src/main/java/com/fieldbook/tracker/views/RangeBoxView.kt +++ b/app/src/main/java/com/fieldbook/tracker/views/RangeBoxView.kt @@ -649,10 +649,6 @@ class RangeBoxView : ConstraintLayout { //if we wrap around the entire range then observations are completed //notify the user and just go to the first range id. if (!firstLoop && prevPos == localPrev) { - Toast.makeText( - context, - R.string.activity_collect_all_obs_made, Toast.LENGTH_SHORT - ).show() return 1 } firstLoop = false diff --git a/app/src/main/java/com/fieldbook/tracker/views/RepeatedValuesView.kt b/app/src/main/java/com/fieldbook/tracker/views/RepeatedValuesView.kt index a63d0e258..764bad7d0 100644 --- a/app/src/main/java/com/fieldbook/tracker/views/RepeatedValuesView.kt +++ b/app/src/main/java/com/fieldbook/tracker/views/RepeatedValuesView.kt @@ -98,7 +98,11 @@ class RepeatedValuesView(context: Context, attributeSet: AttributeSet) : updateButtonVisibility() - act.traitLayoutRefresh() + Handler(Looper.getMainLooper()).postDelayed({ + + act.traitLayoutRefresh() + + }, 100) } } @@ -116,7 +120,11 @@ class RepeatedValuesView(context: Context, attributeSet: AttributeSet) : updateButtonVisibility() - act.traitLayoutRefresh() + Handler(Looper.getMainLooper()).postDelayed({ + + act.traitLayoutRefresh() + + }, 100) } } diff --git a/app/src/main/res/drawable/book_open_variant.xml b/app/src/main/res/drawable/book_open_variant.xml new file mode 100644 index 000000000..63aecf745 --- /dev/null +++ b/app/src/main/res/drawable/book_open_variant.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_pref_general_root_directory.xml b/app/src/main/res/drawable/ic_pref_general_root_directory.xml index 04be68ee7..a324140a0 100644 --- a/app/src/main/res/drawable/ic_pref_general_root_directory.xml +++ b/app/src/main/res/drawable/ic_pref_general_root_directory.xml @@ -1,3 +1,4 @@ + + android:pathData="M18,8H16V4H18M15,8H13V4H15M12,8H10V4H12M18,2H10L4,8V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V4A2,2 0 0,0 18,2Z" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_sv.png b/app/src/main/res/drawable/ic_sv.png new file mode 100644 index 0000000000000000000000000000000000000000..350b363f4c00822a4a762507b85729aa448c8877 GIT binary patch literal 1296 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&w@L)Zt|+Cx8@Zfk$L90|Va?5N4dJ%_j{M ztPAi7adj{7xw~KV>`r;ctfl< zRCmghPCmUuktc87uTxAeEtRj2@Z8=f_Wl^>pVJKY4~Q7lw4L86r%*OgpkOvr*3#vx zEFeZ1*R^h1ZzNDK=iE;D8+#?TZP2%Gt_AA4Wg&hP=n{pJAirRSdN{C$vyd1k4-{xI zFtG4=x;TbZ+YE1~j8hm=R`D=sF&KriGRQK_e8tot z*WmM&@j#XJO*=QnAGfr&@i9D;R?U)Q5Sce=r4qvmTh-uFh7BL4_~6IN<5XfU1#kz_CddTlfTFyI5g1fVn9^V_#ypT7M2 zQ}O@8zooBl?>;^IbNA}aZu_IpQ7K1)a<<=2`J2o0b#)^nKt&vbr>mdKI;Vst03i}V7$PhW@7*PQu37~>N%IDQcU5=4*Y)~!UvhQX zt&At(0RYNQdmP;WfaNR3Ap1`7TO!)k<*N5ijt=h8onwMa-Xtd- zTMmo-vE;+3sd38EHq@VgS2j3WYTAiIeXfxC9FZcGJQ@EsZA32^t z127Q*XflAX-&w=UfCyk3zygR2(8&K@2(FV>f$fSLG7039p!E@;D6%@n{P1KJ$fciJ zi*O<5g3z@{Idr$IAU}2;QVb;($n*$g26)JXlF&peEuQC9o2+&`wLdZ5(P=i?VMNU1`jU`{WmyfT;B&cMSm?Kxg_Hs{c z?yrEtFG}gup@Hc>Xe)t?2>PH=d{$iQCwwYw`ei5|xG1>3mO~C~DmW8G2Z0wb4axz& z^v?(nN;;^xL7mpIIQ9v0)FbI`My$+eX*Ev+QMykE0c#MRT(TmEg7#0|?XW4I!3LY4 zTO;kk+`gZcl$A(aHodkIT%K_?K@^woeEu=C5w#^R)S5`UA2y=EGD9pY4V24$6Yifg zLgiJ!3A!X*OXSVVO=_w*Rvoi&G)J31Q27yOO4F^$XoVIPl*ZL-|wbLvxK zO%^Mywa4CBvZ7we9+Pox>WuVZ@3r)Sa@CKqVFQ!%UH85nyV_r&_B<-JcG~_9AKS@K z;T~RWl5M_1efLa&ud;IzPpVp4`=pIWL1HH+&bQYxcwNu>U%ZbA+zsRB)^c=k&!n3t z=1rSbWqOsnlIPlMB*tavUDk~{@0Qremyig5)fWR^hqm015R77}A=_lH-WT3|C;Khy zc_}(9($?kR?`DB>ZyC6|y;S!H7o*_yxglF+0;SydsgrJl6XxfN5?>y(&Y+m~JrTW4 zW#aVv&Rob!YyVwbQpA>)I454d9l1{QV;LGV?aN$YF%-c2Q17u##)%tGHyaLe9qBus zAv@eq5`?!U&1(6;CSLnB%*Gg^g z)TpJ_t%0Y#bye`q#67#~yDfaDlJkBupqz+$mu^6xy;a<(Pid6Z#;1YJmL9{UdB@S0 zGd1|%j9MH5b@ry%n?CEP$sMscCx5FGD(OVX`3^pSf|y&C zTEwkJ%&7?^+fA%C4cgT%hekG*uR*);;SNFM4DXf+98FFmy8K-qC8MPBmJuR@Q5)tR+CQvy3xBUXM~GcDyR^Y^wVtXo7irdrTZpDx-PSy zHR$=h)@{|7BR#gt8U%&*5rfmKBL7jSko>Evb80^f; z)}#PSmVfM@t=Imr8x}gotVfjE40IPqa+*4+*X^)bXl;;aqeacxN-V*7&WR<$L2VbvQ3-Ta?sz%vnQwN6!m%) zmVBi4c9}wVTl(}zAGXHOS66`Zu_hJ&wJ)=PNx6q52ktjJ%NBE~)&*e{wmR+Q!6eMx zvxCv&mo3g&UchLRxGh?uS}MD+{L)DZ5_HHay5mi6UT@9QBbZ`_!aS48o;Ad;BzYbM zS9YVMa~!+WZpW7>0eyV-UrQ8|!L6U0_u;r8wMKNhZ?tu}OEVs&E21OE8;x2jz>ShFQKGEg zuX5u{d5V2c3k??;odr9xQ{g@DhpG*w)bayeoHBwQ$c@3~xjBqHaI(}Z3R6Q&qqT=D1HtK z+w~W_xhdQiK;4qVo?3rI(%}q-_;=zLeY`7ME&fj}_+PsFKMw%Qvw*$kzl*G?1O6ML a>C1p;kAtSh$)qdGc1}B891FJxoc|kA9`;cH literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/minus.xml b/app/src/main/res/drawable/minus.xml new file mode 100644 index 000000000..7c245638d --- /dev/null +++ b/app/src/main/res/drawable/minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/dialog_geonav_collect.xml b/app/src/main/res/layout/dialog_geonav_collect.xml new file mode 100644 index 000000000..6e1c7a052 --- /dev/null +++ b/app/src/main/res/layout/dialog_geonav_collect.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_info.xml b/app/src/main/res/layout/dialog_info.xml index f5c44616a..438a1a15b 100644 --- a/app/src/main/res/layout/dialog_info.xml +++ b/app/src/main/res/layout/dialog_info.xml @@ -24,7 +24,7 @@ android:gravity="center" android:paddingHorizontal="20dp" android:paddingVertical="10dp" - android:text="@string/brapi_dialog_info_example_text"/> + android:text=""/>