Skip to content

Commit

Permalink
Respect export time range setting when creating CSV transaction exports
Browse files Browse the repository at this point in the history
- Make CSV default export format, and re-order export options to put CSV first
- Performance optimizations for account lookup during CSV export
- Use animations when hiding/displaying export options
- File selection dialog is only opened AFTER export is started (not immediately when export fragment is launched)
  • Loading branch information
codinguser committed Jun 7, 2018
1 parent c83084a commit 41626c5
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@

import org.gnucash.android.app.GnuCashApplication;
import org.gnucash.android.model.AccountType;
import org.gnucash.android.model.Commodity;
import org.gnucash.android.model.Money;
import org.gnucash.android.model.Split;
import org.gnucash.android.model.Transaction;
Expand Down Expand Up @@ -332,6 +331,19 @@ public Cursor fetchTransactionsWithSplits(String [] columns, @Nullable String wh
orderBy);
}

/**
* Fetch all transactions modified since a given timestamp
* @param timestamp Timestamp in milliseconds (since Epoch)
* @return Cursor to the results
*/
public Cursor fetchTransactionsModifiedSince(Timestamp timestamp){
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(TransactionEntry.TABLE_NAME);
String startTimeString = TimestampHelper.getUtcStringFromTimestamp(timestamp);
return queryBuilder.query(mDb, null, TransactionEntry.COLUMN_MODIFIED_AT + " >= \"" + startTimeString + "\"",
null, null, null, TransactionEntry.COLUMN_TIMESTAMP + " ASC", null);
}

