Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #889 - The QIF export now always generates ZIP archive files. #901

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 <code>color</code>.
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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() {
Expand Down
67 changes: 13 additions & 54 deletions app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -93,7 +83,7 @@ public class ExportAsyncTask extends AsyncTask<ExportParams, Void, Boolean> {

private ProgressDialog mProgressDialog;

private SQLiteDatabase mDb;
private final TransactionRepository mTransactionRepository;

/**
* Log tag
Expand All @@ -108,11 +98,12 @@ public class ExportAsyncTask extends AsyncTask<ExportParams, Void, Boolean> {
// File paths generated by the exporter
private List<String> 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
Expand All @@ -138,7 +129,6 @@ protected void onPreExecute() {
@Override
protected Boolean doInBackground(ExportParams... params) {
mExportParams = params[0];
mExporter = getExporter();

try {
mExportedFiles = mExporter.generateExport();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.");
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -432,11 +402,11 @@ private static String getFileLastModifiedTimestamp(String path) {
@Deprecated
private List<String> moveExportToSDCard() throws Exporter.ExporterException {
Log.i(TAG, "Moving exported file to external storage");
new File(Exporter.getExportFolderPath(mExporter.mBookUID));
new File(mExporter.getExportFolderPath());
List<String> 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);
Expand All @@ -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<Transaction> 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();
}

/**
Expand Down Expand Up @@ -502,7 +461,7 @@ private void shareFiles(List<String> paths) {

if (mContext instanceof Activity) {
List<ResolveInfo> 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
77 changes: 51 additions & 26 deletions app/src/main/java/org/gnucash/android/export/Exporter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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-_.]", "_");
}

/**
Expand All @@ -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" : "");
}

/**
Expand All @@ -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);
}
Expand All @@ -194,46 +203,62 @@ public static long getExportTime(String filename){
public abstract List<String> 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
* <p>This path is a temporary cache file whose file extension matches the export format.<br>
* <p>This path is a temporary cache file whose file extension is inferred from the export format.<br>
* This file is deleted every time a new export is started</p>
* @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();
Expand Down
Loading