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
HMS17

πŸ’» zrm22
zrm22

πŸ’» - Joe Gage
Joe Gage

πŸ› + Joe Gage
Joe Gage

πŸ› πŸ’» + mobreza
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 @@ + + + + + + + + + + +