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

Adds daterange picker for case search #2508

Merged
merged 4 commits into from
Jul 12, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ dependencies {
implementation('com.squareup.okhttp3:okhttp-tls:3.12.12') {
exclude(group: 'org.bouncycastle', module: 'bcprov-jdk15on')
}

implementation 'com.google.android.material:material:1.3.0'
}

ext {
Expand Down
9 changes: 9 additions & 0 deletions app/res/drawable-anydpi/ic_create.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>
8 changes: 3 additions & 5 deletions app/res/layout/query_prompt_layout.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,10 @@
android:singleLine="true" />

<ImageView
android:id="@+id/barcode_scanner"
android:id="@+id/assist_view"
android:layout_height="match_parent"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
android:layout_width="32dp"
android:background="@color/blue"
app:srcCompat="@drawable/startup_barcode" />
android:layout_width="32dp" />

</LinearLayout>
</LinearLayout>
7 changes: 6 additions & 1 deletion app/res/values/themes.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">

<style name="CommonTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<style name="CommonTheme" parent="Theme.MaterialComponents.Light.DarkActionBar.Bridge">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, Bridge is the reason why it's working on pre-lollipop right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No not really, Bridge theme is just a workaround for apps not comfortable with changing their theme to a MaterialComponent theme. Bridge theme inherits from the AppCompat Theme and adds any properties defined by the MaterialComponent theme in it. So it has nothing to do with device compatibility. The library support the 14+ devices by default.

<item name="colorPrimary">@color/cc_brand_color</item>
<item name="colorPrimaryDark">@color/cc_brand_text</item>
<item name="colorAccent">@color/cc_dark_cool_accent_color</item>
Expand Down Expand Up @@ -31,6 +31,11 @@
<item name="android:scrollViewStyle">@style/fading_edge</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:dropDownListViewStyle">@style/dropDownListView</item>

<!-- Material Components styling that can be removed when we migrate to a material component theme from the bridge theme-->
<item name="materialCalendarStyle">@style/Widget.MaterialComponents.MaterialCalendar</item>
<item name="materialCalendarFullscreenTheme">@style/ThemeOverlay.MaterialComponents.MaterialCalendar.Fullscreen</item>
<item name="materialCalendarTheme">@style/ThemeOverlay.MaterialComponents.MaterialCalendar</item>
</style>

<style name="AppBaseTheme" parent="CommonTheme" />
Expand Down
94 changes: 80 additions & 14 deletions app/src/org/commcare/activities/QueryRequestActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import android.widget.TextView;
import android.widget.Toast;

import com.google.android.material.datepicker.MaterialDatePicker;

import org.commcare.CommCareApplication;
import org.commcare.core.interfaces.HttpResponseProcessor;
import org.commcare.core.network.AuthInfo;
Expand All @@ -29,7 +31,6 @@
import org.commcare.modern.util.Pair;
import org.commcare.session.RemoteQuerySessionManager;
import org.commcare.suite.model.DisplayData;
import org.commcare.suite.model.DisplayUnit;
import org.commcare.suite.model.QueryPrompt;
import org.commcare.tasks.ModernHttpTask;
import org.commcare.tasks.templates.CommCareTaskConnector;
Expand All @@ -49,6 +50,7 @@

import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
Expand All @@ -57,9 +59,15 @@
import java.util.Vector;

import androidx.annotation.NonNull;
import androidx.core.content.res.ResourcesCompat;

import static org.commcare.activities.EntitySelectActivity.BARCODE_FETCH;
import static org.commcare.suite.model.QueryPrompt.INPUT_TYPE_DATERANGE;
import static org.commcare.suite.model.QueryPrompt.INPUT_TYPE_SELECT1;
import static org.commcare.utils.DateRangeUtils.formatDateRangeAnswer;
import static org.commcare.utils.DateRangeUtils.getDateFromTime;
import static org.commcare.utils.DateRangeUtils.getHumanReadableDateRange;
import static org.commcare.utils.DateRangeUtils.parseHumanReadableDate;

/**
* Collects 'query datum' in the current session. Prompts user for query
Expand All @@ -79,6 +87,8 @@ public class QueryRequestActivity
private static final String IN_ERROR_STATE_KEY = "in-error-state-key";
private static final String ERROR_MESSAGE_KEY = "error-message-key";
private static final String APPEARANCE_BARCODE_SCAN = "barcode_scan";
private static final String DATE_PICKER_FRAGMENT_TAG = "date_picker_dialog";


@UiElement(value = R.id.request_button, locale = "query.button")
private Button queryButton;
Expand Down Expand Up @@ -118,12 +128,12 @@ public void onCreateSessionSafe(Bundle savedInstanceState) {
private ArrayList<String> getSupportedPrompts() {
ArrayList<String> supportedPrompts = new ArrayList<>();
supportedPrompts.add(INPUT_TYPE_SELECT1);
supportedPrompts.add(INPUT_TYPE_DATERANGE);
return supportedPrompts;
}

private void setupUI() {
buildPromptUI();

queryButton.setOnClickListener(v -> {
ViewUtil.hideVirtualKeyboard(QueryRequestActivity.this);
makeQueryRequest();
Expand All @@ -147,12 +157,14 @@ private void buildPromptUI() {
private void buildPromptEntry(LinearLayout promptsLayout, String promptId,
QueryPrompt queryPrompt, boolean isLastPrompt) {
View promptView = LayoutInflater.from(this).inflate(R.layout.query_prompt_layout, promptsLayout, false);
setLabelText(promptView, queryPrompt.getDisplay());
setLabelText(promptView, queryPrompt);
View inputView;
if (remoteQuerySessionManager.isPromptSupported(queryPrompt)) {
String input = queryPrompt.getInput();
if (input != null && input.contentEquals(INPUT_TYPE_SELECT1)) {
inputView = buildSpinnerView(promptView, queryPrompt);
} else if (input != null && input.contentEquals(INPUT_TYPE_DATERANGE)) {
inputView = buildDateRangeView(promptView, queryPrompt);
} else {
inputView = buildEditTextView(promptView, queryPrompt, isLastPrompt);
}
Expand All @@ -163,13 +175,65 @@ private void buildPromptEntry(LinearLayout promptsLayout, String promptId,
}
}

private View buildDateRangeView(View promptView, QueryPrompt queryPrompt) {
EditText promptEditText = promptView.findViewById(R.id.prompt_et);
promptEditText.setVisibility(View.VISIBLE);
promptEditText.setFocusable(false);
promptView.findViewById(R.id.prompt_spinner).setVisibility(View.GONE);

Hashtable<String, String> userAnswers = remoteQuerySessionManager.getUserAnswers();
String humanReadableDateRange = getHumanReadableDateRange(userAnswers.get(queryPrompt.getKey()));
promptEditText.setText(humanReadableDateRange);

// Setup edit button to show date picker
ImageView editDateIcon = promptView.findViewById(R.id.assist_view);
editDateIcon.setVisibility(View.VISIBLE);
editDateIcon.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_create, null));
editDateIcon.setOnClickListener(view -> {
showDateRangePicker(promptEditText, queryPrompt);
});

return promptEditText;
}

private void showDateRangePicker(EditText promptEditText, QueryPrompt queryPrompt) {
MaterialDatePicker.Builder<androidx.core.util.Pair<Long, Long>> dateRangePickerBuilder = MaterialDatePicker.Builder.dateRangePicker()
.setTitleText(getLabel(queryPrompt))
.setInputMode(MaterialDatePicker.INPUT_MODE_CALENDAR);

// Set Current Range
String currentDateRangeText = promptEditText.getText().toString();
if (!TextUtils.isEmpty(currentDateRangeText)) {
try {
dateRangePickerBuilder.setSelection(parseHumanReadableDate(currentDateRangeText));
} catch (ParseException e) {
// do nothing
e.printStackTrace();
}
}

MaterialDatePicker<androidx.core.util.Pair<Long, Long>> dateRangePicker = dateRangePickerBuilder.build();
dateRangePicker.addOnPositiveButtonClickListener(selection -> {
String startDate = getDateFromTime(selection.first);
String endDate = getDateFromTime(selection.second);
remoteQuerySessionManager.answerUserPrompt(queryPrompt.getKey(), formatDateRangeAnswer(startDate, endDate));
promptEditText.setText(getHumanReadableDateRange(startDate, endDate));
});
dateRangePicker.show(getSupportFragmentManager(), DATE_PICKER_FRAGMENT_TAG);
}

private void setUpBarCodeScanButton(View promptView, String promptId, QueryPrompt queryPrompt) {
ImageView barcodeScannerView = promptView.findViewById(R.id.barcode_scanner);
barcodeScannerView.setVisibility(isBarcodeEnabled(queryPrompt) ? View.VISIBLE : View.INVISIBLE);
barcodeScannerView.setTag(promptId);
barcodeScannerView.setOnClickListener(v ->
callBarcodeScanIntent((String)v.getTag())
);
// Only show for free text input
if (queryPrompt.getInput() == null) {
ImageView barcodeScannerView = promptView.findViewById(R.id.assist_view);
barcodeScannerView.setVisibility(isBarcodeEnabled(queryPrompt) ? View.VISIBLE : View.INVISIBLE);
barcodeScannerView.setBackgroundColor(getResources().getColor(R.color.blue));
barcodeScannerView.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.startup_barcode, null));
barcodeScannerView.setTag(promptId);
barcodeScannerView.setOnClickListener(v ->
callBarcodeScanIntent((String)v.getTag())
);
}
}

private Spinner buildSpinnerView(View promptView, QueryPrompt queryPrompt) {
Expand Down Expand Up @@ -316,11 +380,13 @@ protected void onActivityResult(int requestCode, int resultCode, Intent intent)
}
}

private void setLabelText(View promptView, DisplayUnit display) {
DisplayData displayData = display.evaluate();
String promptText =
Localizer.processArguments(displayData.getName(), new String[]{""}).trim();
((TextView)promptView.findViewById(R.id.prompt_label)).setText(promptText);
private void setLabelText(View promptView, QueryPrompt queryPrompt) {
((TextView)promptView.findViewById(R.id.prompt_label)).setText(getLabel(queryPrompt));
}

private String getLabel(QueryPrompt queryPrompt) {
DisplayData displayData = queryPrompt.getDisplay().evaluate();
return Localizer.processArguments(displayData.getName(), new String[]{""}).trim();
}

private void makeQueryRequest() {
Expand Down
77 changes: 77 additions & 0 deletions app/src/org/commcare/utils/DateRangeUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package org.commcare.utils;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

import javax.annotation.Nullable;

import androidx.core.util.Pair;

/**
* Utility functions for DateRangePicker widget
*/
public class DateRangeUtils {

// Changing this will require changing this format on ES end as well
public static final String DATE_RANGE_ANSWER_PREFIX = "__range__";
public static final String DATE_RANGE_ANSWER_DELIMITER = "__";
public static final String DATE_RANGE_ANSWER_HUMAN_READABLE_DELIMITER = " to ";
private static final String DATE_FORMAT = "yyyy-MM-dd";

/**
* @param humanReadableDateRange human readable fomat for date range as 'startDate to endDate'
* @return a Pair of start time and end time that can be supplied to MaterialDatePicker to set a date range,
* @throws ParseException if the given humanReadableDateRange is not in 'yyyy-mm-dd to yyyy-mm-dd' format
*/
@Nullable
public static Pair<Long, Long> parseHumanReadableDate(String humanReadableDateRange) throws ParseException {
if (humanReadableDateRange.contains(DATE_RANGE_ANSWER_HUMAN_READABLE_DELIMITER)) {
String[] humanReadableDateRangeSplit = humanReadableDateRange.split(DATE_RANGE_ANSWER_HUMAN_READABLE_DELIMITER);
if (humanReadableDateRangeSplit.length == 2) {
SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT, Locale.US);
Date startDate = sdf.parse(humanReadableDateRangeSplit[0]);
Date endDate = sdf.parse(humanReadableDateRangeSplit[1]);
return new Pair<>(getTimeFromDateOffsettingTz(startDate), getTimeFromDateOffsettingTz(endDate));
}
}
throw new ParseException("Argument " + humanReadableDateRange + " should be formatted as 'yyyy-mm-dd to yyyy-mm-dd'", 0);
}

private static Long getTimeFromDateOffsettingTz(Date date) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what it returns? Can you see whether a similar utility exists in DataUtils?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't able to find it anywhere else. It removes the timezone offset from the date in millis and return the date. This is because when we set the date to the daterange widget we are setting time in milliseconds without any indication of timezone and the date widget itself doesn't offset the timezone from it to arrive at the offsetted date.

return date.getTime() - date.getTimezoneOffset() * 60 * 1000;
}

