diff --git a/app/src/main/java/org/gnucash/android/db/MigrationHelper.java b/app/src/main/java/org/gnucash/android/db/MigrationHelper.java index fa9519ebb..ce8e6c294 100644 --- a/app/src/main/java/org/gnucash/android/db/MigrationHelper.java +++ b/app/src/main/java/org/gnucash/android/db/MigrationHelper.java @@ -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(); diff --git a/app/src/main/java/org/gnucash/android/service/ScheduledActionService.java b/app/src/main/java/org/gnucash/android/service/ScheduledActionService.java index 535c3b62c..b8de7c520 100644 --- a/app/src/main/java/org/gnucash/android/service/ScheduledActionService.java +++ b/app/src/main/java/org/gnucash/android/service/ScheduledActionService.java @@ -89,7 +89,6 @@ protected void onHandleIntent(Intent intent) { } finally { //release the lock either way wakeLock.release(); } - } /** @@ -105,12 +104,15 @@ public static void processScheduledActions(List 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); } } @@ -119,55 +121,17 @@ public static void processScheduledActions(List 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 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; } @@ -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. + *

If a schedule was missed, all the intervening transactions will be generated, even if + * the end time of the transaction was already reached

+ * @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 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; + } } diff --git a/app/src/test/java/org/gnucash/android/test/unit/service/ScheduledActionServiceTest.java b/app/src/test/java/org/gnucash/android/test/unit/service/ScheduledActionServiceTest.java index a5dcda2bb..0e6adb24d 100644 --- a/app/src/test/java/org/gnucash/android/test/unit/service/ScheduledActionServiceTest.java +++ b/app/src/test/java/org/gnucash/android/test/unit/service/ScheduledActionServiceTest.java @@ -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. + *

This holds only for transactions. Backups will be skipped

*/ @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()); @@ -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()); @@ -283,7 +286,7 @@ 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(); @@ -291,12 +294,12 @@ public void scheduledBackups_shouldRunOnlyOnce(){ 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();