public Cursor fetchTransactionsWithSplitsWithTransactionAccount(String [] columns, String where, String[] whereArgs, String orderBy) {
// table is :
// trans_split_acct , trans_extra_info ON trans_extra_info.trans_acct_t_uid = transactions_uid ,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -105,7 +106,7 @@ public class ExportAsyncTask extends AsyncTask<ExportParams, Void, Boolean> {
private ExportParams mExportParams;

// File paths generated by the exporter
private List<String> mExportedFiles;
private List<String> mExportedFiles = Collections.emptyList();

private Exporter mExporter;

Expand Down Expand Up @@ -221,16 +222,15 @@ private Exporter getExporter() {
switch (mExportParams.getExportFormat()) {
case QIF:
return new QifExporter(mExportParams, mDb);

case OFX:
return new OfxExporter(mExportParams, mDb);

case XML:
return new GncXmlExporter(mExportParams, mDb);
case CSVA:
return new CsvAccountExporter(mExportParams, mDb);
default:
case CSVT:
return new CsvTransactionsExporter(mExportParams, mDb);
case XML:
default:
return new GncXmlExporter(mExportParams, mDb);
}
}

Expand Down Expand Up @@ -284,7 +284,7 @@ private void moveExportToUri() throws Exporter.ExporterException {
if (mExportedFiles.size() > 0){
try {
OutputStream outputStream = mContext.getContentResolver().openOutputStream(exportUri);
// Now we always get just one file exported (QIFs are zipped)
// Now we always get just one file exported (multi-currency QIFs are zipped)
org.gnucash.android.util.FileUtils.moveFile(mExportedFiles.get(0), outputStream);
} catch (IOException ex) {
throw new Exporter.ExporterException(mExportParams, "Error when moving file to URI");
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/org/gnucash/android/export/Exporter.java
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ public static String sanitizeFilename(String inputName) {
public static String buildExportFilename(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 == ExportFormat.CSVA ? "_accounts" : "") +
(format == ExportFormat.CSVT ? "_transactions" : "") +
format.getExtension();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import android.util.Log;

import com.crashlytics.android.Crashlytics;

Expand All @@ -29,15 +30,19 @@
import org.gnucash.android.model.Split;
import org.gnucash.android.model.Transaction;
import org.gnucash.android.model.TransactionType;
import org.gnucash.android.util.PreferencesHelper;
import org.gnucash.android.util.TimestampHelper;

import java.io.FileWriter;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
* Creates a GnuCash CSV transactions representation of the accounts and transactions
Expand Down Expand Up @@ -93,13 +98,26 @@ public List<String> generateExport() throws ExporterException {
*/
private void writeSplitsToCsv(@NonNull List<Split> splits, @NonNull CsvWriter writer) throws IOException {
int index = 0;

Map<String, Account> uidAccountMap = new HashMap<>();

for (Split split : splits) {
if (index++ > 0){ // the first split is on the same line as the transactions. But after that, we
writer.write("" + mCsvSeparator + mCsvSeparator + mCsvSeparator + mCsvSeparator
+ mCsvSeparator + mCsvSeparator + mCsvSeparator + mCsvSeparator);
}
writer.writeToken(split.getMemo());
Account account = mAccountsDbAdapter.getRecord(split.getAccountUID());

//cache accounts so that we do not have to go to the DB each time
String accountUID = split.getAccountUID();
Account account;
if (uidAccountMap.containsKey(accountUID)) {
account = uidAccountMap.get(accountUID);
} else {
account = mAccountsDbAdapter.getRecord(accountUID);
uidAccountMap.put(accountUID, account);
}

writer.writeToken(account.getFullName());
writer.writeToken(account.getName());

Expand All @@ -126,7 +144,8 @@ private void generateExport(final CsvWriter csvWriter) throws ExporterException
csvWriter.newLine();


Cursor cursor = mTransactionsDbAdapter.fetchAllRecords();
Cursor cursor = mTransactionsDbAdapter.fetchTransactionsModifiedSince(mExportParams.getExportStartTime());
Log.d(LOG_TAG, String.format("Exporting %d transactions to CSV", cursor.getCount()));
while (cursor.moveToNext()){
Transaction transaction = mTransactionsDbAdapter.buildModelInstance(cursor);
Date date = new Date(transaction.getTimeMillis());
Expand All @@ -143,6 +162,7 @@ private void generateExport(final CsvWriter csvWriter) throws ExporterException
writeSplitsToCsv(transaction.getSplits(), csvWriter);
}

PreferencesHelper.setLastExportTime(TimestampHelper.getTimestampFromNow());
} catch (IOException e) {
Crashlytics.logException(e);
throw new ExporterException(mExportParams, e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
Expand Down Expand Up @@ -134,7 +136,6 @@ public class ExportFormFragment extends Fragment implements
@BindView(R.id.switch_export_all) SwitchCompat mExportAllSwitch;

@BindView(R.id.export_date_layout) LinearLayout mExportDateLayout;
@BindView(R.id.export_separator_layout) LinearLayout mExportSeparatorLayout;

@BindView(R.id.radio_ofx_format) RadioButton mOfxRadioButton;
@BindView(R.id.radio_qif_format) RadioButton mQifRadioButton;
Expand Down Expand Up @@ -194,8 +195,9 @@ private void onRadioButtonClicked(View view){
} else {
mExportWarningTextView.setVisibility(View.GONE);
}
mExportDateLayout.setVisibility(View.VISIBLE);
mExportSeparatorLayout.setVisibility(View.GONE);

OptionsViewAnimationUtils.expand(mExportDateLayout);
OptionsViewAnimationUtils.collapse(mCsvOptionsLayout);
break;

case R.id.radio_qif_format:
Expand All @@ -207,22 +209,23 @@ private void onRadioButtonClicked(View view){
} else {
mExportWarningTextView.setVisibility(View.GONE);
}
mExportDateLayout.setVisibility(View.VISIBLE);
mCsvOptionsLayout.setVisibility(View.GONE);

OptionsViewAnimationUtils.expand(mExportDateLayout);
OptionsViewAnimationUtils.collapse(mCsvOptionsLayout);
break;

case R.id.radio_xml_format:
mExportFormat = ExportFormat.XML;
mExportWarningTextView.setText(R.string.export_warning_xml);
mExportDateLayout.setVisibility(View.GONE);
mCsvOptionsLayout.setVisibility(View.GONE);
OptionsViewAnimationUtils.collapse(mExportDateLayout);
OptionsViewAnimationUtils.collapse(mCsvOptionsLayout);
break;

case R.id.radio_csv_transactions_format:
mExportFormat = ExportFormat.CSVT;
mExportWarningTextView.setText("Exports registered transactions as CSV");
mExportDateLayout.setVisibility(View.GONE);
mCsvOptionsLayout.setVisibility(View.VISIBLE);
mExportWarningTextView.setText(R.string.export_notice_csv);
OptionsViewAnimationUtils.expand(mExportDateLayout);
OptionsViewAnimationUtils.expand(mCsvOptionsLayout);
break;

case R.id.radio_separator_comma_format:
Expand All @@ -246,9 +249,6 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container,

bindViewListeners();

String[] export_format_strings = getResources().getStringArray(R.array.export_formats);
mCsvTransactionsRadioButton.setText(export_format_strings[3]);

return view;
}
@Override
Expand Down Expand Up @@ -359,12 +359,11 @@ public void onItemSelected(AdapterView<?> parent, View view, int position, long
if (view == null) //the item selection is fired twice by the Android framework. Ignore the first one
return;
switch (position) {
case 0:
case 0: //Save As..
mExportTarget = ExportParams.ExportTarget.URI;
mRecurrenceOptionsView.setVisibility(View.VISIBLE);
if (mExportUri != null)
setExportUriText(mExportUri.toString());
selectExportFile();
break;
case 1: //DROPBOX
setExportUriText(getString(R.string.label_dropbox_export_destination));
Expand All @@ -377,7 +376,7 @@ public void onItemSelected(AdapterView<?> parent, View view, int position, long
Auth.startOAuth2Authentication(getActivity(), dropboxAppKey);
}
break;
case 2:
case 2: //OwnCloud
setExportUriText(null);
mRecurrenceOptionsView.setVisibility(View.VISIBLE);
mExportTarget = ExportParams.ExportTarget.OWNCLOUD;
Expand All @@ -387,7 +386,7 @@ public void onItemSelected(AdapterView<?> parent, View view, int position, long
ocDialog.show(getActivity().getSupportFragmentManager(), "ownCloud dialog");
}
break;
case 3:
case 3: //Share File
setExportUriText(getString(R.string.label_select_destination_after_export));
mExportTarget = ExportParams.ExportTarget.SHARING;
mRecurrenceOptionsView.setVisibility(View.GONE);
Expand All @@ -401,7 +400,7 @@ public void onItemSelected(AdapterView<?> parent, View view, int position, long

@Override
public void onNothingSelected(AdapterView<?> parent) {

//nothing to see here, move along
}
});

Expand Down Expand Up @@ -482,7 +481,7 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
mRecurrenceTextView.setOnClickListener(new RecurrenceViewClickListener((AppCompatActivity) getActivity(), mRecurrenceRule, this));

//this part (setting the export format) must come after the recurrence view bindings above
String defaultExportFormat = sharedPrefs.getString(getString(R.string.key_default_export_format), ExportFormat.QIF.name());
String defaultExportFormat = sharedPrefs.getString(getString(R.string.key_default_export_format), ExportFormat.CSVT.name());
mExportFormat = ExportFormat.valueOf(defaultExportFormat);

View.OnClickListener radioClickListener = new View.OnClickListener() {
Expand Down Expand Up @@ -543,11 +542,6 @@ private void selectExportFile() {
String bookName = BooksDbAdapter.getInstance().getActiveBookDisplayName();

String filename = Exporter.buildExportFilename(mExportFormat, bookName);
if (mExportFormat == ExportFormat.QIF) {
createIntent.setType("application/zip");
filename += ".zip";
}

createIntent.putExtra(Intent.EXTRA_TITLE, filename);
startActivityForResult(createIntent, REQUEST_EXPORT_FILE);
}
Expand Down Expand Up @@ -615,3 +609,57 @@ public void onTimeSet(RadialTimePickerDialogFragment dialog, int hourOfDay, int
}
}

// Gotten from: https://stackoverflow.com/a/31720191
class OptionsViewAnimationUtils {

public static void expand(final View v) {
v.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
final int targetHeight = v.getMeasuredHeight();

v.getLayoutParams().height = 0;
v.setVisibility(View.VISIBLE);
Animation a = new Animation()
{
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
v.getLayoutParams().height = interpolatedTime == 1
? ViewGroup.LayoutParams.WRAP_CONTENT
: (int)(targetHeight * interpolatedTime);
v.requestLayout();
}

@Override
public boolean willChangeBounds() {
return true;
}
};

a.setDuration((int)(3 * targetHeight / v.getContext().getResources().getDisplayMetrics().density));
v.startAnimation(a);
}

public static void collapse(final View v) {
final int initialHeight = v.getMeasuredHeight();

Animation a = new Animation()
{
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
if(interpolatedTime == 1){
v.setVisibility(View.GONE);
}else{
v.getLayoutParams().height = initialHeight - (int)(initialHeight * interpolatedTime);
v.requestLayout();
}
}

@Override
public boolean willChangeBounds() {
return true;
}
};

a.setDuration((int)(3 * initialHeight / v.getContext().getResources().getDisplayMetrics().density));
v.startAnimation(a);
}
}
Loading

0 comments on commit 41626c5

Please sign in to comment.