From d4134b9a1dd24d6e23a5829c1807bb0882f702e0 Mon Sep 17 00:00:00 2001 From: Matija Obreza Date: Sun, 7 May 2023 06:31:13 +0200 Subject: [PATCH 01/25] OAuth: Support authorization code flow --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 13 +- .../activities/brapi/BrapiAuthActivity.java | 132 +++++++++++------- app/src/main/res/values/array.xml | 1 + app/src/main/res/values/strings.xml | 1 + 5 files changed, 98 insertions(+), 51 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 61e859e5a..dbde5dcc7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -137,7 +137,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" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7ef3f90ac..8b6c30a7d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -148,7 +148,8 @@ android:configChanges="orientation|keyboardHidden|screenSize|locale" android:launchMode="singleTask" android:screenOrientation="portrait" - android:exported="true"> + android:exported="true" + tools:node="replace"> @@ -171,6 +172,16 @@ android:scheme="fieldbook" tools:ignore="AppLinkUrlError" /> + + + + + + + { @@ -143,7 +144,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 +171,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(); @@ -182,7 +184,7 @@ public void onFetchConfigurationCompleted( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { authService.performAuthorizationRequest( authRequest, - PendingIntent.getActivity(context, 0, responseIntent, PendingIntent.FLAG_IMMUTABLE)); + PendingIntent.getActivity(context, 0, responseIntent, PendingIntent.FLAG_MUTABLE)); } else { authService.performAuthorizationRequest( authRequest, @@ -197,9 +199,6 @@ public void onFetchConfigurationCompleted( authError(ex); - setResult(RESULT_CANCELED); - - finish(); } } @@ -228,12 +227,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 +265,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 +273,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; + } + + 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; + } - editor.putString(GeneralKeys.BRAPI_TOKEN, token); - editor.apply(); + if (token.startsWith("Bearer ")) { + token = token.replaceFirst("Bearer ", ""); + } - authSuccess(); + authSuccess(token); } } diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index d52b4cb13..4d38fb1f2 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -106,6 +106,7 @@ + @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 f9db87e6e..36928de0b 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 From c620afecb22042913adaf4702f31d1fd5d25f8a8 Mon Sep 17 00:00:00 2001 From: chaneylc Date: Wed, 10 May 2023 11:08:16 -0500 Subject: [PATCH 02/25] toggle oidc preference ui when using code flow --- .../tracker/preferences/BrapiPreferencesFragment.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 52f519def..0b9a31e91 100644 --- a/app/src/main/java/com/fieldbook/tracker/preferences/BrapiPreferencesFragment.java +++ b/app/src/main/java/com/fieldbook/tracker/preferences/BrapiPreferencesFragment.java @@ -429,8 +429,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); From d21d2310f3b6b773440d60ba5c4431c6de6963a7 Mon Sep 17 00:00:00 2001 From: chaneylc Date: Wed, 10 May 2023 12:35:55 -0500 Subject: [PATCH 03/25] fix auth request for older phones --- .../tracker/activities/brapi/BrapiAuthActivity.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 854297ff9..da1a2a03f 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 @@ -182,9 +182,13 @@ public void onFetchConfigurationCompleted( responseIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + int flag = PendingIntent.FLAG_IMMUTABLE; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + flag = PendingIntent.FLAG_MUTABLE; + } authService.performAuthorizationRequest( authRequest, - PendingIntent.getActivity(context, 0, responseIntent, PendingIntent.FLAG_MUTABLE)); + PendingIntent.getActivity(context, 0, responseIntent, flag)); } else { authService.performAuthorizationRequest( authRequest, From 8204223fef91306cff1dc92015af3d87ffd63fa7 Mon Sep 17 00:00:00 2001 From: chaneylc Date: Wed, 17 May 2023 09:40:11 -0500 Subject: [PATCH 04/25] fix #586 --- .../tracker/activities/CollectActivity.java | 18 +- .../tracker/adapters/InfoBarAdapter.java | 177 ++++++++++-------- .../dialogs/CollectAttributeChooserDialog.kt | 155 +++++++++++++++ app/src/main/res/layout/activity_collect.xml | 4 +- .../res/layout/dialog_collect_att_chooser.xml | 50 +++++ ...bar_dropdown.xml => list_item_infobar.xml} | 28 +-- app/src/main/res/values/strings.xml | 4 + 7 files changed, 329 insertions(+), 107 deletions(-) create mode 100644 app/src/main/java/com/fieldbook/tracker/dialogs/CollectAttributeChooserDialog.kt create mode 100644 app/src/main/res/layout/dialog_collect_att_chooser.xml rename app/src/main/res/layout/{item_infobar_dropdown.xml => list_item_infobar.xml} (59%) 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 603aefe2f..d2c073f79 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; @@ -303,14 +304,17 @@ 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); + RecyclerView infoBarRv = findViewById(R.id.act_collect_infobar_rv); + infoBarRv.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)); + infoBarAdapter = new InfoBarAdapter(this, ep.getInt(GeneralKeys.INFOBAR_NUMBER, 2), rangeBox.getPlotID()); + infoBarRv.setAdapter(infoBarAdapter); + initCurrentVals(); } @@ -483,8 +487,12 @@ public void initWidgets(final boolean rangeSuppress) { // Reset dropdowns if (!database.isRangeTableEmpty()) { - String plotID = rangeBox.getPlotID(); - infoBarAdapter.configureDropdownArray(plotID); + infoBarAdapter.notifyDataSetChanged(); + //RecyclerView infoBarRv = findViewById(R.id.act_collect_infobar_rv); + //String plotId = rangeBox.getPlotID(); + //infoBarAdapter.notifyPlotChanged(plotId); + //infoBarAdapter = new InfoBarAdapter(this, ep.getInt(GeneralKeys.INFOBAR_NUMBER, 2), plotId); + //infoBarRv.setAdapter(infoBarAdapter); } traitBox.initTraitDetails(); @@ -1003,7 +1011,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), diff --git a/app/src/main/java/com/fieldbook/tracker/adapters/InfoBarAdapter.java b/app/src/main/java/com/fieldbook/tracker/adapters/InfoBarAdapter.java index 95461ba71..874aa2e65 100644 --- a/app/src/main/java/com/fieldbook/tracker/adapters/InfoBarAdapter.java +++ b/app/src/main/java/com/fieldbook/tracker/adapters/InfoBarAdapter.java @@ -7,60 +7,48 @@ 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.annotation.NonNull; import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.fieldbook.tracker.R; +import com.fieldbook.tracker.activities.CollectActivity; import com.fieldbook.tracker.database.DataHelper; -import com.fieldbook.tracker.database.dao.ObservationDao; +import com.fieldbook.tracker.dialogs.CollectAttributeChooserDialog; 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) { + public InfoBarAdapter(Context context, int maxSelectors, String plotId) { this.context = context; this.maxSelectors = maxSelectors; - this.selectorsView = selectorsView; - + //this.selectorsView = selectorsView; this.dataHelper = new DataHelper(context); } - public void configureDropdownArray(String plotId) { - this.plotId = plotId; - selectorsView.setHasFixedSize(false); + public void notifyPlotChanged(String plotId) { - RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this.context); - selectorsView.setLayoutManager(layoutManager); - - selectorsView.setAdapter(this); } // Create new views (invoked by the layout manager) + @NonNull @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); + .inflate(R.layout.list_item_infobar, parent, false); return new ViewHolder((ConstraintLayout) v); } @@ -70,10 +58,7 @@ 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); + configureSpinner(holder, position); } // Return the size of your dataset (invoked by the layout manager) @@ -86,71 +71,103 @@ 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])); - 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)); + private void setViewHolderText(ViewHolder holder, String label, String value) { + holder.prefixTextView.setText(label + ":"); + holder.valueTextView.setText(value); + } - } else { + private void configureSpinner(final ViewHolder holder, final int position) { - text.setText(infoBarValues.get(0)); + //initialize the dialog that will pop-up when the user clicks on oen of the infobars + CollectAttributeChooserDialog ad = new CollectAttributeChooserDialog((CollectActivity) context, (selectedString, valueString) -> { + holder.prefixTextView.setText(selectedString + ":"); + holder.valueTextView.setText(valueString); + return null; + }, position); - } + //ensure that the initialLabel is actually a plot attribute //TODO check that initial label is trait + String[] attributes = dataHelper.getRangeColumnNames(); - getSharedPref().edit().putString("DROP" + position, allTraits[pos]).apply(); + //if not a plot attribute, it may be a trait name + ArrayList traits = dataHelper.getAllTraitObjects(); + ArrayList traitNames = new ArrayList(); + for (TraitObject t : traits) { + traitNames.add(t.getTrait()); + } - } catch (Exception e) { + String defaultLabel = "Select"; + if (attributes.length > 0) { + defaultLabel = attributes[0]; + } - e.printStackTrace(); + //adapter preferred values are saved as DROP1, DROP2, DROP3, DROP4, DROP5 in preferences, intiailize the label with it + String initialLabel = ((CollectActivity) context).getPreferences().getString("DROP" + position, defaultLabel); + boolean isAttribute = false; + if (!Arrays.asList(attributes).contains(initialLabel)) { + if (!traitNames.contains(initialLabel)) { + if (attributes.length > 0) { + initialLabel = attributes[0]; } - - spinner.requestFocus(); } + } else isAttribute = true; - @Override - public void onNothingSelected(AdapterView arg0) { + //get the database value for the initial label + String initialValue = ad.queryForLabelValue(((CollectActivity) context).getRangeBox().getPlotID(), initialLabel, isAttribute); - } + setViewHolderText(holder, initialLabel, initialValue); + + holder.prefixTextView.setOnClickListener(v -> { + ad.show(); }); + + //TODO set the preference selection, int spinnerPosition = prefixArrayAdapter.getPosition(getSharedPref().getString("DROP" + position, allTraits[0])); + +// 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") @@ -176,11 +193,13 @@ public boolean onTouch(View v, MotionEvent event) { // 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; + TextView prefixTextView; + TextView valueTextView; ViewHolder(ConstraintLayout v) { super(v); - mTextView = v; + prefixTextView = v.findViewById(R.id.list_item_infobar_prefix); + valueTextView = v.findViewById(R.id.list_item_infobar_value); } } } \ No newline at end of file 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..547c1975f --- /dev/null +++ b/app/src/main/java/com/fieldbook/tracker/dialogs/CollectAttributeChooserDialog.kt @@ -0,0 +1,155 @@ +package com.fieldbook.tracker.dialogs + +import android.app.Dialog +import android.os.Bundle +import android.util.Log +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.Button +import android.widget.ListView +import com.fieldbook.tracker.R +import com.fieldbook.tracker.activities.CollectActivity +import com.fieldbook.tracker.database.DataHelper +import com.fieldbook.tracker.objects.TraitObject +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayout.OnTabSelectedListener +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope + +class CollectAttributeChooserDialog(private val activity: CollectActivity, + private val onSelected: (String, String) -> Unit, + private val infoBarPosition: Int): + Dialog(activity, R.style.AppAlertDialog), CoroutineScope by MainScope() { + + companion object { + const val TAG = "AttDialog" + } + + private val helper by lazy { DataHelper(context) } + + private lateinit var tabLayout: TabLayout + private lateinit var listView: ListView + private lateinit var cancelButton: Button + + private var attributes = arrayOf() + private var traits = arrayOf() + private var other = arrayOf() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + + setContentView(R.layout.dialog_collect_att_chooser) + + setTitle(R.string.dialog_collect_att_chooser_title) + + tabLayout = findViewById(R.id.dialog_collect_att_chooser_tl) + listView = 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() + } + + try { + attributes = helper.getAllObservationUnitAttributeNames(activity.studyId.toInt()) + traits = helper.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() + } + + try { + setupTabLayout() + } catch (e: Exception) { + Log.d(TAG, "Error occurred when setting up attribute tab layout.") + e.printStackTrace() + } + } + + private fun setupTabLayout() { + + tabLayout.addOnTabSelectedListener(object : OnTabSelectedListener { + + override fun onTabSelected(tab: TabLayout.Tab?) { + + tab?.let { t -> + + loadTab(t.text.toString()) + } + } + + override fun onTabUnselected(tab: TabLayout.Tab?) {} + override fun onTabReselected(tab: TabLayout.Tab?) {} + }) + + //manually select the first tab + loadTab(activity.getString(R.string.dialog_att_chooser_attributes)) + } + + private fun loadTab(label: String) { + + val plotId = activity.getRangeBox().getPlotID() ?: "" + 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 = ArrayAdapter(context, android.R.layout.simple_list_item_1, infoBarLabels) + + listView.adapter = adapter + + listView.setOnItemClickListener { _, _, position, _ -> + + val infoLabel = when (label) { + attributesLabel -> attributes[position] + traitsLabel -> traits[position].trait + else -> other[position].trait + } + + activity.getPreferences().edit().putString("DROP$infoBarPosition", infoLabel).apply() + + //get the value correlating to the chosen label + val value = queryForLabelValue(plotId, infoLabel, label == attributesLabel) + + onSelected(infoLabel, value) + + dismiss() + + } + } + + public fun queryForLabelValue(plotId: String, label: String, isAttribute: Boolean): String { + + return if (isAttribute) { + + val values = helper.getDropDownRange(label, plotId) + + if (values == null || values.isEmpty()) { + + activity.getString(R.string.main_infobar_data_missing) + + } else { + + values[0] + } + + } else { + + helper.getUserDetail(plotId)[label] ?: "" + } + } +} 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 @@ + + + + + + + + + + +