Skip to content

Commit

Permalink
Merge pull request #5693 from grzesiek2010/COLLECT-5605
Browse files Browse the repository at this point in the history
Making the "Ready to Send" screen more actionable
  • Loading branch information
grzesiek2010 authored Sep 26, 2023
2 parents d68c606 + 67d59bf commit ab1af33
Show file tree
Hide file tree
Showing 22 changed files with 524 additions and 16 deletions.
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 All @@ -127,6 +131,7 @@ public class InstanceUploaderListActivity extends LocalizedActivity implements
private String filterText;

private MultiSelectViewModel multiSelectViewModel;
private ReadyToSendViewModel readyToSendViewModel;
private boolean allSelected;

private boolean isSearchBoxShown;
Expand All @@ -153,6 +158,8 @@ public void onCreate(Bundle savedInstanceState) {

listAdapter.setSelected(ids);
});
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 Expand Up @@ -470,6 +477,7 @@ public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
} else {
findViewById(R.id.buttonholder).setVisibility(View.VISIBLE);
}
readyToSendViewModel.update();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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
} else {
binding.banner.visibility = GONE
}
}
}
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,
private val scheduler: Scheduler,
private val clock: Supplier<Long>
) : ViewModel() {
private val _data = MutableLiveData<Data>()
val data: LiveData<Data> = _data

fun update() {
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<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
46 changes: 46 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,46 @@
<?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_constraintStart_toEndOf="@id/icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/icon"
tools:text="Last sent: 2 days ago"/>

<com.google.android.material.textview.MaterialTextView
android:id="@+id/subtext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="?textAppearanceBodyMedium"
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"/>

</androidx.constraintlayout.widget.ConstraintLayout>
Loading

0 comments on commit ab1af33

Please sign in to comment.