package org.commcare.views.widgets;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.TextView;
import org.commcare.dalvik.R;
import org.javarosa.core.model.data.DateData;
import org.javarosa.core.model.data.IAnswerData;
import org.javarosa.form.api.FormEntryPrompt;
import org.javarosa.xform.util.UniversalDate;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static org.javarosa.xform.util.UniversalDate.MILLIS_IN_DAY;
/**
* Universal Date Widget, extended to work with any calendar system.
*
* @author Alex Little (alex@alexlittle.net), Richard Lu
*/
public abstract class AbstractUniversalDateWidget extends QuestionWidget {
private TextView txtMonth;
private TextView txtDay;
private TextView txtYear;
private TextView txtGregorian;
protected final String[] monthsArray;
protected int monthArrayPointer;
private final Button btnDayUp;
private final Button btnMonthUp;
private final Button btnYearUp;
private final Button btnDayDown;
private final Button btnMonthDown;
private final Button btnYearDown;
private ScheduledExecutorService mUpdater;
private final Handler mDayHandler;
private final Handler mMonthHandler;
private final Handler mYearHandler;
private static final int MSG_INC = 0;
private static final int MSG_DEC = 1;
// Alter this to make the button more/less sensitive to an initial long press
private static final int INITIAL_DELAY = 500;
// Alter this to vary how rapidly the date increases/decreases on long press
private static final int PERIOD = 200;
private class UpdateTask implements Runnable {
private final boolean mInc;
private final Handler mHandler;
public UpdateTask(boolean inc, Handler h) {
mInc = inc;
mHandler = h;
}
@Override
public void run() {
if (mInc) {
mHandler.sendEmptyMessage(MSG_INC);
} else {
mHandler.sendEmptyMessage(MSG_DEC);
}
}
}
public AbstractUniversalDateWidget(Context context, FormEntryPrompt prompt) {
super(context, prompt);
monthsArray = getMonthsArray();
inflateView(context);
/*
* Initialise handlers for incrementing/decrementing dates
*/
mDayHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INC:
incrementDay();
return;
case MSG_DEC:
decrementDay();
return;
}
super.handleMessage(msg);
}
};
mMonthHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INC:
incrementMonth();
return;
case MSG_DEC:
decrementMonth();
return;
}
super.handleMessage(msg);
}
};
mYearHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INC:
incrementYear();
return;
case MSG_DEC:
decrementYear();
return;
}
super.handleMessage(msg);
}
};
initText();
// action buttons
btnDayUp = (Button)findViewById(R.id.dayupbtn);
btnMonthUp = (Button)findViewById(R.id.monthupbtn);
btnYearUp = (Button)findViewById(R.id.yearupbtn);
btnDayDown = (Button)findViewById(R.id.daydownbtn);
btnMonthDown = (Button)findViewById(R.id.monthdownbtn);
btnYearDown = (Button)findViewById(R.id.yeardownbtn);
// button click listeners
btnDayUp.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mUpdater == null) {
incrementDay();
setFocus(getContext());
}
}
});
btnMonthUp.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mUpdater == null) {
incrementMonth();
setFocus(getContext());
}
}
});
btnYearUp.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mUpdater == null) {
setFocus(getContext());
incrementYear();
}
}
});
btnDayDown.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mUpdater == null) {
decrementDay();
setFocus(getContext());
}
}
});
btnMonthDown.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mUpdater == null) {
decrementMonth();
setFocus(getContext());
}
}
});
btnYearDown.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mUpdater == null) {
decrementYear();
setFocus(getContext());
}
}
});
setupTouchListeners();
setupKeyListeners();
// If there's an answer, use it.
setAnswer();
}
protected void setupKeyListeners() {
// button key listeners
btnDayUp.setOnKeyListener(new EDWKeyListener(btnDayUp, mDayHandler));
btnDayDown.setOnKeyListener(new EDWKeyListener(btnDayUp, mDayHandler));
btnMonthUp.setOnKeyListener(new EDWKeyListener(btnMonthUp, mMonthHandler));
btnMonthDown.setOnKeyListener(new EDWKeyListener(btnMonthUp, mMonthHandler));
btnYearUp.setOnKeyListener(new EDWKeyListener(btnYearUp, mYearHandler));
btnYearDown.setOnKeyListener(new EDWKeyListener(btnYearUp, mYearHandler));
}
protected void setupTouchListeners() {
// button touch listeners
btnDayUp.setOnTouchListener(new EDWTouchListener(btnDayUp, mDayHandler));
btnDayDown.setOnTouchListener(new EDWTouchListener(btnDayUp, mDayHandler));
btnMonthUp.setOnTouchListener(new EDWTouchListener(btnMonthUp, mMonthHandler));
btnMonthDown.setOnTouchListener(new EDWTouchListener(btnMonthUp, mMonthHandler));
btnYearUp.setOnTouchListener(new EDWTouchListener(btnYearUp, mYearHandler));
btnYearDown.setOnTouchListener(new EDWTouchListener(btnYearUp, mYearHandler));
}
protected void initText() {
// Date fields
txtDay = (TextView)findViewById(R.id.daytxt);
txtMonth = (TextView)findViewById(R.id.monthtxt);
txtYear = (TextView)findViewById(R.id.yeartxt);
txtGregorian = (TextView)findViewById(R.id.dateGregorian);
}
protected void inflateView(Context context) {
LayoutInflater vi = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View vv = vi.inflate(R.layout.universal_date_widget, null);
addView(vv);
}
/**
* Decrement 1 month in custom calendar system from the given millisecond instant.
*
* @param millisFromJavaEpoch Instant to decrement month from
* @return UniversalDate representing the minus-1-month instant
*/
protected abstract UniversalDate decrementMonth(long millisFromJavaEpoch);
/**
* Decrement 1 year in custom calendar system from the given millisecond instant.
*
* @param millisFromJavaEpoch Instant to decrement year from
* @return UniversalDate representing the minus-1-year instant
*/
protected abstract UniversalDate decrementYear(long millisFromJavaEpoch);
/**
* Get date in custom calendar system from the given millisecond instant.
*
* @param millisFromJavaEpoch Instant to get date with
* @return UniversalDate representing the given instant
*/
protected abstract UniversalDate fromMillis(long millisFromJavaEpoch);
/**
* Fetch the array of Strings representing month names in custom calendar system.
*
* @return Array of strings, length must match number of months in calendar system.
*/
protected abstract String[] getMonthsArray();
/**
* Increment 1 month in custom calendar system from the given millisecond instant.
*
* @param millisFromJavaEpoch Instant to increment month from
* @return UniversalDate representing the plus-1-month instant
*/
protected abstract UniversalDate incrementMonth(long millisFromJavaEpoch);
/**
* Increment 1 year in custom calendar system from the given millisecond instant.
*
* @param millisFromJavaEpoch Instant to increment year from
* @return UniversalDate representing the plus-1-year instant
*/
protected abstract UniversalDate incrementYear(long millisFromJavaEpoch);
/**
* Translate the given date in the custom calendar system to
* standard milliseconds from Java epoch.
*
* @return Milliseconds since Java epoch
*/
protected abstract long toMillisFromJavaEpoch(int year, int month, int day);
/**
* Resets date to today
*/
@Override
public void clearAnswer() {
Date date = new Date();
updateDateDisplay(date.getTime());
updateGregorianDateHelperDisplay();
}
/**
* @return the date for storing in ODK
*/
@Override
public IAnswerData getAnswer() {
Date date = getDateAsGregorian();
return new DateData(date);
}
@Override
public void setFocus(Context context) {
// Hide the soft keyboard if it's showing.
InputMethodManager inputManager =
(InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.hideSoftInputFromWindow(this.getWindowToken(), 0);
}
@Override
public void setOnLongClickListener(OnLongClickListener l) {
//super.setOnLongClickListener(l);
}
/**
* Start Updater, for when using long press to increment/decrement date without repeated pressing on the buttons
*/
private void startUpdating(boolean inc, Handler mHandler) {
if (mUpdater != null) {
Log.e(getClass().getSimpleName(), "Another executor is still active");
return;
}
mUpdater = Executors.newSingleThreadScheduledExecutor();
mUpdater.scheduleAtFixedRate(new UpdateTask(inc, mHandler), INITIAL_DELAY, PERIOD,
TimeUnit.MILLISECONDS);
}
/**
* Stop incrementing/decrementing
*/
private void stopUpdating() {
mUpdater.shutdownNow();
mUpdater = null;
}
/**
* Increase by 1 day
*/
private void incrementDay() {
// get the current date into gregorian, add one and redisplay
updateDateDisplay(getDateAsGregorian().getTime() + MILLIS_IN_DAY);
updateGregorianDateHelperDisplay();
}
/**
* Increase by 1 month
*/
private void incrementMonth() {
UniversalDate dt = incrementMonth(getCurrentMillis());
updateDateDisplay(dt.millisFromJavaEpoch);
updateGregorianDateHelperDisplay();
}
/**
* Increase by 1 year
*/
private void incrementYear() {
UniversalDate dt = incrementYear(getCurrentMillis());
updateDateDisplay(dt.millisFromJavaEpoch);
updateGregorianDateHelperDisplay();
}
/**
* Decrease by 1 day
*/
private void decrementDay() {
updateDateDisplay(getDateAsGregorian().getTime() - MILLIS_IN_DAY);
updateGregorianDateHelperDisplay();
}
/**
* Decrease by 1 month
*/
private void decrementMonth() {
UniversalDate dt = decrementMonth(getCurrentMillis());
updateDateDisplay(dt.millisFromJavaEpoch);
updateGregorianDateHelperDisplay();
}
/**
* Decrease by 1 year
*/
private void decrementYear() {
UniversalDate dt = decrementYear(getCurrentMillis());
updateDateDisplay(dt.millisFromJavaEpoch);
updateGregorianDateHelperDisplay();
}
/**
* Initial date display
*/
protected void setAnswer() {
if (mPrompt.getAnswerValue() != null) {
Date date = (Date)mPrompt.getAnswerValue().getValue();
updateDateDisplay(date.getTime());
updateGregorianDateHelperDisplay();
} else {
// create date widget with current date
clearAnswer();
}
}
/**
* Get the current widget date in Gregorian chronology
*/
protected Date getDateAsGregorian() {
return new Date(getCurrentMillis());
}
/**
* Get the current widget date in milliseconds since Java epoch
*/
protected long getCurrentMillis() {
int day = Integer.parseInt(txtDay.getText().toString());
int month = monthArrayPointer + 1;
int year = Integer.parseInt(txtYear.getText().toString());
return toMillisFromJavaEpoch(year, month, day);
}
/**
* Update the widget date to display the amended date
*/
protected void updateDateDisplay(long millisFromJavaEpoch) {
UniversalDate dateUniv = fromMillis(millisFromJavaEpoch);
txtDay.setText(String.format("%02d", dateUniv.day));
txtMonth.setText(monthsArray[dateUniv.month - 1]);
monthArrayPointer = dateUniv.month - 1;
txtYear.setText(String.format("%04d", dateUniv.year));
}
/**
* Update the widget helper date text (useful for those who don't know the other calendar)
*/
protected void updateGregorianDateHelperDisplay() {
DateTime dtLMDGreg = new DateTime(getCurrentMillis());
DateTimeFormatter fmt = DateTimeFormat.forPattern("d MMMM yyyy");
String str = fmt.print(dtLMDGreg);
txtGregorian.setText("(" + str + ")");
}
/**
* Listens for button being pressed by touchscreen
*
* @author alex
*/
private class EDWTouchListener implements OnTouchListener {
private final View mView;
private final Handler mHandler;
public EDWTouchListener(View mV, Handler mH) {
mView = mV;
mHandler = mH;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
boolean isReleased = event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL;
boolean isPressed = event.getAction() == MotionEvent.ACTION_DOWN;
if (isReleased) {
stopUpdating();
} else if (isPressed) {
startUpdating(v == mView, mHandler);
}
return false;
}
}
/**
* Listens for button being pressed by keypad/trackball
*
* @author alex
*/
private class EDWKeyListener implements OnKeyListener {
private final View mView;
private final Handler mHandler;
public EDWKeyListener(View mV, Handler mH) {
mView = mV;
mHandler = mH;
}
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
boolean isKeyOfInterest = keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER;
boolean isReleased = event.getAction() == KeyEvent.ACTION_UP;
boolean isPressed = event.getAction() == KeyEvent.ACTION_DOWN
&& event.getAction() != KeyEvent.ACTION_MULTIPLE;
if (isKeyOfInterest && isReleased) {
stopUpdating();
} else if (isKeyOfInterest && isPressed) {
startUpdating(v == mView, mHandler);
}
return false;
}
}
}