diff --git a/app/src/main/java/org/gnucash/android/db/adapter/TransactionsDbAdapter.java b/app/src/main/java/org/gnucash/android/db/adapter/TransactionsDbAdapter.java index f86c6fa5b..cc491075e 100644 --- a/app/src/main/java/org/gnucash/android/db/adapter/TransactionsDbAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/adapter/TransactionsDbAdapter.java @@ -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; @@ -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 , 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 c9e190ea4..0336de1ff 100644 --- a/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java +++ b/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java @@ -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; @@ -105,7 +106,7 @@ public class ExportAsyncTask extends AsyncTask { private ExportParams mExportParams; // File paths generated by the exporter - private List mExportedFiles; + private List mExportedFiles = Collections.emptyList(); private Exporter mExporter; @@ -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); } } @@ -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"); 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 a720facde..5fd56f3b5 100644 --- a/app/src/main/java/org/gnucash/android/export/Exporter.java +++ b/app/src/main/java/org/gnucash/android/export/Exporter.java @@ -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(); } diff --git a/app/src/main/java/org/gnucash/android/export/csv/CsvTransactionsExporter.java b/app/src/main/java/org/gnucash/android/export/csv/CsvTransactionsExporter.java index 56f9a0766..f0d082e6e 100644 --- a/app/src/main/java/org/gnucash/android/export/csv/CsvTransactionsExporter.java +++ b/app/src/main/java/org/gnucash/android/export/csv/CsvTransactionsExporter.java @@ -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; @@ -29,6 +30,8 @@ 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; @@ -36,8 +39,10 @@ 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 @@ -93,13 +98,26 @@ public List generateExport() throws ExporterException { */ private void writeSplitsToCsv(@NonNull List splits, @NonNull CsvWriter writer) throws IOException { int index = 0; + + Map 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()); @@ -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()); @@ -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); 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 e883305e2..500c37128 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 @@ -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; @@ -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; @@ -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: @@ -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: @@ -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 @@ -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)); @@ -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; @@ -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); @@ -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 } }); @@ -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() { @@ -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); } @@ -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); + } +} diff --git a/app/src/main/res/layout/fragment_export_form.xml b/app/src/main/res/layout/fragment_export_form.xml index aa3c8d3bd..a4cc5ee71 100644 --- a/app/src/main/res/layout/fragment_export_form.xml +++ b/app/src/main/res/layout/fragment_export_form.xml @@ -75,41 +75,50 @@ android:gravity="center_vertical" android:orientation="horizontal"> - + android:text="CSV"/> - + android:text="QIF" /> - + android:text="OFX"/> + + + - - + + android:text=":" /> - - diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index aa1a56cb0..39cae7266 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -57,6 +57,7 @@ TRADING + CSVT QIF OFX XML diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2644d560e..4ef7ae17e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -175,10 +175,10 @@ TRADING + CSV QIF OFX XML - CSV Select a Color @@ -466,6 +466,8 @@ export_accounts_csv_key Export all accounts (without transactions) to CSV Export as CSV + Separator + Exports transactions as CSV Date Transaction ID