diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/ui/MultiSelectViewModel.kt b/androidshared/src/main/java/org/odk/collect/androidshared/ui/MultiSelectViewModel.kt index e548f51e349..a1e48388f06 100644 --- a/androidshared/src/main/java/org/odk/collect/androidshared/ui/MultiSelectViewModel.kt +++ b/androidshared/src/main/java/org/odk/collect/androidshared/ui/MultiSelectViewModel.kt @@ -1,6 +1,8 @@ package org.odk.collect.androidshared.ui +import android.widget.Button import androidx.lifecycle.ViewModel +import org.odk.collect.androidshared.R import org.odk.collect.androidshared.livedata.MutableNonNullLiveData import org.odk.collect.androidshared.livedata.NonNullLiveData @@ -32,3 +34,15 @@ class MultiSelectViewModel : ViewModel() { } } } + +fun updateSelectAll(button: Button, itemCount: Int, selectedCount: Int): Boolean { + val allSelected = itemCount > 0 && selectedCount == itemCount + + if (allSelected) { + button.setText(R.string.clear_all) + } else { + button.setText(R.string.select_all) + } + + return allSelected +} diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/EncryptedFormTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/EncryptedFormTest.java index a6525a64dc6..e370356df8e 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/EncryptedFormTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/EncryptedFormTest.java @@ -49,7 +49,7 @@ public void instanceOfEncryptedForm_cantBeViewedAfterSending() { .clickFinalize() .clickSendFinalizedForm(1) - .clickOnForm("encrypted") + .clickSelectAll() .clickSendSelected() .clickOK(new SendFinalizedFormPage()) .pressBack(new MainMenuPage()) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FormFinalizingTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FormFinalizingTest.kt index 76f3e52bccc..3042918ef8b 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FormFinalizingTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FormFinalizingTest.kt @@ -7,6 +7,7 @@ import org.junit.rules.RuleChain import org.junit.runner.RunWith import org.odk.collect.android.R import org.odk.collect.android.support.pages.AccessControlPage +import org.odk.collect.android.support.pages.FormEntryPage import org.odk.collect.android.support.pages.MainMenuPage import org.odk.collect.android.support.pages.ProjectSettingsPage import org.odk.collect.android.support.pages.SaveOrDiscardFormDialog @@ -21,27 +22,26 @@ class FormFinalizingTest { val copyFormChain: RuleChain = chain().around(rule) @Test - fun fillingForm_andPressingSaveAsDraft_doesNotFinalizesForm() { + fun fillingForm_andPressingFinalize_finalizesForm() { rule.startAtMainMenu() .copyForm(FORM) .assertNumberOfFinalizedForms(0) .startBlankForm("One Question") - .swipeToEndScreen() - .clickSaveAsDraft() - .assertNumberOfEditableForms(1) - .assertNumberOfFinalizedForms(0) + .fillOutAndFinalize(FormEntryPage.QuestionAndAnswer("what is your age", "52")) + .assertNumberOfEditableForms(0) + .assertNumberOfFinalizedForms(1) } @Test - fun fillingForm_andPressingFinalize_finalizesForm() { + fun fillingForm_andPressingSaveAsDraft_doesNotFinalizesForm() { rule.startAtMainMenu() .copyForm(FORM) .assertNumberOfFinalizedForms(0) .startBlankForm("One Question") .swipeToEndScreen() - .clickFinalize() - .assertNumberOfEditableForms(0) - .assertNumberOfFinalizedForms(1) + .clickSaveAsDraft() + .assertNumberOfEditableForms(1) + .assertNumberOfFinalizedForms(0) } @Test diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/EditSavedFormTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/EditSavedFormTest.java index 570749cc4e4..b062d96ab59 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/EditSavedFormTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/EditSavedFormTest.java @@ -34,7 +34,7 @@ public void whenSubmissionSucceeds_instanceNotEditable() { .clickFinalize() .clickSendFinalizedForm(1) - .clickOnForm("One Question") + .clickSelectAll() .clickSendSelected() .clickOK(new SendFinalizedFormPage()) .pressBack(new MainMenuPage()) @@ -62,7 +62,7 @@ public void whenSubmissionFails_instanceNotEditable() { .clickFinalize() .clickSendFinalizedForm(1) - .clickOnForm("One Question") + .clickSelectAll() .clickSendSelected() .clickOK(new SendFinalizedFormPage()) .pressBack(new MainMenuPage()) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/SendFinalizedFormTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/SendFinalizedFormTest.java deleted file mode 100644 index 90ccd32da07..00000000000 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/SendFinalizedFormTest.java +++ /dev/null @@ -1,130 +0,0 @@ -package org.odk.collect.android.feature.instancemanagement; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.RuleChain; -import org.junit.runner.RunWith; -import org.odk.collect.android.R; -import org.odk.collect.android.support.CollectHelpers; -import org.odk.collect.android.support.TestDependencies; -import org.odk.collect.android.support.pages.MainMenuPage; -import org.odk.collect.android.support.pages.OkDialog; -import org.odk.collect.android.support.pages.ProjectSettingsPage; -import org.odk.collect.android.support.pages.SendFinalizedFormPage; -import org.odk.collect.android.support.rules.CollectTestRule; -import org.odk.collect.android.support.rules.TestRuleChain; -import org.odk.collect.androidtest.RecordedIntentsRule; -import org.odk.collect.projects.Project; - -@RunWith(AndroidJUnit4.class) -public class SendFinalizedFormTest { - - private final TestDependencies testDependencies = new TestDependencies(); - private final CollectTestRule rule = new CollectTestRule(); - - @Rule - public RuleChain chain = TestRuleChain.chain(testDependencies) - .around(new RecordedIntentsRule()) - .around(rule); - - @Test - public void whenThereIsAnAuthenticationError_allowsUserToReenterCredentials() { - testDependencies.server.setCredentials("Draymond", "Green"); - - rule.startAtMainMenu() - .setServer(testDependencies.server.getURL()) - .copyForm("one-question.xml") - .startBlankForm("One Question") - .answerQuestion("what is your age", "123") - .swipeToEndScreen() - .clickFinalize() - - .clickSendFinalizedForm(1) - .clickOnForm("One Question") - .clickSendSelectedWithAuthenticationError() - .fillUsername("Draymond") - .fillPassword("Green") - .clickOK(new OkDialog()) - .assertText("One Question - Success"); - } - - @Test - public void canViewSentForms() { - rule.startAtMainMenu() - .setServer(testDependencies.server.getURL()) - .copyForm("one-question.xml") - .startBlankForm("One Question") - .answerQuestion("what is your age", "123") - .swipeToEndScreen() - .clickFinalize() - - .clickSendFinalizedForm(1) - .clickOnForm("One Question") - .clickSendSelected() - .clickOK(new SendFinalizedFormPage()) - .pressBack(new MainMenuPage()) - - .clickViewSentForm(1) - .clickOnForm("One Question") - .assertText("123") - .assertText(R.string.exit); - } - - @Test - public void whenDeleteAfterSendIsEnabled_deletesFilledForm() { - rule.startAtMainMenu() - .setServer(testDependencies.server.getURL()) - - .openProjectSettingsDialog() - .clickSettings() - .clickFormManagement() - .scrollToRecyclerViewItemAndClickText(R.string.delete_after_send) - .pressBack(new ProjectSettingsPage()) - .pressBack(new MainMenuPage()) - - .copyForm("one-question.xml") - .startBlankForm("One Question") - .answerQuestion("what is your age", "123") - .swipeToEndScreen() - .clickFinalize() - - .clickSendFinalizedForm(1) - .clickOnForm("One Question") - .clickSendSelected() - .clickOK(new SendFinalizedFormPage()) - .pressBack(new MainMenuPage()) - - .clickViewSentForm(1) - .clickOnText("One Question") - .assertOnPage(); - } - - @Test - public void whenGoogleUsedAsServer_sendsSubmissionToSheet() { - CollectHelpers.addGDProject( - new Project.New( - "GD Project", - "G", - "#3e9fcc" - ), - "dani@davey.com", - testDependencies - ); - - rule.startAtMainMenu() - .openProjectSettingsDialog() - .selectProject("GD Project") - .copyForm("one-question-google.xml", null, false, "GD Project") - .startBlankForm("One Question Google") - .answerQuestion("what is your age", "47") - .swipeToEndScreen() - .clickFinalize() - - .clickSendFinalizedForm(1) - .clickOnForm("One Question Google") - .clickSendSelected() - .assertText("One Question Google - Success"); - } -} diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/SendFinalizedFormTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/SendFinalizedFormTest.kt new file mode 100644 index 00000000000..fc413446c63 --- /dev/null +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/SendFinalizedFormTest.kt @@ -0,0 +1,151 @@ +package org.odk.collect.android.feature.instancemanagement + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.runner.RunWith +import org.odk.collect.android.R +import org.odk.collect.android.support.CollectHelpers.addGDProject +import org.odk.collect.android.support.TestDependencies +import org.odk.collect.android.support.pages.FormEntryPage.QuestionAndAnswer +import org.odk.collect.android.support.pages.MainMenuPage +import org.odk.collect.android.support.pages.OkDialog +import org.odk.collect.android.support.pages.ProjectSettingsPage +import org.odk.collect.android.support.pages.SendFinalizedFormPage +import org.odk.collect.android.support.rules.CollectTestRule +import org.odk.collect.android.support.rules.TestRuleChain.chain +import org.odk.collect.androidtest.RecordedIntentsRule +import org.odk.collect.projects.Project.New + +@RunWith(AndroidJUnit4::class) +class SendFinalizedFormTest { + + private val testDependencies = TestDependencies() + private val rule = CollectTestRule(useDemoProject = false) + + @get:Rule + val chain: RuleChain = chain(testDependencies) + .around(RecordedIntentsRule()) + .around(rule) + + @Test + fun canViewFormsBeforeSending() { + rule.withProject(testDependencies.server.url) + .copyForm("one-question.xml", projectName = testDependencies.server.hostName) + .startBlankForm("One Question") + .fillOutAndFinalize(QuestionAndAnswer("what is your age", "52")) + .clickSendFinalizedForm(1) + .clickOnForm("One Question") + .assertText("52") + } + + @Test + fun whenThereIsAnAuthenticationError_allowsUserToReenterCredentials() { + testDependencies.server.setCredentials("Draymond", "Green") + rule.withProject(testDependencies.server.url) + .copyForm("one-question.xml", projectName = testDependencies.server.hostName) + .startBlankForm("One Question") + .answerQuestion("what is your age", "123") + .swipeToEndScreen() + .clickFinalize() + .clickSendFinalizedForm(1) + .clickSelectAll() + .clickSendSelectedWithAuthenticationError() + .fillUsername("Draymond") + .fillPassword("Green") + .clickOK(OkDialog()) + .assertText("One Question - Success") + } + + @Test + fun canViewSentForms() { + rule.withProject(testDependencies.server.url) + .copyForm("one-question.xml", projectName = testDependencies.server.hostName) + .startBlankForm("One Question") + .answerQuestion("what is your age", "123") + .swipeToEndScreen() + .clickFinalize() + .clickSendFinalizedForm(1) + .clickSelectAll() + .clickSendSelected() + .clickOK(SendFinalizedFormPage()) + .pressBack(MainMenuPage()) + .clickViewSentForm(1) + .clickOnForm("One Question") + .assertText("123") + .assertText(R.string.exit) + } + + @Test + fun canSendIndividualForms() { + rule.withProject(testDependencies.server.url) + .copyForm("one-question.xml", projectName = testDependencies.server.hostName) + .startBlankForm("One Question") + .fillOutAndFinalize(QuestionAndAnswer("what is your age", "123")) + .startBlankForm("One Question") + .fillOutAndFinalize(QuestionAndAnswer("what is your age", "124")) + + .clickSendFinalizedForm(2) + .selectForm(0) + .clickSendSelected() + .clickOK(SendFinalizedFormPage()) + .pressBack(MainMenuPage()) + + .assertNumberOfFinalizedForms(1) + .clickViewSentForm(1) + .clickOnForm("One Question") + .assertText("123") + } + + @Test + fun whenDeleteAfterSendIsEnabled_deletesFilledForm() { + rule.withProject(testDependencies.server.url) + .openProjectSettingsDialog() + .clickSettings() + .clickFormManagement() + .scrollToRecyclerViewItemAndClickText(R.string.delete_after_send) + .pressBack(ProjectSettingsPage()) + .pressBack(MainMenuPage()) + .copyForm("one-question.xml", projectName = testDependencies.server.hostName) + .startBlankForm("One Question") + .answerQuestion("what is your age", "123") + .swipeToEndScreen() + .clickFinalize() + .clickSendFinalizedForm(1) + .clickSelectAll() + .clickSendSelected() + .clickOK(SendFinalizedFormPage()) + .pressBack(MainMenuPage()) + .clickViewSentForm(1) + .clickOnText("One Question") + .assertOnPage() + } + + @Test + fun whenGoogleUsedAsServer_sendsSubmissionToSheet() { + addGDProject( + New( + "GD Project", + "G", + "#3e9fcc" + ), + "dani@davey.com", + testDependencies + ) + + rule.startAtFirstLaunch() + .clickTryCollect() + .openProjectSettingsDialog() + .selectProject("GD Project") + .copyForm("one-question-google.xml", null, false, "GD Project") + .startBlankForm("One Question Google") + .answerQuestion("what is your age", "47") + .swipeToEndScreen() + .clickFinalize() + .clickSendFinalizedForm(1) + .clickSelectAll() + .clickSendSelected() + .assertText("One Question Google - Success") + } +} diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/smoke/GetAndSubmitFormTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/feature/smoke/GetAndSubmitFormTest.java index 8a1821c0a14..d86048404d5 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/smoke/GetAndSubmitFormTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/smoke/GetAndSubmitFormTest.java @@ -39,7 +39,7 @@ public void canGetBlankForm_fillItIn_andSubmit() { // Send form .clickSendFinalizedForm(1) - .clickOnForm("One Question") + .clickSelectAll() .clickSendSelected() .assertText("One Question - Success") .clickOK(new SendFinalizedFormPage()) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/SendFinalizedFormPage.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/SendFinalizedFormPage.java index 149c2564d1b..0e1cc5e2b96 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/SendFinalizedFormPage.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/SendFinalizedFormPage.java @@ -1,12 +1,14 @@ package org.odk.collect.android.support.pages; import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.core.AllOf.allOf; +import static org.odk.collect.android.support.matchers.CustomMatchers.withIndex; import org.odk.collect.android.R; @@ -18,9 +20,9 @@ public SendFinalizedFormPage assertOnPage() { return this; } - public SendFinalizedFormPage clickOnForm(String formLabel) { + public ViewFormPage clickOnForm(String formLabel) { clickOnText(formLabel); - return this; + return new ViewFormPage(formLabel).assertOnPage(); } public OkDialog clickSendSelected() { @@ -32,4 +34,14 @@ public ServerAuthDialog clickSendSelectedWithAuthenticationError() { clickOnText(getTranslatedString(R.string.send_selected_data)); return new ServerAuthDialog().assertOnPage(); } + + public SendFinalizedFormPage clickSelectAll() { + clickOnString(R.string.select_all); + return this; + } + + public SendFinalizedFormPage selectForm(int index) { + onView(withIndex(withId(R.id.checkbox), index)).perform(click()); + return this; + } } diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ViewFormPage.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ViewFormPage.kt new file mode 100644 index 00000000000..d3e77d9f35a --- /dev/null +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ViewFormPage.kt @@ -0,0 +1,12 @@ +package org.odk.collect.android.support.pages + +import org.odk.collect.android.R + +class ViewFormPage(private val formName: String) : Page() { + + override fun assertOnPage(): ViewFormPage { + assertToolbarTitle(formName) + assertText(R.string.exit) + return this + } +} diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ViewSentFormPage.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ViewSentFormPage.java index b7b561e5856..c305b691754 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ViewSentFormPage.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ViewSentFormPage.java @@ -1,12 +1,12 @@ package org.odk.collect.android.support.pages; -import org.odk.collect.android.R; -import org.odk.collect.android.database.forms.DatabaseFormColumns; - import static androidx.test.espresso.Espresso.onData; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.matcher.CursorMatchers.withRowString; +import org.odk.collect.android.R; +import org.odk.collect.android.database.forms.DatabaseFormColumns; + public class ViewSentFormPage extends Page { @Override @@ -15,8 +15,8 @@ public ViewSentFormPage assertOnPage() { return this; } - public FormHierarchyPage clickOnForm(String formName) { + public ViewFormPage clickOnForm(String formName) { onData(withRowString(DatabaseFormColumns.DISPLAY_NAME, formName)).perform(click()); - return new FormHierarchyPage(formName); + return new ViewFormPage(formName).assertOnPage(); } } 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 6b988cbc1dc..2dd71dd8c27 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 @@ -14,6 +14,11 @@ package org.odk.collect.android.activities; +import static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_DATE_ASC; +import static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_DATE_DESC; +import static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_NAME_ASC; +import static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_NAME_DESC; + import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; @@ -58,7 +63,7 @@ * @author Yaw Anokwa (yanokwa@gmail.com) * @author Carl Hartung (carlhartung@gmail.com) */ -public class InstanceChooserList extends InstanceListActivity implements AdapterView.OnItemClickListener, LoaderManager.LoaderCallbacks { +public class InstanceChooserList extends AppListActivity implements AdapterView.OnItemClickListener, LoaderManager.LoaderCallbacks { private static final String INSTANCE_LIST_ACTIVITY_SORTING_ORDER = "instanceListActivitySortingOrder"; private static final String VIEW_SENT_FORM_SORTING_ORDER = "ViewSentFormSortingOrder"; @@ -242,4 +247,23 @@ public void onClick(DialogInterface dialog, int i) { alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.ok), errorListener); alertDialog.show(); } + + protected String getSortingOrder() { + String sortingOrder = DatabaseInstanceColumns.DISPLAY_NAME + " COLLATE NOCASE ASC, " + DatabaseInstanceColumns.STATUS + " DESC"; + switch (getSelectedSortingOrder()) { + case BY_NAME_ASC: + sortingOrder = DatabaseInstanceColumns.DISPLAY_NAME + " COLLATE NOCASE ASC, " + DatabaseInstanceColumns.STATUS + " DESC"; + break; + case BY_NAME_DESC: + sortingOrder = DatabaseInstanceColumns.DISPLAY_NAME + " COLLATE NOCASE DESC, " + DatabaseInstanceColumns.STATUS + " DESC"; + break; + case BY_DATE_ASC: + sortingOrder = DatabaseInstanceColumns.LAST_STATUS_CHANGE_DATE + " ASC"; + break; + case BY_DATE_DESC: + sortingOrder = DatabaseInstanceColumns.LAST_STATUS_CHANGE_DATE + " DESC"; + break; + } + return sortingOrder; + } } diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceListActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceListActivity.java deleted file mode 100644 index bb6b66efd85..00000000000 --- a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceListActivity.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.odk.collect.android.activities; - -import org.odk.collect.android.database.instances.DatabaseInstanceColumns; - -import static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_DATE_ASC; -import static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_DATE_DESC; -import static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_NAME_ASC; -import static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_NAME_DESC; -import static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_STATUS_ASC; -import static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_STATUS_DESC; - -abstract class InstanceListActivity extends AppListActivity { - protected String getSortingOrder() { - String sortingOrder = DatabaseInstanceColumns.DISPLAY_NAME + " COLLATE NOCASE ASC, " + DatabaseInstanceColumns.STATUS + " DESC"; - switch (getSelectedSortingOrder()) { - case BY_NAME_ASC: - sortingOrder = DatabaseInstanceColumns.DISPLAY_NAME + " COLLATE NOCASE ASC, " + DatabaseInstanceColumns.STATUS + " DESC"; - break; - case BY_NAME_DESC: - sortingOrder = DatabaseInstanceColumns.DISPLAY_NAME + " COLLATE NOCASE DESC, " + DatabaseInstanceColumns.STATUS + " DESC"; - break; - case BY_DATE_ASC: - sortingOrder = DatabaseInstanceColumns.LAST_STATUS_CHANGE_DATE + " ASC"; - break; - case BY_DATE_DESC: - sortingOrder = DatabaseInstanceColumns.LAST_STATUS_CHANGE_DATE + " DESC"; - break; - case BY_STATUS_ASC: - sortingOrder = DatabaseInstanceColumns.STATUS + " ASC, " + DatabaseInstanceColumns.DISPLAY_NAME + " COLLATE NOCASE ASC"; - break; - case BY_STATUS_DESC: - sortingOrder = DatabaseInstanceColumns.STATUS + " DESC, " + DatabaseInstanceColumns.DISPLAY_NAME + " COLLATE NOCASE ASC"; - break; - } - return sortingOrder; - } -} 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 f9ca9f6bed3..7173cb9c662 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 @@ -14,6 +14,13 @@ package org.odk.collect.android.activities; +import static org.odk.collect.android.activities.AppListActivity.LOADER_ID; +import static org.odk.collect.android.activities.AppListActivity.toggleButtonLabel; +import static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_DATE_ASC; +import static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_DATE_DESC; +import static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_NAME_ASC; +import static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_NAME_DESC; +import static org.odk.collect.androidshared.ui.MultiSelectViewModelKt.updateSelectAll; import static org.odk.collect.settings.keys.ProjectKeys.KEY_PROTOCOL; import android.content.Intent; @@ -25,12 +32,16 @@ import android.view.View; import android.view.View.OnLongClickListener; import android.widget.AdapterView; -import android.widget.Button; import android.widget.ListView; +import android.widget.ProgressBar; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.SearchView; +import androidx.core.content.ContextCompat; +import androidx.core.view.MenuItemCompat; import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModelProvider; import androidx.loader.app.LoaderManager; import androidx.loader.content.Loader; import androidx.work.WorkInfo; @@ -43,21 +54,28 @@ import org.odk.collect.android.backgroundwork.FormUpdateAndInstanceSubmitScheduler; import org.odk.collect.android.backgroundwork.InstanceSubmitScheduler; import org.odk.collect.android.dao.CursorLoaderFactory; +import org.odk.collect.android.database.instances.DatabaseInstanceColumns; import org.odk.collect.android.databinding.InstanceUploaderListBinding; +import org.odk.collect.android.formlists.sorting.FormListSortingBottomSheetDialog; import org.odk.collect.android.formlists.sorting.FormListSortingOption; +import org.odk.collect.android.formmanagement.FormFillingIntentFactory; import org.odk.collect.android.gdrive.GoogleSheetsUploaderActivity; import org.odk.collect.android.injection.DaggerUtils; -import org.odk.collect.androidshared.network.NetworkStateProvider; +import org.odk.collect.android.mainmenu.MainMenuActivity; import org.odk.collect.android.preferences.screens.ProjectPreferencesActivity; import org.odk.collect.android.projects.CurrentProjectProvider; import org.odk.collect.android.utilities.PlayServicesChecker; +import org.odk.collect.androidshared.network.NetworkStateProvider; +import org.odk.collect.androidshared.ui.MultiSelectViewModel; import org.odk.collect.androidshared.ui.ToastUtils; import org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard; import org.odk.collect.settings.SettingsProvider; import org.odk.collect.settings.keys.ProjectKeys; +import org.odk.collect.strings.localization.LocalizedActivity; import java.util.Arrays; import java.util.List; +import java.util.Set; import javax.inject.Inject; @@ -71,11 +89,14 @@ * @author Yaw Anokwa (yanokwa@gmail.com) */ -public class InstanceUploaderListActivity extends InstanceListActivity implements +public class InstanceUploaderListActivity extends LocalizedActivity implements OnLongClickListener, AdapterView.OnItemClickListener, LoaderManager.LoaderCallbacks { private static final String SHOW_ALL_MODE = "showAllMode"; private static final String INSTANCE_UPLOADER_LIST_SORTING_ORDER = "instanceUploaderListSortingOrder"; + private static final String IS_SEARCH_BOX_SHOWN = "isSearchBoxShown"; + private static final String SEARCH_TEXT = "searchText"; + private static final int INSTANCE_UPLOADER = 0; InstanceUploaderListBinding binding; @@ -98,13 +119,41 @@ public class InstanceUploaderListActivity extends InstanceListActivity implement @Inject SettingsProvider settingsProvider; + private ListView listView; + private InstanceUploaderAdapter listAdapter; + private Integer selectedSortingOrder; + private List sortingOptions; + private ProgressBar progressBar; + private String filterText; + + private MultiSelectViewModel multiSelectViewModel; + private boolean allSelected; + + private boolean isSearchBoxShown; + + private SearchView searchView; + private String savedFilterText; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Timber.i("onCreate"); + if (savedInstanceState != null) { + isSearchBoxShown = savedInstanceState.getBoolean(IS_SEARCH_BOX_SHOWN); + savedFilterText = savedInstanceState.getString(SEARCH_TEXT); + } + DaggerUtils.getComponent(this).inject(this); + multiSelectViewModel = new ViewModelProvider(this).get(MultiSelectViewModel.class); + multiSelectViewModel.getSelected().observe(this, ids -> { + binding.uploadButton.setEnabled(!ids.isEmpty()); + allSelected = updateSelectAll(binding.toggleButton, listAdapter.getCount(), ids.size()); + + listAdapter.setSelected(ids); + }); + // set title setTitle(getString(R.string.send_data)); binding = InstanceUploaderListBinding.inflate(LayoutInflater.from(this)); @@ -113,8 +162,6 @@ public void onCreate(Bundle savedInstanceState) { if (savedInstanceState != null) { showAllMode = savedInstanceState.getBoolean(SHOW_ALL_MODE); } - - init(); } public void onUploadButtonsClicked() { @@ -128,48 +175,53 @@ public void onUploadButtonsClicked() { return; } - long[] instanceIds = listView.getCheckedItemIds(); - - if (instanceIds.length > 0) { - selectedInstances.clear(); - setAllToCheckedState(listView, false); - toggleButtonLabel(findViewById(R.id.toggle_button), listView); + Set selectedItems = multiSelectViewModel.getSelected().getValue(); + if (!selectedItems.isEmpty()) { binding.uploadButton.setEnabled(false); - uploadSelectedFiles(instanceIds); + uploadSelectedFiles(selectedItems.stream().mapToLong(Long::longValue).toArray()); + multiSelectViewModel.unselectAll(); } else { // no items selected ToastUtils.showLongToast(this, R.string.noselect_error); } } + @Override + public void setContentView(View view) { + super.setContentView(view); + + listView = findViewById(android.R.id.list); + listView.setOnItemClickListener((AdapterView.OnItemClickListener) this); + listView.setEmptyView(findViewById(android.R.id.empty)); + progressBar = findViewById(R.id.progressBar); + + // Use the nicer-looking drawable with Material Design insets. + listView.setDivider(ContextCompat.getDrawable(this, R.drawable.list_item_divider)); + listView.setDividerHeight(1); + + setSupportActionBar(findViewById(R.id.toolbar)); + + init(); + } + void init() { binding.uploadButton.setText(R.string.send_selected_data); binding.toggleButton.setLongClickable(true); binding.toggleButton.setOnClickListener(v -> { - ListView lv = listView; - boolean allChecked = toggleChecked(lv); - toggleButtonLabel(binding.toggleButton, lv); - binding.uploadButton.setEnabled(allChecked); - if (allChecked) { - for (int i = 0; i < lv.getCount(); i++) { - selectedInstances.add(lv.getItemIdAtPosition(i)); + if (!allSelected) { + for (int i = 0; i < listView.getCount(); i++) { + multiSelectViewModel.select(listView.getItemIdAtPosition(i)); } } else { - selectedInstances.clear(); + multiSelectViewModel.unselectAll(); } }); binding.toggleButton.setOnLongClickListener(this); setupAdapter(); - listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); - listView.setItemsCanFocus(false); - listView.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { - binding.uploadButton.setEnabled(areCheckedItems()); - }); - sortingOptions = Arrays.asList( new FormListSortingOption( R.drawable.ic_sort_by_alpha, @@ -219,6 +271,7 @@ private void updateAutoSendStatus() { @Override protected void onResume() { super.onResume(); + restoreSelectedSortingOrder(); binding.uploadButton.setText(R.string.send_selected_data); } @@ -246,6 +299,49 @@ private void uploadSelectedFiles(long[] instanceIds) { @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.instance_uploader_menu, menu); + + getMenuInflater().inflate(R.menu.form_list_menu, menu); + final MenuItem sortItem = menu.findItem(R.id.menu_sort); + final MenuItem searchItem = menu.findItem(R.id.menu_filter); + searchView = (SearchView) MenuItemCompat.getActionView(searchItem); + searchView.setQueryHint(getResources().getString(R.string.search)); + searchView.setMaxWidth(Integer.MAX_VALUE); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + filterText = query; + updateAdapter(); + searchView.clearFocus(); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + filterText = newText; + updateAdapter(); + return false; + } + }); + + MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + sortItem.setVisible(false); + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + sortItem.setVisible(true); + return true; + } + }); + + if (isSearchBoxShown) { + searchItem.expandActionView(); + searchView.setQuery(savedFilterText, false); + } + return super.onCreateOptionsMenu(menu); } @@ -263,6 +359,24 @@ public boolean onOptionsItemSelected(MenuItem item) { showSentAndUnsentChoices(); return true; } + + if (!MultiClickGuard.allowClick(getClass().getName())) { + 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); } @@ -273,27 +387,30 @@ private void createPreferencesMenu() { @Override public void onItemClick(AdapterView parent, View view, int position, long rowId) { - if (listView.isItemChecked(position)) { - selectedInstances.add(listView.getItemIdAtPosition(position)); - } else { - selectedInstances.remove(listView.getItemIdAtPosition(position)); - } - - binding.uploadButton.setEnabled(areCheckedItems()); - Button toggleSelectionsButton = findViewById(R.id.toggle_button); - toggleButtonLabel(toggleSelectionsButton, listView); + Cursor c = (Cursor) listView.getAdapter().getItem(position); + long instanceId = c.getLong(c.getColumnIndex(DatabaseInstanceColumns._ID)); + Intent intent = FormFillingIntentFactory.editInstanceIntent(this, currentProjectProvider.getCurrentProject().getUuid(), instanceId); + startActivity(intent); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); + + if (searchView != null) { + outState.putBoolean(IS_SEARCH_BOX_SHOWN, !searchView.isIconified()); + outState.putString(SEARCH_TEXT, String.valueOf(searchView.getQuery())); + } else { + Timber.e(new Error("Unexpected null search view (issue #1412)")); + } + outState.putBoolean(SHOW_ALL_MODE, showAllMode); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { if (resultCode == RESULT_CANCELED) { - selectedInstances.clear(); + multiSelectViewModel.unselectAll(); return; } @@ -312,18 +429,18 @@ protected void onActivityResult(int requestCode, int resultCode, Intent intent) } private void setupAdapter() { - listAdapter = new InstanceUploaderAdapter(this, null); + listAdapter = new InstanceUploaderAdapter(this, null, dbId -> { + multiSelectViewModel.toggle(dbId); + }); + listView.setAdapter(listAdapter); - checkPreviouslyCheckedItems(); } - @Override - protected String getSortingOrderKey() { + private String getSortingOrderKey() { return INSTANCE_UPLOADER_LIST_SORTING_ORDER; } - @Override - protected void updateAdapter() { + private void updateAdapter() { getSupportLoaderManager().restartLoader(LOADER_ID, null, this); } @@ -342,7 +459,6 @@ public Loader onCreateLoader(int id, Bundle args) { public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { hideProgressBarAndAllow(); listAdapter.changeCursor(cursor); - checkPreviouslyCheckedItems(); toggleButtonLabel(findViewById(R.id.toggle_button), listView); } @@ -388,4 +504,55 @@ private boolean showSentAndUnsentChoices() { alertDialog.show(); return true; } + + private String getSortingOrder() { + String sortingOrder = DatabaseInstanceColumns.DISPLAY_NAME + " COLLATE NOCASE ASC, " + DatabaseInstanceColumns.STATUS + " DESC"; + switch (getSelectedSortingOrder()) { + case BY_NAME_ASC: + sortingOrder = DatabaseInstanceColumns.DISPLAY_NAME + " COLLATE NOCASE ASC, " + DatabaseInstanceColumns.STATUS + " DESC"; + break; + case BY_NAME_DESC: + sortingOrder = DatabaseInstanceColumns.DISPLAY_NAME + " COLLATE NOCASE DESC, " + DatabaseInstanceColumns.STATUS + " DESC"; + break; + case BY_DATE_ASC: + sortingOrder = DatabaseInstanceColumns.LAST_STATUS_CHANGE_DATE + " ASC"; + break; + case BY_DATE_DESC: + sortingOrder = DatabaseInstanceColumns.LAST_STATUS_CHANGE_DATE + " DESC"; + break; + } + return sortingOrder; + } + + private int getSelectedSortingOrder() { + if (selectedSortingOrder == null) { + restoreSelectedSortingOrder(); + } + return selectedSortingOrder; + } + + private void restoreSelectedSortingOrder() { + selectedSortingOrder = settingsProvider.getUnprotectedSettings().getInt(getSortingOrderKey()); + } + + private void showProgressBar() { + progressBar.setVisibility(View.VISIBLE); + } + + private void hideProgressBarAndAllow() { + hideProgressBar(); + } + + private void hideProgressBar() { + progressBar.setVisibility(View.GONE); + } + + private CharSequence getFilterText() { + return filterText != null ? filterText : ""; + } + + private void saveSelectedSortingOrder(int selectedStringOrder) { + selectedSortingOrder = selectedStringOrder; + settingsProvider.getUnprotectedSettings().save(getSortingOrderKey(), selectedStringOrder); + } } diff --git a/collect_app/src/main/java/org/odk/collect/android/adapters/InstanceUploaderAdapter.java b/collect_app/src/main/java/org/odk/collect/android/adapters/InstanceUploaderAdapter.java index e2fd393bc00..081dcf883a7 100644 --- a/collect_app/src/main/java/org/odk/collect/android/adapters/InstanceUploaderAdapter.java +++ b/collect_app/src/main/java/org/odk/collect/android/adapters/InstanceUploaderAdapter.java @@ -16,13 +16,20 @@ import org.odk.collect.android.database.instances.DatabaseInstanceColumns; import java.util.Date; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; import static org.odk.collect.forms.instances.Instance.STATUS_SUBMISSION_FAILED; import static org.odk.collect.forms.instances.Instance.STATUS_SUBMITTED; public class InstanceUploaderAdapter extends CursorAdapter { - public InstanceUploaderAdapter(Context context, Cursor cursor) { + private final Consumer onItemCheckboxClickListener; + private Set selected = new HashSet<>(); + + public InstanceUploaderAdapter(Context context, Cursor cursor, Consumer onItemCheckboxClickListener) { super(context, cursor); + this.onItemCheckboxClickListener = onItemCheckboxClickListener; Collect.getInstance().getComponent().inject(this); } @@ -55,6 +62,17 @@ public void bindView(View view, Context context, Cursor cursor) { default: viewHolder.statusIcon.setImageResource(R.drawable.form_state_finalized_circle); } + + long dbId = cursor.getLong(cursor.getColumnIndex(DatabaseInstanceColumns._ID)); + viewHolder.checkbox.setChecked(selected.contains(dbId)); + viewHolder.checkbox.setOnClickListener(v -> { + onItemCheckboxClickListener.accept(dbId); + }); + } + + public void setSelected(Set ids) { + this.selected = ids; + notifyDataSetChanged(); } static class ViewHolder { diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt index a606c534d5a..405994678ff 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt @@ -14,6 +14,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.odk.collect.android.R import org.odk.collect.android.databinding.DeleteBlankFormLayoutBinding import org.odk.collect.androidshared.ui.MultiSelectViewModel +import org.odk.collect.androidshared.ui.updateSelectAll class DeleteBlankFormFragment( private val viewModelFactory: ViewModelProvider.Factory, @@ -93,13 +94,11 @@ class DeleteBlankFormFragment( menuHost.addMenuProvider(blankFormListMenuProvider, viewLifecycleOwner, State.RESUMED) } - fun updateAllSelected(binding: DeleteBlankFormLayoutBinding, adapter: SelectableBlankFormListAdapter) { - allSelected = adapter.formItems.isNotEmpty() && adapter.selected.size == adapter.formItems.size - - if (allSelected) { - binding.selectAll.setText(R.string.clear_all) - } else { - binding.selectAll.setText(R.string.select_all) - } + private fun updateAllSelected( + binding: DeleteBlankFormLayoutBinding, + adapter: SelectableBlankFormListAdapter + ) { + allSelected = + updateSelectAll(binding.selectAll, adapter.formItems.size, adapter.selected.size) } } diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/FormFillingIntentFactory.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/FormFillingIntentFactory.kt index 8d6e6b08ca8..86e27952dcd 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/FormFillingIntentFactory.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/FormFillingIntentFactory.kt @@ -20,6 +20,8 @@ object FormFillingIntentFactory { } } + @JvmStatic + @JvmOverloads fun editInstanceIntent( context: Context, projectId: String,