From 48be3abfd000e9258a4bdb272861646f097b6a8f Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 25 Jul 2023 11:45:22 +0200 Subject: [PATCH 01/19] Added new strings --- strings/src/main/res/values/strings.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index fd82e3cdfb8..5176ce804fe 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -1207,6 +1207,17 @@ View Close snackbar + + Last form sent: %d second(s) ago + Last form sent: %d minute(s) ago + Last form sent: %d hour(s) ago + Last form sent: %d day(s) ago + %d form(s) ready to send + In later releases, you will not be able to edit finalized forms. Save forms as draft to edit them later.\n\nYou can check for errors in a draft form by tapping the three dots (⋮) and then Check for errors. From 5d3c075e552f8b79a4c73534e1f4be1e98edc6f3 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 25 Jul 2023 12:17:37 +0200 Subject: [PATCH 02/19] Added banner --- .../InstanceUploaderListActivity.java | 6 +++ .../android/activities/ReadyToSendBanner.kt | 43 ++++++++++++++++++ .../res/layout/instance_uploader_list.xml | 10 ++++- .../main/res/layout/ready_to_send_banner.xml | 45 +++++++++++++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 collect_app/src/main/java/org/odk/collect/android/activities/ReadyToSendBanner.kt create mode 100644 collect_app/src/main/res/layout/ready_to_send_banner.xml 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 c39ad6bbc12..995596e630d 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 @@ -64,6 +64,7 @@ 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.InstancesRepositoryProvider; import org.odk.collect.android.utilities.PlayServicesChecker; import org.odk.collect.androidshared.network.NetworkStateProvider; import org.odk.collect.androidshared.ui.MultiSelectViewModel; @@ -119,6 +120,9 @@ public class InstanceUploaderListActivity extends LocalizedActivity implements @Inject SettingsProvider settingsProvider; + @Inject + InstancesRepositoryProvider instancesRepositoryProvider; + private ListView listView; private InstanceUploaderAdapter listAdapter; private Integer selectedSortingOrder; @@ -470,6 +474,8 @@ public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { } else { findViewById(R.id.buttonholder).setVisibility(View.VISIBLE); } + + binding.readyToSendBanner.init(instancesRepositoryProvider.get()); } @Override diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/ReadyToSendBanner.kt b/collect_app/src/main/java/org/odk/collect/android/activities/ReadyToSendBanner.kt new file mode 100644 index 00000000000..662c5945763 --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/activities/ReadyToSendBanner.kt @@ -0,0 +1,43 @@ +package org.odk.collect.android.activities + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import org.odk.collect.android.databinding.ReadyToSendBannerBinding +import org.odk.collect.forms.instances.Instance +import org.odk.collect.forms.instances.InstancesRepository +import org.odk.collect.strings.R + +private const val ONE_SECOND = 1000 +private const val ONE_MINUTE = 60000 +private const val ONE_HOUR = 3600000 +private const val ONE_DAY = 86400000 + +class ReadyToSendBanner(context: Context, attrs: AttributeSet?) : ConstraintLayout(context, attrs) { + constructor(context: Context) : this(context, null) + + private val binding = ReadyToSendBannerBinding.inflate(LayoutInflater.from(context), this, true) + + fun init(instancesRepository: InstancesRepository) { + val sentInstances: List = instancesRepository.getAllByStatus(Instance.STATUS_SUBMITTED) + val numberOfInstancesReadyToSend = instancesRepository.getCountByStatus(Instance.STATUS_COMPLETE, Instance.STATUS_SUBMISSION_FAILED) + + if (sentInstances.isNotEmpty() && numberOfInstancesReadyToSend > 0) { + val lastSentInstance = sentInstances.maxBy { instance -> instance.lastStatusChangeDate } + val millisecondsAgo = System.currentTimeMillis() - lastSentInstance.lastStatusChangeDate + if (millisecondsAgo >= ONE_DAY) { + binding.title.text = context.getString(R.string.last_form_sent_days_ago, millisecondsAgo / ONE_DAY) + } else if (millisecondsAgo >= ONE_HOUR) { + binding.title.text = context.getString(R.string.last_form_sent_hours_ago, millisecondsAgo / ONE_HOUR) + } else if (millisecondsAgo >= ONE_MINUTE) { + binding.title.text = context.getString(R.string.last_form_sent_minutes_ago, millisecondsAgo / ONE_MINUTE) + } else { + binding.title.text = context.getString(R.string.last_form_sent_seconds_ago, millisecondsAgo / ONE_SECOND) + } + + binding.subtext.text = context.getString(R.string.forms_ready_to_send, numberOfInstancesReadyToSend) + binding.banner.visibility = VISIBLE + } + } +} diff --git a/collect_app/src/main/res/layout/instance_uploader_list.xml b/collect_app/src/main/res/layout/instance_uploader_list.xml index 373d5694301..a0457a7640c 100644 --- a/collect_app/src/main/res/layout/instance_uploader_list.xml +++ b/collect_app/src/main/res/layout/instance_uploader_list.xml @@ -23,6 +23,14 @@ the License. android:layout_height="match_parent" android:layout_below="@id/appBarLayout"> + + + app:layout_constraintTop_toTopOf="@id/ready_to_send_banner" /> + + + + + + + + + \ No newline at end of file From e4eb74a369f49102061f40caffe33d06c415e22a Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 25 Jul 2023 13:41:15 +0200 Subject: [PATCH 03/19] Moved related classes to one package --- .../android/feature/external/InstanceUploadActionTest.kt | 3 +-- collect_app/src/main/AndroidManifest.xml | 8 ++++---- .../odk/collect/android/activities/AppListActivity.java | 2 +- .../android/injection/config/AppDependencyComponent.java | 4 ++-- .../org/odk/collect/android/mainmenu/MainMenuActivity.kt | 2 +- .../builders/FormsSubmissionNotificationBuilder.kt | 2 +- .../InstanceUploaderActivity.java | 3 ++- .../InstanceUploaderListActivity.java | 3 ++- .../{activities => readytosend}/ReadyToSendBanner.kt | 7 +++++-- .../src/main/res/layout/instance_uploader_list.xml | 2 +- .../odk/collect/android/mainmenu/MainMenuActivityTest.kt | 2 +- 11 files changed, 21 insertions(+), 17 deletions(-) rename collect_app/src/main/java/org/odk/collect/android/{activities => readytosend}/InstanceUploaderActivity.java (99%) rename collect_app/src/main/java/org/odk/collect/android/{activities => readytosend}/InstanceUploaderListActivity.java (99%) rename collect_app/src/main/java/org/odk/collect/android/{activities => readytosend}/ReadyToSendBanner.kt (93%) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt index 52a30971fca..5aee6c4fe2d 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt @@ -7,8 +7,7 @@ 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.activities.InstanceUploaderActivity +import org.odk.collect.android.readytosend.InstanceUploaderActivity import org.odk.collect.android.support.pages.OkDialog import org.odk.collect.android.support.rules.CollectTestRule import org.odk.collect.android.support.rules.TestRuleChain diff --git a/collect_app/src/main/AndroidManifest.xml b/collect_app/src/main/AndroidManifest.xml index 2c37a00fe2c..fcaa2f20b1a 100644 --- a/collect_app/src/main/AndroidManifest.xml +++ b/collect_app/src/main/AndroidManifest.xml @@ -136,9 +136,9 @@ the specific language governing permissions and limitations under the License. android:name=".gdrive.GoogleSheetsUploaderActivity" android:configChanges="orientation|screenSize" /> - + @@ -290,7 +290,7 @@ the specific language governing permissions and limitations under the License. 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 9eda01c1ba1..822acfc7c3e 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 @@ -53,7 +53,7 @@ public abstract class AppListActivity extends LocalizedActivity { - protected static final int LOADER_ID = 0x01; + public static final int LOADER_ID = 0x01; private static final String SELECTED_INSTANCES = "selectedInstances"; private static final String IS_SEARCH_BOX_SHOWN = "isSearchBoxShown"; private static final String SEARCH_TEXT = "searchText"; diff --git a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyComponent.java b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyComponent.java index 33c525739ba..75a105c0fc0 100644 --- a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyComponent.java +++ b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyComponent.java @@ -12,8 +12,8 @@ import org.odk.collect.android.activities.FormHierarchyActivity; import org.odk.collect.android.activities.FormMapActivity; import org.odk.collect.android.activities.InstanceChooserList; -import org.odk.collect.android.activities.InstanceUploaderActivity; -import org.odk.collect.android.activities.InstanceUploaderListActivity; +import org.odk.collect.android.readytosend.InstanceUploaderActivity; +import org.odk.collect.android.readytosend.InstanceUploaderListActivity; import org.odk.collect.android.adapters.InstanceUploaderAdapter; import org.odk.collect.android.application.Collect; import org.odk.collect.android.application.initialization.ApplicationInitializer; diff --git a/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt b/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt index b3eb3167163..bed1ff2f2bb 100644 --- a/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt +++ b/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt @@ -19,7 +19,6 @@ import org.odk.collect.android.activities.DeleteSavedFormActivity import org.odk.collect.android.activities.FirstLaunchActivity import org.odk.collect.android.activities.FormDownloadListActivity import org.odk.collect.android.activities.InstanceChooserList -import org.odk.collect.android.activities.InstanceUploaderListActivity import org.odk.collect.android.activities.WebViewActivity import org.odk.collect.android.application.MapboxClassInstanceCreator.createMapBoxInitializationFragment import org.odk.collect.android.application.MapboxClassInstanceCreator.isMapboxAvailable @@ -29,6 +28,7 @@ import org.odk.collect.android.formmanagement.FormFillingIntentFactory import org.odk.collect.android.injection.DaggerUtils import org.odk.collect.android.projects.ProjectIconView import org.odk.collect.android.projects.ProjectSettingsDialog +import org.odk.collect.android.readytosend.InstanceUploaderListActivity import org.odk.collect.android.utilities.ApplicationConstants import org.odk.collect.android.utilities.ThemeUtils import org.odk.collect.androidshared.ui.DialogFragmentUtils.showIfNotShowing diff --git a/collect_app/src/main/java/org/odk/collect/android/notifications/builders/FormsSubmissionNotificationBuilder.kt b/collect_app/src/main/java/org/odk/collect/android/notifications/builders/FormsSubmissionNotificationBuilder.kt index a2c3f7e5b88..02c04e7658d 100644 --- a/collect_app/src/main/java/org/odk/collect/android/notifications/builders/FormsSubmissionNotificationBuilder.kt +++ b/collect_app/src/main/java/org/odk/collect/android/notifications/builders/FormsSubmissionNotificationBuilder.kt @@ -6,9 +6,9 @@ import android.app.PendingIntent import android.content.Intent import androidx.core.app.NotificationCompat import org.odk.collect.android.R -import org.odk.collect.android.activities.InstanceUploaderListActivity import org.odk.collect.android.mainmenu.MainMenuActivity import org.odk.collect.android.notifications.NotificationManagerNotifier +import org.odk.collect.android.readytosend.InstanceUploaderListActivity import org.odk.collect.android.upload.FormUploadException import org.odk.collect.android.utilities.ApplicationConstants.RequestCodes import org.odk.collect.android.utilities.FormsUploadResultInterpreter diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceUploaderActivity.java b/collect_app/src/main/java/org/odk/collect/android/readytosend/InstanceUploaderActivity.java similarity index 99% rename from collect_app/src/main/java/org/odk/collect/android/activities/InstanceUploaderActivity.java rename to collect_app/src/main/java/org/odk/collect/android/readytosend/InstanceUploaderActivity.java index 2833b0625c3..2d1cedf8a26 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceUploaderActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/readytosend/InstanceUploaderActivity.java @@ -12,7 +12,7 @@ * the License. */ -package org.odk.collect.android.activities; +package org.odk.collect.android.readytosend; import static java.util.Arrays.stream; @@ -22,6 +22,7 @@ import android.net.Uri; import android.os.Bundle; +import org.odk.collect.android.activities.FormFillingActivity; import org.odk.collect.android.fragments.dialogs.SimpleDialog; import org.odk.collect.android.injection.DaggerUtils; import org.odk.collect.android.listeners.InstanceUploaderListener; 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/readytosend/InstanceUploaderListActivity.java similarity index 99% rename from collect_app/src/main/java/org/odk/collect/android/activities/InstanceUploaderListActivity.java rename to collect_app/src/main/java/org/odk/collect/android/readytosend/InstanceUploaderListActivity.java index 995596e630d..5fe71a277e7 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceUploaderListActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/readytosend/InstanceUploaderListActivity.java @@ -12,7 +12,7 @@ * the License. */ -package org.odk.collect.android.activities; +package org.odk.collect.android.readytosend; import static org.odk.collect.android.activities.AppListActivity.LOADER_ID; import static org.odk.collect.android.activities.AppListActivity.toggleButtonLabel; @@ -50,6 +50,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.odk.collect.android.R; +import org.odk.collect.android.activities.FormFillingActivity; import org.odk.collect.android.adapters.InstanceUploaderAdapter; import org.odk.collect.android.backgroundwork.FormUpdateAndInstanceSubmitScheduler; import org.odk.collect.android.backgroundwork.InstanceSubmitScheduler; diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/ReadyToSendBanner.kt b/collect_app/src/main/java/org/odk/collect/android/readytosend/ReadyToSendBanner.kt similarity index 93% rename from collect_app/src/main/java/org/odk/collect/android/activities/ReadyToSendBanner.kt rename to collect_app/src/main/java/org/odk/collect/android/readytosend/ReadyToSendBanner.kt index 662c5945763..30fc0ab7ff3 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/ReadyToSendBanner.kt +++ b/collect_app/src/main/java/org/odk/collect/android/readytosend/ReadyToSendBanner.kt @@ -1,4 +1,4 @@ -package org.odk.collect.android.activities +package org.odk.collect.android.readytosend import android.content.Context import android.util.AttributeSet @@ -21,7 +21,10 @@ class ReadyToSendBanner(context: Context, attrs: AttributeSet?) : ConstraintLayo fun init(instancesRepository: InstancesRepository) { val sentInstances: List = instancesRepository.getAllByStatus(Instance.STATUS_SUBMITTED) - val numberOfInstancesReadyToSend = instancesRepository.getCountByStatus(Instance.STATUS_COMPLETE, Instance.STATUS_SUBMISSION_FAILED) + val numberOfInstancesReadyToSend = instancesRepository.getCountByStatus( + Instance.STATUS_COMPLETE, + Instance.STATUS_SUBMISSION_FAILED + ) if (sentInstances.isNotEmpty() && numberOfInstancesReadyToSend > 0) { val lastSentInstance = sentInstances.maxBy { instance -> instance.lastStatusChangeDate } diff --git a/collect_app/src/main/res/layout/instance_uploader_list.xml b/collect_app/src/main/res/layout/instance_uploader_list.xml index a0457a7640c..6bd026a6585 100644 --- a/collect_app/src/main/res/layout/instance_uploader_list.xml +++ b/collect_app/src/main/res/layout/instance_uploader_list.xml @@ -23,7 +23,7 @@ the License. android:layout_height="match_parent" android:layout_below="@id/appBarLayout"> - Date: Tue, 25 Jul 2023 22:35:40 +0200 Subject: [PATCH 04/19] Added tests --- .../SendFinalizedFormTest.kt | 16 + .../InstanceUploaderListActivity.java | 2 +- .../android/readytosend/ReadyToSendBanner.kt | 13 +- .../readytosend/ReadyToSendBannerTest.kt | 315 ++++++++++++++++++ 4 files changed, 339 insertions(+), 7 deletions(-) create mode 100644 collect_app/src/test/java/org/odk/collect/android/readytosend/ReadyToSendBannerTest.kt 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 index e775be1062b..08959061c8b 100644 --- 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 @@ -146,4 +146,20 @@ class SendFinalizedFormTest { .clickSendSelected() .assertText("One Question Google - Success") } + + @Test + fun whenThereAreSentAndReadyToSendForms_displayTheBanner() { + 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()) + .assertText("1 form(s) ready to send") + } } diff --git a/collect_app/src/main/java/org/odk/collect/android/readytosend/InstanceUploaderListActivity.java b/collect_app/src/main/java/org/odk/collect/android/readytosend/InstanceUploaderListActivity.java index 5fe71a277e7..3783df03157 100644 --- a/collect_app/src/main/java/org/odk/collect/android/readytosend/InstanceUploaderListActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/readytosend/InstanceUploaderListActivity.java @@ -476,7 +476,7 @@ public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { findViewById(R.id.buttonholder).setVisibility(View.VISIBLE); } - binding.readyToSendBanner.init(instancesRepositoryProvider.get()); + binding.readyToSendBanner.init(instancesRepositoryProvider.get(), System::currentTimeMillis); } @Override diff --git a/collect_app/src/main/java/org/odk/collect/android/readytosend/ReadyToSendBanner.kt b/collect_app/src/main/java/org/odk/collect/android/readytosend/ReadyToSendBanner.kt index 30fc0ab7ff3..7f00cf0ad67 100644 --- a/collect_app/src/main/java/org/odk/collect/android/readytosend/ReadyToSendBanner.kt +++ b/collect_app/src/main/java/org/odk/collect/android/readytosend/ReadyToSendBanner.kt @@ -8,18 +8,19 @@ import org.odk.collect.android.databinding.ReadyToSendBannerBinding import org.odk.collect.forms.instances.Instance import org.odk.collect.forms.instances.InstancesRepository import org.odk.collect.strings.R +import java.util.function.Supplier -private const val ONE_SECOND = 1000 -private const val ONE_MINUTE = 60000 -private const val ONE_HOUR = 3600000 -private const val ONE_DAY = 86400000 +const val ONE_SECOND = 1000L +const val ONE_MINUTE = 60000L +const val ONE_HOUR = 3600000L +const val ONE_DAY = 86400000L class ReadyToSendBanner(context: Context, attrs: AttributeSet?) : ConstraintLayout(context, attrs) { constructor(context: Context) : this(context, null) private val binding = ReadyToSendBannerBinding.inflate(LayoutInflater.from(context), this, true) - fun init(instancesRepository: InstancesRepository) { + fun init(instancesRepository: InstancesRepository, clock: Supplier) { val sentInstances: List = instancesRepository.getAllByStatus(Instance.STATUS_SUBMITTED) val numberOfInstancesReadyToSend = instancesRepository.getCountByStatus( Instance.STATUS_COMPLETE, @@ -28,7 +29,7 @@ class ReadyToSendBanner(context: Context, attrs: AttributeSet?) : ConstraintLayo if (sentInstances.isNotEmpty() && numberOfInstancesReadyToSend > 0) { val lastSentInstance = sentInstances.maxBy { instance -> instance.lastStatusChangeDate } - val millisecondsAgo = System.currentTimeMillis() - lastSentInstance.lastStatusChangeDate + val millisecondsAgo = clock.get() - lastSentInstance.lastStatusChangeDate if (millisecondsAgo >= ONE_DAY) { binding.title.text = context.getString(R.string.last_form_sent_days_ago, millisecondsAgo / ONE_DAY) } else if (millisecondsAgo >= ONE_HOUR) { diff --git a/collect_app/src/test/java/org/odk/collect/android/readytosend/ReadyToSendBannerTest.kt b/collect_app/src/test/java/org/odk/collect/android/readytosend/ReadyToSendBannerTest.kt new file mode 100644 index 00000000000..3b7d4f29ef1 --- /dev/null +++ b/collect_app/src/test/java/org/odk/collect/android/readytosend/ReadyToSendBannerTest.kt @@ -0,0 +1,315 @@ +package org.odk.collect.android.readytosend + +import android.app.Application +import android.view.View +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.matcher.ViewMatchers.assertThat +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.android.material.textview.MaterialTextView +import org.hamcrest.Matchers.equalTo +import org.junit.Test +import org.junit.runner.RunWith +import org.odk.collect.android.R +import org.odk.collect.forms.instances.Instance +import org.odk.collect.formstest.InMemInstancesRepository + +@RunWith(AndroidJUnit4::class) +class ReadyToSendBannerTest { + private val context: Application = + ApplicationProvider.getApplicationContext().also { + it.setTheme(R.style.Theme_Collect) + } + + private val instancesRepository = InMemInstancesRepository().also { + it.save( + Instance.Builder() + .formId("1") + .status(Instance.STATUS_INCOMPLETE) + .build() + ) + } + + @Test + fun `if there are no sent instances do not display the banner`() { + val view = ReadyToSendBanner(context).also { + it.init(instancesRepository) { 0 } + } + + assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.GONE)) + } + + @Test + fun `if there are no instances ready to send do not display the banner`() { + val view = ReadyToSendBanner(context).also { + it.init(instancesRepository) { 0 } + } + + assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.GONE)) + } + + @Test + fun `if there are sent instances but no instances ready to send do not display the banner`() { + instancesRepository.save( + Instance.Builder() + .formId("2") + .status(Instance.STATUS_SUBMITTED) + .build() + ) + + val view = ReadyToSendBanner(context).also { + it.init(instancesRepository) { 0 } + } + + assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.GONE)) + } + + @Test + fun `if there are instances ready to send (complete) but no sent instances do not display the banner`() { + instancesRepository.save( + Instance.Builder() + .formId("2") + .status(Instance.STATUS_COMPLETE) + .build() + ) + + val view = ReadyToSendBanner(context).also { + it.init(instancesRepository) { 0 } + } + + assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.GONE)) + } + + @Test + fun `if there are instances ready to send (submission failed) but no sent instances do not display the banner`() { + instancesRepository.save( + Instance.Builder() + .formId("2") + .status(Instance.STATUS_SUBMISSION_FAILED) + .build() + ) + + val view = ReadyToSendBanner(context).also { + it.init(instancesRepository) { 0 } + } + + assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.GONE)) + } + + @Test + fun `if there are both sent and ready to send instances display the banner`() { + instancesRepository.save( + Instance.Builder() + .formId("2") + .status(Instance.STATUS_COMPLETE) + .build() + ) + + instancesRepository.save( + Instance.Builder() + .formId("3") + .status(Instance.STATUS_SUBMITTED) + .build() + ) + + val view = ReadyToSendBanner(context).also { + it.init(instancesRepository) { 0 } + } + + assertThat( + view.findViewById(R.id.banner).visibility, + equalTo(View.VISIBLE) + ) + } + + @Test + fun `the banner should display how long ago in seconds the last instance was sent if it was less than a minute ago`() { + instancesRepository.save( + Instance.Builder() + .formId("2") + .status(Instance.STATUS_COMPLETE) + .build() + ) + + instancesRepository.save( + Instance.Builder() + .formId("3") + .status(Instance.STATUS_SUBMITTED) + .lastStatusChangeDate(0) + .build() + ) + + val view = ReadyToSendBanner(context).also { + it.init(instancesRepository) { ONE_SECOND * 5 } + } + + assertThat( + view.findViewById(R.id.title).text, + equalTo("Last form sent: 5 second(s) ago") + ) + } + + @Test + fun `the banner should display how long ago in minutes the last instance was sent if it was less than an hour ago`() { + instancesRepository.save( + Instance.Builder() + .formId("2") + .status(Instance.STATUS_COMPLETE) + .build() + ) + + instancesRepository.save( + Instance.Builder() + .formId("3") + .status(Instance.STATUS_SUBMITTED) + .lastStatusChangeDate(0) + .build() + ) + + val view = ReadyToSendBanner(context).also { + it.init(instancesRepository) { ONE_MINUTE * 10 } + } + + assertThat( + view.findViewById(R.id.title).text, + equalTo("Last form sent: 10 minute(s) ago") + ) + } + + @Test + fun `the banner should display how long ago in hours the last instance was sent if it was less than a day ago`() { + instancesRepository.save( + Instance.Builder() + .formId("2") + .status(Instance.STATUS_COMPLETE) + .build() + ) + + instancesRepository.save( + Instance.Builder() + .formId("3") + .status(Instance.STATUS_SUBMITTED) + .lastStatusChangeDate(0) + .build() + ) + + val view = ReadyToSendBanner(context).also { + it.init(instancesRepository) { ONE_HOUR * 2 } + } + + assertThat( + view.findViewById(R.id.title).text, + equalTo("Last form sent: 2 hour(s) ago") + ) + } + + @Test + fun `the banner should display how long ago in days the last instance was sent if it was more than 24 hours ago`() { + instancesRepository.save( + Instance.Builder() + .formId("2") + .status(Instance.STATUS_COMPLETE) + .build() + ) + + instancesRepository.save( + Instance.Builder() + .formId("3") + .status(Instance.STATUS_SUBMITTED) + .lastStatusChangeDate(0) + .build() + ) + + val view = ReadyToSendBanner(context).also { + it.init(instancesRepository) { ONE_DAY * 34 } + } + + assertThat( + view.findViewById(R.id.title).text, + equalTo("Last form sent: 34 day(s) ago") + ) + } + + @Test + fun `the banner should display how long ago the last instance was sent if there are multiple sent instances`() { + instancesRepository.save( + Instance.Builder() + .formId("2") + .status(Instance.STATUS_COMPLETE) + .build() + ) + + instancesRepository.save( + Instance.Builder() + .formId("3") + .status(Instance.STATUS_SUBMITTED) + .lastStatusChangeDate(0) + .build() + ) + + instancesRepository.save( + Instance.Builder() + .formId("4") + .status(Instance.STATUS_SUBMITTED) + .lastStatusChangeDate(ONE_SECOND * 5) + .build() + ) + + instancesRepository.save( + Instance.Builder() + .formId("5") + .status(Instance.STATUS_SUBMITTED) + .lastStatusChangeDate(ONE_SECOND * 4) + .build() + ) + + val view = ReadyToSendBanner(context).also { + it.init(instancesRepository) { ONE_SECOND * 10 } + } + + assertThat( + view.findViewById(R.id.title).text, + equalTo("Last form sent: 5 second(s) ago") + ) + } + + @Test + fun `the banner should display the number of instances ready to send`() { + instancesRepository.save( + Instance.Builder() + .formId("2") + .status(Instance.STATUS_COMPLETE) + .build() + ) + + instancesRepository.save( + Instance.Builder() + .formId("3") + .status(Instance.STATUS_SUBMISSION_FAILED) + .build() + ) + + instancesRepository.save( + Instance.Builder() + .formId("4") + .status(Instance.STATUS_COMPLETE) + .build() + ) + + instancesRepository.save( + Instance.Builder() + .formId("5") + .status(Instance.STATUS_SUBMITTED) + .build() + ) + + val view = ReadyToSendBanner(context).also { + it.init(instancesRepository) { 0 } + } + + assertThat( + view.findViewById(R.id.subtext).text, + equalTo("3 form(s) ready to send") + ) + } +} From 5858729ffbddf124bbe71c9a3894dbd238d5b18a Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 9 Aug 2023 15:50:39 +0200 Subject: [PATCH 05/19] Improved strings --- .../android/readytosend/ReadyToSendBanner.kt | 14 +++++++---- .../readytosend/ReadyToSendBannerTest.kt | 14 +++++------ strings/src/main/res/values/strings.xml | 25 +++++++++++++++---- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/readytosend/ReadyToSendBanner.kt b/collect_app/src/main/java/org/odk/collect/android/readytosend/ReadyToSendBanner.kt index 7f00cf0ad67..7de6c44c24f 100644 --- a/collect_app/src/main/java/org/odk/collect/android/readytosend/ReadyToSendBanner.kt +++ b/collect_app/src/main/java/org/odk/collect/android/readytosend/ReadyToSendBanner.kt @@ -31,16 +31,20 @@ class ReadyToSendBanner(context: Context, attrs: AttributeSet?) : ConstraintLayo val lastSentInstance = sentInstances.maxBy { instance -> instance.lastStatusChangeDate } val millisecondsAgo = clock.get() - lastSentInstance.lastStatusChangeDate if (millisecondsAgo >= ONE_DAY) { - binding.title.text = context.getString(R.string.last_form_sent_days_ago, millisecondsAgo / ONE_DAY) + val days: Int = (millisecondsAgo / ONE_DAY).toInt() + binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_days_ago, days, days) } else if (millisecondsAgo >= ONE_HOUR) { - binding.title.text = context.getString(R.string.last_form_sent_hours_ago, millisecondsAgo / ONE_HOUR) + val hours: Int = (millisecondsAgo / ONE_HOUR).toInt() + binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_hours_ago, hours, hours) } else if (millisecondsAgo >= ONE_MINUTE) { - binding.title.text = context.getString(R.string.last_form_sent_minutes_ago, millisecondsAgo / ONE_MINUTE) + val minutes: Int = (millisecondsAgo / ONE_MINUTE).toInt() + binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_minutes_ago, minutes, minutes) } else { - binding.title.text = context.getString(R.string.last_form_sent_seconds_ago, millisecondsAgo / ONE_SECOND) + val seconds: Int = (millisecondsAgo / ONE_SECOND).toInt() + binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_seconds_ago, seconds, seconds) } - binding.subtext.text = context.getString(R.string.forms_ready_to_send, numberOfInstancesReadyToSend) + binding.subtext.text = context.resources.getQuantityString(R.plurals.forms_ready_to_send, numberOfInstancesReadyToSend, numberOfInstancesReadyToSend) binding.banner.visibility = VISIBLE } } diff --git a/collect_app/src/test/java/org/odk/collect/android/readytosend/ReadyToSendBannerTest.kt b/collect_app/src/test/java/org/odk/collect/android/readytosend/ReadyToSendBannerTest.kt index 3b7d4f29ef1..f975decb85c 100644 --- a/collect_app/src/test/java/org/odk/collect/android/readytosend/ReadyToSendBannerTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/readytosend/ReadyToSendBannerTest.kt @@ -145,7 +145,7 @@ class ReadyToSendBannerTest { assertThat( view.findViewById(R.id.title).text, - equalTo("Last form sent: 5 second(s) ago") + equalTo("Last form sent: 5 seconds ago") ) } @@ -167,12 +167,12 @@ class ReadyToSendBannerTest { ) val view = ReadyToSendBanner(context).also { - it.init(instancesRepository) { ONE_MINUTE * 10 } + it.init(instancesRepository) { ONE_MINUTE } } assertThat( view.findViewById(R.id.title).text, - equalTo("Last form sent: 10 minute(s) ago") + equalTo("Last form sent: 1 minute ago") ) } @@ -199,7 +199,7 @@ class ReadyToSendBannerTest { assertThat( view.findViewById(R.id.title).text, - equalTo("Last form sent: 2 hour(s) ago") + equalTo("Last form sent: 2 hours ago") ) } @@ -226,7 +226,7 @@ class ReadyToSendBannerTest { assertThat( view.findViewById(R.id.title).text, - equalTo("Last form sent: 34 day(s) ago") + equalTo("Last form sent: 34 days ago") ) } @@ -269,7 +269,7 @@ class ReadyToSendBannerTest { assertThat( view.findViewById(R.id.title).text, - equalTo("Last form sent: 5 second(s) ago") + equalTo("Last form sent: 5 seconds ago") ) } @@ -309,7 +309,7 @@ class ReadyToSendBannerTest { assertThat( view.findViewById(R.id.subtext).text, - equalTo("3 form(s) ready to send") + equalTo("3 forms ready to send") ) } } diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index 5176ce804fe..e1cdbad9c7c 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -1212,11 +1212,26 @@ # Forms ready to send banner ############################################## --> - Last form sent: %d second(s) ago - Last form sent: %d minute(s) ago - Last form sent: %d hour(s) ago - Last form sent: %d day(s) ago - %d form(s) ready to send + + Last form sent: %d second ago + Last form sent: %d seconds ago + + + Last form sent: %d minute ago + Last form sent: %d minutes ago + + + Last form sent: %d hour ago + Last form sent: %d hours ago + + + Last form sent: %d day ago + Last form sent: %d days ago + + + %d form ready to send + %d forms ready to send + In later releases, you will not be able to edit finalized forms. Save forms as draft to edit them later.\n\nYou can check for errors in a draft form by tapping the three dots (⋮) and then Check for errors. From 656ba4eeb7522464d0dc101cfa95f25c5ff3b8d5 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 10 Aug 2023 10:17:00 +0200 Subject: [PATCH 06/19] Fixed margins --- collect_app/src/main/res/layout/ready_to_send_banner.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/collect_app/src/main/res/layout/ready_to_send_banner.xml b/collect_app/src/main/res/layout/ready_to_send_banner.xml index 89ba2c4bcd6..eb6101b71b9 100644 --- a/collect_app/src/main/res/layout/ready_to_send_banner.xml +++ b/collect_app/src/main/res/layout/ready_to_send_banner.xml @@ -15,7 +15,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/margin_large" - android:src="@drawable/ic_send_24" + app:srcCompat="@drawable/ic_send_24" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:tint="?colorPrimary"/> @@ -24,7 +24,8 @@ android:id="@+id/title" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginHorizontal="@dimen/margin_standard" + android:layout_marginStart="@dimen/margin_large" + android:layout_marginEnd="@dimen/margin_standard" android:textAppearance="@style/TextAppearance.Material3.TitleMedium" android:textColor="?colorPrimary" app:layout_constraintEnd_toEndOf="parent" From b29e21e34dfe0c756a14095eeb751ead8f47fe31 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 29 Aug 2023 10:55:30 +0200 Subject: [PATCH 07/19] Fixed whenThereAreSentAndReadyToSendForms_displayTheBanner test --- .../SendFinalizedFormTest.kt | 2 +- .../odk/collect/android/support/pages/Page.kt | 10 ++++++++++ .../localization/LocalizedApplication.kt | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) 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 index 08959061c8b..3ab2f8eb85a 100644 --- 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 @@ -160,6 +160,6 @@ class SendFinalizedFormTest { .selectForm(0) .clickSendSelected() .clickOK(SendFinalizedFormPage()) - .assertText("1 form(s) ready to send") + .assertQuantityText(org.odk.collect.strings.R.plurals.forms_ready_to_send, 1, 1) } } diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt index 5cb1ac89e4a..e25010f0962 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt @@ -43,6 +43,7 @@ import org.odk.collect.android.support.WaitFor.waitFor import org.odk.collect.android.support.actions.RotateAction import org.odk.collect.android.support.matchers.CustomMatchers.withIndex import org.odk.collect.androidshared.ui.ToastUtils.popRecordedToasts +import org.odk.collect.strings.localization.getLocalizedQuantityString import org.odk.collect.strings.localization.getLocalizedString import org.odk.collect.testshared.EspressoHelpers import org.odk.collect.testshared.RecyclerViewMatcher @@ -100,6 +101,11 @@ abstract class Page> { return this as T } + fun assertQuantityText(stringID: Int, quantity: Int, vararg formatArgs: Any): T { + assertText(getTranslatedQuantityString(stringID, quantity, *formatArgs)) + return this as T + } + fun assertText(text: String): T { EspressoHelpers.assertText(text) return this as T @@ -246,6 +252,10 @@ abstract class Page> { return ApplicationProvider.getApplicationContext().getLocalizedString(id!!, *formatArgs) } + fun getTranslatedQuantityString(id: Int?, quantity: Int, vararg formatArgs: Any): String { + return ApplicationProvider.getApplicationContext().getLocalizedQuantityString(id!!, quantity, *formatArgs) + } + fun clickOnAreaWithIndex(clazz: String?, index: Int): T { onView(withIndex(withClassName(endsWith(clazz)), index)).perform(click()) return this as T diff --git a/strings/src/main/java/org/odk/collect/strings/localization/LocalizedApplication.kt b/strings/src/main/java/org/odk/collect/strings/localization/LocalizedApplication.kt index bb9514adf60..ce57c768e24 100644 --- a/strings/src/main/java/org/odk/collect/strings/localization/LocalizedApplication.kt +++ b/strings/src/main/java/org/odk/collect/strings/localization/LocalizedApplication.kt @@ -32,6 +32,24 @@ fun Context.getLocalizedResources(locale: Locale): Resources { return createConfigurationContext(newConfig).resources } +fun Context.getLocalizedQuantityString(stringId: Int, quantity: Int, vararg formatArgs: Any): String { + val locale = when (applicationContext) { + is LocalizedApplication -> (applicationContext as LocalizedApplication).locale + + // Don't explode if the application doesn't implement LocalizedApplication. Useful + // when testing modules in isolation + else -> if (Build.VERSION.SDK_INT >= 24) resources.configuration.locales[0] else resources.configuration.locale + } + + val newConfig = Configuration(resources.configuration).apply { + setLocale(locale) + } + + return createConfigurationContext(newConfig) + .resources + .getQuantityString(stringId, quantity, *formatArgs) +} + fun Context.isLTR(): Boolean { return resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR } From fd65ab5ae16b3c2b8455466529767f9420c8f591 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 29 Aug 2023 11:37:53 +0200 Subject: [PATCH 08/19] Added missing text appearance --- collect_app/src/main/res/layout/ready_to_send_banner.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/collect_app/src/main/res/layout/ready_to_send_banner.xml b/collect_app/src/main/res/layout/ready_to_send_banner.xml index eb6101b71b9..68089c36efd 100644 --- a/collect_app/src/main/res/layout/ready_to_send_banner.xml +++ b/collect_app/src/main/res/layout/ready_to_send_banner.xml @@ -37,6 +37,7 @@ android:id="@+id/subtext" android:layout_width="0dp" android:layout_height="wrap_content" + android:textAppearance="?textAppearanceBodyMedium" app:layout_constraintEnd_toEndOf="@id/title" app:layout_constraintStart_toEndOf="@id/title" app:layout_constraintStart_toStartOf="@id/title" From 7bf40a60455b3f3fc86590c4e0c320da55198520 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 29 Aug 2023 11:55:31 +0200 Subject: [PATCH 09/19] Renamed 'readytosend' package to 'send' and moved it to 'instancemanagemen' --- .../android/feature/external/InstanceUploadActionTest.kt | 2 +- collect_app/src/main/AndroidManifest.xml | 8 ++++---- .../android/injection/config/AppDependencyComponent.java | 4 ++-- .../send}/InstanceUploaderActivity.java | 2 +- .../send}/InstanceUploaderListActivity.java | 2 +- .../send}/ReadyToSendBanner.kt | 2 +- .../org/odk/collect/android/mainmenu/MainMenuActivity.kt | 2 +- .../builders/FormsSubmissionNotificationBuilder.kt | 2 +- .../src/main/res/layout/instance_uploader_list.xml | 2 +- .../send}/ReadyToSendBannerTest.kt | 2 +- .../odk/collect/android/mainmenu/MainMenuActivityTest.kt | 2 +- 11 files changed, 15 insertions(+), 15 deletions(-) rename collect_app/src/main/java/org/odk/collect/android/{readytosend => instancemanagement/send}/InstanceUploaderActivity.java (99%) rename collect_app/src/main/java/org/odk/collect/android/{readytosend => instancemanagement/send}/InstanceUploaderListActivity.java (99%) rename collect_app/src/main/java/org/odk/collect/android/{readytosend => instancemanagement/send}/ReadyToSendBanner.kt (97%) rename collect_app/src/test/java/org/odk/collect/android/{readytosend => instancemanagement/send}/ReadyToSendBannerTest.kt (99%) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt index 5aee6c4fe2d..6bbf4d34f9a 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt @@ -7,7 +7,7 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain import org.junit.runner.RunWith -import org.odk.collect.android.readytosend.InstanceUploaderActivity +import org.odk.collect.android.instancemanagement.send.InstanceUploaderActivity import org.odk.collect.android.support.pages.OkDialog import org.odk.collect.android.support.rules.CollectTestRule import org.odk.collect.android.support.rules.TestRuleChain diff --git a/collect_app/src/main/AndroidManifest.xml b/collect_app/src/main/AndroidManifest.xml index fcaa2f20b1a..cbd306c5b46 100644 --- a/collect_app/src/main/AndroidManifest.xml +++ b/collect_app/src/main/AndroidManifest.xml @@ -136,9 +136,9 @@ the specific language governing permissions and limitations under the License. android:name=".gdrive.GoogleSheetsUploaderActivity" android:configChanges="orientation|screenSize" /> - + @@ -290,7 +290,7 @@ the specific language governing permissions and limitations under the License. diff --git a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyComponent.java b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyComponent.java index 75a105c0fc0..1e3a344ea6e 100644 --- a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyComponent.java +++ b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyComponent.java @@ -12,8 +12,8 @@ import org.odk.collect.android.activities.FormHierarchyActivity; import org.odk.collect.android.activities.FormMapActivity; import org.odk.collect.android.activities.InstanceChooserList; -import org.odk.collect.android.readytosend.InstanceUploaderActivity; -import org.odk.collect.android.readytosend.InstanceUploaderListActivity; +import org.odk.collect.android.instancemanagement.send.InstanceUploaderActivity; +import org.odk.collect.android.instancemanagement.send.InstanceUploaderListActivity; import org.odk.collect.android.adapters.InstanceUploaderAdapter; import org.odk.collect.android.application.Collect; import org.odk.collect.android.application.initialization.ApplicationInitializer; diff --git a/collect_app/src/main/java/org/odk/collect/android/readytosend/InstanceUploaderActivity.java b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderActivity.java similarity index 99% rename from collect_app/src/main/java/org/odk/collect/android/readytosend/InstanceUploaderActivity.java rename to collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderActivity.java index 2d1cedf8a26..22ec10a87c4 100644 --- a/collect_app/src/main/java/org/odk/collect/android/readytosend/InstanceUploaderActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderActivity.java @@ -12,7 +12,7 @@ * the License. */ -package org.odk.collect.android.readytosend; +package org.odk.collect.android.instancemanagement.send; import static java.util.Arrays.stream; diff --git a/collect_app/src/main/java/org/odk/collect/android/readytosend/InstanceUploaderListActivity.java b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java similarity index 99% rename from collect_app/src/main/java/org/odk/collect/android/readytosend/InstanceUploaderListActivity.java rename to collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java index 3783df03157..e027e28e391 100644 --- a/collect_app/src/main/java/org/odk/collect/android/readytosend/InstanceUploaderListActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java @@ -12,7 +12,7 @@ * the License. */ -package org.odk.collect.android.readytosend; +package org.odk.collect.android.instancemanagement.send; import static org.odk.collect.android.activities.AppListActivity.LOADER_ID; import static org.odk.collect.android.activities.AppListActivity.toggleButtonLabel; diff --git a/collect_app/src/main/java/org/odk/collect/android/readytosend/ReadyToSendBanner.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt similarity index 97% rename from collect_app/src/main/java/org/odk/collect/android/readytosend/ReadyToSendBanner.kt rename to collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt index 7de6c44c24f..3fda5e54f12 100644 --- a/collect_app/src/main/java/org/odk/collect/android/readytosend/ReadyToSendBanner.kt +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt @@ -1,4 +1,4 @@ -package org.odk.collect.android.readytosend +package org.odk.collect.android.instancemanagement.send import android.content.Context import android.util.AttributeSet diff --git a/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt b/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt index bed1ff2f2bb..20b5caee989 100644 --- a/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt +++ b/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt @@ -26,9 +26,9 @@ import org.odk.collect.android.databinding.MainMenuBinding import org.odk.collect.android.formlists.blankformlist.BlankFormListActivity import org.odk.collect.android.formmanagement.FormFillingIntentFactory import org.odk.collect.android.injection.DaggerUtils +import org.odk.collect.android.instancemanagement.send.InstanceUploaderListActivity import org.odk.collect.android.projects.ProjectIconView import org.odk.collect.android.projects.ProjectSettingsDialog -import org.odk.collect.android.readytosend.InstanceUploaderListActivity import org.odk.collect.android.utilities.ApplicationConstants import org.odk.collect.android.utilities.ThemeUtils import org.odk.collect.androidshared.ui.DialogFragmentUtils.showIfNotShowing diff --git a/collect_app/src/main/java/org/odk/collect/android/notifications/builders/FormsSubmissionNotificationBuilder.kt b/collect_app/src/main/java/org/odk/collect/android/notifications/builders/FormsSubmissionNotificationBuilder.kt index 02c04e7658d..339a3882195 100644 --- a/collect_app/src/main/java/org/odk/collect/android/notifications/builders/FormsSubmissionNotificationBuilder.kt +++ b/collect_app/src/main/java/org/odk/collect/android/notifications/builders/FormsSubmissionNotificationBuilder.kt @@ -6,9 +6,9 @@ import android.app.PendingIntent import android.content.Intent import androidx.core.app.NotificationCompat import org.odk.collect.android.R +import org.odk.collect.android.instancemanagement.send.InstanceUploaderListActivity import org.odk.collect.android.mainmenu.MainMenuActivity import org.odk.collect.android.notifications.NotificationManagerNotifier -import org.odk.collect.android.readytosend.InstanceUploaderListActivity import org.odk.collect.android.upload.FormUploadException import org.odk.collect.android.utilities.ApplicationConstants.RequestCodes import org.odk.collect.android.utilities.FormsUploadResultInterpreter diff --git a/collect_app/src/main/res/layout/instance_uploader_list.xml b/collect_app/src/main/res/layout/instance_uploader_list.xml index 6bd026a6585..b991e68e8e0 100644 --- a/collect_app/src/main/res/layout/instance_uploader_list.xml +++ b/collect_app/src/main/res/layout/instance_uploader_list.xml @@ -23,7 +23,7 @@ the License. android:layout_height="match_parent" android:layout_below="@id/appBarLayout"> - Date: Tue, 29 Aug 2023 12:48:51 +0200 Subject: [PATCH 10/19] Reworked ReadyToSendBanner to use viewmodel --- .../SendFinalizedFormTest.kt | 2 +- .../injection/config/AppDependencyModule.java | 6 ++ .../send/InstanceUploaderListActivity.java | 7 ++- .../send/ReadyToSendBanner.kt | 48 +++++++-------- .../send/ReadyToSendViewModel.kt | 58 +++++++++++++++++++ .../send/ReadyToSendBannerTest.kt | 53 +++++++++++++---- 6 files changed, 131 insertions(+), 43 deletions(-) create mode 100644 collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModel.kt 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 index 3ab2f8eb85a..3304816b0c3 100644 --- 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 @@ -150,7 +150,7 @@ class SendFinalizedFormTest { @Test fun whenThereAreSentAndReadyToSendForms_displayTheBanner() { rule.withProject(testDependencies.server.url) - .copyForm("one-question.xml", projectName = testDependencies.server.hostName) + .copyForm("one-question.xml", testDependencies.server.hostName) .startBlankForm("One Question") .fillOutAndFinalize(QuestionAndAnswer("what is your age", "123")) .startBlankForm("One Question") 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 6ac96235dc4..68922c6ddd6 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 @@ -65,6 +65,7 @@ import org.odk.collect.android.instancemanagement.autosend.AutoSendSettingsProvider; import org.odk.collect.android.instancemanagement.autosend.InstanceAutoSendFetcher; import org.odk.collect.android.instancemanagement.autosend.InstanceAutoSender; +import org.odk.collect.android.instancemanagement.send.ReadyToSendViewModel; import org.odk.collect.android.itemsets.FastExternalItemsetsRepository; import org.odk.collect.android.mainmenu.MainMenuViewModelFactory; import org.odk.collect.android.notifications.NotificationManagerNotifier; @@ -485,6 +486,11 @@ public ProjectPreferencesViewModel.Factory providesProjectPreferencesViewModel(A return new ProjectPreferencesViewModel.Factory(adminPasswordProvider); } + @Provides + public ReadyToSendViewModel.Factory providesReadyToSendViewModel(InstancesRepositoryProvider instancesRepositoryProvider, Scheduler scheduler) { + return new ReadyToSendViewModel.Factory(instancesRepositoryProvider.get(), scheduler, System::currentTimeMillis); + } + @Provides public MainMenuViewModelFactory providesMainMenuViewModelFactory(VersionInformation versionInformation, Application application, SettingsProvider settingsProvider, InstancesAppState instancesAppState, diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java index e027e28e391..4aa2b9ecbee 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java @@ -65,7 +65,6 @@ 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.InstancesRepositoryProvider; import org.odk.collect.android.utilities.PlayServicesChecker; import org.odk.collect.androidshared.network.NetworkStateProvider; import org.odk.collect.androidshared.ui.MultiSelectViewModel; @@ -122,7 +121,7 @@ public class InstanceUploaderListActivity extends LocalizedActivity implements SettingsProvider settingsProvider; @Inject - InstancesRepositoryProvider instancesRepositoryProvider; + ReadyToSendViewModel.Factory factory; private ListView listView; private InstanceUploaderAdapter listAdapter; @@ -132,6 +131,7 @@ public class InstanceUploaderListActivity extends LocalizedActivity implements private String filterText; private MultiSelectViewModel multiSelectViewModel; + private ReadyToSendViewModel readyToSendViewModel; private boolean allSelected; private boolean isSearchBoxShown; @@ -158,6 +158,7 @@ public void onCreate(Bundle savedInstanceState) { listAdapter.setSelected(ids); }); + readyToSendViewModel = new ViewModelProvider(this, factory).get(ReadyToSendViewModel.class); // set title setTitle(getString(org.odk.collect.strings.R.string.send_data)); @@ -476,7 +477,7 @@ public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { findViewById(R.id.buttonholder).setVisibility(View.VISIBLE); } - binding.readyToSendBanner.init(instancesRepositoryProvider.get(), System::currentTimeMillis); + binding.readyToSendBanner.init(readyToSendViewModel, this); } @Override diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt index 3fda5e54f12..3e67ee238b3 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt @@ -4,11 +4,9 @@ import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import androidx.constraintlayout.widget.ConstraintLayout +import androidx.lifecycle.LifecycleOwner import org.odk.collect.android.databinding.ReadyToSendBannerBinding -import org.odk.collect.forms.instances.Instance -import org.odk.collect.forms.instances.InstancesRepository import org.odk.collect.strings.R -import java.util.function.Supplier const val ONE_SECOND = 1000L const val ONE_MINUTE = 60000L @@ -20,32 +18,28 @@ class ReadyToSendBanner(context: Context, attrs: AttributeSet?) : ConstraintLayo private val binding = ReadyToSendBannerBinding.inflate(LayoutInflater.from(context), this, true) - fun init(instancesRepository: InstancesRepository, clock: Supplier) { - val sentInstances: List = instancesRepository.getAllByStatus(Instance.STATUS_SUBMITTED) - val numberOfInstancesReadyToSend = instancesRepository.getCountByStatus( - Instance.STATUS_COMPLETE, - Instance.STATUS_SUBMISSION_FAILED - ) + fun init(viewModel: ReadyToSendViewModel, owner: LifecycleOwner) { + viewModel.data.observe(owner) { + if (it.numberOfSentInstances > 0 && it.numberOfInstancesReadyToSend > 0) { + if (it.lastInstanceSentTimeMillis >= ONE_DAY) { + val days: Int = (it.lastInstanceSentTimeMillis / ONE_DAY).toInt() + binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_days_ago, days, days) + } else if (it.lastInstanceSentTimeMillis >= ONE_HOUR) { + val hours: Int = (it.lastInstanceSentTimeMillis / ONE_HOUR).toInt() + binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_hours_ago, hours, hours) + } else if (it.lastInstanceSentTimeMillis >= ONE_MINUTE) { + val minutes: Int = (it.lastInstanceSentTimeMillis / ONE_MINUTE).toInt() + binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_minutes_ago, minutes, minutes) + } else { + val seconds: Int = (it.lastInstanceSentTimeMillis / ONE_SECOND).toInt() + binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_seconds_ago, seconds, seconds) + } - if (sentInstances.isNotEmpty() && numberOfInstancesReadyToSend > 0) { - val lastSentInstance = sentInstances.maxBy { instance -> instance.lastStatusChangeDate } - val millisecondsAgo = clock.get() - lastSentInstance.lastStatusChangeDate - if (millisecondsAgo >= ONE_DAY) { - val days: Int = (millisecondsAgo / ONE_DAY).toInt() - binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_days_ago, days, days) - } else if (millisecondsAgo >= ONE_HOUR) { - val hours: Int = (millisecondsAgo / ONE_HOUR).toInt() - binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_hours_ago, hours, hours) - } else if (millisecondsAgo >= ONE_MINUTE) { - val minutes: Int = (millisecondsAgo / ONE_MINUTE).toInt() - binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_minutes_ago, minutes, minutes) - } else { - val seconds: Int = (millisecondsAgo / ONE_SECOND).toInt() - binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_seconds_ago, seconds, seconds) + binding.subtext.text = context.resources.getQuantityString(R.plurals.forms_ready_to_send, it.numberOfInstancesReadyToSend, it.numberOfInstancesReadyToSend) + binding.banner.visibility = VISIBLE } - - binding.subtext.text = context.resources.getQuantityString(R.plurals.forms_ready_to_send, numberOfInstancesReadyToSend, numberOfInstancesReadyToSend) - binding.banner.visibility = VISIBLE } + + viewModel.init() } } diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModel.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModel.kt new file mode 100644 index 00000000000..8fc19c53e1e --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModel.kt @@ -0,0 +1,58 @@ +package org.odk.collect.android.instancemanagement.send + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.odk.collect.async.Scheduler +import org.odk.collect.forms.instances.Instance +import org.odk.collect.forms.instances.InstancesRepository +import java.util.function.Supplier + +class ReadyToSendViewModel( + private val instancesRepository: InstancesRepository, + private val scheduler: Scheduler, + private val clock: Supplier +) : ViewModel() { + private val _data = MutableLiveData() + val data: LiveData = _data + + fun init() { + scheduler.immediate( + background = { + val sentInstances = instancesRepository.getAllByStatus(Instance.STATUS_SUBMITTED) + val numberOfSentInstances = sentInstances.size + val numberOfInstancesReadyToSend = instancesRepository.getCountByStatus( + Instance.STATUS_COMPLETE, + Instance.STATUS_SUBMISSION_FAILED + ) + val lastInstanceSentTimeMillis = if (sentInstances.isNotEmpty()) { + val lastSentInstance = sentInstances.maxBy { instance -> instance.lastStatusChangeDate } + clock.get() - lastSentInstance.lastStatusChangeDate + } else { + 0 + } + Data(numberOfInstancesReadyToSend, numberOfSentInstances, lastInstanceSentTimeMillis) + }, + foreground = { + _data.value = it + } + ) + } + + open class Factory( + private val instancesRepository: InstancesRepository, + private val scheduler: Scheduler, + private val clock: Supplier + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return ReadyToSendViewModel(instancesRepository, scheduler, clock) as T + } + } + + data class Data( + val numberOfInstancesReadyToSend: Int, + val numberOfSentInstances: Int, + val lastInstanceSentTimeMillis: Long + ) +} diff --git a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt index 730604a1f37..3c0e3e2d357 100644 --- a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt @@ -10,9 +10,14 @@ import com.google.android.material.textview.MaterialTextView import org.hamcrest.Matchers.equalTo import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.kotlin.whenever import org.odk.collect.android.R +import org.odk.collect.androidtest.FakeLifecycleOwner import org.odk.collect.forms.instances.Instance import org.odk.collect.formstest.InMemInstancesRepository +import org.odk.collect.testshared.FakeScheduler +import java.util.function.Supplier @RunWith(AndroidJUnit4::class) class ReadyToSendBannerTest { @@ -30,10 +35,18 @@ class ReadyToSendBannerTest { ) } + private val scheduler = FakeScheduler() + private val clock = mock>().apply { + whenever(this.get()).thenReturn(0) + } + private val viewModel = ReadyToSendViewModel(instancesRepository, scheduler, clock) + private val lifecycleOwner = FakeLifecycleOwner() + @Test fun `if there are no sent instances do not display the banner`() { val view = ReadyToSendBanner(context).also { - it.init(instancesRepository) { 0 } + it.init(viewModel, lifecycleOwner) + scheduler.runBackground() } assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.GONE)) @@ -42,7 +55,8 @@ class ReadyToSendBannerTest { @Test fun `if there are no instances ready to send do not display the banner`() { val view = ReadyToSendBanner(context).also { - it.init(instancesRepository) { 0 } + it.init(viewModel, lifecycleOwner) + scheduler.runBackground() } assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.GONE)) @@ -58,7 +72,8 @@ class ReadyToSendBannerTest { ) val view = ReadyToSendBanner(context).also { - it.init(instancesRepository) { 0 } + it.init(viewModel, lifecycleOwner) + scheduler.runBackground() } assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.GONE)) @@ -74,7 +89,8 @@ class ReadyToSendBannerTest { ) val view = ReadyToSendBanner(context).also { - it.init(instancesRepository) { 0 } + it.init(viewModel, lifecycleOwner) + scheduler.runBackground() } assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.GONE)) @@ -90,7 +106,8 @@ class ReadyToSendBannerTest { ) val view = ReadyToSendBanner(context).also { - it.init(instancesRepository) { 0 } + it.init(viewModel, lifecycleOwner) + scheduler.runBackground() } assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.GONE)) @@ -113,7 +130,8 @@ class ReadyToSendBannerTest { ) val view = ReadyToSendBanner(context).also { - it.init(instancesRepository) { 0 } + it.init(viewModel, lifecycleOwner) + scheduler.runBackground() } assertThat( @@ -139,8 +157,10 @@ class ReadyToSendBannerTest { .build() ) + whenever(clock.get()).thenReturn(ONE_SECOND * 5) val view = ReadyToSendBanner(context).also { - it.init(instancesRepository) { ONE_SECOND * 5 } + it.init(viewModel, lifecycleOwner) + scheduler.runBackground() } assertThat( @@ -166,8 +186,10 @@ class ReadyToSendBannerTest { .build() ) + whenever(clock.get()).thenReturn(ONE_MINUTE) val view = ReadyToSendBanner(context).also { - it.init(instancesRepository) { ONE_MINUTE } + it.init(viewModel, lifecycleOwner) + scheduler.runBackground() } assertThat( @@ -193,8 +215,10 @@ class ReadyToSendBannerTest { .build() ) + whenever(clock.get()).thenReturn(ONE_HOUR * 2) val view = ReadyToSendBanner(context).also { - it.init(instancesRepository) { ONE_HOUR * 2 } + it.init(viewModel, lifecycleOwner) + scheduler.runBackground() } assertThat( @@ -220,8 +244,10 @@ class ReadyToSendBannerTest { .build() ) + whenever(clock.get()).thenReturn(ONE_DAY * 34) val view = ReadyToSendBanner(context).also { - it.init(instancesRepository) { ONE_DAY * 34 } + it.init(viewModel, lifecycleOwner) + scheduler.runBackground() } assertThat( @@ -263,8 +289,10 @@ class ReadyToSendBannerTest { .build() ) + whenever(clock.get()).thenReturn(ONE_SECOND * 10) val view = ReadyToSendBanner(context).also { - it.init(instancesRepository) { ONE_SECOND * 10 } + it.init(viewModel, lifecycleOwner) + scheduler.runBackground() } assertThat( @@ -304,7 +332,8 @@ class ReadyToSendBannerTest { ) val view = ReadyToSendBanner(context).also { - it.init(instancesRepository) { 0 } + it.init(viewModel, lifecycleOwner) + scheduler.runBackground() } assertThat( From 0721c664d619e11028241fe999bf6e2dff29243b Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 31 Aug 2023 13:21:42 +0200 Subject: [PATCH 11/19] Layout improvements --- collect_app/src/main/res/layout/instance_uploader_list.xml | 2 +- collect_app/src/main/res/layout/ready_to_send_banner.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/collect_app/src/main/res/layout/instance_uploader_list.xml b/collect_app/src/main/res/layout/instance_uploader_list.xml index b991e68e8e0..6c431038073 100644 --- a/collect_app/src/main/res/layout/instance_uploader_list.xml +++ b/collect_app/src/main/res/layout/instance_uploader_list.xml @@ -41,7 +41,7 @@ the License. android:paddingBottom="8dp" android:scrollbarStyle="outsideOverlay" app:layout_constraintBottom_toTopOf="@id/buttonholder" - app:layout_constraintTop_toTopOf="@id/ready_to_send_banner" /> + app:layout_constraintTop_toBottomOf="@id/ready_to_send_banner" /> Date: Fri, 1 Sep 2023 09:28:50 +0200 Subject: [PATCH 12/19] Factored out TimeInMs object --- .../send/ReadyToSendBanner.kt | 20 ++++++++----------- .../send/ReadyToSendBannerTest.kt | 15 +++++++------- .../java/org/odk/collect/shared/TimeInMs.kt | 8 ++++++++ 3 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 shared/src/main/java/org/odk/collect/shared/TimeInMs.kt diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt index 3e67ee238b3..ba880a9a784 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt @@ -6,13 +6,9 @@ import android.view.LayoutInflater import androidx.constraintlayout.widget.ConstraintLayout import androidx.lifecycle.LifecycleOwner import org.odk.collect.android.databinding.ReadyToSendBannerBinding +import org.odk.collect.shared.TimeInMs import org.odk.collect.strings.R -const val ONE_SECOND = 1000L -const val ONE_MINUTE = 60000L -const val ONE_HOUR = 3600000L -const val ONE_DAY = 86400000L - class ReadyToSendBanner(context: Context, attrs: AttributeSet?) : ConstraintLayout(context, attrs) { constructor(context: Context) : this(context, null) @@ -21,17 +17,17 @@ class ReadyToSendBanner(context: Context, attrs: AttributeSet?) : ConstraintLayo fun init(viewModel: ReadyToSendViewModel, owner: LifecycleOwner) { viewModel.data.observe(owner) { if (it.numberOfSentInstances > 0 && it.numberOfInstancesReadyToSend > 0) { - if (it.lastInstanceSentTimeMillis >= ONE_DAY) { - val days: Int = (it.lastInstanceSentTimeMillis / ONE_DAY).toInt() + if (it.lastInstanceSentTimeMillis >= TimeInMs.ONE_DAY) { + val days: Int = (it.lastInstanceSentTimeMillis / TimeInMs.ONE_DAY).toInt() binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_days_ago, days, days) - } else if (it.lastInstanceSentTimeMillis >= ONE_HOUR) { - val hours: Int = (it.lastInstanceSentTimeMillis / ONE_HOUR).toInt() + } else if (it.lastInstanceSentTimeMillis >= TimeInMs.ONE_HOUR) { + val hours: Int = (it.lastInstanceSentTimeMillis / TimeInMs.ONE_HOUR).toInt() binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_hours_ago, hours, hours) - } else if (it.lastInstanceSentTimeMillis >= ONE_MINUTE) { - val minutes: Int = (it.lastInstanceSentTimeMillis / ONE_MINUTE).toInt() + } else if (it.lastInstanceSentTimeMillis >= TimeInMs.ONE_MINUTE) { + val minutes: Int = (it.lastInstanceSentTimeMillis / TimeInMs.ONE_MINUTE).toInt() binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_minutes_ago, minutes, minutes) } else { - val seconds: Int = (it.lastInstanceSentTimeMillis / ONE_SECOND).toInt() + val seconds: Int = (it.lastInstanceSentTimeMillis / TimeInMs.ONE_SECOND).toInt() binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_seconds_ago, seconds, seconds) } diff --git a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt index 3c0e3e2d357..e7d3efb171c 100644 --- a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt @@ -16,6 +16,7 @@ import org.odk.collect.android.R import org.odk.collect.androidtest.FakeLifecycleOwner import org.odk.collect.forms.instances.Instance import org.odk.collect.formstest.InMemInstancesRepository +import org.odk.collect.shared.TimeInMs import org.odk.collect.testshared.FakeScheduler import java.util.function.Supplier @@ -157,7 +158,7 @@ class ReadyToSendBannerTest { .build() ) - whenever(clock.get()).thenReturn(ONE_SECOND * 5) + whenever(clock.get()).thenReturn(TimeInMs.ONE_SECOND * 5) val view = ReadyToSendBanner(context).also { it.init(viewModel, lifecycleOwner) scheduler.runBackground() @@ -186,7 +187,7 @@ class ReadyToSendBannerTest { .build() ) - whenever(clock.get()).thenReturn(ONE_MINUTE) + whenever(clock.get()).thenReturn(TimeInMs.ONE_MINUTE) val view = ReadyToSendBanner(context).also { it.init(viewModel, lifecycleOwner) scheduler.runBackground() @@ -215,7 +216,7 @@ class ReadyToSendBannerTest { .build() ) - whenever(clock.get()).thenReturn(ONE_HOUR * 2) + whenever(clock.get()).thenReturn(TimeInMs.ONE_HOUR * 2) val view = ReadyToSendBanner(context).also { it.init(viewModel, lifecycleOwner) scheduler.runBackground() @@ -244,7 +245,7 @@ class ReadyToSendBannerTest { .build() ) - whenever(clock.get()).thenReturn(ONE_DAY * 34) + whenever(clock.get()).thenReturn(TimeInMs.ONE_DAY * 34) val view = ReadyToSendBanner(context).also { it.init(viewModel, lifecycleOwner) scheduler.runBackground() @@ -277,7 +278,7 @@ class ReadyToSendBannerTest { Instance.Builder() .formId("4") .status(Instance.STATUS_SUBMITTED) - .lastStatusChangeDate(ONE_SECOND * 5) + .lastStatusChangeDate(TimeInMs.ONE_SECOND * 5) .build() ) @@ -285,11 +286,11 @@ class ReadyToSendBannerTest { Instance.Builder() .formId("5") .status(Instance.STATUS_SUBMITTED) - .lastStatusChangeDate(ONE_SECOND * 4) + .lastStatusChangeDate(TimeInMs.ONE_SECOND * 4) .build() ) - whenever(clock.get()).thenReturn(ONE_SECOND * 10) + whenever(clock.get()).thenReturn(TimeInMs.ONE_SECOND * 10) val view = ReadyToSendBanner(context).also { it.init(viewModel, lifecycleOwner) scheduler.runBackground() diff --git a/shared/src/main/java/org/odk/collect/shared/TimeInMs.kt b/shared/src/main/java/org/odk/collect/shared/TimeInMs.kt new file mode 100644 index 00000000000..f157b5fa00c --- /dev/null +++ b/shared/src/main/java/org/odk/collect/shared/TimeInMs.kt @@ -0,0 +1,8 @@ +package org.odk.collect.shared + +object TimeInMs { + const val ONE_SECOND = 1000L + const val ONE_MINUTE = 60000L + const val ONE_HOUR = 3600000L + const val ONE_DAY = 86400000L +} From 639576ee785f1af28b24a87ab046e0be8312c32c Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 1 Sep 2023 10:55:15 +0200 Subject: [PATCH 13/19] Code improvements --- .../send/InstanceUploaderListActivity.java | 6 +- .../send/ReadyToSendBanner.kt | 39 ++- .../send/ReadyToSendViewModel.kt | 4 +- .../send/ReadyToSendBannerTest.kt | 255 ++---------------- .../send/ReadyToSendViewModelTest.kt | 95 +++++++ 5 files changed, 136 insertions(+), 263 deletions(-) create mode 100644 collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModelTest.kt diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java index 4aa2b9ecbee..582c9ae4d86 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java @@ -131,7 +131,6 @@ public class InstanceUploaderListActivity extends LocalizedActivity implements private String filterText; private MultiSelectViewModel multiSelectViewModel; - private ReadyToSendViewModel readyToSendViewModel; private boolean allSelected; private boolean isSearchBoxShown; @@ -158,7 +157,8 @@ public void onCreate(Bundle savedInstanceState) { listAdapter.setSelected(ids); }); - readyToSendViewModel = new ViewModelProvider(this, factory).get(ReadyToSendViewModel.class); + ReadyToSendViewModel readyToSendViewModel = new ViewModelProvider(this, factory).get(ReadyToSendViewModel.class); + readyToSendViewModel.getData().observe(this, data -> binding.readyToSendBanner.setData(data)); // set title setTitle(getString(org.odk.collect.strings.R.string.send_data)); @@ -476,8 +476,6 @@ public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { } else { findViewById(R.id.buttonholder).setVisibility(View.VISIBLE); } - - binding.readyToSendBanner.init(readyToSendViewModel, this); } @Override diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt index ba880a9a784..af7913173b6 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt @@ -4,7 +4,6 @@ import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import androidx.constraintlayout.widget.ConstraintLayout -import androidx.lifecycle.LifecycleOwner import org.odk.collect.android.databinding.ReadyToSendBannerBinding import org.odk.collect.shared.TimeInMs import org.odk.collect.strings.R @@ -14,28 +13,24 @@ class ReadyToSendBanner(context: Context, attrs: AttributeSet?) : ConstraintLayo private val binding = ReadyToSendBannerBinding.inflate(LayoutInflater.from(context), this, true) - fun init(viewModel: ReadyToSendViewModel, owner: LifecycleOwner) { - viewModel.data.observe(owner) { - if (it.numberOfSentInstances > 0 && it.numberOfInstancesReadyToSend > 0) { - if (it.lastInstanceSentTimeMillis >= TimeInMs.ONE_DAY) { - val days: Int = (it.lastInstanceSentTimeMillis / TimeInMs.ONE_DAY).toInt() - binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_days_ago, days, days) - } else if (it.lastInstanceSentTimeMillis >= TimeInMs.ONE_HOUR) { - val hours: Int = (it.lastInstanceSentTimeMillis / TimeInMs.ONE_HOUR).toInt() - binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_hours_ago, hours, hours) - } else if (it.lastInstanceSentTimeMillis >= TimeInMs.ONE_MINUTE) { - val minutes: Int = (it.lastInstanceSentTimeMillis / TimeInMs.ONE_MINUTE).toInt() - binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_minutes_ago, minutes, minutes) - } else { - val seconds: Int = (it.lastInstanceSentTimeMillis / TimeInMs.ONE_SECOND).toInt() - binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_seconds_ago, seconds, seconds) - } - - binding.subtext.text = context.resources.getQuantityString(R.plurals.forms_ready_to_send, it.numberOfInstancesReadyToSend, it.numberOfInstancesReadyToSend) - binding.banner.visibility = VISIBLE + fun setData(data: ReadyToSendViewModel.Data) { + if (data.numberOfSentInstances > 0 && data.numberOfInstancesReadyToSend > 0) { + if (data.lastInstanceSentTimeMillis >= TimeInMs.ONE_DAY) { + val days: Int = (data.lastInstanceSentTimeMillis / TimeInMs.ONE_DAY).toInt() + binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_days_ago, days, days) + } else if (data.lastInstanceSentTimeMillis >= TimeInMs.ONE_HOUR) { + val hours: Int = (data.lastInstanceSentTimeMillis / TimeInMs.ONE_HOUR).toInt() + binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_hours_ago, hours, hours) + } else if (data.lastInstanceSentTimeMillis >= TimeInMs.ONE_MINUTE) { + val minutes: Int = (data.lastInstanceSentTimeMillis / TimeInMs.ONE_MINUTE).toInt() + binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_minutes_ago, minutes, minutes) + } else { + val seconds: Int = (data.lastInstanceSentTimeMillis / TimeInMs.ONE_SECOND).toInt() + binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_seconds_ago, seconds, seconds) } - } - viewModel.init() + binding.subtext.text = context.resources.getQuantityString(R.plurals.forms_ready_to_send, data.numberOfInstancesReadyToSend, data.numberOfInstancesReadyToSend) + binding.banner.visibility = VISIBLE + } } } diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModel.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModel.kt index 8fc19c53e1e..04ffdc321c8 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModel.kt +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModel.kt @@ -11,13 +11,13 @@ import java.util.function.Supplier class ReadyToSendViewModel( private val instancesRepository: InstancesRepository, - private val scheduler: Scheduler, + scheduler: Scheduler, private val clock: Supplier ) : ViewModel() { private val _data = MutableLiveData() val data: LiveData = _data - fun init() { + init { scheduler.immediate( background = { val sentInstances = instancesRepository.getAllByStatus(Instance.STATUS_SUBMITTED) diff --git a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt index e7d3efb171c..991bddf9432 100644 --- a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt @@ -10,15 +10,8 @@ import com.google.android.material.textview.MaterialTextView import org.hamcrest.Matchers.equalTo import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.mock -import org.mockito.kotlin.whenever import org.odk.collect.android.R -import org.odk.collect.androidtest.FakeLifecycleOwner -import org.odk.collect.forms.instances.Instance -import org.odk.collect.formstest.InMemInstancesRepository import org.odk.collect.shared.TimeInMs -import org.odk.collect.testshared.FakeScheduler -import java.util.function.Supplier @RunWith(AndroidJUnit4::class) class ReadyToSendBannerTest { @@ -27,37 +20,11 @@ class ReadyToSendBannerTest { it.setTheme(R.style.Theme_Collect) } - private val instancesRepository = InMemInstancesRepository().also { - it.save( - Instance.Builder() - .formId("1") - .status(Instance.STATUS_INCOMPLETE) - .build() - ) - } - - private val scheduler = FakeScheduler() - private val clock = mock>().apply { - whenever(this.get()).thenReturn(0) - } - private val viewModel = ReadyToSendViewModel(instancesRepository, scheduler, clock) - private val lifecycleOwner = FakeLifecycleOwner() - - @Test - fun `if there are no sent instances do not display the banner`() { - val view = ReadyToSendBanner(context).also { - it.init(viewModel, lifecycleOwner) - scheduler.runBackground() - } - - assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.GONE)) - } - @Test - fun `if there are no instances ready to send do not display the banner`() { + fun `if there are no sent instances and no instances ready to send do not display the banner`() { + val data = ReadyToSendViewModel.Data(0, 0, 0) val view = ReadyToSendBanner(context).also { - it.init(viewModel, lifecycleOwner) - scheduler.runBackground() + it.setData(data) } assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.GONE)) @@ -65,50 +32,19 @@ class ReadyToSendBannerTest { @Test fun `if there are sent instances but no instances ready to send do not display the banner`() { - instancesRepository.save( - Instance.Builder() - .formId("2") - .status(Instance.STATUS_SUBMITTED) - .build() - ) - - val view = ReadyToSendBanner(context).also { - it.init(viewModel, lifecycleOwner) - scheduler.runBackground() - } - - assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.GONE)) - } - - @Test - fun `if there are instances ready to send (complete) but no sent instances do not display the banner`() { - instancesRepository.save( - Instance.Builder() - .formId("2") - .status(Instance.STATUS_COMPLETE) - .build() - ) - + val data = ReadyToSendViewModel.Data(0, 1, 0) val view = ReadyToSendBanner(context).also { - it.init(viewModel, lifecycleOwner) - scheduler.runBackground() + it.setData(data) } assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.GONE)) } @Test - fun `if there are instances ready to send (submission failed) but no sent instances do not display the banner`() { - instancesRepository.save( - Instance.Builder() - .formId("2") - .status(Instance.STATUS_SUBMISSION_FAILED) - .build() - ) - + fun `if there are instances ready to send but no sent instances do not display the banner`() { + val data = ReadyToSendViewModel.Data(1, 0, 0) val view = ReadyToSendBanner(context).also { - it.init(viewModel, lifecycleOwner) - scheduler.runBackground() + it.setData(data) } assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.GONE)) @@ -116,23 +52,9 @@ class ReadyToSendBannerTest { @Test fun `if there are both sent and ready to send instances display the banner`() { - instancesRepository.save( - Instance.Builder() - .formId("2") - .status(Instance.STATUS_COMPLETE) - .build() - ) - - instancesRepository.save( - Instance.Builder() - .formId("3") - .status(Instance.STATUS_SUBMITTED) - .build() - ) - + val data = ReadyToSendViewModel.Data(1, 1, 0) val view = ReadyToSendBanner(context).also { - it.init(viewModel, lifecycleOwner) - scheduler.runBackground() + it.setData(data) } assertThat( @@ -143,25 +65,9 @@ class ReadyToSendBannerTest { @Test fun `the banner should display how long ago in seconds the last instance was sent if it was less than a minute ago`() { - instancesRepository.save( - Instance.Builder() - .formId("2") - .status(Instance.STATUS_COMPLETE) - .build() - ) - - instancesRepository.save( - Instance.Builder() - .formId("3") - .status(Instance.STATUS_SUBMITTED) - .lastStatusChangeDate(0) - .build() - ) - - whenever(clock.get()).thenReturn(TimeInMs.ONE_SECOND * 5) + val data = ReadyToSendViewModel.Data(1, 1, TimeInMs.ONE_SECOND * 5) val view = ReadyToSendBanner(context).also { - it.init(viewModel, lifecycleOwner) - scheduler.runBackground() + it.setData(data) } assertThat( @@ -172,25 +78,9 @@ class ReadyToSendBannerTest { @Test fun `the banner should display how long ago in minutes the last instance was sent if it was less than an hour ago`() { - instancesRepository.save( - Instance.Builder() - .formId("2") - .status(Instance.STATUS_COMPLETE) - .build() - ) - - instancesRepository.save( - Instance.Builder() - .formId("3") - .status(Instance.STATUS_SUBMITTED) - .lastStatusChangeDate(0) - .build() - ) - - whenever(clock.get()).thenReturn(TimeInMs.ONE_MINUTE) + val data = ReadyToSendViewModel.Data(1, 1, TimeInMs.ONE_MINUTE) val view = ReadyToSendBanner(context).also { - it.init(viewModel, lifecycleOwner) - scheduler.runBackground() + it.setData(data) } assertThat( @@ -201,25 +91,9 @@ class ReadyToSendBannerTest { @Test fun `the banner should display how long ago in hours the last instance was sent if it was less than a day ago`() { - instancesRepository.save( - Instance.Builder() - .formId("2") - .status(Instance.STATUS_COMPLETE) - .build() - ) - - instancesRepository.save( - Instance.Builder() - .formId("3") - .status(Instance.STATUS_SUBMITTED) - .lastStatusChangeDate(0) - .build() - ) - - whenever(clock.get()).thenReturn(TimeInMs.ONE_HOUR * 2) + val data = ReadyToSendViewModel.Data(1, 1, TimeInMs.ONE_HOUR * 2) val view = ReadyToSendBanner(context).also { - it.init(viewModel, lifecycleOwner) - scheduler.runBackground() + it.setData(data) } assertThat( @@ -230,25 +104,9 @@ class ReadyToSendBannerTest { @Test fun `the banner should display how long ago in days the last instance was sent if it was more than 24 hours ago`() { - instancesRepository.save( - Instance.Builder() - .formId("2") - .status(Instance.STATUS_COMPLETE) - .build() - ) - - instancesRepository.save( - Instance.Builder() - .formId("3") - .status(Instance.STATUS_SUBMITTED) - .lastStatusChangeDate(0) - .build() - ) - - whenever(clock.get()).thenReturn(TimeInMs.ONE_DAY * 34) + val data = ReadyToSendViewModel.Data(1, 1, TimeInMs.ONE_DAY * 34) val view = ReadyToSendBanner(context).also { - it.init(viewModel, lifecycleOwner) - scheduler.runBackground() + it.setData(data) } assertThat( @@ -257,84 +115,11 @@ class ReadyToSendBannerTest { ) } - @Test - fun `the banner should display how long ago the last instance was sent if there are multiple sent instances`() { - instancesRepository.save( - Instance.Builder() - .formId("2") - .status(Instance.STATUS_COMPLETE) - .build() - ) - - instancesRepository.save( - Instance.Builder() - .formId("3") - .status(Instance.STATUS_SUBMITTED) - .lastStatusChangeDate(0) - .build() - ) - - instancesRepository.save( - Instance.Builder() - .formId("4") - .status(Instance.STATUS_SUBMITTED) - .lastStatusChangeDate(TimeInMs.ONE_SECOND * 5) - .build() - ) - - instancesRepository.save( - Instance.Builder() - .formId("5") - .status(Instance.STATUS_SUBMITTED) - .lastStatusChangeDate(TimeInMs.ONE_SECOND * 4) - .build() - ) - - whenever(clock.get()).thenReturn(TimeInMs.ONE_SECOND * 10) - val view = ReadyToSendBanner(context).also { - it.init(viewModel, lifecycleOwner) - scheduler.runBackground() - } - - assertThat( - view.findViewById(R.id.title).text, - equalTo("Last form sent: 5 seconds ago") - ) - } - @Test fun `the banner should display the number of instances ready to send`() { - instancesRepository.save( - Instance.Builder() - .formId("2") - .status(Instance.STATUS_COMPLETE) - .build() - ) - - instancesRepository.save( - Instance.Builder() - .formId("3") - .status(Instance.STATUS_SUBMISSION_FAILED) - .build() - ) - - instancesRepository.save( - Instance.Builder() - .formId("4") - .status(Instance.STATUS_COMPLETE) - .build() - ) - - instancesRepository.save( - Instance.Builder() - .formId("5") - .status(Instance.STATUS_SUBMITTED) - .build() - ) - + val data = ReadyToSendViewModel.Data(3, 1, 0) val view = ReadyToSendBanner(context).also { - it.init(viewModel, lifecycleOwner) - scheduler.runBackground() + it.setData(data) } assertThat( diff --git a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModelTest.kt b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModelTest.kt new file mode 100644 index 00000000000..44ab993259e --- /dev/null +++ b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModelTest.kt @@ -0,0 +1,95 @@ +package org.odk.collect.android.instancemanagement.send + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.test.espresso.matcher.ViewMatchers.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.Rule +import org.junit.Test +import org.odk.collect.forms.instances.Instance +import org.odk.collect.formstest.InMemInstancesRepository +import org.odk.collect.shared.TimeInMs +import org.odk.collect.testshared.FakeScheduler + +class ReadyToSendViewModelTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + private val instancesRepository = InMemInstancesRepository().also { + it.save( + Instance.Builder() + .formId("1") + .status(Instance.STATUS_INCOMPLETE) + .lastStatusChangeDate(TimeInMs.ONE_SECOND * 1) + .build() + ) + + it.save( + Instance.Builder() + .formId("2") + .status(Instance.STATUS_COMPLETE) + .lastStatusChangeDate(TimeInMs.ONE_SECOND * 5) + .build() + ) + + it.save( + Instance.Builder() + .formId("3") + .status(Instance.STATUS_SUBMITTED) + .lastStatusChangeDate(TimeInMs.ONE_SECOND * 4) + .build() + ) + + it.save( + Instance.Builder() + .formId("4") + .status(Instance.STATUS_SUBMITTED) + .lastStatusChangeDate(TimeInMs.ONE_SECOND * 6) + .build() + ) + + it.save( + Instance.Builder() + .formId("5") + .status(Instance.STATUS_SUBMISSION_FAILED) + .lastStatusChangeDate(TimeInMs.ONE_SECOND * 7) + .build() + ) + + it.save( + Instance.Builder() + .formId("6") + .status(Instance.STATUS_COMPLETE) + .lastStatusChangeDate(TimeInMs.ONE_SECOND * 3) + .build() + ) + + it.save( + Instance.Builder() + .formId("7") + .status(Instance.STATUS_COMPLETE) + .lastStatusChangeDate(TimeInMs.ONE_SECOND * 10) + .build() + ) + } + private val scheduler = FakeScheduler() + + private val viewModel = ReadyToSendViewModel(instancesRepository, scheduler) { TimeInMs.ONE_SECOND * 10 } + + @Test + fun `numberOfSentInstances should represent the real number of instances with STATUS_SUBMITTED in the database`() { + scheduler.runBackground() + assertThat(viewModel.data.value!!.numberOfSentInstances, equalTo(2)) + } + + @Test + fun `numberOfInstancesReadyToSend should represent the real number of instances with STATUS_COMPLETE and STATUS_SUBMISSION_FAILED in the database`() { + scheduler.runBackground() + assertThat(viewModel.data.value!!.numberOfInstancesReadyToSend, equalTo(4)) + } + + @Test + fun `lastInstanceSentTimeMillis should correctly calculate when the last instance has been sent`() { + scheduler.runBackground() + assertThat(viewModel.data.value!!.lastInstanceSentTimeMillis, equalTo(4000L)) + } +} From c64065c0cdff5cb87b6b6f7cacbf4ff762b030ef Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 13 Sep 2023 21:21:17 +0200 Subject: [PATCH 14/19] Update the banner after sending forms --- .../send/InstanceUploaderListActivity.java | 5 ++++- .../android/instancemanagement/send/ReadyToSendBanner.kt | 2 ++ .../android/instancemanagement/send/ReadyToSendViewModel.kt | 6 +++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java index 582c9ae4d86..4f23c64ba6b 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java @@ -131,6 +131,7 @@ public class InstanceUploaderListActivity extends LocalizedActivity implements private String filterText; private MultiSelectViewModel multiSelectViewModel; + private ReadyToSendViewModel readyToSendViewModel; private boolean allSelected; private boolean isSearchBoxShown; @@ -157,7 +158,7 @@ public void onCreate(Bundle savedInstanceState) { listAdapter.setSelected(ids); }); - ReadyToSendViewModel readyToSendViewModel = new ViewModelProvider(this, factory).get(ReadyToSendViewModel.class); + readyToSendViewModel = new ViewModelProvider(this, factory).get(ReadyToSendViewModel.class); readyToSendViewModel.getData().observe(this, data -> binding.readyToSendBanner.setData(data)); // set title @@ -419,6 +420,8 @@ protected void onSaveInstanceState(Bundle outState) { @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { + readyToSendViewModel.update(); + if (resultCode == RESULT_CANCELED) { multiSelectViewModel.unselectAll(); return; diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt index af7913173b6..bf0280e54a7 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt @@ -31,6 +31,8 @@ class ReadyToSendBanner(context: Context, attrs: AttributeSet?) : ConstraintLayo binding.subtext.text = context.resources.getQuantityString(R.plurals.forms_ready_to_send, data.numberOfInstancesReadyToSend, data.numberOfInstancesReadyToSend) binding.banner.visibility = VISIBLE + } else { + binding.banner.visibility = GONE } } } diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModel.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModel.kt index 04ffdc321c8..ec6a8cfe6eb 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModel.kt +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModel.kt @@ -11,13 +11,17 @@ import java.util.function.Supplier class ReadyToSendViewModel( private val instancesRepository: InstancesRepository, - scheduler: Scheduler, + private val scheduler: Scheduler, private val clock: Supplier ) : ViewModel() { private val _data = MutableLiveData() val data: LiveData = _data init { + update() + } + + fun update() { scheduler.immediate( background = { val sentInstances = instancesRepository.getAllByStatus(Instance.STATUS_SUBMITTED) From 259040b6e4d0e9322d208b9bdc6038b4c7539e0b Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 13 Sep 2023 22:51:35 +0200 Subject: [PATCH 15/19] Improved ReadyToSendViewModelTest --- .../send/ReadyToSendViewModelTest.kt | 11 +++++++++-- .../collect/formstest/InMemInstancesRepository.java | 4 +++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModelTest.kt b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModelTest.kt index 44ab993259e..bae953ea8f8 100644 --- a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModelTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModelTest.kt @@ -14,7 +14,7 @@ class ReadyToSendViewModelTest { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() - private val instancesRepository = InMemInstancesRepository().also { + private var instancesRepository = InMemInstancesRepository().also { it.save( Instance.Builder() .formId("1") @@ -88,8 +88,15 @@ class ReadyToSendViewModelTest { } @Test - fun `lastInstanceSentTimeMillis should correctly calculate when the last instance has been sent`() { + fun `lastInstanceSentTimeMillis should be correctly calculate when the last instance has been sent`() { scheduler.runBackground() assertThat(viewModel.data.value!!.lastInstanceSentTimeMillis, equalTo(4000L)) } + + @Test + fun `lastInstanceSentTimeMillis should be 0 if there are no sent instances`() { + instancesRepository.deleteAll() + scheduler.runBackground() + assertThat(viewModel.data.value!!.lastInstanceSentTimeMillis, equalTo(0L)) + } } diff --git a/formstest/src/main/java/org/odk/collect/formstest/InMemInstancesRepository.java b/formstest/src/main/java/org/odk/collect/formstest/InMemInstancesRepository.java index 97d3bb18084..ac71ec2e9ab 100644 --- a/formstest/src/main/java/org/odk/collect/formstest/InMemInstancesRepository.java +++ b/formstest/src/main/java/org/odk/collect/formstest/InMemInstancesRepository.java @@ -191,6 +191,8 @@ public void removeInstanceById(Long databaseId) { } private void deleteInstanceFiles(Instance instance) { - DirectoryUtils.deleteDirectory(new File(instance.getInstanceFilePath()).getParentFile()); + if (instance.getInstanceFilePath() != null) { + DirectoryUtils.deleteDirectory(new File(instance.getInstanceFilePath()).getParentFile()); + } } } From eea486d818ab81284e7257d07ba670cd7e70f75f Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 13 Sep 2023 23:07:26 +0200 Subject: [PATCH 16/19] Imroved ReadyToSendBannerTest --- .../instancemanagement/send/ReadyToSendBannerTest.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt index 991bddf9432..b40288dbba3 100644 --- a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt @@ -72,7 +72,7 @@ class ReadyToSendBannerTest { assertThat( view.findViewById(R.id.title).text, - equalTo("Last form sent: 5 seconds ago") + equalTo(context.resources.getQuantityString(org.odk.collect.strings.R.plurals.last_form_sent_seconds_ago, 5, 5)) ) } @@ -85,7 +85,7 @@ class ReadyToSendBannerTest { assertThat( view.findViewById(R.id.title).text, - equalTo("Last form sent: 1 minute ago") + equalTo(context.resources.getQuantityString(org.odk.collect.strings.R.plurals.last_form_sent_minutes_ago, 1, 1)) ) } @@ -98,7 +98,7 @@ class ReadyToSendBannerTest { assertThat( view.findViewById(R.id.title).text, - equalTo("Last form sent: 2 hours ago") + equalTo(context.resources.getQuantityString(org.odk.collect.strings.R.plurals.last_form_sent_hours_ago, 2, 2)) ) } @@ -111,7 +111,7 @@ class ReadyToSendBannerTest { assertThat( view.findViewById(R.id.title).text, - equalTo("Last form sent: 34 days ago") + equalTo(context.resources.getQuantityString(org.odk.collect.strings.R.plurals.last_form_sent_days_ago, 34, 34)) ) } @@ -124,7 +124,7 @@ class ReadyToSendBannerTest { assertThat( view.findViewById(R.id.subtext).text, - equalTo("3 forms ready to send") + equalTo(context.resources.getQuantityString(org.odk.collect.strings.R.plurals.forms_ready_to_send, 3, 3)) ) } } From 86adad610a5f7366e2202cf2689b4ef559e476f5 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 13 Sep 2023 23:19:15 +0200 Subject: [PATCH 17/19] Improved ReadyToSendBannerTest --- .../send/ReadyToSendBannerTest.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt index b40288dbba3..a58da636184 100644 --- a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt @@ -30,6 +30,36 @@ class ReadyToSendBannerTest { assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.GONE)) } + @Test + fun `if initially the banner is gone it should become visible when its data changes and there are both sent and ready to send instances`() { + var data = ReadyToSendViewModel.Data(0, 0, 0) + val view = ReadyToSendBanner(context).also { + it.setData(data) + } + + assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.GONE)) + + data = ReadyToSendViewModel.Data(1, 1, 0) + view.setData(data) + + assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.VISIBLE)) + } + + @Test + fun `if initially the banner is visible it should become gone when its data changes and there are no both sent and ready to send instances`() { + var data = ReadyToSendViewModel.Data(1, 1, 0) + val view = ReadyToSendBanner(context).also { + it.setData(data) + } + + assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.VISIBLE)) + + data = ReadyToSendViewModel.Data(0, 0, 0) + view.setData(data) + + assertThat(view.findViewById(R.id.banner).visibility, equalTo(View.GONE)) + } + @Test fun `if there are sent instances but no instances ready to send do not display the banner`() { val data = ReadyToSendViewModel.Data(0, 1, 0) From e52bf130aba40e8fd4b5bfc5271b98d0ab5dd2e4 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 15 Sep 2023 11:07:17 +0200 Subject: [PATCH 18/19] Improve updating the banner --- .../instancemanagement/send/InstanceUploaderListActivity.java | 3 +-- .../android/instancemanagement/send/ReadyToSendViewModel.kt | 4 ---- .../instancemanagement/send/ReadyToSendViewModelTest.kt | 4 +++- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java index 4f23c64ba6b..a8cd16622bc 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java @@ -420,8 +420,6 @@ protected void onSaveInstanceState(Bundle outState) { @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { - readyToSendViewModel.update(); - if (resultCode == RESULT_CANCELED) { multiSelectViewModel.unselectAll(); return; @@ -479,6 +477,7 @@ public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { } else { findViewById(R.id.buttonholder).setVisibility(View.VISIBLE); } + readyToSendViewModel.update(); } @Override diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModel.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModel.kt index ec6a8cfe6eb..f09846e918b 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModel.kt +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModel.kt @@ -17,10 +17,6 @@ class ReadyToSendViewModel( private val _data = MutableLiveData() val data: LiveData = _data - init { - update() - } - fun update() { scheduler.immediate( background = { diff --git a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModelTest.kt b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModelTest.kt index bae953ea8f8..322905e568a 100644 --- a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModelTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModelTest.kt @@ -73,7 +73,9 @@ class ReadyToSendViewModelTest { } private val scheduler = FakeScheduler() - private val viewModel = ReadyToSendViewModel(instancesRepository, scheduler) { TimeInMs.ONE_SECOND * 10 } + private val viewModel = ReadyToSendViewModel(instancesRepository, scheduler) { TimeInMs.ONE_SECOND * 10 }.apply { + update() + } @Test fun `numberOfSentInstances should represent the real number of instances with STATUS_SUBMITTED in the database`() { From 67d59bf2bd69fe31011c7687803af47d3d8f88be Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 26 Sep 2023 13:14:33 +0200 Subject: [PATCH 19/19] Fixed ready_to_sen_banner layout --- collect_app/src/main/res/layout/ready_to_send_banner.xml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/collect_app/src/main/res/layout/ready_to_send_banner.xml b/collect_app/src/main/res/layout/ready_to_send_banner.xml index 179632b3cc9..0ad8db15341 100644 --- a/collect_app/src/main/res/layout/ready_to_send_banner.xml +++ b/collect_app/src/main/res/layout/ready_to_send_banner.xml @@ -28,8 +28,8 @@ android:layout_marginEnd="@dimen/margin_standard" android:textAppearance="?textAppearanceTitleMedium" android:textColor="?colorPrimary" - app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/icon" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/icon" tools:text="Last sent: 2 days ago"/> @@ -38,9 +38,8 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:textAppearance="?textAppearanceBodyMedium" - app:layout_constraintEnd_toEndOf="@id/title" - app:layout_constraintStart_toEndOf="@id/title" app:layout_constraintStart_toStartOf="@id/title" + app:layout_constraintEnd_toEndOf="@id/title" app:layout_constraintTop_toBottomOf="@id/title" tools:text="4 forms ready to send"/>