Skip to content

Commit 41626c5

Browse files
committed
Respect export time range setting when creating CSV transaction exports
- 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)
1 parent c83084a commit 41626c5

File tree

8 files changed

+143
-59
lines changed

8 files changed

+143
-59
lines changed

app/src/main/java/org/gnucash/android/db/adapter/TransactionsDbAdapter.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232

3333
import org.gnucash.android.app.GnuCashApplication;
3434
import org.gnucash.android.model.AccountType;
35-
import org.gnucash.android.model.Commodity;
3635
import org.gnucash.android.model.Money;
3736
import org.gnucash.android.model.Split;
3837
import org.gnucash.android.model.Transaction;
@@ -332,6 +331,19 @@ public Cursor fetchTransactionsWithSplits(String [] columns, @Nullable String wh
332331
orderBy);
333332
}
334333

334+
/**
335+
* Fetch all transactions modified since a given timestamp
336+
* @param timestamp Timestamp in milliseconds (since Epoch)
337+
* @return Cursor to the results
338+
*/
339+
public Cursor fetchTransactionsModifiedSince(Timestamp timestamp){
340+
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
341+
queryBuilder.setTables(TransactionEntry.TABLE_NAME);
342+
String startTimeString = TimestampHelper.getUtcStringFromTimestamp(timestamp);
343+
return queryBuilder.query(mDb, null, TransactionEntry.COLUMN_MODIFIED_AT + " >= \"" + startTimeString + "\"",
344+
null, null, null, TransactionEntry.COLUMN_TIMESTAMP + " ASC", null);
345+
}
346+
335347
public Cursor fetchTransactionsWithSplitsWithTransactionAccount(String [] columns, String where, String[] whereArgs, String orderBy) {
336348
// table is :
337349
// trans_split_acct , trans_extra_info ON trans_extra_info.trans_acct_t_uid = transactions_uid ,

app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
import java.io.OutputStream;
7575
import java.text.SimpleDateFormat;
7676
import java.util.ArrayList;
77+
import java.util.Collections;
7778
import java.util.Date;
7879
import java.util.List;
7980
import java.util.concurrent.TimeUnit;
@@ -105,7 +106,7 @@ public class ExportAsyncTask extends AsyncTask<ExportParams, Void, Boolean> {
105106
private ExportParams mExportParams;
106107

107108
// File paths generated by the exporter
108-
private List<String> mExportedFiles;
109+
private List<String> mExportedFiles = Collections.emptyList();
109110

110111
private Exporter mExporter;
111112

@@ -221,16 +222,15 @@ private Exporter getExporter() {
221222
switch (mExportParams.getExportFormat()) {
222223
case QIF:
223224
return new QifExporter(mExportParams, mDb);
224-
225225
case OFX:
226226
return new OfxExporter(mExportParams, mDb);
227-
228-
case XML:
229-
return new GncXmlExporter(mExportParams, mDb);
230227
case CSVA:
231228
return new CsvAccountExporter(mExportParams, mDb);
232-
default:
229+
case CSVT:
233230
return new CsvTransactionsExporter(mExportParams, mDb);
231+
case XML:
232+
default:
233+
return new GncXmlExporter(mExportParams, mDb);
234234
}
235235
}
236236

@@ -284,7 +284,7 @@ private void moveExportToUri() throws Exporter.ExporterException {
284284
if (mExportedFiles.size() > 0){
285285
try {
286286
OutputStream outputStream = mContext.getContentResolver().openOutputStream(exportUri);
287-
// Now we always get just one file exported (QIFs are zipped)
287+
// Now we always get just one file exported (multi-currency QIFs are zipped)
288288
org.gnucash.android.util.FileUtils.moveFile(mExportedFiles.get(0), outputStream);
289289
} catch (IOException ex) {
290290
throw new Exporter.ExporterException(mExportParams, "Error when moving file to URI");

app/src/main/java/org/gnucash/android/export/Exporter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,8 @@ public static String sanitizeFilename(String inputName) {
161161
public static String buildExportFilename(ExportFormat format, String bookName) {
162162
return EXPORT_FILENAME_DATE_FORMAT.format(new Date(System.currentTimeMillis()))
163163
+ "_gnucash_export_" + sanitizeFilename(bookName) +
164-
(format==ExportFormat.CSVA?"_accounts_":"") +
165-
(format==ExportFormat.CSVT?"_transactions_":"") +
164+
(format == ExportFormat.CSVA ? "_accounts" : "") +
165+
(format == ExportFormat.CSVT ? "_transactions" : "") +
166166
format.getExtension();
167167
}
168168

app/src/main/java/org/gnucash/android/export/csv/CsvTransactionsExporter.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import android.database.Cursor;
2020
import android.database.sqlite.SQLiteDatabase;
2121
import android.support.annotation.NonNull;
22+
import android.util.Log;
2223

2324
import com.crashlytics.android.Crashlytics;
2425

@@ -29,15 +30,19 @@
2930
import org.gnucash.android.model.Split;
3031
import org.gnucash.android.model.Transaction;
3132
import org.gnucash.android.model.TransactionType;
33+
import org.gnucash.android.util.PreferencesHelper;
34+
import org.gnucash.android.util.TimestampHelper;
3235

3336
import java.io.FileWriter;
3437
import java.io.IOException;
3538
import java.text.DateFormat;
3639
import java.text.SimpleDateFormat;
3740
import java.util.Arrays;
3841
import java.util.Date;
42+
import java.util.HashMap;
3943
import java.util.List;
4044
import java.util.Locale;
45+
import java.util.Map;
4146

4247
/**
4348
* Creates a GnuCash CSV transactions representation of the accounts and transactions
@@ -93,13 +98,26 @@ public List<String> generateExport() throws ExporterException {
9398
*/
9499
private void writeSplitsToCsv(@NonNull List<Split> splits, @NonNull CsvWriter writer) throws IOException {
95100
int index = 0;
101+
102+
Map<String, Account> uidAccountMap = new HashMap<>();
103+
96104
for (Split split : splits) {
97105
if (index++ > 0){ // the first split is on the same line as the transactions. But after that, we
98106
writer.write("" + mCsvSeparator + mCsvSeparator + mCsvSeparator + mCsvSeparator
99107
+ mCsvSeparator + mCsvSeparator + mCsvSeparator + mCsvSeparator);
100108
}
101109
writer.writeToken(split.getMemo());
102-
Account account = mAccountsDbAdapter.getRecord(split.getAccountUID());
110+
111+
//cache accounts so that we do not have to go to the DB each time
112+
String accountUID = split.getAccountUID();
113+
Account account;
114+
if (uidAccountMap.containsKey(accountUID)) {
115+
account = uidAccountMap.get(accountUID);
116+
} else {
117+
account = mAccountsDbAdapter.getRecord(accountUID);
118+
uidAccountMap.put(accountUID, account);
119+
}
120+
103121
writer.writeToken(account.getFullName());
104122
writer.writeToken(account.getName());
105123

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

128146

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

165+
PreferencesHelper.setLastExportTime(TimestampHelper.getTimestampFromNow());
146166
} catch (IOException e) {
147167
Crashlytics.logException(e);
148168
throw new ExporterException(mExportParams, e);

app/src/main/java/org/gnucash/android/ui/export/ExportFormFragment.java

Lines changed: 72 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import android.view.MenuItem;
3535
import android.view.View;
3636
import android.view.ViewGroup;
37+
import android.view.animation.Animation;
38+
import android.view.animation.Transformation;
3739
import android.widget.AdapterView;
3840
import android.widget.ArrayAdapter;
3941
import android.widget.CheckBox;
@@ -134,7 +136,6 @@ public class ExportFormFragment extends Fragment implements
134136
@BindView(R.id.switch_export_all) SwitchCompat mExportAllSwitch;
135137

136138
@BindView(R.id.export_date_layout) LinearLayout mExportDateLayout;
137-
@BindView(R.id.export_separator_layout) LinearLayout mExportSeparatorLayout;
138139

139140
@BindView(R.id.radio_ofx_format) RadioButton mOfxRadioButton;
140141
@BindView(R.id.radio_qif_format) RadioButton mQifRadioButton;
@@ -194,8 +195,9 @@ private void onRadioButtonClicked(View view){
194195
} else {
195196
mExportWarningTextView.setVisibility(View.GONE);
196197
}
197-
mExportDateLayout.setVisibility(View.VISIBLE);
198-
mExportSeparatorLayout.setVisibility(View.GONE);
198+
199+
OptionsViewAnimationUtils.expand(mExportDateLayout);
200+
OptionsViewAnimationUtils.collapse(mCsvOptionsLayout);
199201
break;
200202

201203
case R.id.radio_qif_format:
@@ -207,22 +209,23 @@ private void onRadioButtonClicked(View view){
207209
} else {
208210
mExportWarningTextView.setVisibility(View.GONE);
209211
}
210-
mExportDateLayout.setVisibility(View.VISIBLE);
211-
mCsvOptionsLayout.setVisibility(View.GONE);
212+
213+
OptionsViewAnimationUtils.expand(mExportDateLayout);
214+
OptionsViewAnimationUtils.collapse(mCsvOptionsLayout);
212215
break;
213216

214217
case R.id.radio_xml_format:
215218
mExportFormat = ExportFormat.XML;
216219
mExportWarningTextView.setText(R.string.export_warning_xml);
217-
mExportDateLayout.setVisibility(View.GONE);
218-
mCsvOptionsLayout.setVisibility(View.GONE);
220+
OptionsViewAnimationUtils.collapse(mExportDateLayout);
221+
OptionsViewAnimationUtils.collapse(mCsvOptionsLayout);
219222
break;
220223

221224
case R.id.radio_csv_transactions_format:
222225
mExportFormat = ExportFormat.CSVT;
223-
mExportWarningTextView.setText("Exports registered transactions as CSV");
224-
mExportDateLayout.setVisibility(View.GONE);
225-
mCsvOptionsLayout.setVisibility(View.VISIBLE);
226+
mExportWarningTextView.setText(R.string.export_notice_csv);
227+
OptionsViewAnimationUtils.expand(mExportDateLayout);
228+
OptionsViewAnimationUtils.expand(mCsvOptionsLayout);
226229
break;
227230

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

247250
bindViewListeners();
248251

249-
String[] export_format_strings = getResources().getStringArray(R.array.export_formats);
250-
mCsvTransactionsRadioButton.setText(export_format_strings[3]);
251-
252252
return view;
253253
}
254254
@Override
@@ -359,12 +359,11 @@ public void onItemSelected(AdapterView<?> parent, View view, int position, long
359359
if (view == null) //the item selection is fired twice by the Android framework. Ignore the first one
360360
return;
361361
switch (position) {
362-
case 0:
362+
case 0: //Save As..
363363
mExportTarget = ExportParams.ExportTarget.URI;
364364
mRecurrenceOptionsView.setVisibility(View.VISIBLE);
365365
if (mExportUri != null)
366366
setExportUriText(mExportUri.toString());
367-
selectExportFile();
368367
break;
369368
case 1: //DROPBOX
370369
setExportUriText(getString(R.string.label_dropbox_export_destination));
@@ -377,7 +376,7 @@ public void onItemSelected(AdapterView<?> parent, View view, int position, long
377376
Auth.startOAuth2Authentication(getActivity(), dropboxAppKey);
378377
}
379378
break;
380-
case 2:
379+
case 2: //OwnCloud
381380
setExportUriText(null);
382381
mRecurrenceOptionsView.setVisibility(View.VISIBLE);
383382
mExportTarget = ExportParams.ExportTarget.OWNCLOUD;
@@ -387,7 +386,7 @@ public void onItemSelected(AdapterView<?> parent, View view, int position, long
387386
ocDialog.show(getActivity().getSupportFragmentManager(), "ownCloud dialog");
388387
}
389388
break;
390-
case 3:
389+
case 3: //Share File
391390
setExportUriText(getString(R.string.label_select_destination_after_export));
392391
mExportTarget = ExportParams.ExportTarget.SHARING;
393392
mRecurrenceOptionsView.setVisibility(View.GONE);
@@ -401,7 +400,7 @@ public void onItemSelected(AdapterView<?> parent, View view, int position, long
401400

402401
@Override
403402
public void onNothingSelected(AdapterView<?> parent) {
404-
403+
//nothing to see here, move along
405404
}
406405
});
407406

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

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

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

545544
String filename = Exporter.buildExportFilename(mExportFormat, bookName);
546-
if (mExportFormat == ExportFormat.QIF) {
547-
createIntent.setType("application/zip");
548-
filename += ".zip";
549-
}
550-
551545
createIntent.putExtra(Intent.EXTRA_TITLE, filename);
552546
startActivityForResult(createIntent, REQUEST_EXPORT_FILE);
553547
}
@@ -615,3 +609,57 @@ public void onTimeSet(RadialTimePickerDialogFragment dialog, int hourOfDay, int
615609
}
616610
}
617611

612+
// Gotten from: https://stackoverflow.com/a/31720191
613+
class OptionsViewAnimationUtils {
614+
615+
public static void expand(final View v) {
616+
v.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
617+
final int targetHeight = v.getMeasuredHeight();
618+
619+
v.getLayoutParams().height = 0;
620+
v.setVisibility(View.VISIBLE);
621+
Animation a = new Animation()
622+
{
623+
@Override
624+
protected void applyTransformation(float interpolatedTime, Transformation t) {
625+
v.getLayoutParams().height = interpolatedTime == 1
626+
? ViewGroup.LayoutParams.WRAP_CONTENT
627+
: (int)(targetHeight * interpolatedTime);
628+
v.requestLayout();
629+
}
630+
631+
@Override
632+
public boolean willChangeBounds() {
633+
return true;
634+
}
635+
};
636+
637+
a.setDuration((int)(3 * targetHeight / v.getContext().getResources().getDisplayMetrics().density));
638+
v.startAnimation(a);
639+
}
640+
641+
public static void collapse(final View v) {
642+
final int initialHeight = v.getMeasuredHeight();
643+
644+
Animation a = new Animation()
645+
{
646+
@Override
647+
protected void applyTransformation(float interpolatedTime, Transformation t) {
648+
if(interpolatedTime == 1){
649+
v.setVisibility(View.GONE);
650+
}else{
651+
v.getLayoutParams().height = initialHeight - (int)(initialHeight * interpolatedTime);
652+
v.requestLayout();
653+
}
654+
}
655+
656+
@Override
657+
public boolean willChangeBounds() {
658+
return true;
659+
}
660+
};
661+
662+
a.setDuration((int)(3 * initialHeight / v.getContext().getResources().getDisplayMetrics().density));
663+
v.startAnimation(a);
664+
}
665+
}

0 commit comments

Comments
 (0)