diff --git a/.all-contributorsrc b/.all-contributorsrc
index 1f457f93b..ace643d57 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -289,7 +289,17 @@
"avatar_url": "https://avatars.githubusercontent.com/u/10454330?v=4",
"profile": "https://github.com/joegage",
"contributions": [
- "bug"
+ "bug",
+ "code"
+ ]
+ },
+ {
+ "login": "mobreza",
+ "name": "mobreza",
+ "avatar_url": "https://avatars.githubusercontent.com/u/712974?v=4",
+ "profile": "https://github.com/mobreza",
+ "contributions": [
+ "code"
]
}
],
diff --git a/README.md b/README.md
index 1df9e4aa5..f06b32201 100644
--- a/README.md
+++ b/README.md
@@ -62,7 +62,8 @@ Development of Field Book has been supported by the [Collaborative Crop Research
HMS17 π» |
zrm22 π» |
- Joe Gage π |
+ Joe Gage π π» |
+ mobreza π» |
diff --git a/app/build.gradle b/app/build.gradle
index f12095eb1..70c293770 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -157,6 +157,7 @@ dependencies {
implementation 'com.github.breeding-insight:brapi:2.0.3'
implementation("com.squareup.okhttp3:okhttp:4.9.3")
implementation("com.squareup.okhttp3:logging-interceptor:4.9.3")
+ implementation("io.gsonfire:gson-fire:1.8.4") //required v1
implementation 'org.jsoup:jsoup:1.8.1'
implementation 'net.sourceforge.jexcelapi:jxl:2.6.10'
@@ -173,7 +174,7 @@ dependencies {
implementation "com.github.skydoves:colorpickerpreference:2.0.4"
implementation 'com.github.evrencoskun:TableView:v0.8.9.4'
- implementation 'net.openid:appauth:0.9.0'
+ implementation 'net.openid:appauth:0.11.1'
implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' //google messed up some packages, this package is temporary until the issue is fixed (Feb 2021)
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0"
@@ -246,4 +247,3 @@ repositories {
task prepareKotlinBuildScriptModel {}
-android.defaultConfig.manifestPlaceholders = ['appAuthRedirectScheme': 'fieldbook' ]
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b819a9e45..1bb69df00 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -147,16 +147,25 @@
android:launchMode="singleTop"
android:screenOrientation="portrait" />
-
+ android:exported="true"
+ tools:node="replace">
+
+
+
+
+
+
@@ -165,16 +174,25 @@
android:scheme="https"
tools:ignore="AppLinkUrlError" />
+
+
+
+
-
+ android:path="/auth" />
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 7962bfb63..d8e42f35d 100644
--- a/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java
+++ b/app/src/main/java/com/fieldbook/tracker/activities/CollectActivity.java
@@ -36,6 +36,7 @@
import androidx.appcompat.widget.Toolbar;
import androidx.documentfile.provider.DocumentFile;
import androidx.preference.PreferenceManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.fieldbook.tracker.R;
@@ -48,6 +49,7 @@
import com.fieldbook.tracker.database.models.ObservationUnitModel;
import com.fieldbook.tracker.objects.FieldObject;
import com.fieldbook.tracker.objects.GeoNavHelper;
+import com.fieldbook.tracker.objects.InfoBarModel;
import com.fieldbook.tracker.objects.RangeObject;
import com.fieldbook.tracker.objects.TraitObject;
import com.fieldbook.tracker.objects.VerifyPersonHelper;
@@ -57,6 +59,7 @@
import com.fieldbook.tracker.traits.LayoutCollections;
import com.fieldbook.tracker.traits.PhotoTraitLayout;
import com.fieldbook.tracker.utilities.CategoryJsonUtil;
+import com.fieldbook.tracker.utilities.InfoBarHelper;
import com.fieldbook.tracker.utilities.LocationCollectorUtil;
import com.fieldbook.tracker.utilities.SnackbarUtils;
import com.fieldbook.tracker.utilities.TapTargetUtil;
@@ -103,7 +106,8 @@ public class CollectActivity extends ThemedActivity
implements UsbCameraInterface, SummaryFragment.SummaryOpenListener,
com.fieldbook.tracker.interfaces.CollectController,
com.fieldbook.tracker.interfaces.CollectRangeController,
- com.fieldbook.tracker.interfaces.CollectTraitController {
+ com.fieldbook.tracker.interfaces.CollectTraitController,
+ InfoBarAdapter.InfoBarController {
public static final int REQUEST_FILE_EXPLORER_CODE = 1;
public static final int BARCODE_COLLECT_CODE = 99;
@@ -118,6 +122,10 @@ public class CollectActivity extends ThemedActivity
@Inject
VerifyPersonHelper verifyPersonHelper;
+ //used to query for infobar prefix/value pairs and building InfoBarModels
+ @Inject
+ InfoBarHelper infoBarHelper;
+
public static boolean searchReload;
public static String searchRange;
public static String searchPlot;
@@ -139,6 +147,7 @@ public class CollectActivity extends ThemedActivity
private String inputPlotId = "";
private AlertDialog goToId;
private final Object lock = new Object();
+
/**
* Main screen elements
*/
@@ -146,6 +155,8 @@ public class CollectActivity extends ThemedActivity
private InfoBarAdapter infoBarAdapter;
private TraitBoxView traitBox;
private RangeBoxView rangeBox;
+ private RecyclerView infoBarRv;
+
/**
* Trait-related elements
*/
@@ -405,16 +416,46 @@ private void loadScreen() {
//lock = new Object();
- infoBarAdapter = new InfoBarAdapter(this, ep.getInt(GeneralKeys.INFOBAR_NUMBER, 2), (RecyclerView) findViewById(R.id.selectorList));
-
traitLayouts = new LayoutCollections(this);
rangeBox = findViewById(R.id.act_collect_range_box);
traitBox = findViewById(R.id.act_collect_trait_box);
traitBox.connectRangeBox(rangeBox);
rangeBox.connectTraitBox(traitBox);
+ //setup infobar recycler view ui
+ infoBarRv = findViewById(R.id.act_collect_infobar_rv);
+ infoBarRv.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
+
initCurrentVals();
+ Log.d(TAG, "Load screen.");
+
+ refreshInfoBarAdapter();
+ }
+
+ /**
+ * Updates the infobar adapter with the new plot information.
+ */
+ public void refreshInfoBarAdapter() {
+
+ Log.d(TAG, "Refreshing info bar adapter.");
+
+ try {
+
+ infoBarAdapter = new InfoBarAdapter(this);
+
+ infoBarRv.setAdapter(infoBarAdapter);
+
+ List models = infoBarHelper.getInfoBarData();
+
+ infoBarAdapter.submitList(models);
+
+ } catch (Exception e) {
+
+ e.printStackTrace();
+
+ Log.d(TAG, "Error: info bar adapter loading.");
+ }
}
@Override
@@ -423,6 +464,8 @@ public void refreshMain() {
rangeBox.refresh();
traitBox.setNewTraits(rangeBox.getPlotID());
+ Log.d(TAG, "Refresh main.");
+
initWidgets(true);
refreshLock();
@@ -585,8 +628,10 @@ public void initWidgets(final boolean rangeSuppress) {
// Reset dropdowns
if (!database.isRangeTableEmpty()) {
- String plotID = rangeBox.getPlotID();
- infoBarAdapter.configureDropdownArray(plotID);
+
+ Log.d(TAG, "init widgets refreshing info bar");
+
+ refreshInfoBarAdapter();
}
traitBox.initTraitDetails();
@@ -708,7 +753,9 @@ private void moveToResultCore(int j) {
String pid = rangeBox.getPlotID();
- traitBox.setNewTraits(rangeBox.getPlotID());
+ traitBox.setNewTraits(pid);
+
+ Log.d(TAG, "Move to result core: " + j);
initWidgets(false);
}
@@ -730,6 +777,8 @@ private void moveToResultCore(int j, int traitIndex) {
traitBox.setSelection(traitIndex);
+ Log.d(TAG, "Move to result core: " + j);
+
initWidgets(false);
}
@@ -833,6 +882,8 @@ public void onResume() {
rangeBox.reload();
traitBox.setPrefixTraits();
+ Log.d(TAG, "On resume load data.");
+
initWidgets(false);
traitBox.setSelection(0);
@@ -847,6 +898,9 @@ public void onResume() {
partialReload = false;
rangeBox.display();
traitBox.setPrefixTraits();
+
+ Log.d(TAG, "On resume partial reload data.");
+
initWidgets(false);
} else if (searchReload) {
@@ -975,8 +1029,7 @@ public void updateObservation(String traitName, String traitFormat, String value
}
//update the info bar in case a variable is used
- infoBarAdapter.notifyItemRangeChanged(0, infoBarAdapter.getItemCount());
-
+ refreshInfoBarAdapter();
refreshRepeatedValuesToolbarIndicator();
}
@@ -1110,7 +1163,7 @@ public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case helpId:
TapTargetSequence sequence = new TapTargetSequence(this)
- .targets(collectDataTapTargetView(R.id.selectorList, getString(R.string.tutorial_main_infobars_title), getString(R.string.tutorial_main_infobars_description), R.color.main_primary_dark,200),
+ .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),
@@ -1192,8 +1245,6 @@ public boolean onOptionsItemSelected(MenuItem item) {
*/
case geonavId:
- Log.d(GEOTAG, "Menu item clicked.");
-
geoNavHelper.setMGeoNavActivated(!geoNavHelper.getMGeoNavActivated());
MenuItem navItem = systemMenu.findItem(R.id.action_act_collect_geonav_sw);
if (geoNavHelper.getMGeoNavActivated()) {
@@ -1996,4 +2047,11 @@ public void inflateTrait(@NonNull BaseTraitLayout layout) {
layout.init(this);
v.setVisibility(View.VISIBLE);
}
+
+ @Override
+ public void onInfoBarClicked(int position) {
+
+ infoBarHelper.showInfoBarChoiceDialog(position);
+
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/fieldbook/tracker/activities/brapi/BrapiAuthActivity.java b/app/src/main/java/com/fieldbook/tracker/activities/brapi/BrapiAuthActivity.java
index 0faa8f160..87066f6d3 100644
--- a/app/src/main/java/com/fieldbook/tracker/activities/brapi/BrapiAuthActivity.java
+++ b/app/src/main/java/com/fieldbook/tracker/activities/brapi/BrapiAuthActivity.java
@@ -6,7 +6,6 @@
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
-import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
@@ -19,10 +18,12 @@
import net.openid.appauth.AuthorizationException;
import net.openid.appauth.AuthorizationRequest;
+import net.openid.appauth.AuthorizationResponse;
import net.openid.appauth.AuthorizationService;
import net.openid.appauth.AuthorizationServiceConfiguration;
import net.openid.appauth.Preconditions;
import net.openid.appauth.ResponseTypeValues;
+import net.openid.appauth.TokenResponse;
import net.openid.appauth.connectivity.ConnectionBuilder;
import java.net.HttpURLConnection;
@@ -50,10 +51,10 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
//when coming back from deep link this check keeps app from auto-re-authenticating
if (getIntent() != null && getIntent().getData() == null) {
String flow = sp.getString(GeneralKeys.BRAPI_OIDC_FLOW, "");
- if(flow.equals(getString(R.string.preferences_brapi_oidc_flow_oauth_implicit))) {
- authorizeBrAPI(sp, this);
- }else if(flow.equals(getString(R.string.preferences_brapi_oidc_flow_old_custom))) {
+ if (flow.equals(getString(R.string.preferences_brapi_oidc_flow_old_custom))) {
authorizeBrAPI_OLD(sp, this);
+ } else {
+ authorizeBrAPI(sp, this);
}
}
}
@@ -80,27 +81,18 @@ public void onResume() {
if (data != null) {
// authorization completed
String flow = sp.getString(GeneralKeys.BRAPI_OIDC_FLOW, "");
- if (flow.equals(getString(R.string.preferences_brapi_oidc_flow_oauth_implicit))) {
- checkBrapiAuth(data);
- } else if (flow.equals(getString(R.string.preferences_brapi_oidc_flow_old_custom))) {
+ if (flow.equals(getString(R.string.preferences_brapi_oidc_flow_old_custom))) {
checkBrapiAuth_OLD(data);
+ } else {
+ checkBrapiAuth(data);
}
- // Clear our data from our deep link so the app doesn't think it is
- // coming from a deep link if it is coming from deep link on pause and resume.
- getIntent().setData(null);
-
- setResult(RESULT_OK);
-
- finish();
} else if (ex != null) {
// authorization completed in error
authError(ex);
- finish();
-
} else { //returning from deep link with null data should finish activity
//otherwise the progress bar hangs
@@ -119,9 +111,17 @@ public void authorizeBrAPI(SharedPreferences sharedPreferences, Context context)
editor.putString(GeneralKeys.BRAPI_TOKEN, null);
editor.apply();
+ String flow = sharedPreferences.getString(GeneralKeys.BRAPI_OIDC_FLOW, "");
+ final String responseType = flow.equals(getString(R.string.preferences_brapi_oidc_flow_oauth_implicit)) ?
+ ResponseTypeValues.TOKEN : ResponseTypeValues.CODE;
+
try {
String clientId = "fieldbook";
- Uri redirectURI = Uri.parse("https://fieldbook.phenoapps.org/");
+
+ // Authorization code flow works better with custom URL scheme fieldbook://app/auth
+ // https://github.com/openid/AppAuth-Android/issues?q=is%3Aissue+intent+null
+ Uri redirectURI = flow.equals(getString(R.string.preferences_brapi_oidc_flow_oauth_implicit)) ?
+ Uri.parse("https://fieldbook.phenoapps.org/") : Uri.parse("fieldbook://app/auth");
Uri oidcConfigURI = Uri.parse(sharedPreferences.getString(GeneralKeys.BRAPI_OIDC_URL, ""));
ConnectionBuilder builder = uri -> {
@@ -143,7 +143,8 @@ public void authorizeBrAPI(SharedPreferences sharedPreferences, Context context)
String newUrl = conn.getHeaderField("Location");
// get the cookie if need, for login
String cookies = conn.getHeaderField("Set-Cookie");
- // open the new connnection again
+ conn.disconnect();
+ // open the new connection again
conn = (HttpURLConnection) new URL(newUrl).openConnection();
conn.setRequestProperty("Cookie", cookies);
}else{
@@ -169,7 +170,7 @@ public void onFetchConfigurationCompleted(
new AuthorizationRequest.Builder(
serviceConfig, // the authorization service configuration
clientId, // the client ID, typically pre-registered and static
- ResponseTypeValues.TOKEN, // the response_type value: we want a token
+ responseType, // the response_type value: token or code
redirectURI); // the redirect URI to which the auth response is sent
AuthorizationRequest authRequest = authRequestBuilder.setPrompt("login").build();
@@ -179,16 +180,9 @@ public void onFetchConfigurationCompleted(
Intent responseIntent = new Intent(context, BrapiAuthActivity.class);
responseIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- authService.performAuthorizationRequest(
- authRequest,
- PendingIntent.getActivity(context, 0, responseIntent, PendingIntent.FLAG_IMMUTABLE));
- } else {
- authService.performAuthorizationRequest(
- authRequest,
- PendingIntent.getActivity(context, 0, responseIntent, 0));
- }
-
+ authService.performAuthorizationRequest(
+ authRequest,
+ PendingIntent.getActivity(context, 0, responseIntent, PendingIntent.FLAG_MUTABLE));
}
}, builder);
@@ -197,9 +191,6 @@ public void onFetchConfigurationCompleted(
authError(ex);
- setResult(RESULT_CANCELED);
-
- finish();
}
}
@@ -228,12 +219,32 @@ public void authorizeBrAPI_OLD(SharedPreferences sharedPreferences, Context cont
}
private void authError(Exception ex) {
+
+ // Clear our data from our deep link so the app doesn't think it is
+ // coming from a deep link if it is coming from deep link on pause and resume.
+ getIntent().setData(null);
+
Log.e("BrAPI", "Error starting BrAPI auth", ex);
Toast.makeText(this, R.string.brapi_auth_error_starting, Toast.LENGTH_LONG).show();
+ setResult(RESULT_CANCELED);
+ finish();
}
- private void authSuccess() {
+
+ private void authSuccess(String accessToken) {
+
+ SharedPreferences preferences = getSharedPreferences(GeneralKeys.SHARED_PREF_FILE_NAME, 0);
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putString(GeneralKeys.BRAPI_TOKEN, accessToken);
+ editor.apply();
+
+ // Clear our data from our deep link so the app doesn't think it is
+ // coming from a deep link if it is coming from deep link on pause and resume.
+ getIntent().setData(null);
+
Log.d("BrAPI", "Auth successful");
Toast.makeText(this, R.string.brapi_auth_success, Toast.LENGTH_LONG).show();
+ setResult(RESULT_OK);
+ finish();
}
public void checkBrapiAuth_OLD(Uri data) {
@@ -246,8 +257,6 @@ public void checkBrapiAuth_OLD(Uri data) {
return;
}
- SharedPreferences preferences = getSharedPreferences(GeneralKeys.SHARED_PREF_FILE_NAME, 0);
- SharedPreferences.Editor editor = preferences.edit();
if (status == 200) {
String token = data.getQueryParameter("token");
@@ -256,40 +265,57 @@ public void checkBrapiAuth_OLD(Uri data) {
authError(null);
return;
}
+ authSuccess(token);
- editor.putString(GeneralKeys.BRAPI_TOKEN, token);
- editor.apply();
-
- authSuccess();
- return;
} else {
- editor.putString(GeneralKeys.BRAPI_TOKEN, null);
- editor.apply();
-
authError(null);
- return;
}
}
public void checkBrapiAuth(Uri data) {
+ AuthorizationService authService = new AuthorizationService(this);
+ AuthorizationException ex = AuthorizationException.fromIntent(getIntent());
+ AuthorizationResponse response = AuthorizationResponse.fromIntent(getIntent());
- SharedPreferences preferences = getSharedPreferences(GeneralKeys.SHARED_PREF_FILE_NAME, 0);
- SharedPreferences.Editor editor = preferences.edit();
- data = Uri.parse(data.toString().replaceFirst("#", "?"));
- String token = data.getQueryParameter("access_token");
- // Check that we received a token.
- if (token == null) {
- authError(null);
- return;
- }
+ if (ex != null) {
+ authError(ex);
+ return;
+ }
- if(token.startsWith("Bearer ")){
- token = token.replaceFirst("Bearer ", "");
- }
+ if (response != null && response.authorizationCode != null) {
+ authService.performTokenRequest(
+ response.createTokenExchangeRequest(),
+ new AuthorizationService.TokenResponseCallback() {
+ @Override
+ public void onTokenRequestCompleted(@Nullable TokenResponse response, @Nullable AuthorizationException ex) {
+ if (response != null && response.accessToken != null) {
+ authSuccess(response.accessToken);
+ } else {
+ authError(null);
+ }
+ }
+ });
+ return;
+ }
- editor.putString(GeneralKeys.BRAPI_TOKEN, token);
- editor.apply();
+ if (response != null && response.accessToken != null) {
+ authSuccess(response.accessToken);
+ return;
+ }
+
+ // Original check for access_token
+ data = Uri.parse(data.toString().replaceFirst("#", "?"));
+ String token = data.getQueryParameter("access_token");
+ // Check that we received a token.
+ if (token == null) {
+ authError(null);
+ return;
+ }
+
+ if (token.startsWith("Bearer ")) {
+ token = token.replaceFirst("Bearer ", "");
+ }
- authSuccess();
+ authSuccess(token);
}
}
diff --git a/app/src/main/java/com/fieldbook/tracker/adapters/AttributeAdapter.kt b/app/src/main/java/com/fieldbook/tracker/adapters/AttributeAdapter.kt
new file mode 100644
index 000000000..bdd9c7c7b
--- /dev/null
+++ b/app/src/main/java/com/fieldbook/tracker/adapters/AttributeAdapter.kt
@@ -0,0 +1,67 @@
+package com.fieldbook.tracker.adapters
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.fieldbook.tracker.R
+
+/**
+ * Reference:
+ * https://developer.android.com/guide/topics/ui/layout/recyclerview
+ */
+
+class AttributeAdapter(private val controller: AttributeAdapterController) :
+ ListAdapter(DiffCallback()) {
+
+ interface AttributeAdapterController {
+ fun onAttributeClicked(label: String, position: Int)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val v = LayoutInflater.from(parent.context)
+ .inflate(R.layout.list_item_attribute, parent, false)
+ return ViewHolder(v as ConstraintLayout)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+
+ holder.itemView.setOnClickListener {
+ controller.onAttributeClicked(holder.itemView.tag as String, position)
+ }
+
+ with (currentList[position]) {
+ holder.itemView.tag = this
+ setViewHolderText(holder, this)
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return currentList.size
+ }
+
+ private fun setViewHolderText(holder: ViewHolder, label: String?) {
+ holder.attributeTv.text = "$label"
+ }
+
+
+ class ViewHolder(v: ConstraintLayout) : RecyclerView.ViewHolder(v) {
+
+ var attributeTv: TextView = v.findViewById(R.id.list_item_attribute_tv)
+
+ }
+
+ class DiffCallback : DiffUtil.ItemCallback() {
+
+ override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
+ return oldItem == newItem
+ }
+
+ override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
+ return oldItem == newItem
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/fieldbook/tracker/adapters/InfoBarAdapter.java b/app/src/main/java/com/fieldbook/tracker/adapters/InfoBarAdapter.java
deleted file mode 100644
index d73e072fb..000000000
--- a/app/src/main/java/com/fieldbook/tracker/adapters/InfoBarAdapter.java
+++ /dev/null
@@ -1,190 +0,0 @@
-package com.fieldbook.tracker.adapters;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
-import android.widget.TextView;
-
-import androidx.constraintlayout.widget.ConstraintLayout;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.fieldbook.tracker.R;
-import com.fieldbook.tracker.database.DataHelper;
-import com.fieldbook.tracker.database.dao.ObservationDao;
-import com.fieldbook.tracker.objects.TraitObject;
-import com.fieldbook.tracker.preferences.GeneralKeys;
-import com.fieldbook.tracker.views.DynamicWidthSpinner;
-
-import org.apache.commons.lang3.ArrayUtils;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-
-public class InfoBarAdapter extends RecyclerView.Adapter {
-
- private Context context;
- private int maxSelectors;
- private DataHelper dataHelper;
- private String plotId;
- private RecyclerView selectorsView;
-
- // Provide a suitable constructor (depends on the kind of dataset)
- public InfoBarAdapter(Context context, int maxSelectors, RecyclerView selectorsView) {
- this.context = context;
- this.maxSelectors = maxSelectors;
- this.selectorsView = selectorsView;
-
- this.dataHelper = new DataHelper(context);
- }
-
- public void configureDropdownArray(String plotId) {
- this.plotId = plotId;
- selectorsView.setHasFixedSize(false);
-
- RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this.context);
- selectorsView.setLayoutManager(layoutManager);
-
- selectorsView.setAdapter(this);
- }
-
- // Create new views (invoked by the layout manager)
- @Override
- public InfoBarAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- // create a new view
- View v = LayoutInflater.from(parent.getContext())
- .inflate(R.layout.item_infobar_dropdown, parent, false);
- return new ViewHolder((ConstraintLayout) v);
- }
-
- // Replace the contents of a view (invoked by the layout manager)
- @Override
- public void onBindViewHolder(InfoBarAdapter.ViewHolder holder, int position) {
- // - get element from your dataset at this position
- // - replace the contents of the view with that element
-
- DynamicWidthSpinner spinner = holder.mTextView.findViewById(R.id.selectorSpinner);
- TextView text = holder.mTextView.findViewById(R.id.selectorText);
- configureSpinner(spinner, text, position);
- configureText(text);
- }
-
- // Return the size of your dataset (invoked by the layout manager)
- @Override
- public int getItemCount() {
- return maxSelectors;
- }
-
- private SharedPreferences getSharedPref() {
- return this.context.getSharedPreferences(GeneralKeys.SHARED_PREF_FILE_NAME, 0);
- }
-
- private void configureSpinner(final DynamicWidthSpinner spinner, final TextView text, final int position) {
-
- String expId = Integer.toString(getSharedPref().getInt(GeneralKeys.SELECTED_FIELD_ID, 0));
-
- //the prefix obs. unit. traits s.a plot_id, row, column, defined by the user
- String[] prefixTraits = dataHelper.getRangeColumnNames();
-
- //the observation variable traits
- String[] obsTraits = dataHelper.getAllTraitObjects().stream().map(TraitObject::getTrait).toArray(String[]::new);
-
- //combine the traits to be viewed within info bars
- final String[] allTraits = ArrayUtils.addAll(prefixTraits, obsTraits);
-
- ArrayAdapter prefixArrayAdapter = new ArrayAdapter<>(this.context, R.layout.custom_spinner_layout, allTraits);
-
- spinner.setAdapter(prefixArrayAdapter);
-
- int spinnerPosition = prefixArrayAdapter.getPosition(getSharedPref().getString("DROP" + position, allTraits[0]));
-
- //if field changes dont contain same prefixes, this can be -1, so just reset it to 0
- if (spinnerPosition < 0) spinnerPosition = 0;
-
- spinner.setSelection(spinnerPosition);
-
- spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
- @Override
- public void onItemSelected(AdapterView> arg0, View arg1, int pos, long arg3) {
- try {
-
- //when an item is selected, check if its a prefix trait or a variable and populate the info bar values
- String infoTrait = allTraits[pos];
-
- ArrayList infoBarValues = null;
- if (Arrays.asList(prefixTraits).contains(infoTrait)) { //get data from obs. properties (range)
-
- infoBarValues = new ArrayList<>(Arrays.asList(dataHelper.getDropDownRange(infoTrait, plotId)));
-
- } else if (Arrays.asList(obsTraits).contains(infoTrait)) { //get data from observations
-
- infoBarValues = new ArrayList<>(Collections.singletonList(
- ObservationDao.Companion.getUserDetail(expId, plotId).get(infoTrait)));
- }
-
- if (infoBarValues == null || infoBarValues.size() == 0) {
-
- text.setText(context.getString(R.string.main_infobar_data_missing));
-
- } else {
-
- text.setText(infoBarValues.get(0));
-
- }
-
- getSharedPref().edit().putString("DROP" + position, allTraits[pos]).apply();
-
- } catch (Exception e) {
-
- e.printStackTrace();
-
- }
-
- spinner.requestFocus();
- }
-
- @Override
- public void onNothingSelected(AdapterView> arg0) {
-
- }
- });
- }
-
- @SuppressLint("ClickableViewAccessibility")
- private void configureText(final TextView text) {
- text.setOnTouchListener(new View.OnTouchListener() {
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- text.setMaxLines(5);
- break;
- case MotionEvent.ACTION_UP:
- text.setMaxLines(1);
- break;
- }
- return true;
- }
- });
- }
-
- // Provide a reference to the views for each data item
- // Complex data items may need more than one view per item, and
- // you provide access to all the views for a data item in a view holder
- static class ViewHolder extends RecyclerView.ViewHolder {
- // each data item is just a string in this case
- ConstraintLayout mTextView;
-
- ViewHolder(ConstraintLayout v) {
- super(v);
- mTextView = v;
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/fieldbook/tracker/adapters/InfoBarAdapter.kt b/app/src/main/java/com/fieldbook/tracker/adapters/InfoBarAdapter.kt
new file mode 100644
index 000000000..4a763b318
--- /dev/null
+++ b/app/src/main/java/com/fieldbook/tracker/adapters/InfoBarAdapter.kt
@@ -0,0 +1,93 @@
+package com.fieldbook.tracker.adapters
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.fieldbook.tracker.R
+import com.fieldbook.tracker.objects.InfoBarModel
+
+/**
+ * Reference:
+ * https://developer.android.com/guide/topics/ui/layout/recyclerview
+ *
+ * Infobar adapter handles data within the infobar recycler view on the collect screen.
+ * The infobars are a user preference, and can be set to display any of the plot attributes or traits,
+ * each list item has a prefix and value, where the value represents the attr's value for the current plot.
+ * e.g:
+ * prefix: value
+ * col: 1,
+ * row: 2,
+ * height: 21
+ * They are displayed in the top left corner of the collect screen
+ */
+class InfoBarAdapter(private val context: Context) :
+ ListAdapter(DiffCallback()) {
+
+ interface InfoBarController {
+ fun onInfoBarClicked(position: Int)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+
+ val v = LayoutInflater.from(parent.context)
+ .inflate(R.layout.list_item_infobar, parent, false)
+ return ViewHolder(v as ConstraintLayout)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+
+ with (currentList[position]) {
+ setViewHolderText(holder, prefix, value)
+ }
+
+ holder.prefixTextView.setOnClickListener {
+
+ (context as InfoBarController).onInfoBarClicked(position)
+
+ }
+
+ holder.valueTextView.setOnTouchListener { v, event ->
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> holder.valueTextView.maxLines = 5
+ MotionEvent.ACTION_UP -> holder.valueTextView.maxLines = 1
+ }
+ true
+ }
+ }
+
+ private fun setViewHolderText(holder: ViewHolder, label: String?, value: String) {
+ holder.prefixTextView.text = "$label: "
+ holder.valueTextView.text = value
+ }
+
+ class ViewHolder(v: ConstraintLayout) : RecyclerView.ViewHolder(v) {
+
+ var prefixTextView: TextView
+ var valueTextView: TextView
+
+ init {
+ prefixTextView = v.findViewById(R.id.list_item_infobar_prefix)
+ valueTextView = v.findViewById(R.id.list_item_infobar_value)
+ }
+ }
+
+ // Return the size of your dataset (invoked by the layout manager)
+ override fun getItemCount() = currentList.size
+
+ class DiffCallback : DiffUtil.ItemCallback() {
+
+ override fun areItemsTheSame(oldItem: InfoBarModel, newItem: InfoBarModel): Boolean {
+ return oldItem.prefix == newItem.prefix && oldItem.value == newItem.value
+ }
+
+ override fun areContentsTheSame(oldItem: InfoBarModel, newItem: InfoBarModel): Boolean {
+ return oldItem.prefix == newItem.prefix && oldItem.value == newItem.value
+ }
+ }
+}
diff --git a/app/src/main/java/com/fieldbook/tracker/brapi/service/BrAPIServiceV1.java b/app/src/main/java/com/fieldbook/tracker/brapi/service/BrAPIServiceV1.java
index 03a89558f..589086165 100644
--- a/app/src/main/java/com/fieldbook/tracker/brapi/service/BrAPIServiceV1.java
+++ b/app/src/main/java/com/fieldbook/tracker/brapi/service/BrAPIServiceV1.java
@@ -441,6 +441,23 @@ public void getPlotDetails(final String studyDbId,
final Function function,
final Function failFunction) {
try {
+
+ //level is not a required field, so passing null here is fine
+ //otherwise this was causing a NPE when observationLevel was null
+ //check if observationLevel is null
+ @Nullable
+ String levelName = null;
+ if (observationLevel != null) {
+
+ if (observationLevel.getObservationLevelName() != null) {
+
+ levelName = observationLevel.getObservationLevelName();
+
+ }
+ }
+
+ final String level = levelName;
+
final AtomicInteger currentPage = new AtomicInteger(0);
final Integer pageSize = Integer.parseInt(context.getSharedPreferences(GeneralKeys.SHARED_PREF_FILE_NAME, 0)
.getString(GeneralKeys.BRAPI_PAGE_SIZE, "50"));
@@ -481,13 +498,6 @@ public void onSuccess(ObservationUnitsResponse1 response, int i, Map> map)
};
studiesApi.studiesStudyDbIdObservationunitsGetAsync(
- studyDbId, observationLevel.getObservationLevelName(), 0, pageSize,
+ studyDbId, level, 0, pageSize,
getBrapiToken(), callback);
} catch (ApiException e) {
@@ -1006,6 +1016,7 @@ private String buildCategoryList(List categories) {
c.setValue(value);
c.setLabel(value);
}
+ scale.add(c);
}
return CategoryJsonUtil.Companion.encode(scale);
} catch (Exception e) {
diff --git a/app/src/main/java/com/fieldbook/tracker/dialogs/CollectAttributeChooserDialog.kt b/app/src/main/java/com/fieldbook/tracker/dialogs/CollectAttributeChooserDialog.kt
new file mode 100644
index 000000000..455eb612e
--- /dev/null
+++ b/app/src/main/java/com/fieldbook/tracker/dialogs/CollectAttributeChooserDialog.kt
@@ -0,0 +1,159 @@
+package com.fieldbook.tracker.dialogs
+
+import android.app.Dialog
+import android.os.Bundle
+import android.util.Log
+import android.view.ViewGroup
+import android.widget.Button
+import androidx.recyclerview.widget.RecyclerView
+import com.fieldbook.tracker.R
+import com.fieldbook.tracker.activities.CollectActivity
+import com.fieldbook.tracker.adapters.AttributeAdapter
+import com.fieldbook.tracker.objects.TraitObject
+import com.fieldbook.tracker.preferences.GeneralKeys
+import com.google.android.material.tabs.TabLayout
+import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.MainScope
+
+/**
+ * A tab layout with tabs: attributes, traits, and other.
+ * Each tab will load data into a recycler view that lets user choose infobar prefixes.
+ */
+class CollectAttributeChooserDialog(private val activity: CollectActivity):
+ Dialog(activity, R.style.AppAlertDialog),
+ AttributeAdapter.AttributeAdapterController,
+ CoroutineScope by MainScope() {
+
+ companion object {
+ const val TAG = "AttDialog"
+ }
+
+ private lateinit var tabLayout: TabLayout
+ private lateinit var recyclerView: RecyclerView
+ private lateinit var cancelButton: Button
+
+ private var attributes = arrayOf()
+ private var traits = arrayOf()
+ private var other = arrayOf()
+
+ var infoBarPosition: Int = 0
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ //setup dialog ui
+ window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+
+ setContentView(R.layout.dialog_collect_att_chooser)
+
+ setTitle(R.string.dialog_collect_att_chooser_title)
+
+ //initialize ui elements
+ tabLayout = findViewById(R.id.dialog_collect_att_chooser_tl)
+ recyclerView = findViewById(R.id.dialog_collect_att_chooser_lv)
+ cancelButton = findViewById(R.id.dialog_collect_att_chooser_cancel_btn)
+
+ //setCancelable(false)
+
+ setCanceledOnTouchOutside(true)
+
+ cancelButton.setOnClickListener {
+ this.cancel()
+ }
+
+ setOnShowListener {
+
+ //query database for attributes/traits to use
+ try {
+ attributes = activity.getDatabase().getAllObservationUnitAttributeNames(activity.studyId.toInt())
+ traits = activity.getDatabase().allTraitObjects.toTypedArray()
+ other = traits.filter { !it.visible }.toTypedArray()
+ traits = traits.filter { it.visible }.toTypedArray()
+ } catch (e: Exception) {
+ Log.d(TAG, "Error occurred when querying for attributes in Collect Activity.")
+ e.printStackTrace()
+ }
+
+ //setup ui
+ try {
+ setupTabLayout()
+ } catch (e: Exception) {
+ Log.d(TAG, "Error occurred when setting up attribute tab layout.")
+ e.printStackTrace()
+ }
+ }
+ }
+
+ /**
+ * data automatically changes when tab is selected,
+ * select first tab programmatically to load initial data
+ * save the selected tab as preference
+ */
+ private fun setupTabLayout() {
+
+ tabLayout.addOnTabSelectedListener(object : OnTabSelectedListener {
+
+ override fun onTabSelected(tab: TabLayout.Tab?) {
+
+ tab?.let { t ->
+
+ loadTab(t.text.toString())
+
+ activity.getPreferences()
+ .edit().putInt(GeneralKeys.ATTR_CHOOSER_DIALOG_TAB, t.position)
+ .apply()
+ }
+ }
+
+ override fun onTabUnselected(tab: TabLayout.Tab?) {}
+ override fun onTabReselected(tab: TabLayout.Tab?) {
+
+ tab?.let { t -> loadTab(t.text.toString()) }
+ }
+ })
+
+ //manually select the first tab based on preferences
+ val tabIndex = activity.getPreferences()
+ .getInt(GeneralKeys.ATTR_CHOOSER_DIALOG_TAB, 1)
+
+ tabLayout.selectTab(tabLayout.getTabAt(tabIndex))
+ }
+
+ /**
+ * handles loading data into the recycler view adapter
+ */
+ private fun loadTab(label: String) {
+
+ val attributesLabel = activity.getString(R.string.dialog_att_chooser_attributes)
+ val traitsLabel = activity.getString(R.string.dialog_att_chooser_traits)
+
+ //get values to display based on cached arrays
+ val infoBarLabels = when (label) {
+ attributesLabel -> attributes
+ traitsLabel -> traits.filter { it.visible }.map { it.trait }.toTypedArray()
+ else -> other.map { it.trait }.toTypedArray()
+ }
+
+ //create adapter of labels s.a : plot/column/block or height/picture/notes depending on what tab is selected
+ val adapter = AttributeAdapter(this)
+
+ recyclerView.adapter = adapter
+
+ adapter.submitList(infoBarLabels.toList())
+ }
+
+ /**
+ * Tab recycler view list item click listener.
+ * When user selects a prefix, it is saved in preferences for this infobar position (global)
+ * then the dialog is dismissed after refreshing collect
+ */
+ override fun onAttributeClicked(label: String, position: Int) {
+
+ activity.getPreferences().edit().putString("DROP$infoBarPosition", label).apply()
+
+ activity.refreshInfoBarAdapter()
+
+ dismiss()
+ }
+}
diff --git a/app/src/main/java/com/fieldbook/tracker/objects/FieldFileObject.java b/app/src/main/java/com/fieldbook/tracker/objects/FieldFileObject.java
index 2fb8a76c9..002be5181 100644
--- a/app/src/main/java/com/fieldbook/tracker/objects/FieldFileObject.java
+++ b/app/src/main/java/com/fieldbook/tracker/objects/FieldFileObject.java
@@ -1,5 +1,9 @@
package com.fieldbook.tracker.objects;
+import static org.apache.poi.ss.usermodel.Cell.CELL_TYPE_BOOLEAN;
+import static org.apache.poi.ss.usermodel.Cell.CELL_TYPE_NUMERIC;
+import static org.apache.poi.ss.usermodel.Cell.CELL_TYPE_STRING;
+
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
@@ -11,8 +15,10 @@
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;
+import org.apache.poi.ss.usermodel.FormulaEvaluator;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.usermodel.XSSFCell;
+import org.apache.poi.xssf.usermodel.XSSFFormulaEvaluator;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
@@ -350,7 +356,16 @@ public String[] readNext() {
}
public void close() {
- //TODO check for memory leak
+ try {
+ if (wb != null) {
+ wb.close();
+ }
+ if (super.getInputStream() != null) {
+ super.getInputStream().close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
}
}
@@ -381,23 +396,26 @@ public boolean isOther() {
public String[] getColumns() {
try {
-
- wb = new XSSFWorkbook(super.getInputStream());
- XSSFSheet sheet = wb.getSheetAt(0);
+ if (super.getInputStream() != null) {
- XSSFRow headerRow = sheet.getRow(0);
+ wb = new XSSFWorkbook(super.getInputStream());
- ArrayList columns = new ArrayList<>();
+ XSSFSheet sheet = wb.getSheetAt(0);
- for (Iterator it = headerRow.cellIterator(); it.hasNext();) {
- XSSFCell cell = (XSSFCell) it.next();
+ XSSFRow headerRow = sheet.getRow(0);
- columns.add(cell.getStringCellValue());
+ ArrayList columns = new ArrayList<>();
- }
+ for (Iterator it = headerRow.cellIterator(); it.hasNext();) {
+ XSSFCell cell = (XSSFCell) it.next();
+
+ columns.add(cell.getStringCellValue());
+
+ }
- return columns.toArray(new String[] {});
+ return columns.toArray(new String[] {});
+ }
} catch (IOException format) {
@@ -443,6 +461,8 @@ public String[] readNext() {
ArrayList rows = new ArrayList<>();
XSSFSheet sheet = wb.getSheetAt(0);
+ XSSFFormulaEvaluator evaluator = wb.getCreationHelper().createFormulaEvaluator();
+
for (Iterator it = sheet.rowIterator(); it.hasNext();) {
rows.add((XSSFRow) it.next());
}
@@ -454,7 +474,22 @@ public String[] readNext() {
ArrayList data = new ArrayList<>();
for (Iterator it = rows.get(currentRow).cellIterator(); it.hasNext();) {
XSSFCell cell = (XSSFCell) it.next();
- data.add(fmt.formatCellValue(cell));
+ if (cell.getCellType() == Cell.CELL_TYPE_FORMULA) {//formula
+ int type = evaluator.evaluateFormulaCell(cell);
+ switch (type) {
+ case CELL_TYPE_BOOLEAN:
+ data.add(String.valueOf(cell.getBooleanCellValue()));
+ break;
+ case CELL_TYPE_NUMERIC:
+ data.add(String.valueOf(cell.getNumericCellValue()));
+ break;
+ default:
+ data.add(cell.getStringCellValue());
+ break;
+ }
+ } else {
+ data.add(fmt.formatCellValue(cell));
+ }
}
currentRow += 1;
@@ -462,7 +497,16 @@ public String[] readNext() {
}
public void close() {
- //todo check for memory leak
+ try {
+ if (wb != null) {
+ wb.close();
+ }
+ if (super.getInputStream() != null) {
+ super.getInputStream().close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
}
}
@@ -472,6 +516,9 @@ public void close() {
* @return attempt to parse the string value of the cell
*/
private static String getCellStringValue(XSSFCell cell) {
+
+ FormulaEvaluator evaluator = cell.getSheet().getWorkbook().getCreationHelper().createFormulaEvaluator();
+
switch (cell.getCellType()) {
case 0: { //numeric
return String.valueOf(cell.getNumericCellValue());
@@ -479,6 +526,16 @@ private static String getCellStringValue(XSSFCell cell) {
case 1: { //text
return cell.getStringCellValue();
}
+ case Cell.CELL_TYPE_FORMULA: { //formula
+ switch (evaluator.evaluateFormulaCell(cell)) {
+ case CELL_TYPE_BOOLEAN:
+ return String.valueOf(cell.getBooleanCellValue());
+ case CELL_TYPE_NUMERIC:
+ return String.valueOf(cell.getNumericCellValue());
+ case CELL_TYPE_STRING:
+ return cell.getStringCellValue();
+ }
+ }
case 3: { //boolean
return String.valueOf(cell.getBooleanCellValue());
}
diff --git a/app/src/main/java/com/fieldbook/tracker/objects/InfoBarModel.kt b/app/src/main/java/com/fieldbook/tracker/objects/InfoBarModel.kt
new file mode 100644
index 000000000..e2d73c257
--- /dev/null
+++ b/app/src/main/java/com/fieldbook/tracker/objects/InfoBarModel.kt
@@ -0,0 +1,10 @@
+package com.fieldbook.tracker.objects
+
+/**
+ * Simple data class to hold info bar data and make a ":" delimited string.
+ */
+data class InfoBarModel(val prefix: String, val value: String) {
+ override fun toString(): String {
+ return "$prefix: $value"
+ }
+}
\ No newline at end of file
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 24c4589ce..8a4ed0300 100644
--- a/app/src/main/java/com/fieldbook/tracker/preferences/BrapiPreferencesFragment.java
+++ b/app/src/main/java/com/fieldbook/tracker/preferences/BrapiPreferencesFragment.java
@@ -66,6 +66,7 @@ public void onAttach(@NonNull Context context) {
super.onAttach(context);
// Occurs before onCreate function. We get the context this way.
BrapiPreferencesFragment.this.context = context;
+
}
@Override
@@ -82,6 +83,14 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
prefMgr = getPreferenceManager();
prefMgr.setSharedPreferencesName(GeneralKeys.SHARED_PREF_FILE_NAME);
+ //remove old custom fb auth if it is being used
+ if (prefMgr.getSharedPreferences().getString(GeneralKeys.BRAPI_OIDC_FLOW, getString(R.string.preferences_brapi_oidc_flow_oauth_implicit))
+ .equals(getString(R.string.preferences_brapi_oidc_flow_old_custom))) {
+
+ prefMgr.getSharedPreferences().edit().putString(GeneralKeys.BRAPI_OIDC_FLOW, getString(R.string.preferences_brapi_oidc_flow_oauth_implicit)).apply();
+
+ }
+
setPreferencesFromResource(R.xml.preferences_brapi, rootKey);
setupToolbar();
@@ -424,8 +433,9 @@ private void setOidcFlowUi() {
if (preferenceCategory != null) {
- if(prefMgr.getSharedPreferences().getString(GeneralKeys.BRAPI_OIDC_FLOW, getString(R.string.preferences_brapi_oidc_flow_oauth_implicit))
- .equals(getString(R.string.preferences_brapi_oidc_flow_oauth_implicit))) {
+ if(!prefMgr.getSharedPreferences().getString(GeneralKeys.BRAPI_OIDC_FLOW, getString(R.string.preferences_brapi_oidc_flow_oauth_implicit))
+ .equals(getString(R.string.preferences_brapi_oidc_flow_old_custom))
+ ) {
preferenceCategory.addPreference(brapiOIDCURLPreference);
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 360dfde63..cc2395774 100644
--- a/app/src/main/java/com/fieldbook/tracker/preferences/GeneralKeys.java
+++ b/app/src/main/java/com/fieldbook/tracker/preferences/GeneralKeys.java
@@ -45,6 +45,7 @@ public class GeneralKeys {
public static final String USE_DAY_OF_YEAR = "UseDay";
public static final String DISABLE_SHARE = "DisableShare";
public static final String GENERAL_LOCATION_COLLECTION = "com.fieldbook.tracker.GENERAL_LOCATION_COLLECTION";
+ public static final String ATTR_CHOOSER_DIALOG_TAB = "ATTR_CHOOSER_DIALOG_TAB";
// Files and Naming
public static final String DEFAULT_STORAGE_LOCATION_PREFERENCE = "DEFAULT_STORAGE_LOCATION_PREFERENCE";
diff --git a/app/src/main/java/com/fieldbook/tracker/utilities/InfoBarHelper.kt b/app/src/main/java/com/fieldbook/tracker/utilities/InfoBarHelper.kt
new file mode 100644
index 000000000..c51f028a2
--- /dev/null
+++ b/app/src/main/java/com/fieldbook/tracker/utilities/InfoBarHelper.kt
@@ -0,0 +1,159 @@
+package com.fieldbook.tracker.utilities
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.util.Log
+import com.fieldbook.tracker.R
+import com.fieldbook.tracker.activities.CollectActivity
+import com.fieldbook.tracker.database.DataHelper
+import com.fieldbook.tracker.dialogs.CollectAttributeChooserDialog
+import com.fieldbook.tracker.objects.InfoBarModel
+import com.fieldbook.tracker.preferences.GeneralKeys
+import dagger.hilt.android.qualifiers.ActivityContext
+import java.util.*
+import javax.inject.Inject
+
+/**
+ * Helper class for handling all infobar data and logic.
+ * Used in collect activity.
+ */
+class InfoBarHelper @Inject constructor(@ActivityContext private val context: Context) {
+
+ companion object {
+ const val TAG = "InfoBarHelper"
+ }
+
+ @Inject
+ lateinit var database: DataHelper
+
+ private val ad = CollectAttributeChooserDialog(context as CollectActivity)
+
+ private val ep: SharedPreferences by lazy {
+ context.getSharedPreferences(GeneralKeys.SHARED_PREF_FILE_NAME, Context.MODE_PRIVATE)
+ }
+
+ /**
+ * Queries the database for the value of the label.
+ * Attributes use the getDropDownRange call, where traits use getUserDetail
+ */
+ private fun queryForLabelValue(plotId: String, label: String, isAttribute: Boolean): String {
+
+ val dataMissingString: String = context.getString(R.string.main_infobar_data_missing)
+
+ return if (isAttribute) {
+
+ val values = database.getDropDownRange(label, plotId)
+ if (values == null || values.isEmpty()) {
+ dataMissingString
+ } else {
+ values[0]
+ }
+
+ } else {
+
+ var value = database.getUserDetail(plotId)[label] ?: dataMissingString
+
+ value = try {
+
+ val labelValPref: String = (context as CollectActivity).getPreferences()
+ .getString(GeneralKeys.LABELVAL_CUSTOMIZE, "value") ?: "value"
+
+ val joiner = StringJoiner(":")
+ val scale = CategoryJsonUtil.decode(value)
+ for (s in scale) {
+ if (labelValPref == "label") {
+ joiner.add(s.label)
+ } else joiner.add(s.value)
+ }
+
+ joiner.toString()
+
+ } catch (ignore: Exception) { value }
+
+ value
+ }
+ }
+
+ /**
+ * Reads the number of preference infobars and creates models for each one
+ */
+ fun getInfoBarData(): ArrayList? {
+
+ //get the preference number of infobars to load
+ val numInfoBars: Int = ep.getInt(GeneralKeys.INFOBAR_NUMBER, 2)
+
+ //initialize a list of infobar models that will be served to the adapter
+ val infoBarModels = ArrayList()
+
+ //iterate and build teh arraylist
+ for (i in 0 until numInfoBars) {
+
+ //ensure that the initialLabel is actually a plot attribute
+
+ //get all plot attribute names for the study
+ val attributes: List = ArrayList(Arrays.asList(*database.rangeColumnNames))
+
+ //get all traits for this study
+ val traits = database.allTraitObjects
+
+ //create a new array with just trait names
+ val traitNames = ArrayList()
+ for (t in traits) {
+ traitNames.add(t.trait)
+ }
+
+ //get the default label for the infobar using 'Select' (used in original adapter code)
+ // or the first item in the attributes list
+ var defaultLabel = "Select"
+ if (attributes.size > 0) {
+ defaultLabel = attributes[0]
+ }
+
+ //get the preferred infobar label, default to above if it doesn't exist for this position
+ //adapter preferred values are saved as DROP1, DROP2, DROP3, DROP4, DROP5 in preferences, intiailize the label with it
+ var initialLabel: String = ep.getString("DROP$i", defaultLabel) ?: defaultLabel
+
+ //check if the label is an attribute or a trait, this will decide how to query the database for the value
+ val isAttribute = attributes.contains(initialLabel)
+
+ //check if the label actually exists in the attributes/traits (this will reset on field switch)
+ //if it doesn't exist, default to the first attribute in the list
+ if (!isAttribute) {
+ if (!traitNames.contains(initialLabel)) {
+ if (attributes.isNotEmpty()) {
+ initialLabel = attributes[0]
+ }
+ }
+ }
+
+ //query the database for the label's value
+ (context as? CollectActivity)?.getRangeBox()?.getPlotID()?.let { plot ->
+
+ val value = queryForLabelValue(plot, initialLabel, isAttribute)
+
+ infoBarModels.add(InfoBarModel(initialLabel, value))
+ }
+ }
+
+ return infoBarModels
+ }
+
+ /**
+ * Sets the infoBar position state and calls the dialog for users to choose an attribute.
+ */
+ fun showInfoBarChoiceDialog(position: Int) {
+
+ try {
+
+ ad.infoBarPosition = position
+
+ ad.show()
+
+ } catch (e: Exception) {
+
+ e.printStackTrace()
+
+ Log.d(TAG, "Error showing infobar dialog: ${e.message}")
+ }
+ }
+}
\ 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 78ef4af5c..f71a51473 100644
--- a/app/src/main/java/com/fieldbook/tracker/views/RangeBoxView.kt
+++ b/app/src/main/java/com/fieldbook/tracker/views/RangeBoxView.kt
@@ -7,6 +7,7 @@ import android.os.Handler
import android.text.Editable
import android.text.TextWatcher
import android.util.AttributeSet
+import android.util.Log
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
@@ -197,7 +198,6 @@ class RangeBoxView : ConstraintLayout {
controller.getTraitBox().setNewTraits(getPlotID())
- controller.initWidgets(true)
}
private fun truncate(s: String, maxLen: Int): String? {
@@ -390,6 +390,8 @@ class RangeBoxView : ConstraintLayout {
display()
controller.getTraitBox().setNewTraits(getPlotID())
controller.initWidgets(true)
+
+ Log.d("Field Book", "refresh widgets range box repeate key press")
}
}
diff --git a/app/src/main/res/layout/activity_collect.xml b/app/src/main/res/layout/activity_collect.xml
index ea886a3b6..3ca563fbd 100644
--- a/app/src/main/res/layout/activity_collect.xml
+++ b/app/src/main/res/layout/activity_collect.xml
@@ -82,7 +82,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/list_item_attribute.xml b/app/src/main/res/layout/list_item_attribute.xml
new file mode 100644
index 000000000..43f99ad46
--- /dev/null
+++ b/app/src/main/res/layout/list_item_attribute.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_infobar_dropdown.xml b/app/src/main/res/layout/list_item_infobar.xml
similarity index 59%
rename from app/src/main/res/layout/item_infobar_dropdown.xml
rename to app/src/main/res/layout/list_item_infobar.xml
index eedede4c2..e3602f10d 100644
--- a/app/src/main/res/layout/item_infobar_dropdown.xml
+++ b/app/src/main/res/layout/list_item_infobar.xml
@@ -7,48 +7,34 @@
android:layout_height="wrap_content"
android:orientation="horizontal">
-
-
diff --git a/app/src/main/res/layout/list_item_search_constructor.xml b/app/src/main/res/layout/list_item_search_constructor.xml
index 472f8895f..51eeae50b 100644
--- a/app/src/main/res/layout/list_item_search_constructor.xml
+++ b/app/src/main/res/layout/list_item_search_constructor.xml
@@ -13,6 +13,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
+ android:layout_weight="1"
android:orientation="vertical">
+ New InfoBar prefix selector
Improved trait reordering
Improved external barcode scanning
Fixed issue with trait cloud imports
diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml
index d52b4cb13..ad744e9c8 100644
--- a/app/src/main/res/values/array.xml
+++ b/app/src/main/res/values/array.xml
@@ -106,8 +106,8 @@
+ - @string/preferences_brapi_oidc_flow_oauth_code
- @string/preferences_brapi_oidc_flow_oauth_implicit
- - @string/preferences_brapi_oidc_flow_old_custom
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6ff411360..b2458c327 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -206,6 +206,7 @@
V1
V2
OIDC Flow
+ OAuth2 Authorization Code
OAuth2 Implicit Grant
Original Field Book Custom
@@ -789,6 +790,10 @@
Delete Observation Sound
Notification sound when observation is deleted in Collect activity.
File already exists.
+ Attributes
+ Traits
+ Other
+ Choose an attribute
Authorize BrAPI
Downloading install fileβ¦
Update Check
diff --git a/version.properties b/version.properties
index 802b91f8b..262395aac 100644
--- a/version.properties
+++ b/version.properties
@@ -1,4 +1,4 @@
majorVersion=5
minorVersion=4
-patchVersion=9
-buildNumber=14
+patchVersion=14
+buildNumber=19
| | |