diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/AppListActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/AppListActivity.java index 3d73e241485..06892b89ca1 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/AppListActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/AppListActivity.java @@ -29,21 +29,14 @@ import android.widget.ProgressBar; import androidx.annotation.LayoutRes; -import androidx.annotation.NonNull; import androidx.appcompat.widget.SearchView; import androidx.core.content.ContextCompat; import androidx.core.view.MenuItemCompat; -import androidx.recyclerview.widget.DefaultItemAnimator; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.bottomsheet.BottomSheetDialog; import org.odk.collect.android.R; -import org.odk.collect.android.adapters.SortDialogAdapter; import org.odk.collect.android.database.instances.DatabaseInstanceColumns; -import org.odk.collect.android.listeners.RecyclerViewClickListener; -import org.odk.collect.android.utilities.SnackbarUtils; +import org.odk.collect.android.formlists.sorting.FormListSortingBottomSheetDialog; +import org.odk.collect.android.formlists.sorting.FormListSortingOption; import org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard; import java.util.ArrayList; @@ -61,12 +54,11 @@ abstract class AppListActivity extends CollectAbstractActivity { protected CursorAdapter listAdapter; protected LinkedHashSet selectedInstances = new LinkedHashSet<>(); - protected int[] sortingOptions; + protected List sortingOptions; protected Integer selectedSortingOrder; protected ListView listView; protected LinearLayout llParent; protected ProgressBar progressBar; - private BottomSheetDialog bottomSheetDialog; private String filterText; private String savedFilterText; @@ -74,9 +66,6 @@ abstract class AppListActivity extends CollectAbstractActivity { private SearchView searchView; - private boolean canHideProgressBar; - private boolean progressBarVisible; - // toggles to all checked or all unchecked // returns: // true if result is all checked @@ -221,20 +210,22 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; } - switch (item.getItemId()) { - case R.id.menu_sort: - showBottomSheetDialog(); - return true; + if (item.getItemId() == R.id.menu_sort) { + new FormListSortingBottomSheetDialog( + this, + sortingOptions, + selectedSortingOrder, + selectedOption -> { + saveSelectedSortingOrder(selectedOption); + updateAdapter(); + } + ).show(); + return true; } return super.onOptionsItemSelected(item); } - private void performSelectedSearch(int position) { - saveSelectedSortingOrder(position); - updateAdapter(); - } - protected void checkPreviouslyCheckedItems() { listView.clearChoices(); List selectedPositions = new ArrayList<>(); @@ -291,50 +282,15 @@ protected void clearSearchView() { searchView.setQuery("", false); } - private void showBottomSheetDialog() { - bottomSheetDialog = new BottomSheetDialog(this); - final View sheetView = getLayoutInflater().inflate(R.layout.bottom_sheet, null); - final RecyclerView recyclerView = sheetView.findViewById(R.id.recyclerView); - - final SortDialogAdapter adapter = new SortDialogAdapter(this, recyclerView, sortingOptions, getSelectedSortingOrder(), new RecyclerViewClickListener() { - @Override - public void onItemClicked(SortDialogAdapter.ViewHolder holder, int position) { - holder.updateItemColor(selectedSortingOrder); - performSelectedSearch(position); - bottomSheetDialog.dismiss(); - } - }); - RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getApplicationContext()); - recyclerView.setLayoutManager(layoutManager); - recyclerView.setAdapter(adapter); - recyclerView.setItemAnimator(new DefaultItemAnimator()); - - bottomSheetDialog.setContentView(sheetView); - bottomSheetDialog.show(); - } - - protected void showSnackbar(@NonNull String result) { - SnackbarUtils.showShortSnackbar(llParent, result); - } - - protected void hideProgressBarIfAllowed() { - if (canHideProgressBar && progressBarVisible) { - hideProgressBar(); - } - } - protected void hideProgressBarAndAllow() { - this.canHideProgressBar = true; hideProgressBar(); } private void hideProgressBar() { progressBar.setVisibility(View.GONE); - progressBarVisible = false; } protected void showProgressBar() { progressBar.setVisibility(View.VISIBLE); - progressBarVisible = true; } } diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/FormDownloadListActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/FormDownloadListActivity.java index 0d4a95e4a41..3bc5221eeb7 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/FormDownloadListActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/FormDownloadListActivity.java @@ -39,6 +39,7 @@ import org.odk.collect.android.activities.viewmodels.FormDownloadListViewModel; import org.odk.collect.android.adapters.FormDownloadListAdapter; import org.odk.collect.android.formentry.RefreshFormListDialogFragment; +import org.odk.collect.android.formlists.sorting.FormListSortingOption; import org.odk.collect.android.formmanagement.FormDownloadException; import org.odk.collect.android.formmanagement.FormDownloader; import org.odk.collect.android.formmanagement.FormSourceExceptionMapper; @@ -64,6 +65,7 @@ import java.io.Serializable; import java.net.URI; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -246,9 +248,16 @@ && getLastCustomNonConfigurationInstance() == null listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); listView.setItemsCanFocus(false); - sortingOptions = new int[]{ - R.string.sort_by_name_asc, R.string.sort_by_name_desc - }; + sortingOptions = Arrays.asList( + new FormListSortingOption( + R.drawable.ic_sort_by_alpha, + R.string.sort_by_name_asc + ), + new FormListSortingOption( + R.drawable.ic_sort_by_alpha, + R.string.sort_by_name_desc + ) + ); } private void clearChoices() { diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java index 4272ff6ba00..4daf4959da8 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java @@ -38,6 +38,7 @@ import org.odk.collect.android.dao.CursorLoaderFactory; import org.odk.collect.android.database.instances.DatabaseInstanceColumns; import org.odk.collect.android.external.InstancesContract; +import org.odk.collect.android.formlists.sorting.FormListSortingOption; import org.odk.collect.android.injection.DaggerUtils; import org.odk.collect.android.projects.CurrentProjectProvider; import org.odk.collect.android.utilities.ApplicationConstants; @@ -46,6 +47,8 @@ import org.odk.collect.forms.Form; import org.odk.collect.forms.instances.Instance; +import java.util.Arrays; + import javax.inject.Inject; /** @@ -79,18 +82,53 @@ public void onCreate(Bundle savedInstanceState) { setTitle(getString(R.string.review_data)); editMode = true; - sortingOptions = new int[]{ - R.string.sort_by_name_asc, R.string.sort_by_name_desc, - R.string.sort_by_date_desc, R.string.sort_by_date_asc, - R.string.sort_by_status_asc, R.string.sort_by_status_desc - }; + sortingOptions = Arrays.asList( + new FormListSortingOption( + R.drawable.ic_sort_by_alpha, + R.string.sort_by_name_asc + ), + new FormListSortingOption( + R.drawable.ic_sort_by_alpha, + R.string.sort_by_name_desc + ), + new FormListSortingOption( + R.drawable.ic_access_time, + R.string.sort_by_date_desc + ), + new FormListSortingOption( + R.drawable.ic_access_time, + R.string.sort_by_date_asc + ), + new FormListSortingOption( + R.drawable.ic_assignment_turned_in, + R.string.sort_by_status_asc + ), + new FormListSortingOption( + R.drawable.ic_assignment_late, + R.string.sort_by_status_desc + ) + ); } else { setTitle(getString(R.string.view_sent_forms)); - sortingOptions = new int[]{ - R.string.sort_by_name_asc, R.string.sort_by_name_desc, - R.string.sort_by_date_desc, R.string.sort_by_date_asc - }; + sortingOptions = Arrays.asList( + new FormListSortingOption( + R.drawable.ic_sort_by_alpha, + R.string.sort_by_name_asc + ), + new FormListSortingOption( + R.drawable.ic_sort_by_alpha, + R.string.sort_by_name_desc + ), + new FormListSortingOption( + R.drawable.ic_access_time, + R.string.sort_by_date_desc + ), + new FormListSortingOption( + R.drawable.ic_access_time, + R.string.sort_by_date_asc + ) + ); ((TextView) findViewById(android.R.id.empty)).setText(R.string.no_items_display_sent_forms); } diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceUploaderListActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceUploaderListActivity.java index a111791177a..b7c05832f71 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceUploaderListActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceUploaderListActivity.java @@ -45,6 +45,7 @@ import org.odk.collect.android.backgroundwork.InstanceSubmitScheduler; import org.odk.collect.android.dao.CursorLoaderFactory; import org.odk.collect.android.databinding.InstanceUploaderListBinding; +import org.odk.collect.android.formlists.sorting.FormListSortingOption; import org.odk.collect.android.gdrive.GoogleSheetsUploaderActivity; import org.odk.collect.android.injection.DaggerUtils; import org.odk.collect.androidshared.network.NetworkStateProvider; @@ -55,6 +56,7 @@ import org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard; import org.odk.collect.settings.keys.ProjectKeys; +import java.util.Arrays; import java.util.List; import javax.inject.Inject; @@ -168,10 +170,24 @@ void init() { binding.uploadButton.setEnabled(areCheckedItems()); }); - sortingOptions = new int[]{ - R.string.sort_by_name_asc, R.string.sort_by_name_desc, - R.string.sort_by_date_desc, R.string.sort_by_date_asc - }; + sortingOptions = Arrays.asList( + new FormListSortingOption( + R.drawable.ic_sort_by_alpha, + R.string.sort_by_name_asc + ), + new FormListSortingOption( + R.drawable.ic_sort_by_alpha, + R.string.sort_by_name_desc + ), + new FormListSortingOption( + R.drawable.ic_access_time, + R.string.sort_by_date_desc + ), + new FormListSortingOption( + R.drawable.ic_access_time, + R.string.sort_by_date_asc + ) + ); getSupportLoaderManager().initLoader(LOADER_ID, null, this); diff --git a/collect_app/src/main/java/org/odk/collect/android/adapters/SortDialogAdapter.java b/collect_app/src/main/java/org/odk/collect/android/adapters/SortDialogAdapter.java deleted file mode 100644 index d6afa9afda0..00000000000 --- a/collect_app/src/main/java/org/odk/collect/android/adapters/SortDialogAdapter.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2017 Shobhit - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package org.odk.collect.android.adapters; - -import android.content.Context; -import android.content.res.ColorStateList; -import androidx.annotation.NonNull; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.recyclerview.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import org.odk.collect.android.R; -import org.odk.collect.android.listeners.RecyclerViewClickListener; -import org.odk.collect.android.utilities.ApplicationConstants; -import org.odk.collect.android.utilities.ThemeUtils; - -import timber.log.Timber; - -public class SortDialogAdapter extends RecyclerView.Adapter { - private final RecyclerViewClickListener listener; - private final int selectedSortingOrder; - private final RecyclerView recyclerView; - private final ThemeUtils themeUtils; - private final int[] sortList; - - public SortDialogAdapter(Context context, RecyclerView recyclerView, int[] sortList, int selectedSortingOrder, RecyclerViewClickListener recyclerViewClickListener) { - themeUtils = new ThemeUtils(context); - this.recyclerView = recyclerView; - this.sortList = sortList; - this.selectedSortingOrder = selectedSortingOrder; - listener = recyclerViewClickListener; - } - - @NonNull - @Override - public SortDialogAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View itemLayoutView = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.sort_item_layout, parent, false); - return new ViewHolder(itemLayoutView); - } - - @Override - public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) { - viewHolder.txtViewTitle.setText(sortList[position]); - int color = position == selectedSortingOrder ? themeUtils.getAccentColor() : themeUtils.getColorOnSurface(); - viewHolder.txtViewTitle.setTextColor(color); - try { - int iconId = ApplicationConstants.getSortLabelToIconMap().get(sortList[position]); - viewHolder.imgViewIcon.setImageResource(iconId); - viewHolder.imgViewIcon.setTag(iconId); - viewHolder.imgViewIcon.setImageDrawable(DrawableCompat.wrap(viewHolder.imgViewIcon.getDrawable()).mutate()); - DrawableCompat.setTintList(viewHolder.imgViewIcon.getDrawable(), position == selectedSortingOrder ? ColorStateList.valueOf(color) : null); - } catch (NullPointerException e) { - Timber.i(e); - } - } - - // Return the size of your itemsData (invoked by the layout manager) - @Override - public int getItemCount() { - return sortList.length; - } - - // inner class to hold a reference to each item of RecyclerView - public class ViewHolder extends RecyclerView.ViewHolder { - - TextView txtViewTitle; - ImageView imgViewIcon; - - ViewHolder(final View itemLayoutView) { - super(itemLayoutView); - txtViewTitle = itemLayoutView.findViewById(R.id.title); - imgViewIcon = itemLayoutView.findViewById(R.id.icon); - - itemLayoutView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - listener.onItemClicked(ViewHolder.this, getLayoutPosition()); - } - }); - } - - public void updateItemColor(int selectedSortingOrder) { - ViewHolder previousHolder = (ViewHolder) recyclerView.findViewHolderForAdapterPosition(selectedSortingOrder); - previousHolder.txtViewTitle.setTextColor(themeUtils.getColorOnSurface()); - txtViewTitle.setTextColor(themeUtils.getColorPrimary()); - try { - DrawableCompat.setTintList(previousHolder.imgViewIcon.getDrawable(), null); - DrawableCompat.setTint(imgViewIcon.getDrawable(), themeUtils.getAccentColor()); - } catch (NullPointerException e) { - Timber.i(e); - } - } - } -} diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListActivity.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListActivity.kt index 39887cf006d..4c2c4546b29 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListActivity.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListActivity.kt @@ -37,7 +37,7 @@ class BlankFormListActivity : LocalizedActivity(), OnFormItemClickListener { private val viewModel: BlankFormListViewModel by viewModels { viewModelFactory } - private val adapter: FormListAdapter = FormListAdapter(this) + private val adapter: BlankFormListAdapter = BlankFormListAdapter(this) private lateinit var menuDelegate: BlankFormListMenuDelegate diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListAdapter.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListAdapter.kt index dc196cf1482..358a42ce1c9 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListAdapter.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListAdapter.kt @@ -12,9 +12,9 @@ import timber.log.Timber import java.text.SimpleDateFormat import java.util.Locale -class FormListAdapter( +class BlankFormListAdapter( val listener: OnFormItemClickListener -) : RecyclerView.Adapter() { +) : RecyclerView.Adapter() { private var formItems = emptyList() diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListItem.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListItem.kt index 188f186c1d3..d818e7cea0a 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListItem.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListItem.kt @@ -1,6 +1,9 @@ package org.odk.collect.android.formlists.blankformlist import android.net.Uri +import org.odk.collect.android.external.FormsContract +import org.odk.collect.forms.Form +import org.odk.collect.forms.instances.InstancesRepository data class BlankFormListItem( val databaseId: Long, @@ -12,3 +15,17 @@ data class BlankFormListItem( val dateOfLastUsage: Long, val contentUri: Uri ) + +fun Form.toBlankFormListItem(projectId: String, instancesRepository: InstancesRepository) = BlankFormListItem( + databaseId = this.dbId, + formId = this.formId, + formName = this.displayName, + formVersion = this.version ?: "", + geometryPath = this.geometryXpath ?: "", + dateOfCreation = this.date, + dateOfLastUsage = instancesRepository + .getAllByFormId(this.formId) + .filter { it.formVersion == this.version } + .maxByOrNull { it.lastStatusChangeDate }?.lastStatusChangeDate ?: 0L, + contentUri = FormsContract.getUri(projectId, this.dbId) +) diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListMenuDelegate.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListMenuDelegate.kt index 840304001de..d2d72a1f557 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListMenuDelegate.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListMenuDelegate.kt @@ -6,7 +6,8 @@ import android.view.MenuItem import androidx.activity.ComponentActivity import androidx.appcompat.widget.SearchView import org.odk.collect.android.R -import org.odk.collect.android.formlists.FormListSortingBottomSheetDialog +import org.odk.collect.android.formlists.sorting.FormListSortingBottomSheetDialog +import org.odk.collect.android.formlists.sorting.FormListSortingOption import org.odk.collect.android.utilities.MenuDelegate import org.odk.collect.androidshared.network.NetworkStateProvider import org.odk.collect.androidshared.ui.ToastUtils @@ -98,11 +99,27 @@ class BlankFormListMenuDelegate( R.id.menu_sort -> { FormListSortingBottomSheetDialog( activity, - intArrayOf( - R.string.sort_by_name_asc, - R.string.sort_by_name_desc, - R.string.sort_by_date_desc, - R.string.sort_by_date_asc + listOf( + FormListSortingOption( + R.drawable.ic_sort_by_alpha, + R.string.sort_by_name_asc + ), + FormListSortingOption( + R.drawable.ic_sort_by_alpha, + R.string.sort_by_name_desc + ), + FormListSortingOption( + R.drawable.ic_access_time, + R.string.sort_by_date_desc + ), + FormListSortingOption( + R.drawable.ic_access_time, + R.string.sort_by_date_asc + ), + FormListSortingOption( + R.drawable.ic_sort_by_last_saved, + R.string.sort_by_last_saved + ) ), viewModel.sortingOrder ) { newSortingOrder -> diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListViewModel.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListViewModel.kt index 60c1efad1fc..e3c81da7d3c 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListViewModel.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListViewModel.kt @@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import org.odk.collect.analytics.Analytics import org.odk.collect.android.analytics.AnalyticsEvents -import org.odk.collect.android.external.FormsContract import org.odk.collect.android.formmanagement.FormsUpdater import org.odk.collect.android.formmanagement.matchexactly.SyncStatusAppState import org.odk.collect.android.preferences.utilities.FormUpdateMode @@ -23,6 +22,7 @@ import org.odk.collect.async.Scheduler import org.odk.collect.forms.FormSourceException import org.odk.collect.forms.FormSourceException.AuthRequired import org.odk.collect.forms.FormsRepository +import org.odk.collect.forms.instances.InstancesRepository import org.odk.collect.settings.keys.ProjectKeys import org.odk.collect.shared.settings.Settings import org.odk.collect.shared.strings.Md5.getMd5Hash @@ -30,6 +30,7 @@ import java.io.ByteArrayInputStream class BlankFormListViewModel( private val formsRepository: FormsRepository, + private val instancesRepository: InstancesRepository, private val application: Application, private val syncRepository: SyncStatusAppState, private val formsUpdater: FormsUpdater, @@ -89,16 +90,7 @@ class BlankFormListViewModel( return formsRepository .all .map { form -> - BlankFormListItem( - databaseId = form.dbId, - formId = form.formId, - formName = form.displayName, - formVersion = form.version ?: "", - geometryPath = form.geometryXpath ?: "", - dateOfCreation = form.date, - dateOfLastUsage = 0, - contentUri = FormsContract.getUri(projectId, form.dbId) - ) + form.toBlankFormListItem(projectId, instancesRepository) } } @@ -111,16 +103,7 @@ class BlankFormListViewModel( .filter { !it.isDeleted }.map { form -> - BlankFormListItem( - databaseId = form.dbId, - formId = form.formId, - formName = form.displayName, - formVersion = form.version ?: "", - geometryPath = form.geometryXpath ?: "", - dateOfCreation = form.date, - dateOfLastUsage = 0, - contentUri = FormsContract.getUri(projectId, form.dbId) - ) + form.toBlankFormListItem(projectId, instancesRepository) } if (shouldHideOldFormVersions) { @@ -224,6 +207,7 @@ class BlankFormListViewModel( 1 -> _allForms.value.sortedByDescending { it.formName.lowercase() } 2 -> _allForms.value.sortedByDescending { it.dateOfCreation } 3 -> _allForms.value.sortedBy { it.dateOfCreation } + 4 -> _allForms.value.sortedByDescending { it.dateOfLastUsage } else -> { _allForms.value } }.filter { filterText.isBlank() || it.formName.contains(filterText, true) @@ -232,6 +216,7 @@ class BlankFormListViewModel( class Factory( private val formsRepository: FormsRepository, + private val instancesRepository: InstancesRepository, private val application: Application, private val syncRepository: SyncStatusAppState, private val formsUpdater: FormsUpdater, @@ -246,6 +231,7 @@ class BlankFormListViewModel( override fun create(modelClass: Class): T { return BlankFormListViewModel( formsRepository, + instancesRepository, application, syncRepository, formsUpdater, diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/sorting/FormListSortingAdapter.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/sorting/FormListSortingAdapter.kt new file mode 100644 index 00000000000..1d73fbc6dd7 --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/sorting/FormListSortingAdapter.kt @@ -0,0 +1,55 @@ +package org.odk.collect.android.formlists.sorting + +import android.content.res.ColorStateList +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.graphics.drawable.DrawableCompat +import androidx.recyclerview.widget.RecyclerView +import org.odk.collect.android.R +import org.odk.collect.android.databinding.SortItemLayoutBinding +import org.odk.collect.androidshared.system.ContextUtils.getThemeAttributeValue +import java.util.function.Consumer + +class FormListSortingAdapter( + private val sortingOptions: List, + private val selectedSortingOrder: Int, + private val listener: Consumer +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = SortItemLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.binding.root.setOnClickListener { + listener.accept(position) + selectItem(holder.binding) + } + + val sortingOption = sortingOptions[position] + + holder.binding.title.setText(sortingOption.text) + holder.binding.icon.setImageResource(sortingOption.icon) + holder.binding.icon.tag = sortingOption.icon + holder.binding.icon.setImageDrawable( + DrawableCompat.wrap(holder.binding.icon.drawable).mutate() + ) + + if (position == selectedSortingOrder) { + selectItem(holder.binding) + } + } + + private fun selectItem(binding: SortItemLayoutBinding) { + binding.title.setTextColor(getThemeAttributeValue(binding.root.context, R.attr.colorAccent)) + DrawableCompat.setTintList( + binding.icon.drawable, + ColorStateList.valueOf(getThemeAttributeValue(binding.root.context, R.attr.colorAccent)) + ) + } + + override fun getItemCount() = sortingOptions.size + + class ViewHolder(val binding: SortItemLayoutBinding) : RecyclerView.ViewHolder(binding.root) +} diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/FormListSortingBottomSheetDialog.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/sorting/FormListSortingBottomSheetDialog.kt similarity index 67% rename from collect_app/src/main/java/org/odk/collect/android/formlists/FormListSortingBottomSheetDialog.kt rename to collect_app/src/main/java/org/odk/collect/android/formlists/sorting/FormListSortingBottomSheetDialog.kt index 1fa45617a10..5a28e220f87 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/FormListSortingBottomSheetDialog.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/sorting/FormListSortingBottomSheetDialog.kt @@ -1,4 +1,4 @@ -package org.odk.collect.android.formlists +package org.odk.collect.android.formlists.sorting import android.content.Context import android.os.Bundle @@ -8,13 +8,13 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetDialog import org.odk.collect.android.R -import org.odk.collect.android.adapters.SortDialogAdapter +import java.util.function.Consumer class FormListSortingBottomSheetDialog( context: Context, - private val options: IntArray, + private val options: List, private val selectedOption: Int, - private val onSelectedOptionChanged: (option: Int) -> Unit + private val onSelectedOptionChanged: Consumer ) : BottomSheetDialog(context) { override fun onCreate(savedInstanceState: Bundle?) { @@ -23,11 +23,11 @@ class FormListSortingBottomSheetDialog( setContentView(LayoutInflater.from(context).inflate(R.layout.bottom_sheet, null)) findViewById(R.id.recyclerView)?.apply { - adapter = SortDialogAdapter( - context, this, options, selectedOption - ) { holder, position -> - holder.updateItemColor(position) - onSelectedOptionChanged(position) + adapter = FormListSortingAdapter( + options, + selectedOption + ) { position -> + onSelectedOptionChanged.accept(position) dismiss() } layoutManager = LinearLayoutManager(context) diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/sorting/FormListSortingOption.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/sorting/FormListSortingOption.kt new file mode 100644 index 00000000000..a97a9be63c4 --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/sorting/FormListSortingOption.kt @@ -0,0 +1,6 @@ +package org.odk.collect.android.formlists.sorting + +data class FormListSortingOption( + val icon: Int, + val text: Int +) diff --git a/collect_app/src/main/java/org/odk/collect/android/fragments/AppListFragment.java b/collect_app/src/main/java/org/odk/collect/android/fragments/AppListFragment.java index c81dfcb17c4..e30f32d0493 100644 --- a/collect_app/src/main/java/org/odk/collect/android/fragments/AppListFragment.java +++ b/collect_app/src/main/java/org/odk/collect/android/fragments/AppListFragment.java @@ -30,18 +30,11 @@ import androidx.core.content.ContextCompat; import androidx.core.view.MenuItemCompat; import androidx.fragment.app.ListFragment; -import androidx.recyclerview.widget.DefaultItemAnimator; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.bottomsheet.BottomSheetDialog; - import org.odk.collect.android.R; -import org.odk.collect.android.activities.CollectAbstractActivity; -import org.odk.collect.android.adapters.SortDialogAdapter; import org.odk.collect.android.database.instances.DatabaseInstanceColumns; +import org.odk.collect.android.formlists.sorting.FormListSortingBottomSheetDialog; +import org.odk.collect.android.formlists.sorting.FormListSortingOption; import org.odk.collect.android.injection.DaggerUtils; -import org.odk.collect.android.listeners.RecyclerViewClickListener; import org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard; import org.odk.collect.settings.SettingsProvider; @@ -51,19 +44,16 @@ import javax.inject.Inject; -import timber.log.Timber; - public abstract class AppListFragment extends ListFragment { @Inject SettingsProvider settingsProvider; - protected int[] sortingOptions; + protected List sortingOptions; protected SimpleCursorAdapter listAdapter; protected LinkedHashSet selectedInstances = new LinkedHashSet<>(); protected View rootView; private Integer selectedSortingOrder; - private BottomSheetDialog bottomSheetDialog; private String filterText; // toggles to all checked or all unchecked @@ -169,54 +159,21 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; } - switch (item.getItemId()) { - case R.id.menu_sort: - bottomSheetDialog.show(); - return true; + if (item.getItemId() == R.id.menu_sort) { + new FormListSortingBottomSheetDialog( + requireContext(), + sortingOptions, + selectedSortingOrder, + selectedOption -> { + saveSelectedSortingOrder(selectedOption); + updateAdapter(); + } + ).show(); + return true; } return super.onOptionsItemSelected(item); } - private void performSelectedSearch(int position) { - saveSelectedSortingOrder(position); - updateAdapter(); - } - - @Override - public void onResume() { - super.onResume(); - if (bottomSheetDialog == null) { - setupBottomSheet(); - } - } - - private void setupBottomSheet() { - CollectAbstractActivity activity = (CollectAbstractActivity) getActivity(); - if (activity == null) { - Timber.e(new Error("Activity is null")); - return; - } - - bottomSheetDialog = new BottomSheetDialog(activity); - View sheetView = getActivity().getLayoutInflater().inflate(R.layout.bottom_sheet, null); - final RecyclerView recyclerView = sheetView.findViewById(R.id.recyclerView); - - final SortDialogAdapter adapter = new SortDialogAdapter(getActivity(), recyclerView, sortingOptions, getSelectedSortingOrder(), new RecyclerViewClickListener() { - @Override - public void onItemClicked(SortDialogAdapter.ViewHolder holder, int position) { - holder.updateItemColor(selectedSortingOrder); - performSelectedSearch(position); - bottomSheetDialog.dismiss(); - } - }); - RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getActivity()); - recyclerView.setLayoutManager(layoutManager); - recyclerView.setAdapter(adapter); - recyclerView.setItemAnimator(new DefaultItemAnimator()); - - bottomSheetDialog.setContentView(sheetView); - } - protected void checkPreviouslyCheckedItems() { getListView().clearChoices(); List selectedPositions = new ArrayList<>(); diff --git a/collect_app/src/main/java/org/odk/collect/android/fragments/FileManagerFragment.java b/collect_app/src/main/java/org/odk/collect/android/fragments/FileManagerFragment.java index ffc36e2719a..cda33e33e2c 100644 --- a/collect_app/src/main/java/org/odk/collect/android/fragments/FileManagerFragment.java +++ b/collect_app/src/main/java/org/odk/collect/android/fragments/FileManagerFragment.java @@ -30,8 +30,11 @@ import android.widget.ProgressBar; import org.odk.collect.android.R; +import org.odk.collect.android.formlists.sorting.FormListSortingOption; import org.odk.collect.android.utilities.SnackbarUtils; +import java.util.Arrays; + public abstract class FileManagerFragment extends AppListFragment implements LoaderManager.LoaderCallbacks { private static final int LOADER_ID = 0x01; protected Button deleteButton; @@ -62,10 +65,24 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { getListView().setItemsCanFocus(false); deleteButton.setEnabled(false); - sortingOptions = new int[]{ - R.string.sort_by_name_asc, R.string.sort_by_name_desc, - R.string.sort_by_date_desc, R.string.sort_by_date_asc - }; + sortingOptions = Arrays.asList( + new FormListSortingOption( + R.drawable.ic_sort_by_alpha, + R.string.sort_by_name_asc + ), + new FormListSortingOption( + R.drawable.ic_sort_by_alpha, + R.string.sort_by_name_desc + ), + new FormListSortingOption( + R.drawable.ic_access_time, + R.string.sort_by_date_desc + ), + new FormListSortingOption( + R.drawable.ic_access_time, + R.string.sort_by_date_asc + ) + ); getLoaderManager().initLoader(LOADER_ID, null, this); super.onViewCreated(view, savedInstanceState); } diff --git a/collect_app/src/main/java/org/odk/collect/android/gdrive/GoogleDriveActivity.java b/collect_app/src/main/java/org/odk/collect/android/gdrive/GoogleDriveActivity.java index 73f14f5be48..f92dcc14470 100644 --- a/collect_app/src/main/java/org/odk/collect/android/gdrive/GoogleDriveActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/gdrive/GoogleDriveActivity.java @@ -44,6 +44,7 @@ import org.odk.collect.android.activities.FormListActivity; import org.odk.collect.android.adapters.FileArrayAdapter; import org.odk.collect.android.exception.MultipleFoldersFoundException; +import org.odk.collect.android.formlists.sorting.FormListSortingOption; import org.odk.collect.android.gdrive.sheets.DriveHelper; import org.odk.collect.android.injection.DaggerUtils; import org.odk.collect.android.listeners.TaskListener; @@ -64,6 +65,7 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -215,9 +217,16 @@ public void onCreate(Bundle savedInstanceState) { listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); listView.setItemsCanFocus(false); - sortingOptions = new int[]{ - R.string.sort_by_name_asc, R.string.sort_by_name_desc - }; + sortingOptions = Arrays.asList( + new FormListSortingOption( + R.drawable.ic_sort_by_alpha, + R.string.sort_by_name_asc + ), + new FormListSortingOption( + R.drawable.ic_sort_by_alpha, + R.string.sort_by_name_desc + ) + ); driveHelper = new DriveHelper(googleApiProvider.getDriveApi(settingsProvider .getUnprotectedSettings() diff --git a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java index 89afe995397..450afb86cbf 100644 --- a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java +++ b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java @@ -656,7 +656,7 @@ public ProjectDependencyProviderFactory providesProjectDependencyProviderFactory } @Provides - public BlankFormListViewModel.Factory providesBlankFormListViewModel(FormsRepositoryProvider formsRepositoryProvider, Application application, SyncStatusAppState syncStatusAppState, FormsUpdater formsUpdater, Scheduler scheduler, SettingsProvider settingsProvider, Analytics analytics, ChangeLockProvider changeLockProvider, CurrentProjectProvider currentProjectProvider) { - return new BlankFormListViewModel.Factory(formsRepositoryProvider.get(), application, syncStatusAppState, formsUpdater, scheduler, settingsProvider.getUnprotectedSettings(), analytics, changeLockProvider, new FormsDirDiskFormsSynchronizer(), currentProjectProvider.getCurrentProject().getUuid()); + public BlankFormListViewModel.Factory providesBlankFormListViewModel(FormsRepositoryProvider formsRepositoryProvider, InstancesRepositoryProvider instancesRepositoryProvider, Application application, SyncStatusAppState syncStatusAppState, FormsUpdater formsUpdater, Scheduler scheduler, SettingsProvider settingsProvider, Analytics analytics, ChangeLockProvider changeLockProvider, CurrentProjectProvider currentProjectProvider) { + return new BlankFormListViewModel.Factory(formsRepositoryProvider.get(), instancesRepositoryProvider.get(), application, syncStatusAppState, formsUpdater, scheduler, settingsProvider.getUnprotectedSettings(), analytics, changeLockProvider, new FormsDirDiskFormsSynchronizer(), currentProjectProvider.getCurrentProject().getUuid()); } } diff --git a/collect_app/src/main/java/org/odk/collect/android/listeners/RecyclerViewClickListener.java b/collect_app/src/main/java/org/odk/collect/android/listeners/RecyclerViewClickListener.java deleted file mode 100644 index 110d28bb6f8..00000000000 --- a/collect_app/src/main/java/org/odk/collect/android/listeners/RecyclerViewClickListener.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (C) 2017 Shobhit - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package org.odk.collect.android.listeners; - -import org.odk.collect.android.adapters.SortDialogAdapter; - -public interface RecyclerViewClickListener { - - void onItemClicked(SortDialogAdapter.ViewHolder holder, int position); -} diff --git a/collect_app/src/main/java/org/odk/collect/android/utilities/ApplicationConstants.java b/collect_app/src/main/java/org/odk/collect/android/utilities/ApplicationConstants.java index 32fd063b07b..ef0e66071ca 100644 --- a/collect_app/src/main/java/org/odk/collect/android/utilities/ApplicationConstants.java +++ b/collect_app/src/main/java/org/odk/collect/android/utilities/ApplicationConstants.java @@ -18,10 +18,6 @@ import com.google.zxing.integration.android.IntentIntegrator; -import org.odk.collect.android.R; - -import java.util.HashMap; - public class ApplicationConstants { // based on http://www.sqlite.org/limits.html @@ -37,17 +33,6 @@ private ApplicationConstants() { } - public static HashMap getSortLabelToIconMap() { - HashMap hashMap = new HashMap<>(); - hashMap.put(R.string.sort_by_name_asc, R.drawable.ic_sort_by_alpha); - hashMap.put(R.string.sort_by_name_desc, R.drawable.ic_sort_by_alpha); - hashMap.put(R.string.sort_by_date_desc, R.drawable.ic_access_time); - hashMap.put(R.string.sort_by_date_asc, R.drawable.ic_access_time); - hashMap.put(R.string.sort_by_status_asc, R.drawable.ic_assignment_turned_in); - hashMap.put(R.string.sort_by_status_desc, R.drawable.ic_assignment_late); - return hashMap; - } - public abstract static class BundleKeys { public static final String FORM_MODE = "formMode"; public static final String SUCCESS_KEY = "SUCCESSFUL"; diff --git a/collect_app/src/main/res/drawable/ic_sort_by_last_saved.xml b/collect_app/src/main/res/drawable/ic_sort_by_last_saved.xml new file mode 100644 index 00000000000..b20e7f6c935 --- /dev/null +++ b/collect_app/src/main/res/drawable/ic_sort_by_last_saved.xml @@ -0,0 +1,9 @@ + + + diff --git a/collect_app/src/test/java/org/odk/collect/android/formlists/blankformlist/BlankFormListItemTest.kt b/collect_app/src/test/java/org/odk/collect/android/formlists/blankformlist/BlankFormListItemTest.kt new file mode 100644 index 00000000000..1986dac09d7 --- /dev/null +++ b/collect_app/src/test/java/org/odk/collect/android/formlists/blankformlist/BlankFormListItemTest.kt @@ -0,0 +1,137 @@ +package org.odk.collect.android.formlists.blankformlist + +import android.net.Uri +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.odk.collect.forms.Form +import org.odk.collect.forms.instances.Instance +import org.odk.collect.formstest.InMemInstancesRepository +import org.odk.collect.projects.Project + +@RunWith(AndroidJUnit4::class) +class BlankFormListItemTest { + private val instancesRepository = InMemInstancesRepository() + + @Test + fun `Form should be properly converted to BlankFormListItem`() { + val form = Form.Builder() + .dbId(1) + .formId("1") + .displayName("Sample form B") + .version("1") + .geometryXpath("blah") + .date(1665742651522) + .build() + + instancesRepository.save( + Instance.Builder() + .formId(form.formId) + .formVersion(form.version) + .lastStatusChangeDate(3) + .build() + ) + + val blankFormListItem = form.toBlankFormListItem(Project.DEMO_PROJECT_ID, instancesRepository) + + assertThat(blankFormListItem.databaseId, `is`(form.dbId)) + assertThat(blankFormListItem.formId, `is`(form.formId)) + assertThat(blankFormListItem.formName, `is`(form.displayName)) + assertThat(blankFormListItem.formVersion, `is`(form.version)) + assertThat(blankFormListItem.geometryPath, `is`(form.geometryXpath)) + assertThat(blankFormListItem.dateOfCreation, `is`(form.date)) + assertThat(blankFormListItem.dateOfLastUsage, `is`(3L)) + assertThat(blankFormListItem.contentUri, `is`(Uri.parse("content://org.odk.collect.android.provider.odk.forms/forms/1?projectId=DEMO"))) + } + + @Test + fun `When there are no saved instances dateOfLastUsage in BlankFormListItem should be set to 0`() { + val form = Form.Builder() + .dbId(1) + .formId("1") + .displayName("Sample form") + .date(1665742651521) + .build() + + val blankFormListItem = form.toBlankFormListItem(Project.DEMO_PROJECT_ID, instancesRepository) + + assertThat(blankFormListItem.dateOfLastUsage, `is`(0L)) + } + + @Test + fun `When version in form is not set should be represented as an empty sting in BlankFormListItem`() { + val form = Form.Builder() + .dbId(1) + .formId("1") + .displayName("Sample form") + .date(1665742651521) + .build() + + val blankFormListItem = form.toBlankFormListItem(Project.DEMO_PROJECT_ID, instancesRepository) + + assertThat(blankFormListItem.formVersion, `is`("")) + } + + @Test + fun `When geometryXpath in form is not set should be represented as an empty sting in BlankFormListItem`() { + val form = Form.Builder() + .dbId(1) + .formId("1") + .displayName("Sample form") + .date(1665742651521) + .build() + + val blankFormListItem = form.toBlankFormListItem(Project.DEMO_PROJECT_ID, instancesRepository) + + assertThat(blankFormListItem.geometryPath, `is`("")) + } + + @Test + fun `dateOfLastUsage should be set taking into account forms saved not only for given formId but also formVersion`() { + val formV1 = Form.Builder() + .dbId(1) + .formId("1") + .displayName("Sample form v1") + .version("1") + .geometryXpath("blah") + .date(1665742651521) + .build() + + val formV2 = Form.Builder(formV1) + .dbId(2) + .displayName("Sample form v2") + .version("2") + .date(1665742651522) + .build() + + instancesRepository.save( + Instance.Builder() + .formId(formV1.formId) + .formVersion(formV1.version) + .lastStatusChangeDate(5) + .build() + ) + + instancesRepository.save( + Instance.Builder() + .formId(formV2.formId) + .formVersion(formV2.version) + .lastStatusChangeDate(3) + .build() + ) + + instancesRepository.save( + Instance.Builder() + .formId(formV2.formId) + .formVersion(formV2.version) + .lastStatusChangeDate(4) + .build() + ) + + val blankFormListItem = formV2.toBlankFormListItem(Project.DEMO_PROJECT_ID, instancesRepository) + + assertThat(blankFormListItem.dateOfLastUsage, `is`(4L)) + } +} diff --git a/collect_app/src/test/java/org/odk/collect/android/formlists/blankformlist/BlankFormListMenuDelegateTest.kt b/collect_app/src/test/java/org/odk/collect/android/formlists/blankformlist/BlankFormListMenuDelegateTest.kt index 5985ae6df56..610df86485f 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formlists/blankformlist/BlankFormListMenuDelegateTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/formlists/blankformlist/BlankFormListMenuDelegateTest.kt @@ -20,7 +20,7 @@ import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.odk.collect.android.R -import org.odk.collect.android.formlists.FormListSortingBottomSheetDialog +import org.odk.collect.android.formlists.sorting.FormListSortingBottomSheetDialog import org.odk.collect.android.support.CollectHelpers import org.odk.collect.androidshared.network.NetworkStateProvider import org.robolectric.Shadows diff --git a/collect_app/src/test/java/org/odk/collect/android/formlists/blankformlist/BlankFormListViewModelTest.kt b/collect_app/src/test/java/org/odk/collect/android/formlists/blankformlist/BlankFormListViewModelTest.kt index c76164b0233..b13461f951e 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formlists/blankformlist/BlankFormListViewModelTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/formlists/blankformlist/BlankFormListViewModelTest.kt @@ -16,7 +16,6 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import org.odk.collect.analytics.Analytics -import org.odk.collect.android.external.FormsContract import org.odk.collect.android.formmanagement.FormsUpdater import org.odk.collect.android.formmanagement.matchexactly.SyncStatusAppState import org.odk.collect.android.preferences.utilities.FormUpdateMode @@ -25,8 +24,10 @@ import org.odk.collect.android.utilities.FormsDirDiskFormsSynchronizer import org.odk.collect.androidtest.getOrAwaitValue import org.odk.collect.forms.Form import org.odk.collect.forms.FormSourceException +import org.odk.collect.forms.instances.Instance import org.odk.collect.formstest.FormUtils import org.odk.collect.formstest.InMemFormsRepository +import org.odk.collect.formstest.InMemInstancesRepository import org.odk.collect.settings.keys.ProjectKeys import org.odk.collect.shared.settings.InMemSettings import org.odk.collect.testshared.BooleanChangeLock @@ -35,6 +36,7 @@ import org.odk.collect.testshared.FakeScheduler @RunWith(AndroidJUnit4::class) class BlankFormListViewModelTest { private val formsRepository = InMemFormsRepository() + private val instancesRepository = InMemInstancesRepository() private val context = ApplicationProvider.getApplicationContext() private val syncRepository: SyncStatusAppState = mock() private val formsUpdater: FormsUpdater = mock() @@ -227,7 +229,7 @@ class BlankFormListViewModelTest { } @Test - fun `list of forms should be sorted when sorting order is changed`() { + fun `when list of forms sorted 'by name ASC', saved should forms be ordered properly`() { saveForms( form(dbId = 1, formId = "1", formName = "1Form"), form(dbId = 2, formId = "2", formName = "BForm"), @@ -238,32 +240,71 @@ class BlankFormListViewModelTest { createViewModel() - // Sort by name ASC viewModel.sortingOrder = 0 + assertFormItem(viewModel.formsToDisplay.value!![0], form(dbId = 1, formId = "1", formName = "1Form")) assertFormItem(viewModel.formsToDisplay.value!![1], form(dbId = 5, formId = "5", formName = "2Form")) assertFormItem(viewModel.formsToDisplay.value!![2], form(dbId = 3, formId = "3", formName = "aForm")) assertFormItem(viewModel.formsToDisplay.value!![3], form(dbId = 4, formId = "4", formName = "AForm")) assertFormItem(viewModel.formsToDisplay.value!![4], form(dbId = 2, formId = "2", formName = "BForm")) + } + + @Test + fun `when list of forms sorted 'by name DESC', saved should forms be ordered properly`() { + saveForms( + form(dbId = 1, formId = "1", formName = "1Form"), + form(dbId = 2, formId = "2", formName = "BForm"), + form(dbId = 3, formId = "3", formName = "aForm"), + form(dbId = 4, formId = "4", formName = "AForm"), + form(dbId = 5, formId = "5", formName = "2Form") + ) + + createViewModel() - // Sort by name DESC viewModel.sortingOrder = 1 + assertFormItem(viewModel.formsToDisplay.value!![0], form(dbId = 2, formId = "2", formName = "BForm")) assertFormItem(viewModel.formsToDisplay.value!![1], form(dbId = 3, formId = "3", formName = "aForm")) assertFormItem(viewModel.formsToDisplay.value!![2], form(dbId = 4, formId = "4", formName = "AForm")) assertFormItem(viewModel.formsToDisplay.value!![3], form(dbId = 5, formId = "5", formName = "2Form")) assertFormItem(viewModel.formsToDisplay.value!![4], form(dbId = 1, formId = "1", formName = "1Form")) + } + + @Test + fun `when list of forms sorted 'by date newest first', saved should forms be ordered properly`() { + saveForms( + form(dbId = 1, formId = "1", formName = "1Form"), + form(dbId = 2, formId = "2", formName = "BForm"), + form(dbId = 3, formId = "3", formName = "aForm"), + form(dbId = 4, formId = "4", formName = "AForm"), + form(dbId = 5, formId = "5", formName = "2Form") + ) + + createViewModel() - // Sort by date newest first viewModel.sortingOrder = 2 + assertFormItem(viewModel.formsToDisplay.value!![0], form(dbId = 5, formId = "5", formName = "2Form")) assertFormItem(viewModel.formsToDisplay.value!![1], form(dbId = 4, formId = "4", formName = "AForm")) assertFormItem(viewModel.formsToDisplay.value!![2], form(dbId = 3, formId = "3", formName = "aForm")) assertFormItem(viewModel.formsToDisplay.value!![3], form(dbId = 2, formId = "2", formName = "BForm")) assertFormItem(viewModel.formsToDisplay.value!![4], form(dbId = 1, formId = "1", formName = "1Form")) + } + + @Test + fun `when list of forms sorted 'by date oldest first', saved should forms be ordered properly`() { + saveForms( + form(dbId = 1, formId = "1", formName = "1Form"), + form(dbId = 2, formId = "2", formName = "BForm"), + form(dbId = 3, formId = "3", formName = "aForm"), + form(dbId = 4, formId = "4", formName = "AForm"), + form(dbId = 5, formId = "5", formName = "2Form") + ) + + createViewModel() - // Sort by date oldest first viewModel.sortingOrder = 3 + assertFormItem(viewModel.formsToDisplay.value!![0], form(dbId = 1, formId = "1", formName = "1Form")) assertFormItem(viewModel.formsToDisplay.value!![1], form(dbId = 2, formId = "2", formName = "BForm")) assertFormItem(viewModel.formsToDisplay.value!![2], form(dbId = 3, formId = "3", formName = "aForm")) @@ -271,6 +312,105 @@ class BlankFormListViewModelTest { assertFormItem(viewModel.formsToDisplay.value!![4], form(dbId = 5, formId = "5", formName = "2Form")) } + @Test + fun `when list of forms sorted 'by last saved', should forms be ordered properly`() { + saveForms( + form(dbId = 1, formId = "1", formName = "1Form"), + form(dbId = 2, formId = "2", formName = "BForm"), + form(dbId = 3, formId = "3", formName = "aForm"), + form(dbId = 4, formId = "4", formName = "AForm"), + form(dbId = 5, formId = "5", formName = "2Form") + ) + + saveInstances( + instance(formId = "1", lastStatusChangeDate = 1L), + instance(formId = "3", lastStatusChangeDate = 2L), + instance(formId = "5", lastStatusChangeDate = 3L), + instance(formId = "4", lastStatusChangeDate = 4L), + instance(formId = "2", lastStatusChangeDate = 5L), + ) + + createViewModel() + + viewModel.sortingOrder = 4 + + assertFormItem(viewModel.formsToDisplay.value!![0], form(dbId = 2, formId = "2", formName = "BForm"), 5L) + assertFormItem(viewModel.formsToDisplay.value!![1], form(dbId = 4, formId = "4", formName = "AForm"), 4L) + assertFormItem(viewModel.formsToDisplay.value!![2], form(dbId = 5, formId = "5", formName = "2Form"), 3L) + assertFormItem(viewModel.formsToDisplay.value!![3], form(dbId = 3, formId = "3", formName = "aForm"), 2L) + assertFormItem(viewModel.formsToDisplay.value!![4], form(dbId = 1, formId = "1", formName = "1Form"), 1L) + } + + @Test + fun `when list of forms sorted 'by last saved' and there are no saved instances, should the order from database be preserved`() { + saveForms( + form(dbId = 1, formId = "1", formName = "1Form"), + form(dbId = 2, formId = "2", formName = "BForm"), + form(dbId = 3, formId = "3", formName = "aForm"), + form(dbId = 4, formId = "4", formName = "AForm"), + form(dbId = 5, formId = "5", formName = "2Form") + ) + + createViewModel() + + viewModel.sortingOrder = 4 + + assertFormItem(viewModel.formsToDisplay.value!![0], form(dbId = 1, formId = "1", formName = "1Form")) + assertFormItem(viewModel.formsToDisplay.value!![1], form(dbId = 2, formId = "2", formName = "BForm")) + assertFormItem(viewModel.formsToDisplay.value!![2], form(dbId = 3, formId = "3", formName = "aForm")) + assertFormItem(viewModel.formsToDisplay.value!![3], form(dbId = 4, formId = "4", formName = "AForm")) + assertFormItem(viewModel.formsToDisplay.value!![4], form(dbId = 5, formId = "5", formName = "2Form")) + } + + @Test + fun `when list of forms sorted 'by last saved', forms with no saved instances should be displayed at the bottom`() { + saveForms( + form(dbId = 1, formId = "1", formName = "1Form"), + form(dbId = 2, formId = "2", formName = "BForm"), + form(dbId = 3, formId = "3", formName = "aForm"), + form(dbId = 4, formId = "4", formName = "AForm"), + form(dbId = 5, formId = "5", formName = "2Form") + ) + + saveInstances( + instance(formId = "1", lastStatusChangeDate = 1L), + instance(formId = "3", lastStatusChangeDate = 2L), + ) + + createViewModel() + + viewModel.sortingOrder = 4 + + assertFormItem(viewModel.formsToDisplay.value!![0], form(dbId = 3, formId = "3", formName = "aForm"), 2L) + assertFormItem(viewModel.formsToDisplay.value!![1], form(dbId = 1, formId = "1", formName = "1Form"), 1L) + assertFormItem(viewModel.formsToDisplay.value!![2], form(dbId = 2, formId = "2", formName = "BForm")) + assertFormItem(viewModel.formsToDisplay.value!![3], form(dbId = 4, formId = "4", formName = "AForm")) + assertFormItem(viewModel.formsToDisplay.value!![4], form(dbId = 5, formId = "5", formName = "2Form")) + } + + @Test + fun `when list of forms sorted 'by last saved', and there are different versions of the same form, sorting should distinguish different form versions`() { + saveForms( + form(dbId = 1, formId = "1", formName = "AForm v1", version = "1"), + form(dbId = 2, formId = "1", formName = "AForm v2", version = "2"), + form(dbId = 3, formId = "2", formName = "BForm") + ) + + saveInstances( + instance(formId = "1", lastStatusChangeDate = 1L, version = "1"), + instance(formId = "2", lastStatusChangeDate = 2L), + instance(formId = "1", lastStatusChangeDate = 3L, version = "2"), + ) + + createViewModel(shouldHideOldFormVersions = false) + + viewModel.sortingOrder = 4 + + assertFormItem(viewModel.formsToDisplay.value!![0], form(dbId = 2, formId = "1", formName = "AForm v2", version = "2"), 3L) + assertFormItem(viewModel.formsToDisplay.value!![1], form(dbId = 3, formId = "2", formName = "BForm"), 2L) + assertFormItem(viewModel.formsToDisplay.value!![2], form(dbId = 1, formId = "1", formName = "AForm v1", version = "1"), 1L) + } + @Test fun `list of forms should be filtered when filterText is changed`() { saveForms( @@ -345,12 +485,21 @@ class BlankFormListViewModelTest { } } + private fun saveInstances(vararg instances: Instance) { + instancesRepository.deleteAll() + + instances.forEach { + instancesRepository.save(it) + } + } + private fun createViewModel(runAllBackgroundTasks: Boolean = true, shouldHideOldFormVersions: Boolean = true) { whenever(changeLockProvider.getFormLock(projectId)).thenReturn(changeLock) generalSettings.save(ProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS, shouldHideOldFormVersions) viewModel = BlankFormListViewModel( formsRepository, + instancesRepository, context, syncRepository, formsUpdater, @@ -368,20 +517,11 @@ class BlankFormListViewModelTest { } } - private fun assertFormItem(blankFormListItem: BlankFormListItem, form: Form) { + private fun assertFormItem(blankFormListItem: BlankFormListItem, form: Form, lastStatusChangeDate: Long = 0) { assertThat( blankFormListItem, `is`( - BlankFormListItem( - databaseId = form.dbId, - formId = form.formId, - formName = form.displayName, - formVersion = form.version ?: "", - geometryPath = form.geometryXpath ?: "", - dateOfCreation = form.date, - dateOfLastUsage = 0, - contentUri = FormsContract.getUri(projectId, form.dbId) - ) + form.toBlankFormListItem(projectId, instancesRepository) ) ) } @@ -389,7 +529,7 @@ class BlankFormListViewModelTest { private fun form( dbId: Long, formId: String = "1", - version: String = "1", + version: String? = null, formName: String = "Form $formId", deleted: Boolean = false ) = Form.Builder() @@ -401,4 +541,14 @@ class BlankFormListViewModelTest { .deleted(deleted) .formFilePath(FormUtils.createXFormFile(formId, version).absolutePath) .build() + + private fun instance( + formId: String, + lastStatusChangeDate: Long, + version: String? = null + ) = Instance.Builder() + .formId(formId) + .formVersion(version) + .lastStatusChangeDate(lastStatusChangeDate) + .build() } diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index 0c990c67697..c44a1346df4 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -59,6 +59,7 @@ Name, Z-A Date, newest first Date, oldest first + Last saved first Status, finalized first Status, unfinalized first Filter the list