Skip to content

Commit

Permalink
Ensure that scheduled backups are run only once regardless of any mis…
Browse files Browse the repository at this point in the history
…sed schedules (added test)

Refactored ScheduledActionService for more modularity
Backups and exports are already migrated - closes codinguser#555
  • Loading branch information
codinguser committed Aug 25, 2016
1 parent e5daf4b commit 976dbf2
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 50 deletions.
2 changes: 0 additions & 2 deletions app/src/main/java/org/gnucash/android/db/MigrationHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -1438,8 +1438,6 @@ static int upgradeDbToVersion13(SQLiteDatabase db){
db.endTransaction();
}

//TODO: Move old files from old export folders into new book-specific export folders

//Migrate book-specific preferences away from shared preferences
Log.d(LOG_TAG, "Migrating shared preferences into book preferences");
Context context = GnuCashApplication.getAppContext();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ protected void onHandleIntent(Intent intent) {
} finally { //release the lock either way
wakeLock.release();
}

}

/**
Expand All @@ -105,12 +104,15 @@ public static void processScheduledActions(List<ScheduledAction> scheduledAction
int totalPlannedExecutions = scheduledAction.getTotalPlannedExecutionCount();
int executionCount = scheduledAction.getExecutionCount();

//the end time of the ScheduledAction is not handled here because
//it is handled differently for transactions and backups. See the individual methods.
if (scheduledAction.getStartTime() > now //if schedule begins in the future
|| !scheduledAction.isEnabled() // of if schedule is disabled
|| (totalPlannedExecutions > 0 && executionCount >= totalPlannedExecutions)) { //limit was set and we reached or exceeded it
Log.i(LOG_TAG, "Skipping scheduled action: " + scheduledAction.toString());
continue;
}

executeScheduledEvent(scheduledAction, db);
}
}
Expand All @@ -119,55 +121,17 @@ public static void processScheduledActions(List<ScheduledAction> scheduledAction
* Executes a scheduled event according to the specified parameters
* @param scheduledAction ScheduledEvent to be executed
*/
//made public static for testing. Do not call directly
@VisibleForTesting
public static void executeScheduledEvent(ScheduledAction scheduledAction, SQLiteDatabase db){
private static void executeScheduledEvent(ScheduledAction scheduledAction, SQLiteDatabase db){
Log.i(LOG_TAG, "Executing scheduled action: " + scheduledAction.toString());
int executionCount = scheduledAction.getExecutionCount();

switch (scheduledAction.getActionType()){
case TRANSACTION:
String actionUID = scheduledAction.getActionUID();
TransactionsDbAdapter transactionsDbAdapter = new TransactionsDbAdapter(db, new SplitsDbAdapter(db));
Transaction trxnTemplate = transactionsDbAdapter.getRecord(actionUID);

long now = System.currentTimeMillis();
//if there is an end time in the past, we execute all schedules up to the end time.
//if the end time is in the future, we execute all schedules until now (current time)
//if there is no end time, we execute all schedules until now
long endTime = scheduledAction.getEndTime() > 0 ? Math.min(scheduledAction.getEndTime(), now) : now;
int totalPlannedExecutions = scheduledAction.getTotalPlannedExecutionCount();
List<Transaction> transactions = new ArrayList<>();

//we may be executing scheduled action significantly after scheduled time (depending on when Android fires the alarm)
//so compute the actual transaction time from pre-known values
long transactionTime = scheduledAction.computeNextScheduledExecutionTime();
while (transactionTime <= endTime) {
Transaction recurringTrxn = new Transaction(trxnTemplate, true);
recurringTrxn.setTime(transactionTime);
transactions.add(recurringTrxn);
recurringTrxn.setScheduledActionUID(scheduledAction.getUID());
scheduledAction.setExecutionCount(++executionCount);

if (totalPlannedExecutions > 0 && executionCount >= totalPlannedExecutions)
break; //if we hit the total planned executions set, then abort
transactionTime = scheduledAction.computeNextScheduledExecutionTime();
}

transactionsDbAdapter.bulkAddRecords(transactions, DatabaseAdapter.UpdateMethod.insert);
executionCount += executeTransactions(scheduledAction, db);
break;

case BACKUP:
ExportParams params = ExportParams.parseCsv(scheduledAction.getTag());
try {
//wait for async task to finish before we proceed (we are holding a wake lock)
new ExportAsyncTask(GnuCashApplication.getAppContext(), db).execute(params).get();
scheduledAction.setExecutionCount(++executionCount);
} catch (InterruptedException | ExecutionException e) {
Crashlytics.logException(e);
Log.e(LOG_TAG, e.getMessage());
return; //return immediately, do not update last run time of event
}
executionCount += executeBackup(scheduledAction, db);
break;
}

Expand All @@ -183,4 +147,72 @@ public static void executeScheduledEvent(ScheduledAction scheduledAction, SQLite
//set the values in the object because they will be checked for the next iteration in the calling loop
scheduledAction.setExecutionCount(executionCount);
}

/**
* Executes scheduled backups for a given scheduled action.
* The backup will be executed only once, even if multiple schedules were missed
* @param scheduledAction Scheduled action referencing the backup
* @param db SQLiteDatabase to backup
* @return Number of times backup is executed. This should either be 1 or 0
*/
private static int executeBackup(ScheduledAction scheduledAction, SQLiteDatabase db) {
int executionCount = 0;
long now = System.currentTimeMillis();
long endTime = scheduledAction.getEndTime();

if (endTime > 0 && endTime < now)
return executionCount;

ExportParams params = ExportParams.parseCsv(scheduledAction.getTag());
try {
//wait for async task to finish before we proceed (we are holding a wake lock)
new ExportAsyncTask(GnuCashApplication.getAppContext(), db).execute(params).get();
scheduledAction.setExecutionCount(++executionCount);
} catch (InterruptedException | ExecutionException e) {
Crashlytics.logException(e);
Log.e(LOG_TAG, e.getMessage());
}
return executionCount;
}

/**
* Executes scheduled transactions which are to be added to the database.
* <p>If a schedule was missed, all the intervening transactions will be generated, even if
* the end time of the transaction was already reached</p>
* @param scheduledAction Scheduled action which references the transaction
* @param db SQLiteDatabase where the transactions are to be executed
* @return Number of transactions created as a result of this action
*/
private static int executeTransactions(ScheduledAction scheduledAction, SQLiteDatabase db) {
int executionCount = 0;
String actionUID = scheduledAction.getActionUID();
TransactionsDbAdapter transactionsDbAdapter = new TransactionsDbAdapter(db, new SplitsDbAdapter(db));
Transaction trxnTemplate = transactionsDbAdapter.getRecord(actionUID);

long now = System.currentTimeMillis();
//if there is an end time in the past, we execute all schedules up to the end time.
//if the end time is in the future, we execute all schedules until now (current time)
//if there is no end time, we execute all schedules until now
long endTime = scheduledAction.getEndTime() > 0 ? Math.min(scheduledAction.getEndTime(), now) : now;
int totalPlannedExecutions = scheduledAction.getTotalPlannedExecutionCount();
List<Transaction> transactions = new ArrayList<>();

//we may be executing scheduled action significantly after scheduled time (depending on when Android fires the alarm)
//so compute the actual transaction time from pre-known values
long transactionTime = scheduledAction.computeNextScheduledExecutionTime();
while (transactionTime <= endTime) {
Transaction recurringTrxn = new Transaction(trxnTemplate, true);
recurringTrxn.setTime(transactionTime);
transactions.add(recurringTrxn);
recurringTrxn.setScheduledActionUID(scheduledAction.getUID());
scheduledAction.setExecutionCount(++executionCount); //required for computingNextScheduledExecutionTime

if (totalPlannedExecutions > 0 && executionCount >= totalPlannedExecutions)
break; //if we hit the total planned executions set, then abort
transactionTime = scheduledAction.computeNextScheduledExecutionTime();
}

transactionsDbAdapter.bulkAddRecords(transactions, DatabaseAdapter.UpdateMethod.insert);
return executionCount;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,13 @@ public void endTimeInTheFuture_shouldExecuteOnlyUntilPresent(){
}

/**
* Test that the end time for scheduled actions should be respected
* Test that if the end time of a scheduled transaction has passed, but the schedule was missed
* (either because the book was not opened or similar) then the scheduled transactions for the
* relevant period should still be executed even though end time has passed.
* <p>This holds only for transactions. Backups will be skipped</p>
*/
@Test
public void scheduledActionsWithEndTimeInPast_shouldBeExecuted(){
public void scheduledTransactionsWithEndTimeInPast_shouldBeExecuted(){
ScheduledAction scheduledAction = new ScheduledAction(ScheduledAction.ActionType.TRANSACTION);
DateTime startTime = new DateTime(2016, 6, 6, 9, 0);
scheduledAction.setStartTime(startTime.getMillis());
Expand Down Expand Up @@ -272,7 +275,7 @@ public void recurringTransactions_shouldHaveScheduledActionUID(){
ScheduledActionService.processScheduledActions(actions, mDb);
}

//// FIXME: 16.08.2016 Cannot find the file after export. But the export task is called and run
@Test
public void scheduledBackups_shouldRunOnlyOnce(){
ScheduledAction scheduledBackup = new ScheduledAction(ScheduledAction.ActionType.BACKUP);
scheduledBackup.setStartTime(new DateTime(2016, 2, 17, 17, 0).getMillis());
Expand All @@ -283,20 +286,20 @@ public void scheduledBackups_shouldRunOnlyOnce(){
backupParams.setExportTarget(ExportParams.ExportTarget.SD_CARD);
scheduledBackup.setTag(backupParams.toCsv());

File backupFolder = new File(Exporter.getBackupFolderPath(BooksDbAdapter.getInstance().getActiveBookUID()));
File backupFolder = new File(Exporter.getExportFolderPath(BooksDbAdapter.getInstance().getActiveBookUID()));
assertThat(backupFolder).exists();
assertThat(backupFolder.listFiles()).isEmpty();

List<ScheduledAction> actions = new ArrayList<>();
actions.add(scheduledBackup);
ScheduledActionService.processScheduledActions(actions, mDb);

assertThat(scheduledBackup.getExecutionCount()).isEqualTo(3);
File[] backupFiles = backupFolder.listFiles();
assertThat(backupFiles).hasSize(1);
assertThat(backupFiles[0]).hasExtension("gnca");
assertThat(backupFiles[0]).exists().hasExtension("gnca");
}


@After
public void tearDown(){
TransactionsDbAdapter.getInstance().deleteAllRecords();
Expand Down

0 comments on commit 976dbf2

Please sign in to comment.