/**
* Formats __range__startDate__endDate as 'startDate to EndDate'
*
* @param dateRangeAnswer A date range value in form of '__range__startDate__endDate'
* @return human readable format 'startDate to EndDate' for given dateRangeAnswer
*/
public static String getHumanReadableDateRange(String dateRangeAnswer) {
if (dateRangeAnswer != null && dateRangeAnswer.startsWith(DATE_RANGE_ANSWER_PREFIX)) {
String[] dateRangeSplit = dateRangeAnswer.split(DATE_RANGE_ANSWER_DELIMITER);
if (dateRangeSplit.length == 3) {
return getHumanReadableDateRange(dateRangeSplit[2], dateRangeSplit[3]);
}
}
return dateRangeAnswer;
}


// Formats as 'startDate to endDate'
public static String getHumanReadableDateRange(String startDate, String endDate) {
return startDate + DATE_RANGE_ANSWER_HUMAN_READABLE_DELIMITER + endDate;
}

// Formats as '__range__startDate__endDate'
public static String formatDateRangeAnswer(String startDate, String endDate) {
return DATE_RANGE_ANSWER_PREFIX + startDate + DATE_RANGE_ANSWER_DELIMITER + endDate;
}

// Convers given time as yyyy-mm-dd
public static String getDateFromTime(Long time) {
return new SimpleDateFormat(DATE_FORMAT, Locale.US).format(new Date(time));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ query.name=Patient Name
query.id=Patient ID
query.state=State
query.district=District
query.date=Date
case_sharing.exactly_one_group=The case sharing settings for your user are incorrect. This user must be in exactly one case sharing group. Please contact your supervisor.
cchq.case=Case
cchq.referral=Referral
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@

<remote-request>
<post url="https://www.fake.com/claim_patient/"
relevant="count(instance('casedb')/casedb/case[@case_id=instance('session')/session/data/case_id]) = 0">
relevant="count(instance('casedb')/casedb/case[@case_id=instance('session')/session/data/case_id]) = 0">
<data key="selected_case_id" ref="instance('session')/session/data/case_id"/>
</post>
<command id="patient-search">
Expand Down Expand Up @@ -382,6 +382,20 @@
<sort ref="id"/>
</itemset>
</prompt>
<prompt key="date" input="daterange">
<display>
<text>
<locale id="query.date"/>
</text>
</display>
</prompt>
<prompt key="invalid" input="foobar">
<display>
<text>
<locale id="query.district"/>
</text>
</display>
</prompt>
</query>
<datum id="case_id" nodeset="instance('patients')/patients/patient" value="./id" detail-select="patient_short" detail-confirm="patient_long"/>
</session>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ private ActivityController<QueryRequestActivity> buildActivityAndSetViews() {
// set views
LinearLayout promptsLayout = queryRequestActivity.findViewById(R.id.query_prompts);

assertEquals(promptsLayout.getChildCount(), 4);
assertEquals(5, promptsLayout.getChildCount());

EditText patientName = promptsLayout.getChildAt(0).findViewById(R.id.prompt_et);
patientName.setText("francisco");
Expand Down
22 changes: 22 additions & 0 deletions app/unit-tests/src/org/commcare/utils/DateRangeUtilsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.commcare.utils;

import org.junit.Test;

import java.text.ParseException;

import androidx.core.util.Pair;

public class DateRangeUtilsTest {

@Test
public void testDateConversion() throws ParseException {
String dateRange = "2020-02-15 to 2021-03-18";
Pair<Long, Long> selection = DateRangeUtils.parseHumanReadableDate(dateRange);
String startDate = DateRangeUtils.getDateFromTime(selection.first);
String endDate = DateRangeUtils.getDateFromTime(selection.second);
String humanReadableDateRange = DateRangeUtils.getHumanReadableDateRange(startDate, endDate);
assert dateRange.contentEquals(humanReadableDateRange);
assert DateRangeUtils.formatDateRangeAnswer(startDate, endDate).contentEquals("__range__2020-02-15__2021-03-18");
}

}