/*******************************************************************************
* Copyright (c) 2010 Denis Solonenko.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v2.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*
* Contributors:
* Denis Solonenko - initial API and implementation
******************************************************************************/
package ru.orangesoftware.financisto2.recur;
import android.app.DatePickerDialog;
import android.app.DatePickerDialog.OnDateSetListener;
import android.content.Context;
import android.text.InputType;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;
import ru.orangesoftware.financisto2.R;
import ru.orangesoftware.financisto2.activity.ActivityLayout;
import ru.orangesoftware.financisto2.activity.ActivityLayoutListener;
import ru.orangesoftware.financisto2.activity.RecurrenceActivity;
import ru.orangesoftware.financisto2.model.MultiChoiceItem;
import ru.orangesoftware.financisto2.datetime.DateUtils;
import ru.orangesoftware.financisto2.utils.EnumUtils;
import ru.orangesoftware.financisto2.utils.LocalizableEnum;
import ru.orangesoftware.financisto2.utils.Utils;
import ru.orangesoftware.financisto2.view.NodeInflater;
import java.text.ParseException;
import java.util.*;
public class RecurrenceViewFactory {
private final RecurrenceActivity activity;
public RecurrenceViewFactory(RecurrenceActivity activity) {
this.activity = activity;
}
public RecurrenceView create(RecurrencePattern p) {
switch (p.frequency) {
case DAILY:
return new DailyView();
case WEEKLY:
return new WeeklyView();
case MONTHLY:
return new MonthlyView(p.params);
// case SEMI_MONTHLY:
// return new SemiMonthlyView();
case GEEKY:
return new GeekyView();
default:
return null;
}
}
public RecurrenceView create(RecurrenceUntil r) {
switch (r) {
case EXACTLY_TIMES:
return new ExactlyTimesView();
case STOPS_ON_DATE:
return new StopsOnDateView();
default:
return null;
}
}
public static HashMap<String, String> parseState(String state) {
if (state == null) {
return new HashMap<String, String>();
}
HashMap<String, String> map = new HashMap<String, String>();
String[] a = state.split("#");
for (String s : a) {
String[] p = s.split("@");
if (p.length == 2) {
map.put(p[0], p[1]);
}
}
return map;
}
abstract class AbstractView implements RecurrenceView, ActivityLayoutListener {
private final LocalizableEnum r;
protected final ActivityLayout x;
public AbstractView(LocalizableEnum r) {
this.r = r;
LayoutInflater layoutInflater = (LayoutInflater)activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
NodeInflater nodeInflater = new NodeInflater(layoutInflater);
this.x = new ActivityLayout(nodeInflater, this);
}
@Override
public String stateToString() {
StringBuilder sb = new StringBuilder();
sb.append(r.name()).append(":");
HashMap<String, String> state = new HashMap<String, String>();
stateToMap(state);
for (Map.Entry<String, String> e : state.entrySet()) {
sb.append(e.getKey()).append("@").append(e.getValue()).append("#");
}
return sb.toString();
}
@Override
public void stateFromString(String state) {
HashMap<String, String> map = parseState(state);
stateFromMap(map);
}
@Override
public abstract boolean validateState();
protected abstract void stateToMap(HashMap<String, String> state);
protected abstract void stateFromMap(HashMap<String, String> state);
@Override
public void onSelected(int id, List<? extends MultiChoiceItem> items) {
}
@Override
public void onSelectedId(int id, long selectedId) {
}
@Override
public void onSelectedPos(int id, int selectedPos) {
}
@Override
public void onClick(View v) {
int id = v.getId();
onClick(v, id);
}
protected abstract void onClick(View v, int id);
}
public static final String P_INTERVAL = "interval";
public static final String P_DAYS = "days";
public static final String P_COUNT = "count";
public static final String P_DATE = "date";
public static final String P_MONTHLY_PATTERN = "monthly_pattern";
public static final String P_MONTHLY_PATTERN_PARAMS = "monthly_pattern_params";
// ******************************************************************
// DAILY
// ******************************************************************
class GeekyView extends AbstractView {
private final EditText geekyEditText = new EditText(activity);
public GeekyView() {
super(RecurrenceFrequency.GEEKY);
geekyEditText.setText("FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13");
geekyEditText.setMinLines(3);
geekyEditText.setMaxLines(5);
}
@Override
public void createNodes(LinearLayout layout) {
removeAllViewsFromParent(geekyEditText);
x.addEditNode(layout, R.string.recur_rrule, geekyEditText);
}
@Override
protected void onClick(View v, int id) {
}
@Override
public boolean validateState() {
if (Utils.isEmpty(geekyEditText)) {
geekyEditText.setError(activity.getString(R.string.specify_value));
return false;
}
return true;
}
@Override
protected void stateToMap(HashMap<String, String> state) {
state.put(P_INTERVAL, geekyEditText.getText().toString().toUpperCase());
}
@Override
protected void stateFromMap(HashMap<String, String> state) {
String interval = state.get(P_INTERVAL);
geekyEditText.setText(interval != null ? interval.toUpperCase() : "");
}
}
// ******************************************************************
// DAILY
// ******************************************************************
class DailyView extends AbstractView {
private final EditText repeatDaysEditText = numericEditText(activity);
public DailyView() {
super(RecurrenceFrequency.DAILY);
repeatDaysEditText.setText("1");
}
@Override
public void createNodes(LinearLayout layout) {
removeAllViewsFromParent(repeatDaysEditText);
x.addEditNode(layout, R.string.recur_interval_every_x_day, repeatDaysEditText);
}
@Override
protected void onClick(View v, int id) {
}
@Override
public boolean validateState() {
if (Utils.isEmpty(repeatDaysEditText)) {
repeatDaysEditText.setError(activity.getString(R.string.specify_value));
return false;
}
return true;
}
@Override
protected void stateToMap(HashMap<String, String> state) {
state.put(P_INTERVAL, repeatDaysEditText.getText().toString());
}
@Override
protected void stateFromMap(HashMap<String, String> state) {
repeatDaysEditText.setText(state.get(P_INTERVAL));
}
}
// ******************************************************************
// WEEKLY
// ******************************************************************
enum DayOfWeek implements LocalizableEnum {
MON(R.id.dayMon, R.string.day_mon, "MO"),
TUE(R.id.dayTue, R.string.day_tue, "TU"),
WED(R.id.dayWed, R.string.day_wed, "WE"),
THR(R.id.dayThr, R.string.day_thr, "TH"),
FRI(R.id.dayFri, R.string.day_fri, "FR"),
SAT(R.id.daySat, R.string.day_sat, "SA"),
SUN(R.id.daySun, R.string.day_sun, "SU");
public final int checkboxId;
public final int titleId;
public final String rfcName;
private DayOfWeek(int checkboxId, int titleId, String rfcName) {
this.checkboxId = checkboxId;
this.titleId = titleId;
this.rfcName = rfcName;
}
@Override
public int getTitleId() {
return titleId;
}
}
class DayOfWeekItem implements MultiChoiceItem {
public final DayOfWeek d;
private final long id;
private final String title;
private boolean checked;
public DayOfWeekItem(DayOfWeek d) {
this.d = d;
this.id = d.checkboxId;
this.title = activity.getString(d.titleId);
}
@Override
public long getId() {
return id;
}
@Override
public String getTitle() {
return title;
}
@Override
public boolean isChecked() {
return checked;
}
@Override
public void setChecked(boolean checked) {
this.checked = checked;
}
}
class WeeklyView extends AbstractView {
private final EditText repeatWeeksEditText = numericEditText(activity);
private TextView daysOfWeekText;
private EnumSet<DayOfWeek> days = EnumSet.allOf(DayOfWeek.class);
public WeeklyView() {
super(RecurrenceFrequency.WEEKLY);
repeatWeeksEditText.setText("1");
days.remove(DayOfWeek.SAT);
days.remove(DayOfWeek.SUN);
}
private void updateDaysOfWeekText() {
daysOfWeekText.setText(daysToString());
daysOfWeekText.setError(null);
}
private String daysToString() {
StringBuilder sb = new StringBuilder();
for (DayOfWeek d : days) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(activity.getString(d.titleId));
}
if (sb.length() == 0) {
sb.append(activity.getString(R.string.no_recur));
}
return sb.toString();
}
@Override
public void createNodes(LinearLayout layout) {
removeAllViewsFromParent(repeatWeeksEditText);
x.addEditNode(layout, R.string.recur_interval_every_x_week, repeatWeeksEditText);
daysOfWeekText = x.addListNode(layout, R.id.recurrence_pattern, R.string.recurrence_weekly_days, daysToString());
}
@Override
protected void onClick(View v, int id) {
if (id == R.id.recurrence_pattern) {
ArrayList<MultiChoiceItem> items = new ArrayList<MultiChoiceItem>();
for (DayOfWeek d : DayOfWeek.values()) {
DayOfWeekItem i = new DayOfWeekItem(d);
i.setChecked(days.contains(d));
items.add(i);
}
x.selectMultiChoice(activity, R.id.recurrence_pattern, R.string.recur_interval_every_x_week, items);
}
}
@Override
public void onSelected(int id, List<? extends MultiChoiceItem> items) {
if (id == R.id.recurrence_pattern) {
days.clear();
for (MultiChoiceItem i : items) {
DayOfWeekItem di = (DayOfWeekItem)i;
if (di.isChecked()) {
days.add(di.d);
}
}
updateDaysOfWeekText();
}
}
@Override
public boolean validateState() {
if (Utils.isEmpty(repeatWeeksEditText)) {
repeatWeeksEditText.setError(activity.getString(R.string.specify_value));
return false;
}
if (days.isEmpty()) {
daysOfWeekText.setError(activity.getString(R.string.specify_value));
return false;
}
return true;
}
@Override
protected void stateToMap(HashMap<String, String> state) {
state.put(P_INTERVAL, repeatWeeksEditText.getText().toString());
StringBuilder sb = new StringBuilder();
for (DayOfWeek d : days) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(d.name());
}
state.put(P_DAYS, sb.toString());
}
@Override
protected void stateFromMap(HashMap<String, String> state) {
repeatWeeksEditText.setText(state.get(P_INTERVAL));
String s = state.get(P_DAYS);
String[] a = s.split(",");
days.clear();
for (String d : a) {
days.add(DayOfWeek.valueOf(d));
}
}
}
// ******************************************************************
// MONTHLY
// ******************************************************************
public enum MonthlyPattern implements LocalizableEnum {
EVERY_NTH_DAY(R.string.recurrence_monthly_every_nth_day),
SPECIFIC_DAY(R.string.recurrence_monthly_specific_day);
private final int titleId;
MonthlyPattern(int titleId) {
this.titleId = titleId;
}
@Override
public int getTitleId() {
return titleId;
}
}
public enum SpecificDayPrefix implements LocalizableEnum {
FIRST(R.string.first), SECOND(R.string.second),
THIRD(R.string.third), FOURTH(R.string.fourth),
LAST(R.string.last);
private final int titleId;
SpecificDayPrefix(int titleId) {
this.titleId = titleId;
}
@Override
public int getTitleId() {
return titleId;
}
}
public enum SpecificDayPostfix implements LocalizableEnum {
DAY(R.string.day),
WEEKDAY(R.string.weekday),
WEEKEND_DAY(R.string.weekend_day),
SUNDAY(R.string.sunday),
MONDAY(R.string.monday),
TUESDAY(R.string.tuesday),
WEDNESDAY(R.string.wednesday),
THURSDAY(R.string.thursday),
FRIDAY(R.string.friday),
SATURDAY(R.string.saturday);
private final int titleId;
SpecificDayPostfix(int titleId) {
this.titleId = titleId;
}
@Override
public int getTitleId() {
return titleId;
}
}
private static final int[] DAY_TITLES = {R.string.recur_interval_semi_monthly_1, R.string.recur_interval_semi_monthly_2};
abstract class AbstractMonthlyView extends AbstractView {
private int num;
public final MonthlyPattern[] pattern;
public final SpecificDayPrefix[] prefix;
public final SpecificDayPostfix[] postfix;
private final EditText repeatMonthsEditText = numericEditText(activity);
private final EditText[] everyNthDayEditText;
private final TextView[] patternText;
private final TextView[] specificDayText;
public AbstractMonthlyView(RecurrenceFrequency frequency, int num) {
super(frequency);
this.num = num;
pattern = new MonthlyPattern[num];
for (int i=0; i<num; i++) {
pattern[i] = MonthlyPattern.EVERY_NTH_DAY;
}
prefix = new SpecificDayPrefix[num];
for (int i=0; i<num; i++) {
prefix[i] = SpecificDayPrefix.FIRST;
}
postfix = new SpecificDayPostfix[num];
for (int i=0; i<num; i++) {
postfix[i] = SpecificDayPostfix.DAY;
}
everyNthDayEditText = new EditText[num];
for (int i=0; i<num; i++) {
everyNthDayEditText[i] = numericEditText(activity);
}
specificDayText = new TextView[num];
patternText = new TextView[num];
repeatMonthsEditText.setText("1");
}
@Override
public void createNodes(LinearLayout layout) {
int num = this.num;
for (int i=0; i<num; i++) {
if (num > 1) {
x.addTitleNodeNoDivider(layout, DAY_TITLES[i]);
}
patternText[i] = x.addListNode(layout, 100+i, R.string.recurrence_monthly_pattern, activity.getString(pattern[i].titleId));
switch (pattern[i]) {
case EVERY_NTH_DAY:
removeAllViewsFromParent(everyNthDayEditText[i]);
x.addEditNode(layout, R.string.recurrence_monthly_every_nth_day, everyNthDayEditText[i]);
everyNthDayEditText[i].setText("15");
break;
case SPECIFIC_DAY:
specificDayText[i] = x.addListNode(layout, 200+i, R.string.recurrence_monthly_specific_day, specificDayStr(i));
break;
}
}
removeAllViewsFromParent(repeatMonthsEditText);
x.addEditNode(layout, R.string.recur_interval_every_x_month, repeatMonthsEditText);
}
private String specificDayStr(int i) {
return activity.getString(prefix[i].titleId)+" "+activity.getString(postfix[i].titleId);
}
@Override
protected void onClick(View v, int id) {
if (id > 199) {
int k = id-200;
String[] prefixes = EnumUtils.getLocalizedValues(activity, SpecificDayPrefix.values());
String[] postfixes = EnumUtils.getLocalizedValues(activity, SpecificDayPostfix.values());
int prefixesLength = prefixes.length;
int postfixesLength = postfixes.length;
String[] items = new String[prefixesLength*postfixesLength];
for (int i=0; i<prefixesLength; i++) {
for (int j=0; j<postfixesLength; j++) {
items[i*postfixesLength+j] = prefixes[i]+" "+postfixes[j];
}
}
ArrayAdapter<String> adapter = new ArrayAdapter<String>(activity, android.R.layout.simple_spinner_dropdown_item, items);
int selected = prefix[k].ordinal()*postfixesLength+postfix[k].ordinal();
x.selectPosition(activity, id, R.string.recurrence_period, adapter, selected);
} else {
int k = id-100;
ArrayAdapter<String> adapter = EnumUtils.createDropDownAdapter(activity, MonthlyPattern.values());
x.selectPosition(activity, id, R.string.recurrence_period, adapter, pattern[k].ordinal());
}
}
@Override
public void onSelectedPos(int id, int selectedPos) {
if (id > 199) {
int k = id-200;
SpecificDayPrefix[] prefixes = SpecificDayPrefix.values();
SpecificDayPostfix[] postfixes = SpecificDayPostfix.values();
int postfixesLength = postfixes.length;
int selectedPrefix = selectedPos/postfixesLength;
int selectedPostfix = selectedPos - selectedPrefix*postfixesLength;
prefix[k] = prefixes[selectedPrefix];
postfix[k] = postfixes[selectedPostfix];
specificDayText[k].setText(specificDayStr(k));
} else {
int k = id-100;
pattern[k] = MonthlyPattern.values()[selectedPos];
activity.createNodes();
}
}
@Override
public boolean validateState() {
int num = this.num;
for (int i=0; i<num; i++) {
if (pattern[i] == MonthlyPattern.EVERY_NTH_DAY) {
if (Utils.isEmpty(everyNthDayEditText[i])) {
everyNthDayEditText[i].setError(activity.getString(R.string.specify_value));
return false;
}
}
}
if (Utils.isEmpty(repeatMonthsEditText)) {
repeatMonthsEditText.setError(activity.getString(R.string.specify_value));
return false;
}
return true;
}
@Override
protected void stateToMap(HashMap<String, String> state) {
state.put(P_INTERVAL, repeatMonthsEditText.getText().toString());
state.put(P_COUNT, String.valueOf(num));
for (int i=0; i<num; i++) {
String pfx = "_"+i;
state.put(P_MONTHLY_PATTERN+pfx, pattern[i].name());
switch (pattern[i]) {
case EVERY_NTH_DAY:
state.put(P_MONTHLY_PATTERN_PARAMS+pfx, everyNthDayEditText[i].getText().toString());
break;
case SPECIFIC_DAY:
state.put(P_MONTHLY_PATTERN_PARAMS+pfx, prefix[i].name()+"-"+postfix[i].name());
break;
}
}
}
@Override
protected void stateFromMap(HashMap<String, String> state) {
repeatMonthsEditText.setText(state.get(P_INTERVAL));
num = Integer.parseInt(state.get(P_COUNT));
for (int i=0; i<num; i++) {
String pfx = "_"+i;
pattern[i] = MonthlyPattern.valueOf(state.get(P_MONTHLY_PATTERN+pfx));
patternText[i].setText(pattern[i].titleId);
switch (pattern[i]) {
case EVERY_NTH_DAY:
everyNthDayEditText[i].setText(state.get(P_MONTHLY_PATTERN_PARAMS+pfx));
break;
case SPECIFIC_DAY:
String s = state.get(P_MONTHLY_PATTERN_PARAMS+pfx);
String[] a = s.split("-");
prefix[i] = SpecificDayPrefix.valueOf(a[0]);
postfix[i] = SpecificDayPostfix.valueOf(a[1]);
specificDayText[i].setText(specificDayStr(i));
break;
}
}
}
}
class MonthlyView extends AbstractMonthlyView {
public MonthlyView(String state) {
super(RecurrenceFrequency.MONTHLY, 1);
HashMap<String, String> map = parseState(state);
if (!map.isEmpty()) {
pattern[0] = MonthlyPattern.valueOf(map.get(P_MONTHLY_PATTERN+"_0"));
}
}
}
// ******************************************************************
// SEMI-MONTHLY
// ******************************************************************
// class SemiMonthlyView extends AbstractMonthlyView {
//
// public SemiMonthlyView() {
// super(RecurrenceFrequency.SEMI_MONTHLY, 2);
// }
//
// }
// EXACTLY_TIMES
class ExactlyTimesView extends AbstractView {
private final EditText countEditText = numericEditText(activity);
public ExactlyTimesView() {
super(RecurrenceUntil.EXACTLY_TIMES);
countEditText.setText("10");
}
@Override
public void createNodes(LinearLayout layout) {
removeAllViewsFromParent(countEditText);
x.addEditNode(layout, R.string.recur_exactly_n_times, countEditText);
}
@Override
protected void onClick(View v, int id) {
}
@Override
public boolean validateState() {
if (Utils.isEmpty(countEditText)) {
countEditText.setError(activity.getString(R.string.specify_value));
return false;
}
return true;
}
@Override
protected void stateToMap(HashMap<String, String> state) {
state.put(P_COUNT, countEditText.getText().toString());
}
@Override
protected void stateFromMap(HashMap<String, String> state) {
countEditText.setText(state.get(P_COUNT));
}
}
// STOPS_ON_DATE
class StopsOnDateView extends AbstractView {
private TextView onDateText;
private final Calendar c = Calendar.getInstance();
public StopsOnDateView() {
super(RecurrenceUntil.STOPS_ON_DATE);
DateUtils.startOfDay(c);
c.add(Calendar.MONTH, 6);
}
@Override
public void createNodes(LinearLayout layout) {
onDateText = x.addInfoNode(layout, R.id.date, R.string.recur_repeat_stops_on,
DateUtils.getMediumDateFormat(activity).format(c.getTime()));
}
@Override
protected void onClick(View v, int id) {
new DatePickerDialog(activity, new OnDateSetListener(){
@Override
public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
c.set(Calendar.YEAR, year);
c.set(Calendar.MONTH, monthOfYear);
c.set(Calendar.DAY_OF_MONTH, dayOfMonth);
onDateText.setText(DateUtils.getMediumDateFormat(activity).format(c.getTime()));
}
}, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH)).show();
}
@Override
public boolean validateState() {
return true;
}
@Override
protected void stateToMap(HashMap<String, String> state) {
DateUtils.startOfDay(c);
state.put(P_DATE, DateUtils.FORMAT_DATE_RFC_2445.format(c.getTime()));
}
@Override
protected void stateFromMap(HashMap<String, String> state) {
Date d;
try {
d = DateUtils.FORMAT_DATE_RFC_2445.parse(state.get(P_DATE));
} catch (ParseException e) {
throw new IllegalArgumentException(state.get(P_DATE));
}
c.setTime(d);
DateUtils.startOfDay(c);
onDateText.setText(DateUtils.getMediumDateFormat(activity).format(c.getTime()));
}
}
private static EditText numericEditText(Context context) {
EditText et = new EditText(context);
et.setInputType(InputType.TYPE_CLASS_NUMBER);
return et;
}
public void removeAllViewsFromParent(View v) {
if (v.getParent() != null) {
((ViewGroup)v.getParent()).removeAllViews();
}
}
}