diff --git a/app/src/main/java/com/todoroo/astrid/data/Task.java b/app/src/main/java/com/todoroo/astrid/data/Task.java index 13c40c132..672782b2a 100644 --- a/app/src/main/java/com/todoroo/astrid/data/Task.java +++ b/app/src/main/java/com/todoroo/astrid/data/Task.java @@ -10,6 +10,7 @@ import android.content.ContentValues; import android.net.Uri; import android.text.TextUtils; +import com.google.ical.values.RRule; import com.todoroo.andlib.data.Property; import com.todoroo.andlib.data.Property.IntegerProperty; import com.todoroo.andlib.data.Property.LongProperty; @@ -427,6 +428,10 @@ public class Task extends RemoteModel { setValue(RECURRENCE, recurrence); } + public void setRecurrence(RRule rrule, boolean afterCompletion) { + setRecurrence(rrule.toIcal() + (afterCompletion ? ";FROM=COMPLETION" : "")); + } + public Long getCreationDate() { return getValue(CREATION_DATE); } diff --git a/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.java b/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.java index 4f6a6619a..f626a1965 100644 --- a/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.java +++ b/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.java @@ -36,7 +36,6 @@ import org.tasks.dialogs.DialogBuilder; import org.tasks.injection.ForActivity; import org.tasks.injection.FragmentComponent; import org.tasks.locale.Locale; -import org.tasks.preferences.Preferences; import org.tasks.repeats.CustomRecurrenceDialog; import org.tasks.themes.Theme; import org.tasks.time.DateTime; @@ -96,7 +95,6 @@ public class RepeatControlSet extends TaskEditControlFragment public static final int TYPE_COMPLETION_DATE = 2; @Inject DialogBuilder dialogBuilder; - @Inject Preferences preferences; @Inject @ForActivity Context context; @Inject Theme theme; @Inject Locale locale; @@ -194,7 +192,8 @@ public class RepeatControlSet extends TaskEditControlFragment frequency == HOURLY || frequency == MINUTELY || rrule.getUntil() != null || - rrule.getInterval() != 1; + rrule.getInterval() != 1 || + rrule.getCount() != 0; } @OnClick(R.id.display_row_edit) @@ -344,15 +343,21 @@ public class RepeatControlSet extends TaskEditControlFragment int interval = rrule.getInterval(); Frequency frequency = rrule.getFreq(); DateTime repeatUntil = rrule.getUntil() == null ? null : DateTime.from(rrule.getUntil()); + int count = rrule.getCount(); + String countString = count > 0 ? getContext().getResources().getQuantityString(R.plurals.repeat_times, count) : ""; if (interval == 1) { String frequencyString = getString(getSingleFrequencyResource(frequency)); if (frequency == WEEKLY && !rrule.getByDay().isEmpty()) { String dayString = getDayString(); - if (repeatUntil == null) { + if (count > 0) { + return getString(R.string.repeats_single_on_number_of_times, frequencyString, dayString, count, countString); + } else if (repeatUntil == null) { return getString(R.string.repeats_single_on, frequencyString, dayString); } else { return getString(R.string.repeats_single_on_until, frequencyString, dayString, DateUtilities.getLongDateString(repeatUntil)); } + } else if (count > 0) { + return getString(R.string.repeats_single_number_of_times, frequencyString, count, countString); } else if (repeatUntil == null) { return getString(R.string.repeats_single, frequencyString); } else { @@ -363,11 +368,15 @@ public class RepeatControlSet extends TaskEditControlFragment String frequencyPlural = getResources().getQuantityString(plural, interval, interval); if (frequency == WEEKLY && !rrule.getByDay().isEmpty()) { String dayString = getDayString(); - if (repeatUntil == null) { + if (count > 0) { + return getString(R.string.repeats_plural_on_number_of_times, frequencyPlural, dayString, count, countString); + } else if (repeatUntil == null) { return getString(R.string.repeats_plural_on, frequencyPlural, dayString); } else { return getString(R.string.repeats_plural_on_until, frequencyPlural, dayString, DateUtilities.getLongDateString(repeatUntil)); } + } else if (count > 0) { + return getString(R.string.repeats_plural_number_of_times, frequencyPlural, count, countString); } else if (repeatUntil == null) { return getString(R.string.repeats_plural, frequencyPlural); } else { diff --git a/app/src/main/java/com/todoroo/astrid/repeats/RepeatTaskHelper.java b/app/src/main/java/com/todoroo/astrid/repeats/RepeatTaskHelper.java index 5ecc0a4dd..2a4fdec31 100644 --- a/app/src/main/java/com/todoroo/astrid/repeats/RepeatTaskHelper.java +++ b/app/src/main/java/com/todoroo/astrid/repeats/RepeatTaskHelper.java @@ -61,7 +61,9 @@ public class RepeatTaskHelper { if(recurrence != null && recurrence.length() > 0) { long newDueDate; + RRule rrule; try { + rrule = initRRule(task.getRecurrenceWithoutFrom()); newDueDate = computeNextDueDate(task, recurrence, repeatAfterCompletion); if(newDueDate == -1) { return; @@ -78,6 +80,12 @@ public class RepeatTaskHelper { return; } + int count = rrule.getCount(); + if (count > 1) { + rrule.setCount(count - 1); + task.setRecurrence(rrule, repeatAfterCompletion); + } + rescheduleTask(task, newDueDate); rescheduleAlarms(task.getId(), oldDueDate, newDueDate); diff --git a/app/src/main/java/org/tasks/receivers/RepeatConfirmationReceiver.java b/app/src/main/java/org/tasks/receivers/RepeatConfirmationReceiver.java index 552adfe09..c48f938d5 100644 --- a/app/src/main/java/org/tasks/receivers/RepeatConfirmationReceiver.java +++ b/app/src/main/java/org/tasks/receivers/RepeatConfirmationReceiver.java @@ -5,7 +5,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import com.todoroo.andlib.data.Property; +import com.google.ical.values.RRule; import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.astrid.activity.TaskListActivity; import com.todoroo.astrid.activity.TaskListFragment; @@ -16,21 +16,14 @@ import com.todoroo.astrid.data.Task; import org.tasks.R; import org.tasks.analytics.Tracker; +import java.text.ParseException; + import javax.inject.Inject; import timber.log.Timber; public class RepeatConfirmationReceiver extends BroadcastReceiver { - private final Property[] REPEAT_RESCHEDULED_PROPERTIES = - new Property[]{ - Task.ID, - Task.TITLE, - Task.DUE_DATE, - Task.HIDE_UNTIL, - Task.REPEAT_UNTIL - }; - private final Activity activity; private final Tracker tracker; private final TaskDao taskDao; @@ -57,7 +50,7 @@ public class RepeatConfirmationReceiver extends BroadcastReceiver { if (taskId > 0) { long oldDueDate = intent.getLongExtra(AstridApiConstants.EXTRAS_OLD_DUE_DATE, 0); long newDueDate = intent.getLongExtra(AstridApiConstants.EXTRAS_NEW_DUE_DATE, 0); - Task task = taskDao.fetch(taskId, REPEAT_RESCHEDULED_PROPERTIES); + Task task = taskDao.fetch(taskId); try { showSnackbar(taskListFragment, task, oldDueDate, newDueDate); @@ -74,6 +67,16 @@ public class RepeatConfirmationReceiver extends BroadcastReceiver { .setAction(R.string.DLG_undo, v -> { task.setDueDateAdjustingHideUntil(oldDueDate); task.setCompletionDate(0L); + try { + RRule rrule = new RRule(task.getRecurrenceWithoutFrom()); + int count = rrule.getCount(); + if (count > 0) { + rrule.setCount(count + 1); + } + task.setRecurrence(rrule, task.repeatAfterCompletion()); + } catch (ParseException e) { + Timber.e(e, e.getMessage()); + } taskDao.save(task); }) .show(); diff --git a/app/src/main/java/org/tasks/repeats/CustomRecurrenceDialog.java b/app/src/main/java/org/tasks/repeats/CustomRecurrenceDialog.java index f0cd09cb5..c44bce3b4 100644 --- a/app/src/main/java/org/tasks/repeats/CustomRecurrenceDialog.java +++ b/app/src/main/java/org/tasks/repeats/CustomRecurrenceDialog.java @@ -11,6 +11,8 @@ import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.LinearLayout; @@ -39,7 +41,6 @@ import org.tasks.themes.ThemeAccent; import org.tasks.time.DateTime; import java.text.DateFormatSymbols; -import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; @@ -51,7 +52,6 @@ import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnItemSelected; import butterknife.OnTextChanged; -import timber.log.Timber; import static android.support.v4.content.ContextCompat.getColor; import static com.google.ical.values.Frequency.DAILY; @@ -103,8 +103,10 @@ public class CustomRecurrenceDialog extends InjectingDialogFragment { @BindView(R.id.repeat_until) Spinner repeatUntilSpinner; @BindView(R.id.frequency) Spinner frequencySpinner; - @BindView(R.id.repeatValue) EditText intervalEditText; + @BindView(R.id.intervalValue) EditText intervalEditText; @BindView(R.id.intervalText) TextView intervalTextView; + @BindView(R.id.repeatTimesValue) EditText repeatTimes; + @BindView(R.id.repeatTimesText) TextView repeatTimesText; private ArrayAdapter repeatUntilAdapter; private final List repeatUntilOptions = new ArrayList<>(); @@ -140,7 +142,27 @@ public class CustomRecurrenceDialog extends InjectingDialogFragment { intervalEditText.setText(locale.formatNumber(rrule.getInterval())); - repeatUntilAdapter = new ArrayAdapter<>(context, R.layout.simple_spinner_item, repeatUntilOptions); + repeatUntilAdapter = new ArrayAdapter(context, R.layout.simple_spinner_item, repeatUntilOptions) { + @Override + public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + ViewGroup vg = (ViewGroup) inflater.inflate(R.layout.simple_spinner_dropdown_item, parent, false); + ((TextView) vg.findViewById(R.id.text1)).setText(getItem(position)); + return vg; + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + int selectedItemPosition = position; + if (parent instanceof AdapterView) { + selectedItemPosition = ((AdapterView) parent).getSelectedItemPosition(); + } + TextView tv = (TextView) inflater.inflate(android.R.layout.simple_spinner_item, parent, false); + tv.setPadding(0, 0, 0, 0); + tv.setText(repeatUntilOptions.get(selectedItemPosition)); + return tv; + } + }; repeatUntilAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); repeatUntilSpinner.setAdapter(repeatUntilAdapter); updateRepeatUntilOptions(); @@ -220,6 +242,18 @@ public class CustomRecurrenceDialog extends InjectingDialogFragment { intervalTextView.setText(quantityString); } + private void setCount(int count, boolean updateEditText) { + rrule.setCount(count); + if (updateEditText) { + intervalEditText.setText(locale.formatNumber(count)); + } + updateCountText(); + } + + private void updateCountText() { + repeatTimesText.setText(getResources().getQuantityString(R.plurals.repeat_times, rrule.getCount())); + } + private int getFrequencyPlural() { switch (rrule.getFreq()) { case MINUTELY: @@ -241,18 +275,19 @@ public class CustomRecurrenceDialog extends InjectingDialogFragment { @OnItemSelected(R.id.repeat_until) public void onRepeatUntilChanged(int position) { - if (repeatUntilOptions.size() == 2) { - if (position == 0) { - setRepeatUntilValue(0); - } else { - repeatUntilClick(); - } - } else { - if (position == 1) { - setRepeatUntilValue(0); - } else if (position == 2) { - repeatUntilClick(); - } + if (repeatUntilOptions.size() == 4) { + position--; + } + if (position == 0) { + rrule.setUntil(null); + rrule.setCount(0); + updateRepeatUntilOptions(); + } else if (position == 1) { + repeatUntilClick(); + } else if (position == 2) { + rrule.setUntil(null); + rrule.setCount(Math.max(rrule.getCount(), 1)); + updateRepeatUntilOptions(); } } @@ -261,7 +296,7 @@ public class CustomRecurrenceDialog extends InjectingDialogFragment { setFrequency(FREQUENCIES.get(position)); } - @OnTextChanged(R.id.repeatValue) + @OnTextChanged(R.id.intervalValue) public void onRepeatValueChanged(CharSequence text) { Integer value = locale.parseInteger(text.toString()); if (value == null) { @@ -274,9 +309,17 @@ public class CustomRecurrenceDialog extends InjectingDialogFragment { } } - private void setRepeatUntilValue(long newValue) { - rrule.setUntil(new DateTime(newValue).toDateValue()); - updateRepeatUntilOptions(); + @OnTextChanged(R.id.repeatTimesValue) + public void onRepeatTimesValueChanged(CharSequence text) { + Integer value = locale.parseInteger(text.toString()); + if (value == null) { + return; + } + if (value < 1) { + setCount(1, true); + } else { + setCount(value, false); + } } private void repeatUntilClick() { @@ -289,11 +332,24 @@ public class CustomRecurrenceDialog extends InjectingDialogFragment { private void updateRepeatUntilOptions() { repeatUntilOptions.clear(); long repeatUntil = DateTime.from(rrule.getUntil()).getMillis(); + int count = rrule.getCount(); if (repeatUntil > 0) { repeatUntilOptions.add(getString(R.string.repeat_until, getDisplayString(context, repeatUntil))); + repeatTimes.setVisibility(View.GONE); + repeatTimesText.setVisibility(View.GONE); + } else if (count > 0) { + repeatUntilOptions.add(getString(R.string.repeat_occurs)); + repeatTimes.setText(locale.formatNumber(count)); + repeatTimes.setVisibility(View.VISIBLE); + updateCountText(); + repeatTimesText.setVisibility(View.VISIBLE); + } else { + repeatTimes.setVisibility(View.GONE); + repeatTimesText.setVisibility(View.GONE); } repeatUntilOptions.add(getString(R.string.repeat_forever)); repeatUntilOptions.add(getString(R.string.repeat_until, "").trim()); + repeatUntilOptions.add(getString(R.string.repeat_number_of_times)); repeatUntilAdapter.notifyDataSetChanged(); repeatUntilSpinner.setSelection(0); } @@ -302,10 +358,10 @@ public class CustomRecurrenceDialog extends InjectingDialogFragment { public void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_PICK_DATE) { if (resultCode == Activity.RESULT_OK) { - setRepeatUntilValue(data.getLongExtra(DatePickerActivity.EXTRA_TIMESTAMP, 0L)); - } else { - setRepeatUntilValue(DateTime.from(rrule.getUntil()).getMillis()); + rrule.setUntil(new DateTime(data.getLongExtra(DatePickerActivity.EXTRA_TIMESTAMP, 0L)).toDateValue()); + rrule.setCount(0); } + updateRepeatUntilOptions(); } super.onActivityResult(requestCode, resultCode, data); } diff --git a/app/src/main/res/layout/control_set_repeat.xml b/app/src/main/res/layout/control_set_repeat.xml index 6145ec81b..df469312c 100644 --- a/app/src/main/res/layout/control_set_repeat.xml +++ b/app/src/main/res/layout/control_set_repeat.xml @@ -10,7 +10,6 @@ android:orientation="vertical"> @@ -54,7 +53,7 @@ android:textAppearance="@style/TextAppearance" /> - + android:orientation="horizontal"> + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 53816c8dc..5879fb13a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -520,6 +520,10 @@ File %1$s contained %2$s.\n\n REPEAT MONTHLY REPEAT YEARLY + + time + times + minute minutes @@ -582,6 +586,8 @@ File %1$s contained %2$s.\n\n Repeat until %s + Occurs a number of times + Occurs %1$s rescheduled for %2$s @@ -835,7 +841,9 @@ File %1$s contained %2$s.\n\n Repeats %s Repeats %s on %s Repeats %s until %s + Repeats %s, occurs %d %s Repeats %s on %s until %s + Repeats %s on %s, occurs %d %s minutely hourly daily @@ -843,9 +851,11 @@ File %1$s contained %2$s.\n\n monthly yearly Repeats every %s - Repeats every %s until %s Repeats every %s on %s + Repeats every %s until %s + Repeats every %s, occurs %d %s Repeats every %s on %s until %s + Repeats every %s on %s, occurs %d %s ,\u0020 Don\'t add to calendar Default calendar