package org.commcare.views.widgets;
import android.content.Context;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import org.commcare.dalvik.R;
import org.javarosa.core.model.data.InvalidDateData;
import org.javarosa.xform.util.UniversalDate;
import org.javarosa.core.model.data.DateData;
import org.javarosa.core.model.data.IAnswerData;
import org.javarosa.core.services.locale.Localization;
import org.javarosa.form.api.FormEntryPrompt;
import org.joda.time.DateTime;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class GregorianDateWidget extends AbstractUniversalDateWidget
implements CalendarFragment.CalendarCloseListener {
private EditText dayText;
private EditText yearText;
private TextView dayOfWeek;
private Calendar calendar;
private LinearLayout gregorianView;
private Spinner monthSpinner;
private long dateOfLastWidgetUpdateNotice = -1;
private final ImageButton openCalButton;
private static final int EMPTY_MONTH_ENTRY_INDEX = 12;
private List<String> monthList;
private final int maxYear;
private final long todaysDateInMillis;
private long timeBeforeCalendarOpened;
private final CalendarFragment myCalendarFragment;
private final FragmentManager fm;
public static final int MINYEAR = 1900;
private static final String DAYFORMAT = "%02d";
private static final String YEARFORMAT = "%04d";
public static final int YEARSINFUTURE = 4;
public GregorianDateWidget(Context context, FormEntryPrompt prompt, boolean closeButton) {
super(context, prompt);
maxYear = calendar.get(Calendar.YEAR) + YEARSINFUTURE;
todaysDateInMillis = calendar.getTimeInMillis();
ImageButton clearAll = (ImageButton)findViewById(R.id.clear_all);
if (closeButton) {
clearAll.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
clearAll();
}
});
} else {
clearAll.setVisibility(View.GONE);
}
fm = ((FragmentActivity)getContext()).getSupportFragmentManager();
myCalendarFragment = new CalendarFragment();
setupCalendarFragment();
openCalButton = (ImageButton)findViewById(R.id.open_calendar_bottom);
openCalButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
openCalendar();
}
});
setAnswer();
}
private void setupCalendarFragment() {
myCalendarFragment.setCalendar(calendar, todaysDateInMillis);
myCalendarFragment.setListener(this);
myCalendarFragment.setCancelable(false);
}
@Override
protected void initText() {
dayOfWeek = (TextView)findViewById(R.id.greg_day_of_week);
dayText = (EditText)findViewById(R.id.day_txt_field);
yearText = (EditText)findViewById(R.id.year_txt_field);
dayText.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
dayText.clearFocus();
dayText.requestFocus();
}
});
yearText.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
yearText.clearFocus();
yearText.requestFocus();
}
});
setupMonthComponents();
}
private void setupMonthComponents() {
monthSpinner = (Spinner)gregorianView.findViewById(R.id.month_spinner);
monthList.add("");
monthSpinner.setAdapter(new ArrayAdapter<>(getContext(), R.layout.calendar_date, monthList));
monthSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (position != EMPTY_MONTH_ENTRY_INDEX) {
validateDayText();
int previouslySelectedMonth = monthArrayPointer;
// Need to have a valid monthArrayPointer if they pick the empty option, so mod everything by 12
monthArrayPointer = position % 12;
int monthDifference = monthArrayPointer - previouslySelectedMonth;
DateTime dt = new DateTime(calendar.getTimeInMillis()).plusMonths(monthDifference);
calendar.setTimeInMillis(dt.getMillis());
refreshDisplay();
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
});
}
@Override
protected void inflateView(Context context) {
gregorianView = (LinearLayout)LayoutInflater.from(context).inflate(R.layout.list_gregorian_widget, null);
addView(gregorianView);
}
@Override
protected void updateDateDisplay(long millisFromJavaEpoch) {
UniversalDate dateUniv = fromMillis(millisFromJavaEpoch);
monthArrayPointer = dateUniv.month - 1;
dayText.setText(String.format(DAYFORMAT, dateUniv.day));
monthSpinner.setSelection(monthArrayPointer);
yearText.setText(String.format(YEARFORMAT, dateUniv.year));
calendar.setTimeInMillis(millisFromJavaEpoch);
dayOfWeek.setText(calendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG, Locale.getDefault()));
//The setSelection response above is delayed in its execution, which means that on a given
//ui loop where the widget is created, we're guaranteed to get this execution path
//on the next loop. If something changes, we should fire the event, but otherwise
//this can end up supressing QuestionWidget state like validation messages
if(dateOfLastWidgetUpdateNotice != millisFromJavaEpoch) {
dateOfLastWidgetUpdateNotice = millisFromJavaEpoch;
widgetEntryChanged();
}
}
//Used to calculate new time when a button is pressed
@Override
protected long getCurrentMillis() {
autoFillEmptyTextFields();
validateDayText();
updateCalendar();
int day = Integer.parseInt(dayText.getText().toString());
int month = monthArrayPointer + 1; //monthArray and Java calendar assume january = 0, millis from java epoch assumes january = 1
int year = Integer.parseInt(yearText.getText().toString());
return toMillisFromJavaEpoch(year, month, day);
}
//Autofills any empty text fields whenever a button is pressed
private void autoFillEmptyTextFields() {
if (dayText.getText().toString().isEmpty()) {
dayText.setText(String.valueOf(calendar.get(Calendar.DAY_OF_MONTH)));
}
if (((String)monthSpinner.getSelectedItem()).isEmpty()) {
monthSpinner.setSelection(monthArrayPointer);
}
if (yearText.getText().toString().isEmpty()) {
yearText.setText(String.valueOf(calendar.get(Calendar.YEAR)));
}
}
private void updateCalendar() {
monthArrayPointer = monthSpinner.getSelectedItemPosition();
calendar.set(Calendar.MONTH, monthArrayPointer);
String yearTextValue = yearText.getText().toString();
calendar.set(Calendar.YEAR, Integer.parseInt(yearTextValue));
}
//Makes sure that value of day text field is valid given values of month and year fields
private void validateDayText() {
String dayTextString = dayText.getText().toString();
if(dayTextString.isEmpty()){
return;
}
int dayTextValue = Integer.parseInt(dayTextString);
int maxDayOfMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
if (dayTextValue >= maxDayOfMonth) {
dayTextValue = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
dayText.setText(String.valueOf(dayTextValue));
} else if (dayTextValue < 1) {
dayTextValue = calendar.getActualMinimum(Calendar.DAY_OF_MONTH);
dayText.setText(String.valueOf(dayTextValue));
}
calendar.set(Calendar.DAY_OF_MONTH, dayTextValue);
}
@Override
protected UniversalDate decrementMonth(long millisFromJavaEpoch) {
DateTime dt = new DateTime(millisFromJavaEpoch).minusMonths(1);
return constructUniversalDate(dt);
}
@Override
protected UniversalDate decrementYear(long millisFromJavaEpoch) {
DateTime dt = new DateTime(millisFromJavaEpoch).minusYears(1);
return constructUniversalDate(dt);
}
@Override
protected UniversalDate fromMillis(long millisFromJavaEpoch) {
return constructUniversalDate(new DateTime(millisFromJavaEpoch));
}
@Override
protected UniversalDate incrementMonth(long millisFromJavaEpoch) {
DateTime dt = new DateTime(millisFromJavaEpoch).plusMonths(1);
return constructUniversalDate(dt);
}
@Override
protected UniversalDate incrementYear(long millisFromJavaEpoch) {
DateTime dt = new DateTime(millisFromJavaEpoch).plusYears(1);
return constructUniversalDate(dt);
}
@Override
protected String[] getMonthsArray() {
calendar = Calendar.getInstance();
String[] monthNames = new String[12];
final Map<String, Integer> monthMap =
calendar.getDisplayNames(Calendar.MONTH, Calendar.LONG, Locale.getDefault());
monthList = new ArrayList<>(monthMap.keySet());
Collections.sort(monthList, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return monthMap.get(a) - monthMap.get(b);
}
});
monthNames = monthList.toArray(monthNames);
return monthNames;
}
@Override
protected long toMillisFromJavaEpoch(int year, int month, int day) {
DateTime dt = new DateTime()
.withYear(year)
.withMonthOfYear(month)
.withDayOfMonth(day);
return dt.getMillis();
}
@Override
protected void updateGregorianDateHelperDisplay() {
}
@Override
protected void setupTouchListeners() {
}
@Override
protected void setupKeyListeners() {
}
@Override
public IAnswerData getAnswer() {
String month = (String)monthSpinner.getSelectedItem();
String day = dayText.getText().toString();
String year = yearText.getText().toString();
//All fields are empty - Like submitting a blank date
if (month.isEmpty() && day.isEmpty() && year.isEmpty()) {
setFocus(getContext());
return null;
}
//Some but not all fields are empty
if (month.isEmpty() || day.isEmpty() || year.isEmpty()) {
setFocus(getContext());
return new InvalidDateData(Localization.get("calendar.empty.fields"), new DateData(calendar.getTime()), day, month, year);
}
//Invalid year (too low)
if (Integer.parseInt(year) < MINYEAR) {
setFocus(getContext());
return new InvalidDateData(Localization.get("calendar.low.year", "" + MINYEAR), new DateData(calendar.getTime()), day, month, year);
}
//Invalid day (too high)
if (Integer.parseInt(day) > calendar.getActualMaximum(Calendar.DAY_OF_MONTH)) {
setFocus(getContext());
return new InvalidDateData(Localization.get("calendar.high.day", "" + calendar.getActualMaximum(Calendar.DAY_OF_MONTH)), new DateData(calendar.getTime()), day, month, year);
}
//Invalid day (too low)
if (Integer.parseInt(day) < 1) {
setFocus(getContext());
return new InvalidDateData(Localization.get("calendar.low.day"), new DateData(calendar.getTime()), day, month, year);
}
//Invalid year (too high)
if (Integer.parseInt(year) > maxYear) {
setFocus(getContext());
return new InvalidDateData(Localization.get("calendar.high.year", "" + maxYear), new DateData(calendar.getTime()), day, month, year);
}
return super.getAnswer();
}
private UniversalDate constructUniversalDate(DateTime dt) {
return new UniversalDate(
dt.getYear(),
dt.getMonthOfYear(),
dt.getDayOfMonth(),
dt.getMillis()
);
}
private void clearAll() {
dayText.setText("");
yearText.setText("");
monthSpinner.setSelection(EMPTY_MONTH_ENTRY_INDEX);
setFocus(getContext());
}
@Override
public void setFocus(Context context) {
super.setFocus(context);
dayText.setCursorVisible(false);
yearText.setCursorVisible(false);
}
private void refreshDisplay() {
updateDateDisplay(calendar.getTimeInMillis());
}
private void openCalendar() {
setFocus(getContext());
timeBeforeCalendarOpened = calendar.getTimeInMillis();
myCalendarFragment.show(fm, "Calendar Popup");
}
@Override
public void onCalendarClose() {
refreshDisplay();
setFocus(getContext());
}
@Override
public void onCalendarCancel() {
calendar.setTimeInMillis(timeBeforeCalendarOpened);
onCalendarClose();
}
@Override
public void setAnswer() {
if (mPrompt.getAnswerValue() != null) {
if (mPrompt.getAnswerValue() instanceof InvalidDateData) {
InvalidDateData previousDate = (InvalidDateData)mPrompt.getAnswerValue();
String day = previousDate.getDayText();
String month = previousDate.getMonthText();
String year = previousDate.getYearText();
monthSpinner.setSelection(monthList.indexOf(month)); //Update month first because selecting a month item will call refreshDisplay().
dayText.setText(day);
yearText.setText(year);
}else{
Date date = (Date)mPrompt.getAnswerValue().getValue();
updateDateDisplay(date.getTime());
}
} else {
super.clearAnswer();
}
}
}