Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Making the "Ready to Send" screen more actionable #5693

Merged
merged 19 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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", 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())
.assertQuantityText(org.odk.collect.strings.R.plurals.forms_ready_to_send, 1, 1)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -100,6 +101,11 @@ abstract class Page<T : Page<T>> {
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
Expand Down Expand Up @@ -246,6 +252,10 @@ abstract class Page<T : Page<T>> {
return ApplicationProvider.getApplicationContext<Collect>().getLocalizedString(id!!, *formatArgs)
}

fun getTranslatedQuantityString(id: Int?, quantity: Int, vararg formatArgs: Any): String {
return ApplicationProvider.getApplicationContext<Collect>().getLocalizedQuantityString(id!!, quantity, *formatArgs)
}

fun clickOnAreaWithIndex(clazz: String?, index: Int): T {
onView(withIndex(withClassName(endsWith(clazz)), index)).perform(click())
return this as T
Expand Down
8 changes: 4 additions & 4 deletions collect_app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ the specific language governing permissions and limitations under the License.
android:name=".gdrive.GoogleSheetsUploaderActivity"
android:configChanges="orientation|screenSize" />
<activity
android:name=".activities.InstanceUploaderListActivity"
android:name=".instancemanagement.send.InstanceUploaderListActivity"
android:configChanges="orientation|screenSize" />
<activity android:name=".activities.InstanceUploaderActivity" />
<activity android:name=".instancemanagement.send.InstanceUploaderActivity" />
<activity android:name=".activities.AboutActivity" />
<activity android:name=".configure.qr.QRCodeTabsActivity" />
<activity
Expand Down Expand Up @@ -278,7 +278,7 @@ the specific language governing permissions and limitations under the License.

<activity-alias
android:name=".activities.InstanceUploaderList"
android:targetActivity=".activities.InstanceUploaderListActivity"
android:targetActivity=".instancemanagement.send.InstanceUploaderListActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
Expand All @@ -290,7 +290,7 @@ the specific language governing permissions and limitations under the License.

<activity-alias
android:name=".activities.InstanceUploaderActivity"
android:targetActivity=".activities.InstanceUploaderActivity"
android:targetActivity=".instancemanagement.send.InstanceUploaderActivity"
android:exported="true">
<intent-filter>
<action android:name="org.odk.collect.android.INSTANCE_UPLOAD" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* the License.
*/

package org.odk.collect.android.activities;
package org.odk.collect.android.instancemanagement.send;

import static java.util.Arrays.stream;

Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* the License.
*/

package org.odk.collect.android.activities;
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -119,6 +120,9 @@ public class InstanceUploaderListActivity extends LocalizedActivity implements
@Inject
SettingsProvider settingsProvider;

@Inject
ReadyToSendViewModel.Factory factory;

private ListView listView;
private InstanceUploaderAdapter listAdapter;
private Integer selectedSortingOrder;
Expand Down Expand Up @@ -153,6 +157,8 @@ public void onCreate(Bundle savedInstanceState) {

listAdapter.setSelected(ids);
});
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));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.odk.collect.android.instancemanagement.send

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.shared.TimeInMs
import org.odk.collect.strings.R

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 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)
}

binding.subtext.text = context.resources.getQuantityString(R.plurals.forms_ready_to_send, data.numberOfInstancesReadyToSend, data.numberOfInstancesReadyToSend)
binding.banner.visibility = VISIBLE
}
}
}
Original file line number Diff line number Diff line change
@@ -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,
scheduler: Scheduler,
private val clock: Supplier<Long>
) : ViewModel() {
private val _data = MutableLiveData<Data>()
val data: LiveData<Data> = _data

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 {
grzesiek2010 marked this conversation as resolved.
Show resolved Hide resolved
0
}
Data(numberOfInstancesReadyToSend, numberOfSentInstances, lastInstanceSentTimeMillis)
},
foreground = {
_data.value = it
}
)
}

open class Factory(
private val instancesRepository: InstancesRepository,
private val scheduler: Scheduler,
private val clock: Supplier<Long>
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ReadyToSendViewModel(instancesRepository, scheduler, clock) as T
}
}

data class Data(
val numberOfInstancesReadyToSend: Int,
val numberOfSentInstances: Int,
val lastInstanceSentTimeMillis: Long
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ 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
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.utilities.ApplicationConstants
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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.instancemanagement.send.InstanceUploaderListActivity
import org.odk.collect.android.mainmenu.MainMenuActivity
import org.odk.collect.android.notifications.NotificationManagerNotifier
import org.odk.collect.android.upload.FormUploadException
Expand Down
10 changes: 9 additions & 1 deletion collect_app/src/main/res/layout/instance_uploader_list.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ the License.
android:layout_height="match_parent"
android:layout_below="@id/appBarLayout">

<org.odk.collect.android.instancemanagement.send.ReadyToSendBanner
android:id="@+id/ready_to_send_banner"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>

<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
Expand All @@ -33,7 +41,7 @@ the License.
android:paddingBottom="8dp"
android:scrollbarStyle="outsideOverlay"
app:layout_constraintBottom_toTopOf="@id/buttonholder"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@id/ready_to_send_banner" />

<TextView
android:id="@android:id/empty"
Expand Down
47 changes: 47 additions & 0 deletions collect_app/src/main/res/layout/ready_to_send_banner.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorSurfaceContainerLow"
android:paddingVertical="@dimen/margin_standard"
android:visibility="gone"
tools:visibility="visible">

<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_large"
app:srcCompat="@drawable/ic_send_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?colorPrimary"/>

<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_large"
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_constraintTop_toTopOf="@id/icon"
tools:text="Last sent: 2 days ago"/>

<com.google.android.material.textview.MaterialTextView
grzesiek2010 marked this conversation as resolved.
Show resolved Hide resolved
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"
app:layout_constraintTop_toBottomOf="@id/title"
tools:text="4 forms ready to send"/>

</androidx.constraintlayout.widget.ConstraintLayout>
Loading