/* * Copyright (c) 2014 - 2015 Ngewi Fet <ngewif@gmail.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.gnucash.android.model; import android.content.Context; import android.support.annotation.NonNull; import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; import org.joda.time.LocalDateTime; import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.Locale; import java.util.TimeZone; /** * Represents a scheduled event which is stored in the database and run at regular mPeriod * * @author Ngewi Fet <ngewif@gmail.com> */ public class ScheduledAction extends BaseModel{ private long mStartDate; private long mEndDate; private String mTag; /** * Recurrence of this scheduled action */ private Recurrence mRecurrence; /** * Types of events which can be scheduled */ public enum ActionType {TRANSACTION, BACKUP} /** * Next scheduled run of Event */ private long mLastRun = 0; /** * Unique ID of the template from which the recurring event will be executed. * For example, transaction UID */ private String mActionUID; /** * Flag indicating if this event is enabled or not */ private boolean mIsEnabled; /** * Type of event being scheduled */ private ActionType mActionType; /** * Number of times this event is planned to be executed */ private int mTotalFrequency = 0; /** * How many times this action has already been executed */ private int mExecutionCount = 0; /** * Flag for whether the scheduled transaction should be auto-created */ private boolean mAutoCreate = true; private boolean mAutoNotify = false; private int mAdvanceCreateDays = 0; private int mAdvanceNotifyDays = 0; private String mTemplateAccountUID; public ScheduledAction(ActionType actionType){ mActionType = actionType; mEndDate = 0; mIsEnabled = true; //all actions are enabled by default } /** * Returns the type of action to be performed by this scheduled action * @return ActionType of the scheduled action */ public ActionType getActionType() { return mActionType; } /** * Sets the {@link ActionType} * @param actionType Type of action */ public void setActionType(ActionType actionType) { this.mActionType = actionType; } /** * Returns the GUID of the action covered by this scheduled action * @return GUID of action */ public String getActionUID() { return mActionUID; } /** * Sets the GUID of the action being scheduled * @param actionUID GUID of the action */ public void setActionUID(String actionUID) { this.mActionUID = actionUID; } /** * Returns the timestamp of the last execution of this scheduled action * <p>This is not necessarily the time when the scheduled action was due, only when it was actually last executed.</p> * @return Timestamp in milliseconds since Epoch */ public long getLastRunTime() { return mLastRun; } /** * Returns the time when the last schedule in the sequence of planned executions was executed. * This relies on the number of executions of the scheduled action * <p>This is different from {@link #getLastRunTime()} which returns the date when the system last * run the scheduled action.</p> * @return Time of last schedule, or -1 if the scheduled action has never been run */ public long getTimeOfLastSchedule(){ if (mExecutionCount == 0) return -1; LocalDateTime startTime = LocalDateTime.fromDateFields(new Date(mStartDate)); int multiplier = mRecurrence.getMultiplier(); int factor = (mExecutionCount-1) * multiplier; switch (mRecurrence.getPeriodType()){ case HOUR: startTime = startTime.plusHours(factor); break; case DAY: startTime = startTime.plusDays(factor); break; case WEEK: startTime = startTime.plusWeeks(factor); break; case MONTH: startTime = startTime.plusMonths(factor); break; case YEAR: startTime = startTime.plusYears(factor); break; } return startTime.toDate().getTime(); } /** * Computes the next time that this scheduled action is supposed to be * executed based on the execution count. * * <p>This method does not consider the end time, or number of times it should be run. * It only considers when the next execution would theoretically be due.</p> * * @return Next run time in milliseconds */ public long computeNextCountBasedScheduledExecutionTime(){ return computeNextScheduledExecutionTimeStartingAt(getTimeOfLastSchedule()); } /** * Computes the next time that this scheduled action is supposed to be * executed based on the time of the last run. * * <p>This method does not consider the end time, or number of times it should be run. * It only considers when the next execution would theoretically be due.</p> * * @return Next run time in milliseconds */ public long computeNextTimeBasedScheduledExecutionTime() { return computeNextScheduledExecutionTimeStartingAt(getLastRunTime()); } /** * Computes the next time that this scheduled action is supposed to be * executed starting at startTime. * * <p>This method does not consider the end time, or number of times it should be run. * It only considers when the next execution would theoretically be due.</p> * * @param startTime time in milliseconds to use as start to compute the next schedule. * * @return Next run time in milliseconds */ private long computeNextScheduledExecutionTimeStartingAt(long startTime) { if (startTime <= 0){ // has never been run return mStartDate; } int multiplier = mRecurrence.getMultiplier(); LocalDateTime nextScheduledExecution = LocalDateTime.fromDateFields(new Date(startTime)); switch (mRecurrence.getPeriodType()) { case HOUR: nextScheduledExecution = nextScheduledExecution.plusHours(multiplier); break; case DAY: nextScheduledExecution = nextScheduledExecution.plusDays(multiplier); break; case WEEK: nextScheduledExecution = computeNextWeeklyExecutionStartingAt(nextScheduledExecution); break; case MONTH: nextScheduledExecution = nextScheduledExecution.plusMonths(multiplier); break; case YEAR: nextScheduledExecution = nextScheduledExecution.plusYears(multiplier); break; } return nextScheduledExecution.toDate().getTime(); } /** * Computes the next time that this weekly scheduled action is supposed to be * executed starting at startTime. * * If no weekdays have been set (GnuCash desktop allows it), it will return a * date in the future to ensure ScheduledActionService doesn't execute it. * * @param startTime LocalDateTime to use as start to compute the next schedule. * * @return Next run time as a LocalDateTime. A date in the future, if no weekdays * were set in the Recurrence. */ @NonNull private LocalDateTime computeNextWeeklyExecutionStartingAt(LocalDateTime startTime) { if (mRecurrence.getByDays().isEmpty()) return LocalDateTime.now().plusDays(1); // Just a date in the future // Look into the week of startTime for another scheduled weekday for (int weekDay : mRecurrence.getByDays() ) { int jodaWeekDay = convertCalendarWeekdayToJoda(weekDay); LocalDateTime candidateNextDueTime = startTime.withDayOfWeek(jodaWeekDay); if (candidateNextDueTime.isAfter(startTime)) return candidateNextDueTime; } // Return the first scheduled weekday from the next due week int firstScheduledWeekday = convertCalendarWeekdayToJoda(mRecurrence.getByDays().get(0)); return startTime.plusWeeks(mRecurrence.getMultiplier()) .withDayOfWeek(firstScheduledWeekday); } /** * Converts a java.util.Calendar weekday constant to the * org.joda.time.DateTimeConstants equivalent. * * @param calendarWeekday weekday constant from java.util.Calendar * @return weekday constant equivalent from org.joda.time.DateTimeConstants */ private int convertCalendarWeekdayToJoda(int calendarWeekday) { Calendar cal = Calendar.getInstance(); cal.set(Calendar.DAY_OF_WEEK, calendarWeekday); return LocalDateTime.fromCalendarFields(cal).getDayOfWeek(); } /** * Set time of last execution of the scheduled action * @param nextRun Timestamp in milliseconds since Epoch */ public void setLastRun(long nextRun) { this.mLastRun = nextRun; } /** * Returns the period of this scheduled action in milliseconds. * @return Period in milliseconds since Epoch * @deprecated Uses fixed values for time of months and years (which actually vary depending on number of days in month or leap year) */ public long getPeriod() { return mRecurrence.getPeriod(); } /** * Returns the time of first execution of the scheduled action * @return Start time of scheduled action in milliseconds since Epoch */ public long getStartTime() { return mStartDate; } /** * Sets the time of first execution of the scheduled action * @param startDate Timestamp in milliseconds since Epoch */ public void setStartTime(long startDate) { this.mStartDate = startDate; if (mRecurrence != null) { mRecurrence.setPeriodStart(new Timestamp(startDate)); } } /** * Returns the time of last execution of the scheduled action * @return Timestamp in milliseconds since Epoch */ public long getEndTime() { return mEndDate; } /** * Sets the end time of the scheduled action * @param endDate Timestamp in milliseconds since Epoch */ public void setEndTime(long endDate) { this.mEndDate = endDate; if (mRecurrence != null){ mRecurrence.setPeriodEnd(new Timestamp(mEndDate)); } } /** * Returns the tag of this scheduled action * <p>The tag saves additional information about the scheduled action, * e.g. such as export parameters for scheduled backups</p> * @return Tag of scheduled action */ public String getTag() { return mTag; } /** * Sets the tag of the schedules action. * <p>The tag saves additional information about the scheduled action, * e.g. such as export parameters for scheduled backups</p> * @param tag Tag of scheduled action */ public void setTag(String tag) { this.mTag = tag; } /** * Returns {@code true} if the scheduled action is enabled, {@code false} otherwise * @return {@code true} if the scheduled action is enabled, {@code false} otherwise */ public boolean isEnabled(){ return mIsEnabled; } /** * Toggles the enabled state of the scheduled action * Disabled scheduled actions will not be executed * @param enabled Flag if the scheduled action is enabled or not */ public void setEnabled(boolean enabled){ this.mIsEnabled = enabled; } /** * Returns the total number of planned occurrences of this scheduled action. * @return Total number of planned occurrences of this action */ public int getTotalPlannedExecutionCount(){ return mTotalFrequency; } /** * Sets the number of occurences of this action * @param plannedExecutions Number of occurences */ public void setTotalPlannedExecutionCount(int plannedExecutions){ this.mTotalFrequency = plannedExecutions; } /** * Returns how many times this scheduled action has already been executed * @return Number of times this action has been executed */ public int getExecutionCount(){ return mExecutionCount; } /** * Sets the number of times this scheduled action has been executed * @param executionCount Number of executions */ public void setExecutionCount(int executionCount){ mExecutionCount = executionCount; } /** * Returns flag if transactions should be automatically created or not * <p>This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML</p> * @return {@code true} if the transaction should be auto-created, {@code false} otherwise */ public boolean shouldAutoCreate() { return mAutoCreate; } /** * Set flag for automatically creating transaction based on this scheduled action * <p>This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML</p> * @param autoCreate Flag for auto creating transactions */ public void setAutoCreate(boolean autoCreate) { this.mAutoCreate = autoCreate; } /** * Check if user will be notified of creation of scheduled transactions * <p>This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML</p> * @return {@code true} if user will be notified, {@code false} otherwise */ public boolean shouldAutoNotify() { return mAutoNotify; } /** * Sets whether to notify the user that scheduled transactions have been created * <p>This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML</p> * @param autoNotify Boolean flag */ public void setAutoNotify(boolean autoNotify) { this.mAutoNotify = autoNotify; } /** * Returns number of days in advance to create the transaction * <p>This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML</p> * @return Number of days in advance to create transaction */ public int getAdvanceCreateDays() { return mAdvanceCreateDays; } /** * Set number of days in advance to create the transaction * <p>This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML</p> * @param advanceCreateDays Number of days */ public void setAdvanceCreateDays(int advanceCreateDays) { this.mAdvanceCreateDays = advanceCreateDays; } /** * Returns the number of days in advance to notify of scheduled transactions * <p>This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML</p> * @return {@code true} if user will be notified, {@code false} otherwise */ public int getAdvanceNotifyDays() { return mAdvanceNotifyDays; } /** * Set number of days in advance to notify of scheduled transactions * <p>This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML</p> * @param advanceNotifyDays Number of days */ public void setAdvanceNotifyDays(int advanceNotifyDays) { this.mAdvanceNotifyDays = advanceNotifyDays; } /** * Return the template account GUID for this scheduled action * <p>This method generates one if none was set</p> * @return String GUID of template account */ public String getTemplateAccountUID() { if (mTemplateAccountUID == null) return mTemplateAccountUID = generateUID(); else return mTemplateAccountUID; } /** * Set the template account GUID * @param templateAccountUID String GUID of template account */ public void setTemplateAccountUID(String templateAccountUID) { this.mTemplateAccountUID = templateAccountUID; } /** * Returns the event schedule (start, end and recurrence) * @return String description of repeat schedule */ public String getRepeatString(){ StringBuilder ruleBuilder = new StringBuilder(mRecurrence.getRepeatString()); Context context = GnuCashApplication.getAppContext(); if (mEndDate <= 0 && mTotalFrequency > 0){ ruleBuilder.append(", ").append(context.getString(R.string.repeat_x_times, mTotalFrequency)); } return ruleBuilder.toString(); } /** * Creates an RFC 2445 string which describes this recurring event * <p>See http://recurrance.sourceforge.net/</p> * @return String describing event */ public String getRuleString(){ String separator = ";"; StringBuilder ruleBuilder = new StringBuilder(mRecurrence.getRuleString()); if (mEndDate > 0){ SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US); df.setTimeZone(TimeZone.getTimeZone("UTC")); ruleBuilder.append("UNTIL=").append(df.format(new Date(mEndDate))).append(separator); } else if (mTotalFrequency > 0){ ruleBuilder.append("COUNT=").append(mTotalFrequency).append(separator); } return ruleBuilder.toString(); } /** * Return GUID of recurrence pattern for this scheduled action * @return {@link Recurrence} object */ public Recurrence getRecurrence() { return mRecurrence; } /** * Overloaded method for setting the recurrence of the scheduled action. * <p>This method allows you to specify the periodicity and the ordinal of it. For example, * a recurrence every fortnight would give parameters: {@link PeriodType#WEEK}, ordinal:2</p> * @param periodType Periodicity of the scheduled action * @param ordinal Ordinal of the periodicity. If unsure, specify 1 * @see #setRecurrence(Recurrence) */ public void setRecurrence(PeriodType periodType, int ordinal){ Recurrence recurrence = new Recurrence(periodType); recurrence.setMultiplier(ordinal); setRecurrence(recurrence); } /** * Sets the recurrence pattern of this scheduled action * <p>This also sets the start period of the recurrence object, if there is one</p> * @param recurrence {@link Recurrence} object */ public void setRecurrence(@NonNull Recurrence recurrence) { this.mRecurrence = recurrence; //if we were parsing XML and parsed the start and end date from the scheduled action first, //then use those over the values which might be gotten from the recurrence if (mStartDate > 0){ mRecurrence.setPeriodStart(new Timestamp(mStartDate)); } else { mStartDate = mRecurrence.getPeriodStart().getTime(); } if (mEndDate > 0){ mRecurrence.setPeriodEnd(new Timestamp(mEndDate)); } else if (mRecurrence.getPeriodEnd() != null){ mEndDate = mRecurrence.getPeriodEnd().getTime(); } } /** * Creates a ScheduledAction from a Transaction and a period * @param transaction Transaction to be scheduled * @param period Period in milliseconds since Epoch * @return Scheduled Action * @deprecated Used for parsing legacy backup files. Use {@link Recurrence} instead */ @Deprecated public static ScheduledAction parseScheduledAction(Transaction transaction, long period){ ScheduledAction scheduledAction = new ScheduledAction(ActionType.TRANSACTION); scheduledAction.mActionUID = transaction.getUID(); Recurrence recurrence = Recurrence.fromLegacyPeriod(period); scheduledAction.setRecurrence(recurrence); return scheduledAction; } @Override public String toString() { return mActionType.name() + " - " + getRepeatString(); } }