From b1bf679f2c8a79f7bf8b9f2dbd9cd607c1c439c3 Mon Sep 17 00:00:00 2001 From: thomas Date: Sun, 20 Jun 2021 18:39:26 +0200 Subject: [PATCH] Fix #889 - The QIF export now always generates ZIP archive files. Synchronization of active database. Some refactoring en passant. Fixed some IntelliJ warnings. --- .../android/app/GnuCashApplication.java | 13 +++- .../android/export/ExportAsyncTask.java | 67 ++++------------ .../gnucash/android/export/ExportFormat.java | 5 +- .../org/gnucash/android/export/Exporter.java | 77 ++++++++++++------- .../android/export/ExporterFactory.java | 57 ++++++++++++++ .../android/export/qif/QifExporter.java | 33 ++++++-- .../repository/TransactionRepository.java | 41 ++++++++++ .../service/ScheduledActionService.java | 16 +++- .../android/ui/export/ExportFormFragment.java | 13 +++- .../settings/AccountPreferencesFragment.java | 14 +++- .../service/ScheduledActionServiceTest.java | 20 +++-- 11 files changed, 254 insertions(+), 102 deletions(-) create mode 100644 app/src/main/java/org/gnucash/android/export/ExporterFactory.java create mode 100644 app/src/main/java/org/gnucash/android/repository/TransactionRepository.java diff --git a/app/src/main/java/org/gnucash/android/app/GnuCashApplication.java b/app/src/main/java/org/gnucash/android/app/GnuCashApplication.java index 6c828609c..5d9b201b9 100644 --- a/app/src/main/java/org/gnucash/android/app/GnuCashApplication.java +++ b/app/src/main/java/org/gnucash/android/app/GnuCashApplication.java @@ -58,6 +58,9 @@ import java.util.Currency; import java.util.Locale; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import io.fabric.sdk.android.Fabric; @@ -104,7 +107,10 @@ public class GnuCashApplication extends MultiDexApplication { private static RecurrenceDbAdapter mRecurrenceDbAdapter; private static BooksDbAdapter mBooksDbAdapter; - private static DatabaseHelper mDbHelper; + private static volatile DatabaseHelper mDbHelper; + + // lock for accessing the current database + public static final ReadWriteLock dbLock = new ReentrantReadWriteLock(); /** * Returns darker version of specified color. @@ -142,6 +148,9 @@ public void onCreate(){ * This method should be called every time a new book is opened */ public static void initializeDatabaseAdapters() { + final Lock exclusiveLock = dbLock.writeLock(); + exclusiveLock.lock(); + if (mDbHelper != null){ //close if open mDbHelper.getReadableDatabase().close(); } @@ -172,6 +181,8 @@ public static void initializeDatabaseAdapters() { mCommoditiesDbAdapter = new CommoditiesDbAdapter(mainDb); mBudgetAmountsDbAdapter = new BudgetAmountsDbAdapter(mainDb); mBudgetsDbAdapter = new BudgetsDbAdapter(mainDb, mBudgetAmountsDbAdapter, mRecurrenceDbAdapter); + + exclusiveLock.unlock(); } public static AccountsDbAdapter getAccountsDbAdapter() { diff --git a/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java b/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java index 0336de1ff..bf71bb4c8 100644 --- a/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java +++ b/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java @@ -23,7 +23,6 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ResolveInfo; -import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.AsyncTask; import android.preference.PreferenceManager; @@ -52,16 +51,7 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.db.adapter.AccountsDbAdapter; -import org.gnucash.android.db.adapter.DatabaseAdapter; -import org.gnucash.android.db.adapter.SplitsDbAdapter; -import org.gnucash.android.db.adapter.TransactionsDbAdapter; -import org.gnucash.android.export.csv.CsvAccountExporter; -import org.gnucash.android.export.csv.CsvTransactionsExporter; -import org.gnucash.android.export.ofx.OfxExporter; -import org.gnucash.android.export.qif.QifExporter; -import org.gnucash.android.export.xml.GncXmlExporter; -import org.gnucash.android.model.Transaction; +import org.gnucash.android.repository.TransactionRepository; import org.gnucash.android.ui.account.AccountsActivity; import org.gnucash.android.ui.account.AccountsListFragment; import org.gnucash.android.ui.settings.BackupPreferenceFragment; @@ -93,7 +83,7 @@ public class ExportAsyncTask extends AsyncTask { private ProgressDialog mProgressDialog; - private SQLiteDatabase mDb; + private final TransactionRepository mTransactionRepository; /** * Log tag @@ -108,11 +98,12 @@ public class ExportAsyncTask extends AsyncTask { // File paths generated by the exporter private List mExportedFiles = Collections.emptyList(); - private Exporter mExporter; + final private Exporter mExporter; - public ExportAsyncTask(Context context, SQLiteDatabase db){ + public ExportAsyncTask(Context context, Exporter exporter, TransactionRepository transactionRepository){ this.mContext = context; - this.mDb = db; + this.mExporter = exporter; + this.mTransactionRepository = transactionRepository; } @Override @@ -138,7 +129,6 @@ protected void onPreExecute() { @Override protected Boolean doInBackground(ExportParams... params) { mExportParams = params[0]; - mExporter = getExporter(); try { mExportedFiles = mExporter.generateExport(); @@ -214,26 +204,6 @@ private void dismissProgressDialog() { } } - /** - * Returns an exporter corresponding to the user settings. - * @return Object of one of {@link QifExporter}, {@link OfxExporter} or {@link GncXmlExporter}, {@Link CsvAccountExporter} or {@Link CsvTransactionsExporter} - */ - private Exporter getExporter() { - switch (mExportParams.getExportFormat()) { - case QIF: - return new QifExporter(mExportParams, mDb); - case OFX: - return new OfxExporter(mExportParams, mDb); - case CSVA: - return new CsvAccountExporter(mExportParams, mDb); - case CSVT: - return new CsvTransactionsExporter(mExportParams, mDb); - case XML: - default: - return new GncXmlExporter(mExportParams, mDb); - } - } - /** * Moves the generated export files to the target specified by the user * @throws Exporter.ExporterException if the move fails @@ -378,7 +348,7 @@ private void moveExportToOwnCloud() throws Exporter.ExporterException { SharedPreferences mPrefs = mContext.getSharedPreferences(mContext.getString(R.string.owncloud_pref), Context.MODE_PRIVATE); - Boolean mOC_sync = mPrefs.getBoolean(mContext.getString(R.string.owncloud_sync), false); + boolean mOC_sync = mPrefs.getBoolean(mContext.getString(R.string.owncloud_sync), false); if (!mOC_sync) { throw new Exporter.ExporterException(mExportParams, "ownCloud not enabled."); @@ -419,8 +389,8 @@ private void moveExportToOwnCloud() throws Exporter.ExporterException { } private static String getFileLastModifiedTimestamp(String path) { - Long timeStampLong = new File(path).lastModified() / 1000; - return timeStampLong.toString(); + long timeStampLong = new File(path).lastModified() / 1000; + return Long.toString(timeStampLong); } /** @@ -432,11 +402,11 @@ private static String getFileLastModifiedTimestamp(String path) { @Deprecated private List moveExportToSDCard() throws Exporter.ExporterException { Log.i(TAG, "Moving exported file to external storage"); - new File(Exporter.getExportFolderPath(mExporter.mBookUID)); + new File(mExporter.getExportFolderPath()); List dstFiles = new ArrayList<>(); for (String src: mExportedFiles) { - String dst = Exporter.getExportFolderPath(mExporter.mBookUID) + stripPathPart(src); + String dst = mExporter.getExportFolderPath() + stripPathPart(src); try { org.gnucash.android.util.FileUtils.moveFile(src, dst); dstFiles.add(dst); @@ -460,18 +430,7 @@ private String stripPathPart(String fullPathName) { private void backupAndDeleteTransactions(){ Log.i(TAG, "Backup and deleting transactions after export"); BackupManager.backupActiveBook(); //create backup before deleting everything - List openingBalances = new ArrayList<>(); - boolean preserveOpeningBalances = GnuCashApplication.shouldSaveOpeningBalances(false); - - TransactionsDbAdapter transactionsDbAdapter = new TransactionsDbAdapter(mDb, new SplitsDbAdapter(mDb)); - if (preserveOpeningBalances) { - openingBalances = new AccountsDbAdapter(mDb, transactionsDbAdapter).getAllOpeningBalanceTransactions(); - } - transactionsDbAdapter.deleteAllNonTemplateTransactions(); - - if (preserveOpeningBalances) { - transactionsDbAdapter.bulkAddRecords(openingBalances, DatabaseAdapter.UpdateMethod.insert); - } + mTransactionRepository.deleteTransactions(); } /** @@ -502,7 +461,7 @@ private void shareFiles(List paths) { if (mContext instanceof Activity) { List activities = mContext.getPackageManager().queryIntentActivities(shareIntent, 0); - if (activities != null && !activities.isEmpty()) { + if (!activities.isEmpty()) { mContext.startActivity(Intent.createChooser(shareIntent, mContext.getString(R.string.title_select_export_destination))); } else { diff --git a/app/src/main/java/org/gnucash/android/export/ExportFormat.java b/app/src/main/java/org/gnucash/android/export/ExportFormat.java index 7b6fc99c1..01b63b964 100644 --- a/app/src/main/java/org/gnucash/android/export/ExportFormat.java +++ b/app/src/main/java/org/gnucash/android/export/ExportFormat.java @@ -36,13 +36,14 @@ public enum ExportFormat { } /** - * Returns the file extension for this export format including the period e.g. ".qif" + * Returns the file extension for this export format including the period e.g. ".csv" * @return String file extension for the export format */ public String getExtension(){ switch (this) { case QIF: - return ".qif"; + // zip qif files by default + return ".zip"; case OFX: return ".ofx"; case XML: diff --git a/app/src/main/java/org/gnucash/android/export/Exporter.java b/app/src/main/java/org/gnucash/android/export/Exporter.java index 5fd56f3b5..3a157520d 100644 --- a/app/src/main/java/org/gnucash/android/export/Exporter.java +++ b/app/src/main/java/org/gnucash/android/export/Exporter.java @@ -19,8 +19,10 @@ import android.content.Context; import android.database.sqlite.SQLiteDatabase; +import android.os.Build; import android.os.Environment; import android.support.annotation.NonNull; +import android.support.annotation.RequiresApi; import android.util.Log; import com.crashlytics.android.Crashlytics; @@ -40,6 +42,12 @@ import org.gnucash.android.db.adapter.TransactionsDbAdapter; import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; @@ -99,7 +107,6 @@ public abstract class Exporter { protected final CommoditiesDbAdapter mCommoditiesDbAdapter; protected final BudgetsDbAdapter mBudgetsDbAdapter; protected final Context mContext; - private String mExportCacheFilePath; /** * Database being currently exported @@ -136,10 +143,9 @@ public Exporter(ExportParams params, SQLiteDatabase db) { } mBookUID = new File(mDb.getPath()).getName(); //this depends on the database file always having the name of the book GUID - mExportCacheFilePath = null; mCacheDir = new File(mContext.getCacheDir(), params.getExportFormat().name()); mCacheDir.mkdir(); - purgeDirectory(mCacheDir); + deleteRecursively(mCacheDir); } /** @@ -149,7 +155,7 @@ public Exporter(ExportParams params, SQLiteDatabase db) { * @return Sanitized file name */ public static String sanitizeFilename(String inputName) { - return inputName.replaceAll("[^a-zA-Z0-9-_\\.]", "_"); + return inputName.replaceAll("[^a-zA-Z0-9-_.]", "_"); } /** @@ -159,11 +165,14 @@ public static String sanitizeFilename(String inputName) { * @return String containing the file name */ public static String buildExportFilename(ExportFormat format, String bookName) { + return buildExportFileBaseName(format, bookName) + format.getExtension(); + } + + protected static String buildExportFileBaseName(ExportFormat format, String bookName) { return EXPORT_FILENAME_DATE_FORMAT.format(new Date(System.currentTimeMillis())) + "_gnucash_export_" + sanitizeFilename(bookName) + (format == ExportFormat.CSVA ? "_accounts" : "") + - (format == ExportFormat.CSVT ? "_transactions" : "") + - format.getExtension(); + (format == ExportFormat.CSVT ? "_transactions" : ""); } /** @@ -180,7 +189,7 @@ public static long getExportTime(String filename){ try { Date date = EXPORT_FILENAME_DATE_FORMAT.parse(tokens[0] + "_" + tokens[1]); timeMillis = date.getTime(); - } catch (ParseException e) { + } catch (ParseException|NullPointerException e) { Log.e("Exporter", "Error parsing time from file name: " + e.getMessage()); Crashlytics.logException(e); } @@ -194,46 +203,62 @@ public static long getExportTime(String filename){ public abstract List generateExport() throws ExporterException; /** - * Recursively delete all files in a directory - * @param directory File descriptor for directory + * Recursively delete all files in a directory or deletes a file that is not a directory + * @param file File descriptor for file or directory */ - private void purgeDirectory(File directory){ - for (File file : directory.listFiles()) { - if (file.isDirectory()) - purgeDirectory(file); - else - file.delete(); + private void deleteRecursively(File file) { + if (file == null) { + return; + } else if (file.isDirectory()) { + File[] children = file.listFiles(); + if (children == null) { + return; + } + for (File child : children) { + if (child.isDirectory()) { + deleteRecursively(child); + } else { + file.delete(); + } + } + } else { + file.delete(); } } /** * Returns the path to the file where the exporter should save the export during generation - *

This path is a temporary cache file whose file extension matches the export format.
+ *

This path is a temporary cache file whose file extension is inferred from the export format.
* This file is deleted every time a new export is started

* @return Absolute path to file */ protected String getExportCacheFilePath(){ + + return getCachePath() + buildExportFilename(mExportParams.getExportFormat(), getBookName()); + } + + protected String getCachePath() { // The file name contains a timestamp, so ensure it doesn't change with multiple calls to // avoid issues like #448 - if (mExportCacheFilePath == null) { - String cachePath = mCacheDir.getAbsolutePath(); - if (!cachePath.endsWith("/")) - cachePath += "/"; - String bookName = BooksDbAdapter.getInstance().getAttribute(mBookUID, DatabaseSchema.BookEntry.COLUMN_DISPLAY_NAME); - mExportCacheFilePath = cachePath + buildExportFilename(mExportParams.getExportFormat(), bookName); + String cachePath = mCacheDir.getAbsolutePath(); + if (!cachePath.endsWith("/")) { + cachePath += "/"; } - return mExportCacheFilePath; + return cachePath; + } + + protected String getBookName() { + return BooksDbAdapter.getInstance().getAttribute(mBookUID, DatabaseSchema.BookEntry.COLUMN_DISPLAY_NAME); } /** * Returns that path to the export folder for the book with GUID {@code bookUID}. * This is the folder where exports like QIF and OFX will be saved for access by external programs - * @param bookUID GUID of the book being exported. Each book has its own export path * @return Absolute path to export folder for active book */ - public static String getExportFolderPath(String bookUID){ - String path = BASE_FOLDER_PATH + "/" + bookUID + "/exports/"; + public String getExportFolderPath(){ + String path = BASE_FOLDER_PATH + "/" + mBookUID + "/exports/"; File file = new File(path); if (!file.exists()) file.mkdirs(); diff --git a/app/src/main/java/org/gnucash/android/export/ExporterFactory.java b/app/src/main/java/org/gnucash/android/export/ExporterFactory.java new file mode 100644 index 000000000..71630c4fb --- /dev/null +++ b/app/src/main/java/org/gnucash/android/export/ExporterFactory.java @@ -0,0 +1,57 @@ +package org.gnucash.android.export; + +import android.database.sqlite.SQLiteDatabase; + +import org.gnucash.android.export.csv.CsvAccountExporter; +import org.gnucash.android.export.csv.CsvTransactionsExporter; +import org.gnucash.android.export.ofx.OfxExporter; +import org.gnucash.android.export.qif.QifExporter; +import org.gnucash.android.export.xml.GncXmlExporter; + +public class ExporterFactory { + + private static volatile ExporterFactory instance; + + //private constructor. + private ExporterFactory(){ + + //Prevent form the reflection api. + if (instance != null){ + throw new RuntimeException("Use getInstance() method to get the single instance of this class."); + } + } + + public static ExporterFactory getInstance() { + //Double check locking pattern + if (instance == null) { //Check for the first time + + synchronized (ExporterFactory.class) { //Check for the second time. + //if there is no instance available... create new one + if (instance == null) instance = new ExporterFactory(); + } + } + + return instance; + } + + + /** + * Returns an exporter corresponding to the user settings. + * @return Object of one of {@link QifExporter}, {@link OfxExporter} or {@link GncXmlExporter}, {@link CsvAccountExporter} or {@link CsvTransactionsExporter} + */ + public Exporter getExporter(final ExportParams exportParams, final SQLiteDatabase db) { + switch (exportParams.getExportFormat()) { + case QIF: + return new QifExporter(exportParams, db); + case OFX: + return new OfxExporter(exportParams, db); + case CSVA: + return new CsvAccountExporter(exportParams, db); + case CSVT: + return new CsvTransactionsExporter(exportParams, db); + case XML: + default: + return new GncXmlExporter(exportParams, db); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/gnucash/android/export/qif/QifExporter.java b/app/src/main/java/org/gnucash/android/export/qif/QifExporter.java index 9f1478df8..2f12474af 100644 --- a/app/src/main/java/org/gnucash/android/export/qif/QifExporter.java +++ b/app/src/main/java/org/gnucash/android/export/qif/QifExporter.java @@ -21,7 +21,9 @@ import android.database.sqlite.SQLiteDatabase; import android.support.annotation.NonNull; +import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.BooksDbAdapter; import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.export.ExportParams; import org.gnucash.android.export.Exporter; @@ -39,6 +41,7 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -115,7 +118,7 @@ public List generateExport() throws ExporterException { ); File file = new File(getExportCacheFilePath()); - BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8")); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)); try { String currentCurrencyCode = ""; @@ -195,7 +198,7 @@ public List generateExport() throws ExporterException { .append(newLine); } String splitType = cursor.getString(cursor.getColumnIndexOrThrow("split_type")); - Double quantity_num = cursor.getDouble(cursor.getColumnIndexOrThrow("split_quantity_num")); + double quantity_num = cursor.getDouble(cursor.getColumnIndexOrThrow("split_quantity_num")); int quantity_denom = cursor.getInt(cursor.getColumnIndexOrThrow("split_quantity_denom")); int precision = 0; switch (quantity_denom) { @@ -225,7 +228,7 @@ public List generateExport() throws ExporterException { default: throw new ExporterException(mExportParams, "split quantity has illegal denominator: "+ quantity_denom); } - Double quantity = 0.0; + double quantity = 0.0; if (quantity_denom != 0) { quantity = quantity_num / quantity_denom; } @@ -253,17 +256,31 @@ public List generateExport() throws ExporterException { PreferencesHelper.setLastExportTime(TimestampHelper.getTimestampFromNow()); List exportedFiles = splitQIF(file); - if (exportedFiles.isEmpty()) + if (exportedFiles.isEmpty()) { return Collections.emptyList(); - else if (exportedFiles.size() > 1) + } else { + // always zip exported file(s) return zipQifs(exportedFiles); - else - return exportedFiles; + } } catch (IOException e) { throw new ExporterException(mExportParams, e); } } + /** + * Returns the path to the file where the exporter should save the export during generation. + *

This path is a temporary cache file. For the QIF export, the file extension differs from + * the final file name, because multiple QIF files may be written during generation, + * but the export result is always a ZIP archive.
+ * This file is deleted every time a new export is started

+ * @return Absolute path to file + */ + @Override + protected String getExportCacheFilePath(){ + + return getCachePath() + buildExportFileBaseName(mExportParams.getExportFormat(), getBookName()) + ".qif"; + } + @NonNull private List zipQifs(List exportedFiles) throws IOException { String zipFileName = getExportCacheFilePath() + ".zip"; @@ -280,7 +297,7 @@ private List zipQifs(List exportedFiles) throws IOException { */ private List splitQIF(File file) throws IOException { // split only at the last dot - String[] pathParts = file.getPath().split("(?=\\.[^\\.]+$)"); + String[] pathParts = file.getPath().split("(?=\\.[^.]+$)"); ArrayList splitFiles = new ArrayList<>(); String line; BufferedReader in = new BufferedReader(new FileReader(file)); diff --git a/app/src/main/java/org/gnucash/android/repository/TransactionRepository.java b/app/src/main/java/org/gnucash/android/repository/TransactionRepository.java new file mode 100644 index 000000000..878f39a9b --- /dev/null +++ b/app/src/main/java/org/gnucash/android/repository/TransactionRepository.java @@ -0,0 +1,41 @@ +package org.gnucash.android.repository; + +import android.database.sqlite.SQLiteDatabase; + +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.db.adapter.DatabaseAdapter; +import org.gnucash.android.db.adapter.SplitsDbAdapter; +import org.gnucash.android.db.adapter.TransactionsDbAdapter; +import org.gnucash.android.model.Transaction; + +import java.util.List; + +public class TransactionRepository { + + final private SQLiteDatabase db; + + public TransactionRepository(SQLiteDatabase db) { + this.db = db; + } + + /** + * Backups of the database, saves opening balances (if necessary) + * and deletes all non-template transactions in the database. + */ + public void deleteTransactions(){ + final List openingBalances; + final boolean preserveOpeningBalances = GnuCashApplication.shouldSaveOpeningBalances(false); + + final TransactionsDbAdapter transactionsDbAdapter = new TransactionsDbAdapter(db, new SplitsDbAdapter(db)); + if (preserveOpeningBalances) { + openingBalances = new AccountsDbAdapter(db, transactionsDbAdapter).getAllOpeningBalanceTransactions(); + + transactionsDbAdapter.deleteAllNonTemplateTransactions(); + + transactionsDbAdapter.bulkAddRecords(openingBalances, DatabaseAdapter.UpdateMethod.insert); + } else { + transactionsDbAdapter.deleteAllNonTemplateTransactions(); + } + } +} 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 b1a3f7a11..42ae44a25 100644 --- a/app/src/main/java/org/gnucash/android/service/ScheduledActionService.java +++ b/app/src/main/java/org/gnucash/android/service/ScheduledActionService.java @@ -38,15 +38,19 @@ import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.export.ExportAsyncTask; import org.gnucash.android.export.ExportParams; +import org.gnucash.android.export.Exporter; +import org.gnucash.android.export.ExporterFactory; import org.gnucash.android.model.Book; import org.gnucash.android.model.ScheduledAction; import org.gnucash.android.model.Transaction; +import org.gnucash.android.repository.TransactionRepository; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.ExecutionException; +import java.util.concurrent.locks.Lock; /** * Service for running scheduled events. @@ -88,9 +92,16 @@ protected void onHandleWork(@NonNull Intent intent) { scheduledActions.size(), book.getDisplayName())); processScheduledActions(scheduledActions, db); + final Lock readLock = GnuCashApplication.dbLock.readLock(); + readLock.lock(); + final SQLiteDatabase activeDb = GnuCashApplication.getActiveDb(); + //close all databases except the currently active database - if (!db.getPath().equals(GnuCashApplication.getActiveDb().getPath())) + if (!db.getPath().equals(activeDb.getPath())) { db.close(); + } + + readLock.unlock(); } Log.i(LOG_TAG, "Completed service @ " + java.text.DateFormat.getDateTimeInstance().format(new Date())); @@ -173,8 +184,9 @@ private static int executeBackup(ScheduledAction scheduledAction, SQLiteDatabase params.setExportStartTime(new Timestamp(scheduledAction.getLastRunTime())); Boolean result = false; try { + final Exporter exporter = ExporterFactory.getInstance().getExporter(params, db); //wait for async task to finish before we proceed (we are holding a wake lock) - result = new ExportAsyncTask(GnuCashApplication.getAppContext(), db).execute(params).get(); + result = new ExportAsyncTask(GnuCashApplication.getAppContext(), exporter, new TransactionRepository(db)).execute(params).get(); } catch (InterruptedException | ExecutionException e) { Crashlytics.logException(e); Log.e(LOG_TAG, e.getMessage()); diff --git a/app/src/main/java/org/gnucash/android/ui/export/ExportFormFragment.java b/app/src/main/java/org/gnucash/android/ui/export/ExportFormFragment.java index 500c37128..4ca398a75 100644 --- a/app/src/main/java/org/gnucash/android/ui/export/ExportFormFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/export/ExportFormFragment.java @@ -19,6 +19,7 @@ import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; +import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; @@ -62,8 +63,10 @@ import org.gnucash.android.export.ExportFormat; import org.gnucash.android.export.ExportParams; import org.gnucash.android.export.Exporter; +import org.gnucash.android.export.ExporterFactory; import org.gnucash.android.model.BaseModel; import org.gnucash.android.model.ScheduledAction; +import org.gnucash.android.repository.TransactionRepository; import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.settings.BackupPreferenceFragment; import org.gnucash.android.ui.settings.dialog.OwnCloudDialogFragment; @@ -78,6 +81,7 @@ import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; +import java.util.concurrent.locks.Lock; import butterknife.BindView; import butterknife.ButterKnife; @@ -324,7 +328,14 @@ private void startExport(){ exportParameters.setCsvSeparator(mExportCsvSeparator); Log.i(TAG, "Commencing async export of transactions"); - new ExportAsyncTask(getActivity(), GnuCashApplication.getActiveDb()).execute(exportParameters); + final Lock readLock = GnuCashApplication.dbLock.readLock(); + readLock.lock(); + final SQLiteDatabase db = GnuCashApplication.getActiveDb(); + + final Exporter exporter = ExporterFactory.getInstance().getExporter(exportParameters, db); + new ExportAsyncTask(getActivity(), exporter, new TransactionRepository(db)).execute(exportParameters); + + readLock.unlock(); if (mRecurrenceRule != null) { ScheduledAction scheduledAction = new ScheduledAction(ScheduledAction.ActionType.BACKUP); diff --git a/app/src/main/java/org/gnucash/android/ui/settings/AccountPreferencesFragment.java b/app/src/main/java/org/gnucash/android/ui/settings/AccountPreferencesFragment.java index 977b06b57..2e6277b19 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/AccountPreferencesFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/AccountPreferencesFragment.java @@ -21,6 +21,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; @@ -40,13 +41,16 @@ import org.gnucash.android.export.ExportFormat; import org.gnucash.android.export.ExportParams; import org.gnucash.android.export.Exporter; +import org.gnucash.android.export.ExporterFactory; import org.gnucash.android.model.Money; +import org.gnucash.android.repository.TransactionRepository; import org.gnucash.android.ui.account.AccountsActivity; import org.gnucash.android.ui.settings.dialog.DeleteAllAccountsConfirmationDialog; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; +import java.util.concurrent.locks.Lock; /** * Account settings fragment inside the Settings activity @@ -207,7 +211,13 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { ExportParams exportParams = new ExportParams(ExportFormat.CSVA); exportParams.setExportTarget(ExportParams.ExportTarget.URI); exportParams.setExportLocation(data.getData().toString()); - ExportAsyncTask exportTask = new ExportAsyncTask(getActivity(), GnuCashApplication.getActiveDb()); + + final Lock readLock = GnuCashApplication.dbLock.readLock(); + readLock.lock(); + final SQLiteDatabase db = GnuCashApplication.getActiveDb(); + + final Exporter exporter = ExporterFactory.getInstance().getExporter(exportParams, db); + final ExportAsyncTask exportTask = new ExportAsyncTask(getActivity(), exporter, new TransactionRepository(db)); try { exportTask.execute(exportParams).get(); @@ -215,6 +225,8 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { Crashlytics.logException(e); Toast.makeText(getActivity(), "An error occurred during the Accounts CSV export", Toast.LENGTH_LONG).show(); + } finally { + readLock.unlock(); } } } 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 fd555a7f1..25d270914 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 @@ -30,6 +30,7 @@ import org.gnucash.android.export.ExportFormat; import org.gnucash.android.export.ExportParams; import org.gnucash.android.export.Exporter; +import org.gnucash.android.export.ExporterFactory; import org.gnucash.android.importer.GncXmlImporter; import org.gnucash.android.model.Account; import org.gnucash.android.model.Commodity; @@ -318,7 +319,9 @@ public void scheduledBackups_shouldRunOnlyOnce(){ backupParams.setExportTarget(ExportParams.ExportTarget.SD_CARD); scheduledBackup.setTag(backupParams.toCsv()); - File backupFolder = new File(Exporter.getExportFolderPath(BooksDbAdapter.getInstance().getActiveBookUID())); + final Exporter dummyExporter = ExporterFactory.getInstance().getExporter(backupParams, mDb); + + File backupFolder = new File(dummyExporter.getExportFolderPath()); assertThat(backupFolder).exists(); assertThat(backupFolder.listFiles()).isEmpty(); @@ -366,8 +369,9 @@ public void scheduledBackups_shouldNotRunBeforeNextScheduledExecution(){ backupParams.setExportTarget(ExportParams.ExportTarget.SD_CARD); scheduledBackup.setTag(backupParams.toCsv()); - File backupFolder = new File( - Exporter.getExportFolderPath(BooksDbAdapter.getInstance().getActiveBookUID())); + final Exporter dummyExporter = ExporterFactory.getInstance().getExporter(backupParams, mDb); + + File backupFolder = new File(dummyExporter.getExportFolderPath()); assertThat(backupFolder).exists(); assertThat(backupFolder.listFiles()).isEmpty(); @@ -413,8 +417,9 @@ public void scheduledBackups_shouldNotIncludeTransactionsPreviousToTheLastRun() setTransactionInDbModifiedTimestamp(transaction.getUID(), new Timestamp(LocalDateTime.now().minusDays(9).toDate().getTime())); - File backupFolder = new File( - Exporter.getExportFolderPath(BooksDbAdapter.getInstance().getActiveBookUID())); + final Exporter dummyExporter = ExporterFactory.getInstance().getExporter(backupParams, mDb); + + File backupFolder = new File(dummyExporter.getExportFolderPath()); assertThat(backupFolder).exists(); assertThat(backupFolder.listFiles()).isEmpty(); @@ -469,8 +474,9 @@ public void scheduledBackups_shouldIncludeTransactionsAfterTheLastRun() { transaction.addSplit(split.createPair(mTransferAccount.getUID())); mTransactionsDbAdapter.addRecord(transaction); - File backupFolder = new File( - Exporter.getExportFolderPath(BooksDbAdapter.getInstance().getActiveBookUID())); + final Exporter dummyExporter = ExporterFactory.getInstance().getExporter(backupParams, mDb); + + File backupFolder = new File(dummyExporter.getExportFolderPath()); assertThat(backupFolder).exists(); assertThat(backupFolder.listFiles()).isEmpty();