diff --git a/.travis.yml b/.travis.yml index ee3a22909..9dcd1ce95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,22 +5,25 @@ android: - platform-tools - tools - tools #not a typo. Needed for SDK update - - build-tools-27.0.0 + - build-tools-27.0.3 # The SDK version used to compile your project - - android-26 + - android-27 # Additional components - extra-android-support - extra-google-google_play_services - extra-google-m2repository - extra-android-m2repository - - addon-google_apis-google-25 + - addon-google_apis-google-26 # Specify at least one system image, # if you need to run emulator(s) during your tests #- sys-img-armeabi-v7a-android-23 +# XXX: Temporary workaround. Remove once fixed +before_install: + - yes | sdkmanager "platforms;android-27" # Emulator Management: Create, Start and Wait # Re-enable this when we figure out how to reliably build on Travis diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dda3af41..17a7f8258 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ Change Log =============================================================================== +Version 2.4.0 *(2018-06-15)* +---------------------------- +* Feature #665: Adds CSV export format +* Feature #544: Add extra checkbox confirmation for irreversible actions +* Feature #767: Backup before destructive actions +* Feature #465: Account balances now include future transactions +* Fixed #764: Crash when importing XML files from Gnucash desktop v2.7 and up +* Fixed #768: ScheduledActionService crashes on Android 8 (Oreo) +* Fixed #731: Double display of Persian currency symbol +* Fixed #771: QIF export crashes due to illegal denominator +* Fixed #757: Backups are created every hour +* Fixed #766: Backups are kept forever + + Version 2.3.0 *(2018-01-10)* ---------------------------- * Feature #544: Use double confirmation dialog boxes before irreversible actions diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index fe12546fb..d5f100d38 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -27,6 +27,7 @@ The following people (in alphabetical order) contributed (commits on GitHub) to * Falk Brockmann * Felipe Morato * Geert Janssens +* Gleb Semyannikov * Jörg Möller * Israel Buitron * Jesse Shieh @@ -54,6 +55,8 @@ The following people (in alphabetical order) contributed (commits on GitHub) to * Stephan Windmüller * Terry Chung * thesebas thesebas@thesebas.net +* Timur Badretdinov +* Timur Khuzin * Vladimir Rutsky * Weslly Oliveira * windwarrior lennartbuit@gmail.com diff --git a/README.md b/README.md index 245c2fb20..b0dc7dfb6 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Accounts | Transactions | Reports :-------------------------:|:-------------------------:|:-------------------------: ![Accounts List](docs/images/v2.0.0_home.png) | ![Transactions List](docs/images/v2.0.0_transactions_list.png) | ![Reports](docs/images/v2.0.0_reports.png) -The application supports Android 4.4 KitKat (API level 19) and above. +The application supports Android 4.4 KitKat (API level 19) and above. Features include: @@ -90,15 +90,16 @@ There are several ways you could contribute to the development. * Pull requests are always welcome! You could contribute code by fixing bugs, adding new features or automated tests. Take a look at the [bug tracker](https://github.com/codinguser/gnucash-android/issues?state=open) -for ideas where to start. Also make sure to read our [contribution guidlines](https://github.com/codinguser/gnucash-android/blob/master/.github/CONTRIBUTING.md) +for ideas where to start. It is also preferable to target issues in the current [milestone](https://github.com/codinguser/gnucash-android/milestones). +* Make sure to read our [contribution guidelines](https://github.com/codinguser/gnucash-android/blob/master/.github/CONTRIBUTING.md) before starting to code. -* One way is providing translations for locales which are not yet available, or improving translations. +* Another way to contribute is by providing translations for languages, or improving translations. Please visit [CrowdIn](https://crowdin.com/project/gnucash-android) in order to update and create new translations For development, it is recommended to use the Android Studio for development which is available for free. Import the project into the IDE using the build.gradle file. The IDE will resolve dependencies automatically. -# Licence +# License GnuCash Android is free software; you can redistribute it and/or modify it under the terms of the Apache license, version 2.0. You may obtain a copy of the License at diff --git a/app/build.gradle b/app/build.gradle index 1a604260e..674ca8088 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,9 +4,9 @@ apply plugin: 'com.android.application' apply plugin: 'io.fabric' def versionMajor = 2 -def versionMinor = 3 +def versionMinor = 4 def versionPatch = 0 -def versionBuild = 4 +def versionBuild = 3 static def buildTime() { def df = new SimpleDateFormat("yyyyMMdd HH:mm 'UTC'") @@ -20,13 +20,13 @@ static def gitSha() { android { - compileSdkVersion 26 - buildToolsVersion '27.0.0' + compileSdkVersion 27 + buildToolsVersion '27.0.3' defaultConfig { applicationId "org.gnucash.android" testApplicationId 'org.gnucash.android.test' minSdkVersion 19 - targetSdkVersion 26 + targetSdkVersion 27 versionCode versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild versionName "${versionMajor}.${versionMinor}.${versionPatch}" resValue "string", "app_version_name", "${versionName}" @@ -179,7 +179,7 @@ afterEvaluate { } -def androidSupportVersion = "26.0.1" +def androidSupportVersion = "27.0.2" def androidEspressoVersion = "3.0.0" def androidSupportTestVersion = "1.0.0" @@ -190,69 +190,70 @@ repositories{ } dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.github.nextcloud:android-library:1.0.31' - compile('com.android.support:support-v4:' + androidSupportVersion, - 'com.android.support:appcompat-v7:' + androidSupportVersion, - 'com.android.support:design:' + androidSupportVersion, - 'com.android.support:cardview-v7:' + androidSupportVersion, - 'com.android.support:preference-v7:' + androidSupportVersion, - 'com.android.support:recyclerview-v7:' + androidSupportVersion, - 'com.code-troopers.betterpickers:library:3.1.0', - 'org.jraf:android-switch-backport:2.0.1@aar', - 'com.github.PhilJay:MPAndroidChart:v2.1.3', - 'joda-time:joda-time:2.9.4', - 'com.google.android.gms:play-services-drive:9.6.1', - 'io.github.kobakei:ratethisapp:1.1.3', - 'com.squareup:android-times-square:1.6.5@aar', - 'com.github.techfreak:wizardpager:1.0.3', - 'net.objecthunter:exp4j:0.4.7', - 'org.apache.jackrabbit:jackrabbit-webdav:2.13.3', - 'com.dropbox.core:dropbox-core-sdk:3.0.3', - 'com.android.support:multidex:1.0.1' + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'com.github.nextcloud:android-library:1.0.31' + implementation('com.android.support:support-v4:' + androidSupportVersion, + 'com.android.support:appcompat-v7:' + androidSupportVersion, + 'com.android.support:design:' + androidSupportVersion, + 'com.android.support:cardview-v7:' + androidSupportVersion, + 'com.android.support:preference-v7:' + androidSupportVersion, + 'com.android.support:recyclerview-v7:' + androidSupportVersion, + 'com.code-troopers.betterpickers:library:3.1.0', + 'org.jraf:android-switch-backport:2.0.1@aar', + 'com.github.PhilJay:MPAndroidChart:v2.1.3', + 'joda-time:joda-time:2.9.4', + 'com.google.android.gms:play-services-drive:9.6.1', + 'io.github.kobakei:ratethisapp:1.1.3', + 'com.squareup:android-times-square:1.6.5@aar', + 'com.github.techfreak:wizardpager:1.0.3', + 'net.objecthunter:exp4j:0.4.7', + 'org.apache.jackrabbit:jackrabbit-webdav:2.13.3', + 'com.dropbox.core:dropbox-core-sdk:3.0.3', + 'com.android.support:multidex:1.0.1' ) - debugCompile 'com.facebook.stetho:stetho:1.5.0' + debugImplementation 'com.facebook.stetho:stetho:1.5.0' - compile 'com.jakewharton:butterknife:8.8.1' + implementation 'com.jakewharton:butterknife:8.8.1' annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' - compile ('com.uservoice:uservoice-android-sdk:1.2.6') { + implementation ('com.uservoice:uservoice-android-sdk:1.2.6') { exclude module: 'commons-logging' exclude module: 'httpcore' exclude module: 'httpclient' } - compile('com.crashlytics.sdk.android:crashlytics:2.6.7@aar') { + implementation('com.crashlytics.sdk.android:crashlytics:2.6.7@aar') { transitive = true; } testImplementation 'org.robolectric:robolectric:3.5.1' - testCompile( + testImplementation( 'junit:junit:4.12', 'joda-time:joda-time:2.9.4', 'org.assertj:assertj-core:1.7.1' ) - testCompile 'org.robolectric:shadows-multidex:3.0' - - androidTestCompile ('com.android.support:support-annotations:' + androidSupportVersion, - 'com.android.support.test:runner:' + androidSupportTestVersion, - 'com.android.support.test:rules:' + androidSupportTestVersion, - 'com.android.support.test.espresso:espresso-core:' + androidEspressoVersion, - 'com.android.support.test.espresso:espresso-intents:' + androidEspressoVersion, + testImplementation 'org.robolectric:shadows-multidex:3.0' + + androidTestImplementation ( + 'com.android.support:support-annotations:' + androidSupportVersion, + 'com.android.support.test:runner:' + androidSupportTestVersion, + 'com.android.support.test:rules:' + androidSupportTestVersion, + 'com.android.support.test.espresso:espresso-core:' + androidEspressoVersion, + 'com.android.support.test.espresso:espresso-intents:' + androidEspressoVersion, //the following are only added so that the app and test version both us the same versions - 'com.android.support:appcompat-v7:' + androidSupportVersion, - 'com.android.support:design:' + androidSupportVersion) - androidTestCompile ('com.android.support.test.espresso:espresso-contrib:' + androidEspressoVersion) { + 'com.android.support:appcompat-v7:' + androidSupportVersion, + 'com.android.support:design:' + androidSupportVersion) + androidTestImplementation ('com.android.support.test.espresso:espresso-contrib:' + androidEspressoVersion) { exclude group: 'com.android.support', module: 'support-v4' exclude module: 'recyclerview-v7' } - androidTestCompile('com.squareup.assertj:assertj-android:1.1.1'){ + androidTestImplementation('com.squareup.assertj:assertj-android:1.1.1'){ exclude group: 'com.android.support', module:'support-annotations' } - androidTestCompile 'com.squareup.spoon:spoon-client:1.6.4' + androidTestImplementation 'com.squareup.spoon:spoon-client:1.6.4' } diff --git a/app/src/androidTest/java/org/gnucash/android/test/ui/AccountsActivityTest.java b/app/src/androidTest/java/org/gnucash/android/test/ui/AccountsActivityTest.java index d57cfd240..eb7f43c13 100644 --- a/app/src/androidTest/java/org/gnucash/android/test/ui/AccountsActivityTest.java +++ b/app/src/androidTest/java/org/gnucash/android/test/ui/AccountsActivityTest.java @@ -17,13 +17,11 @@ package org.gnucash.android.test.ui; import android.Manifest; -import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences.Editor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; -import android.os.Build; import android.preference.PreferenceManager; import android.support.test.espresso.Espresso; import android.support.test.espresso.matcher.ViewMatchers; @@ -32,6 +30,7 @@ import android.support.test.runner.AndroidJUnit4; import android.support.v4.app.Fragment; import android.util.Log; +import android.view.View; import com.kobakei.ratethisapp.RateThisApp; @@ -54,6 +53,9 @@ import org.gnucash.android.test.ui.util.DisableAnimationsRule; import org.gnucash.android.ui.account.AccountsActivity; import org.gnucash.android.ui.account.AccountsListFragment; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; @@ -87,6 +89,7 @@ import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @@ -236,6 +239,21 @@ public void testCreateAccount(){ assertThat(newestAccount.isPlaceholderAccount()).isTrue(); } + @Test + public void should_IncludeFutureTransactionsInAccountBalance(){ + Transaction transaction = new Transaction("Future transaction"); + Split split1 = new Split(new Money("4.15", ACCOUNTS_CURRENCY_CODE), SIMPLE_ACCOUNT_UID); + transaction.addSplit(split1); + transaction.setTime(System.currentTimeMillis() + 4815162342L); + mTransactionsDbAdapter.addRecord(transaction); + + refreshAccountsList(); + + List trxns = mTransactionsDbAdapter.getAllTransactions(); + + onView(first(withText(containsString("4.15")))).check(matches(isDisplayed())); + } + @Test public void testChangeParentAccount() { final String accountName = "Euro Account"; @@ -511,4 +529,31 @@ public void run() { System.err.println("Failed to refresh fragment"); } } + + /** + * Matcher to select the first of multiple views which are matched in the UI + * @param expected Matcher which fits multiple views + * @return Single match + */ + public static Matcher first(final Matcher expected){ + + return new TypeSafeMatcher() { + private boolean first = false; + + @Override + protected boolean matchesSafely(View item) { + + if( expected.matches(item) && !first ){ + return first = true; + } + + return false; + } + + @Override + public void describeTo(Description description) { + description.appendText("Matcher.first( " + expected.toString() + " )" ); + } + }; + } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c8857485a..b2f2575f8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -120,7 +120,10 @@ + @@ -154,6 +157,13 @@ + + + + + + accountList, UpdateMethod upda stmt.bindString(3, account.getAccountType().name()); stmt.bindString(4, account.getCommodity().getCurrencyCode()); if (account.getColor() != Account.DEFAULT_COLOR) { - stmt.bindString(5, convertToRGBHexString(account.getColor())); + stmt.bindString(5, account.getColorHexString()); } stmt.bindLong(6, account.isFavorite() ? 1 : 0); stmt.bindString(7, account.getFullName()); @@ -230,10 +230,6 @@ public long bulkAddRecords(@NonNull List accountList, UpdateMethod upda return stmt; } - private String convertToRGBHexString(int color) { - return String.format("#%06X", (0xFFFFFF & color)); - } - /** * Marks all transactions for a given account as exported * @param accountUID Unique ID of the record to be marked as exported 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 d6279aefe..0336de1ff 100644 --- a/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java +++ b/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java @@ -56,6 +56,8 @@ 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; @@ -64,6 +66,7 @@ import org.gnucash.android.ui.account.AccountsListFragment; import org.gnucash.android.ui.settings.BackupPreferenceFragment; import org.gnucash.android.ui.transaction.TransactionsActivity; +import org.gnucash.android.util.BackupManager; import java.io.File; import java.io.FileInputStream; @@ -71,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; @@ -102,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; @@ -212,16 +216,18 @@ private void dismissProgressDialog() { /** * Returns an exporter corresponding to the user settings. - * @return Object of one of {@link QifExporter}, {@link OfxExporter} or {@link GncXmlExporter} + * @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); @@ -278,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"); @@ -453,7 +459,7 @@ private String stripPathPart(String fullPathName) { */ private void backupAndDeleteTransactions(){ Log.i(TAG, "Backup and deleting transactions after export"); - GncXmlExporter.createBackup(); //create backup before deleting everything + BackupManager.backupActiveBook(); //create backup before deleting everything List openingBalances = new ArrayList<>(); boolean preserveOpeningBalances = GnuCashApplication.shouldSaveOpeningBalances(false); 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 99ca1a347..7b6fc99c1 100644 --- a/app/src/main/java/org/gnucash/android/export/ExportFormat.java +++ b/app/src/main/java/org/gnucash/android/export/ExportFormat.java @@ -22,7 +22,9 @@ public enum ExportFormat { QIF("Quicken Interchange Format"), OFX("Open Financial eXchange"), - XML("GnuCash XML"); + XML("GnuCash XML"), + CSVA("GnuCash accounts CSV"), + CSVT("GnuCash transactions CSV"); /** * Full name of the export format acronym @@ -45,6 +47,9 @@ public String getExtension(){ return ".ofx"; case XML: return ".gnca"; + case CSVA: + case CSVT: + return ".csv"; default: return ".txt"; } diff --git a/app/src/main/java/org/gnucash/android/export/ExportParams.java b/app/src/main/java/org/gnucash/android/export/ExportParams.java index 90c4e4221..e85ad3e8f 100644 --- a/app/src/main/java/org/gnucash/android/export/ExportParams.java +++ b/app/src/main/java/org/gnucash/android/export/ExportParams.java @@ -78,6 +78,11 @@ public String getDescription(){ */ private String mExportLocation; + /** + * CSV-separator char + */ + private char mCsvSeparator = ','; + /** * Creates a new set of paramters and specifies the export format * @param format Format to use when exporting the transactions @@ -169,6 +174,22 @@ public void setExportLocation(String exportLocation){ mExportLocation = exportLocation; } + /** + * Get the CSV-separator char + * @return CSV-separator char + */ + public char getCsvSeparator(){ + return mCsvSeparator; + } + + /** + * Set the CSV-separator char + * @param separator CSV-separator char + */ + public void setCsvSeparator(char separator) { + mCsvSeparator = separator; + } + @Override public String toString() { return "Export all transactions created since " + TimestampHelper.getUtcStringFromTimestamp(mExportStartTime) + " UTC" 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 87c60d569..5fd56f3b5 100644 --- a/app/src/main/java/org/gnucash/android/export/Exporter.java +++ b/app/src/main/java/org/gnucash/android/export/Exporter.java @@ -160,7 +160,10 @@ 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.getExtension(); + + "_gnucash_export_" + sanitizeFilename(bookName) + + (format == ExportFormat.CSVA ? "_accounts" : "") + + (format == ExportFormat.CSVT ? "_transactions" : "") + + format.getExtension(); } /** @@ -237,20 +240,6 @@ public static String getExportFolderPath(String bookUID){ return path; } - /** - * Returns the path to the backups folder for the book with GUID {@code bookUID} - * Each book has its own backup path - * - * @return Absolute path to backup folder for the book - */ - public static String getBackupFolderPath(String bookUID){ - String path = BASE_FOLDER_PATH + "/" + bookUID + "/backups/"; - File file = new File(path); - if (!file.exists()) - file.mkdirs(); - return path; - } - /** * Returns the MIME type for this exporter. diff --git a/app/src/main/java/org/gnucash/android/export/csv/CsvAccountExporter.java b/app/src/main/java/org/gnucash/android/export/csv/CsvAccountExporter.java new file mode 100644 index 000000000..47907c163 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/export/csv/CsvAccountExporter.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2018 Semyannikov Gleb + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.export.csv; + +import android.database.sqlite.SQLiteDatabase; + +import com.crashlytics.android.Crashlytics; + +import org.gnucash.android.R; +import org.gnucash.android.export.ExportParams; +import org.gnucash.android.export.Exporter; +import org.gnucash.android.model.Account; + +import java.io.FileWriter; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +/** + * Creates a GnuCash CSV account representation of the accounts and transactions + * + * @author Semyannikov Gleb + */ +public class CsvAccountExporter extends Exporter{ + private char mCsvSeparator; + + /** + * Construct a new exporter with export parameters + * @param params Parameters for the export + */ + public CsvAccountExporter(ExportParams params) { + super(params, null); + mCsvSeparator = params.getCsvSeparator(); + LOG_TAG = "GncXmlExporter"; + } + + /** + * Overloaded constructor. + * Creates an exporter with an already open database instance. + * @param params Parameters for the export + * @param db SQLite database + */ + public CsvAccountExporter(ExportParams params, SQLiteDatabase db) { + super(params, db); + mCsvSeparator = params.getCsvSeparator(); + LOG_TAG = "GncXmlExporter"; + } + + @Override + public List generateExport() throws ExporterException { + String outputFile = getExportCacheFilePath(); + try (CsvWriter writer = new CsvWriter(new FileWriter(outputFile), mCsvSeparator + "")) { + generateExport(writer); + } catch (IOException ex){ + Crashlytics.log("Error exporting CSV"); + Crashlytics.logException(ex); + throw new ExporterException(mExportParams, ex); + } + + return Arrays.asList(outputFile); + } + + /** + * Writes out all the accounts in the system as CSV to the provided writer + * @param csvWriter Destination for the CSV export + * @throws ExporterException if an error occurred while writing to the stream + */ + public void generateExport(final CsvWriter csvWriter) throws ExporterException { + try { + List names = Arrays.asList(mContext.getResources().getStringArray(R.array.csv_account_headers)); + List accounts = mAccountsDbAdapter.getAllRecords(); + + for(int i = 0; i < names.size(); i++) { + csvWriter.writeToken(names.get(i)); + } + + csvWriter.newLine(); + for (Account account : accounts) { + csvWriter.writeToken(account.getAccountType().toString()); + csvWriter.writeToken(account.getFullName()); + csvWriter.writeToken(account.getName()); + + csvWriter.writeToken(null); //Account code + csvWriter.writeToken(account.getDescription()); + csvWriter.writeToken(account.getColorHexString()); + csvWriter.writeToken(null); //Account notes + + csvWriter.writeToken(account.getCommodity().getCurrencyCode()); + csvWriter.writeToken("CURRENCY"); + csvWriter.writeToken(account.isHidden() ? "T" : "F"); + + csvWriter.writeToken("F"); //Tax + csvWriter.writeEndToken(account.isPlaceholderAccount() ? "T": "F"); + } + } catch (IOException e) { + Crashlytics.logException(e); + throw new ExporterException(mExportParams, e); + } + } +} 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 new file mode 100644 index 000000000..f0d082e6e --- /dev/null +++ b/app/src/main/java/org/gnucash/android/export/csv/CsvTransactionsExporter.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2018 Semyannikov Gleb + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.export.csv; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.support.annotation.NonNull; +import android.util.Log; + +import com.crashlytics.android.Crashlytics; + +import org.gnucash.android.R; +import org.gnucash.android.export.ExportParams; +import org.gnucash.android.export.Exporter; +import org.gnucash.android.model.Account; +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 + * + * @author Semyannikov Gleb + */ +public class CsvTransactionsExporter extends Exporter{ + + private char mCsvSeparator; + + private DateFormat dateFormat = new SimpleDateFormat("YYYY-MM-dd", Locale.US); + + /** + * Construct a new exporter with export parameters + * @param params Parameters for the export + */ + public CsvTransactionsExporter(ExportParams params) { + super(params, null); + mCsvSeparator = params.getCsvSeparator(); + LOG_TAG = "GncXmlExporter"; + } + + /** + * Overloaded constructor. + * Creates an exporter with an already open database instance. + * @param params Parameters for the export + * @param db SQLite database + */ + public CsvTransactionsExporter(ExportParams params, SQLiteDatabase db) { + super(params, db); + mCsvSeparator = params.getCsvSeparator(); + LOG_TAG = "GncXmlExporter"; + } + + @Override + public List generateExport() throws ExporterException { + String outputFile = getExportCacheFilePath(); + + try (CsvWriter csvWriter = new CsvWriter(new FileWriter(outputFile), "" + mCsvSeparator)){ + generateExport(csvWriter); + } catch (IOException ex){ + Crashlytics.log("Error exporting CSV"); + Crashlytics.logException(ex); + throw new ExporterException(mExportParams, ex); + } + + return Arrays.asList(outputFile); + } + + /** + * Write splits to CSV format + * @param splits Splits to be written + */ + 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()); + + //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()); + + String sign = split.getType() == TransactionType.CREDIT ? "-" : ""; + writer.writeToken(sign + split.getQuantity().formattedString()); + writer.writeToken(sign + split.getQuantity().toLocaleString()); + writer.writeToken("" + split.getReconcileState()); + if (split.getReconcileState() == Split.FLAG_RECONCILED) { + String recDateString = dateFormat.format(new Date(split.getReconcileDate().getTime())); + writer.writeToken(recDateString); + } else { + writer.writeToken(null); + } + writer.writeEndToken(split.getQuantity().divide(split.getValue()).toLocaleString()); + } + } + + private void generateExport(final CsvWriter csvWriter) throws ExporterException { + try { + List names = Arrays.asList(mContext.getResources().getStringArray(R.array.csv_transaction_headers)); + for(int i = 0; i < names.size(); i++) { + csvWriter.writeToken(names.get(i)); + } + csvWriter.newLine(); + + + 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()); + csvWriter.writeToken(dateFormat.format(date)); + csvWriter.writeToken(transaction.getUID()); + csvWriter.writeToken(null); //Transaction number + + csvWriter.writeToken(transaction.getDescription()); + csvWriter.writeToken(transaction.getNote()); + + csvWriter.writeToken("CURRENCY::" + transaction.getCurrencyCode()); + csvWriter.writeToken(null); // Void Reason + csvWriter.writeToken(null); // Action + 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/export/csv/CsvWriter.java b/app/src/main/java/org/gnucash/android/export/csv/CsvWriter.java new file mode 100644 index 000000000..4c885aa53 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/export/csv/CsvWriter.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2018 Semyannikov Gleb + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.export.csv; + + +import android.support.annotation.NonNull; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.Writer; + +/** + * Format data to be CSV-compatible + * + * @author Semyannikov Gleb + * @author Ngewi Fet + */ +public class CsvWriter extends BufferedWriter { + private String separator = ","; + + public CsvWriter(Writer writer){ + super(writer); + } + + public CsvWriter(Writer writer, String separator){ + super(writer); + this.separator = separator; + } + + @Override + public void write(@NonNull String str) throws IOException { + this.write(str, 0, str.length()); + } + + /** + * Writes a CSV token and the separator to the underlying output stream. + * + * The token **MUST NOT** not contain the CSV separator. If the separator is found in the token, then + * the token will be escaped as specified by RFC 4180 + * @param token Token to be written to file + * @throws IOException if the token could not be written to the underlying stream + */ + public void writeToken(String token) throws IOException { + if (token == null || token.isEmpty()){ + write(separator); + } else { + token = escape(token); + write(token + separator); + } + } + + /** + * Escape any CSV separators by surrounding the token in double quotes + * @param token String token to be written to CSV + * @return Escaped CSV token + */ + @NonNull + private String escape(@NonNull String token) { + if (token.contains(separator)){ + return "\"" + token + "\""; + } + return token; + } + + /** + * Writes a token to the CSV file and appends end of line to it. + * + * The token **MUST NOT** not contain the CSV separator. If the separator is found in the token, then + * the token will be escaped as specified by RFC 4180 + * @param token The token to be written to the file + * @throws IOException if token could not be written to underlying writer + */ + public void writeEndToken(String token) throws IOException { + if (token != null && !token.isEmpty()) { + write(escape(token)); + } + this.newLine(); + } + +} 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 b42bb8dbd..9f1478df8 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 @@ -213,6 +213,15 @@ public List generateExport() throws ExporterException { case 1000: precision = 3; break; + case 10000: + precision = 4; + break; + case 100000: + precision = 5; + break; + case 1000000: + precision = 6; + break; default: throw new ExporterException(mExportParams, "split quantity has illegal denominator: "+ quantity_denom); } @@ -242,11 +251,14 @@ public List generateExport() throws ExporterException { /// export successful PreferencesHelper.setLastExportTime(TimestampHelper.getTimestampFromNow()); + List exportedFiles = splitQIF(file); if (exportedFiles.isEmpty()) return Collections.emptyList(); - else + else if (exportedFiles.size() > 1) return zipQifs(exportedFiles); + else + return exportedFiles; } catch (IOException e) { throw new ExporterException(mExportParams, e); } diff --git a/app/src/main/java/org/gnucash/android/export/xml/GncXmlExporter.java b/app/src/main/java/org/gnucash/android/export/xml/GncXmlExporter.java index 8782cc47a..d10268e30 100644 --- a/app/src/main/java/org/gnucash/android/export/xml/GncXmlExporter.java +++ b/app/src/main/java/org/gnucash/android/export/xml/GncXmlExporter.java @@ -908,55 +908,4 @@ public String getExportMimeType(){ return "text/xml"; } - /** - * Creates a backup of current database contents to the directory {@link Exporter#getBackupFolderPath(String)} - * @return {@code true} if backup was successful, {@code false} otherwise - */ - public static boolean createBackup(){ - return createBackup(BooksDbAdapter.getInstance().getActiveBookUID()); - } - - /** - * Create a backup of the book in the default backup location - * @param bookUID Unique ID of the book - * @return {@code true} if backup was successful, {@code false} otherwise - */ - public static boolean createBackup(String bookUID){ - OutputStream outputStream; - try { - String backupFile = BookUtils.getBookBackupFileUri(bookUID); - if (backupFile != null){ - outputStream = GnuCashApplication.getAppContext().getContentResolver().openOutputStream(Uri.parse(backupFile)); - } else { //no Uri set by user, use default location on SD card - backupFile = getBackupFilePath(bookUID); - outputStream = new FileOutputStream(backupFile); - } - - BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); - GZIPOutputStream gzipOutputStream = new GZIPOutputStream(bufferedOutputStream); - OutputStreamWriter writer = new OutputStreamWriter(gzipOutputStream); - - ExportParams params = new ExportParams(ExportFormat.XML); - new GncXmlExporter(params).generateExport(writer); - writer.close(); - return true; - } catch (IOException | ExporterException e) { - Crashlytics.logException(e); - Log.e("GncXmlExporter", "Error creating XML backup", e); - return false; - } - } - - /** - * Returns the full path of a file to make database backup of the specified book. - * Backups are done in XML format and are Gzipped (with ".gnca" extension). - * @param bookUID GUID of the book - * @return the file path for backups of the database. - * @see #getBackupFolderPath(String) - */ - private static String getBackupFilePath(String bookUID){ - Book book = BooksDbAdapter.getInstance().getRecord(bookUID); - return Exporter.getBackupFolderPath(book.getUID()) - + buildExportFilename(ExportFormat.XML, book.getDisplayName()); - } } diff --git a/app/src/main/java/org/gnucash/android/importer/GncXmlHandler.java b/app/src/main/java/org/gnucash/android/importer/GncXmlHandler.java index e2518b911..3c9e046af 100644 --- a/app/src/main/java/org/gnucash/android/importer/GncXmlHandler.java +++ b/app/src/main/java/org/gnucash/android/importer/GncXmlHandler.java @@ -432,7 +432,7 @@ public void endElement(String uri, String localName, String qualifiedName) throw mAccount.setHidden(accountType == AccountType.ROOT); //flag root account as hidden break; case GncXmlHelper.TAG_COMMODITY_SPACE: - if (characterString.equals("ISO4217")) { + if (characterString.equals("ISO4217") || characterString.equals("CURRENCY") ) { mISO4217Currency = true; } else { // price of non-ISO4217 commodities cannot be handled diff --git a/app/src/main/java/org/gnucash/android/model/Account.java b/app/src/main/java/org/gnucash/android/model/Account.java index 4fa09fecb..f6234d304 100644 --- a/app/src/main/java/org/gnucash/android/model/Account.java +++ b/app/src/main/java/org/gnucash/android/model/Account.java @@ -282,6 +282,14 @@ public int getColor() { return mColor; } + /** + * Returns the account color as an RGB hex string + * @return Hex color of the account + */ + public String getColorHexString(){ + return String.format("#%06X", (0xFFFFFF & mColor)); + } + /** * Sets the color of the account. * @param color Color as an int as returned by {@link Color}. diff --git a/app/src/main/java/org/gnucash/android/model/Money.java b/app/src/main/java/org/gnucash/android/model/Money.java index 1f7fbfaf4..eedc67ac1 100644 --- a/app/src/main/java/org/gnucash/android/model/Money.java +++ b/app/src/main/java/org/gnucash/android/model/Money.java @@ -28,7 +28,6 @@ import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; -import java.util.Currency; import java.util.Locale; /** @@ -427,13 +426,24 @@ public boolean isNegative(){ /** * Returns the string representation of the amount (without currency) of the Money object. - *

This string is not locale-formatted. The decimal operator is a period (.)

+ * + *

This string is not locale-formatted. The decimal operator is a period (.) + * For a locale-formatted version, see the method overload {@link #toLocaleString(Locale)}

* @return String representation of the amount (without currency) of the Money object */ public String toPlainString(){ return mAmount.setScale(mCommodity.getSmallestFractionDigits(), ROUNDING_MODE).toPlainString(); } + /** + * Returns a locale-specific representation of the amount of the Money object (excluding the currency) + * + * @return String representation of the amount (without currency) of the Money object + */ + public String toLocaleString(){ + return String.format(Locale.getDefault(), "%.2f", asDouble()); + } + /** * Returns the string representation of the Money object (value + currency) formatted according * to the default locale diff --git a/app/src/main/java/org/gnucash/android/receivers/BootReceiver.java b/app/src/main/java/org/gnucash/android/receivers/BootReceiver.java index 3b3249700..6d17e9b1a 100644 --- a/app/src/main/java/org/gnucash/android/receivers/BootReceiver.java +++ b/app/src/main/java/org/gnucash/android/receivers/BootReceiver.java @@ -21,10 +21,11 @@ import android.content.Intent; import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.util.BackupManager; /** * Receiver which is called when the device finishes booting. - * It starts the service for running scheduled events + * It schedules periodic jobs. * @author Ngewi Fet */ public class BootReceiver extends BroadcastReceiver { @@ -32,5 +33,6 @@ public class BootReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { GnuCashApplication.startScheduledActionExecutionService(context); + BackupManager.schedulePeriodicBackups(context); } } diff --git a/app/src/main/java/org/gnucash/android/receivers/PeriodicJobReceiver.java b/app/src/main/java/org/gnucash/android/receivers/PeriodicJobReceiver.java new file mode 100644 index 000000000..0cd5743cb --- /dev/null +++ b/app/src/main/java/org/gnucash/android/receivers/PeriodicJobReceiver.java @@ -0,0 +1,52 @@ +/* Copyright (c) 2018 Àlex Magaz Graça + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.receivers; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import org.gnucash.android.service.ScheduledActionService; +import org.gnucash.android.util.BackupJob; + +/** + * Receiver to run periodic jobs. + * + *

For now, backups and scheduled actions.

+ * + * @author Àlex Magaz Graça + */ +public class PeriodicJobReceiver extends BroadcastReceiver { + private static final String LOG_TAG = "PeriodicJobReceiver"; + + public static final String ACTION_BACKUP = "org.gnucash.android.action_backup"; + public static final String ACTION_SCHEDULED_ACTIONS = "org.gnucash.android.action_scheduled_actions"; + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction() == null) { + Log.w(LOG_TAG, "No action was set in the intent. Ignoring..."); + return; + } + + if (intent.getAction().equals(ACTION_BACKUP)) { + BackupJob.enqueueWork(context); + } else if (intent.getAction().equals(ACTION_SCHEDULED_ACTIONS)) { + ScheduledActionService.enqueueWork(context); + } + } +} 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 5c2470371..b1a3f7a11 100644 --- a/app/src/main/java/org/gnucash/android/service/ScheduledActionService.java +++ b/app/src/main/java/org/gnucash/android/service/ScheduledActionService.java @@ -16,14 +16,13 @@ package org.gnucash.android.service; -import android.app.IntentService; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.os.PowerManager; +import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; +import android.support.v4.app.JobIntentService; import android.util.Log; import com.crashlytics.android.Crashlytics; @@ -38,72 +37,63 @@ import org.gnucash.android.db.adapter.SplitsDbAdapter; import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.export.ExportAsyncTask; -import org.gnucash.android.export.ExportFormat; import org.gnucash.android.export.ExportParams; -import org.gnucash.android.export.xml.GncXmlExporter; import org.gnucash.android.model.Book; import org.gnucash.android.model.ScheduledAction; import org.gnucash.android.model.Transaction; -import org.gnucash.android.util.BookUtils; -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.ExecutionException; -import java.util.zip.GZIPOutputStream; /** * Service for running scheduled events. - *

The service is started and goes through all scheduled event entries in the the database and executes them. - * Then it is stopped until the next time it is run.
- * Scheduled runs of the service should be achieved using an {@link android.app.AlarmManager}

+ * + *

It's run every time the enqueueWork is called. It goes + * through all scheduled event entries in the the database and executes them.

+ * + *

Scheduled runs of the service should be achieved using an + * {@link android.app.AlarmManager}, with + * {@link org.gnucash.android.receivers.PeriodicJobReceiver} as an intermediary.

+ * * @author Ngewi Fet */ -public class ScheduledActionService extends IntentService { +public class ScheduledActionService extends JobIntentService { - public static final String LOG_TAG = "ScheduledActionService"; + private static final String LOG_TAG = "ScheduledActionService"; + private static final int JOB_ID = 1001; - public ScheduledActionService() { - super(LOG_TAG); + + public static void enqueueWork(Context context) { + Intent intent = new Intent(context, ScheduledActionService.class); + enqueueWork(context, ScheduledActionService.class, JOB_ID, intent); } @Override - protected void onHandleIntent(Intent intent) { + protected void onHandleWork(@NonNull Intent intent) { Log.i(LOG_TAG, "Starting scheduled action service"); - PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); - PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG); - wakeLock.acquire(); - - autoBackup(); //First run automatic backup of all books before doing anything else - try { - BooksDbAdapter booksDbAdapter = BooksDbAdapter.getInstance(); - List books = booksDbAdapter.getAllRecords(); - for (Book book : books) { //// TODO: 20.04.2017 Retrieve only the book UIDs with new method - DatabaseHelper dbHelper = new DatabaseHelper(GnuCashApplication.getAppContext(), book.getUID()); - SQLiteDatabase db = dbHelper.getWritableDatabase(); - RecurrenceDbAdapter recurrenceDbAdapter = new RecurrenceDbAdapter(db); - ScheduledActionDbAdapter scheduledActionDbAdapter = new ScheduledActionDbAdapter(db, recurrenceDbAdapter); - - List scheduledActions = scheduledActionDbAdapter.getAllEnabledScheduledActions(); - Log.i(LOG_TAG, String.format("Processing %d total scheduled actions for Book: %s", - scheduledActions.size(), book.getDisplayName())); - processScheduledActions(scheduledActions, db); - - //close all databases except the currently active database - if (!db.getPath().equals(GnuCashApplication.getActiveDb().getPath())) - db.close(); - } - - Log.i(LOG_TAG, "Completed service @ " + java.text.DateFormat.getDateTimeInstance().format(new Date())); - - } finally { //release the lock either way - wakeLock.release(); + BooksDbAdapter booksDbAdapter = BooksDbAdapter.getInstance(); + List books = booksDbAdapter.getAllRecords(); + for (Book book : books) { //// TODO: 20.04.2017 Retrieve only the book UIDs with new method + DatabaseHelper dbHelper = new DatabaseHelper(GnuCashApplication.getAppContext(), book.getUID()); + SQLiteDatabase db = dbHelper.getWritableDatabase(); + RecurrenceDbAdapter recurrenceDbAdapter = new RecurrenceDbAdapter(db); + ScheduledActionDbAdapter scheduledActionDbAdapter = new ScheduledActionDbAdapter(db, recurrenceDbAdapter); + + List scheduledActions = scheduledActionDbAdapter.getAllEnabledScheduledActions(); + Log.i(LOG_TAG, String.format("Processing %d total scheduled actions for Book: %s", + scheduledActions.size(), book.getDisplayName())); + processScheduledActions(scheduledActions, db); + + //close all databases except the currently active database + if (!db.getPath().equals(GnuCashApplication.getActiveDb().getPath())) + db.close(); } + + Log.i(LOG_TAG, "Completed service @ " + java.text.DateFormat.getDateTimeInstance().format(new Date())); } /** @@ -189,7 +179,14 @@ private static int executeBackup(ScheduledAction scheduledAction, SQLiteDatabase Crashlytics.logException(e); Log.e(LOG_TAG, e.getMessage()); } - Log.i(LOG_TAG, "Backup/export did not occur. There might have beeen no new transactions to export or it might have crashed"); + if (!result) { + Log.i(LOG_TAG, "Backup/export did not occur. There might have been no" + + " new transactions to export or it might have crashed"); + // We don't know if something failed or there weren't transactions to export, + // so fall on the safe side and return as if something had failed. + // FIXME: Change ExportAsyncTask to distinguish between the two cases + return 0; + } return 1; } @@ -198,6 +195,7 @@ private static int executeBackup(ScheduledAction scheduledAction, SQLiteDatabase * @param scheduledAction Scheduled action * @return {@code true} if execution is due, {@code false} otherwise */ + @SuppressWarnings("RedundantIfStatement") private static boolean shouldExecuteScheduledBackup(ScheduledAction scheduledAction) { long now = System.currentTimeMillis(); long endTime = scheduledAction.getEndTime(); @@ -223,7 +221,7 @@ private static int executeTransactions(ScheduledAction scheduledAction, SQLiteDa int executionCount = 0; String actionUID = scheduledAction.getActionUID(); TransactionsDbAdapter transactionsDbAdapter = new TransactionsDbAdapter(db, new SplitsDbAdapter(db)); - Transaction trxnTemplate = null; + Transaction trxnTemplate; try { trxnTemplate = transactionsDbAdapter.getRecord(actionUID); } catch (IllegalArgumentException ex){ //if the record could not be found, abort @@ -261,34 +259,4 @@ private static int executeTransactions(ScheduledAction scheduledAction, SQLiteDa scheduledAction.setExecutionCount(previousExecutionCount); return executionCount; } - - /** - * Perform an automatic backup of all books in the database. - * This method is run everytime the service is executed - */ - private static void autoBackup(){ - BooksDbAdapter booksDbAdapter = BooksDbAdapter.getInstance(); - List bookUIDs = booksDbAdapter.getAllBookUIDs(); - Context context = GnuCashApplication.getAppContext(); - - for (String bookUID : bookUIDs) { - String backupFile = BookUtils.getBookBackupFileUri(bookUID); - if (backupFile == null){ - GncXmlExporter.createBackup(); - continue; - } - - try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(context.getContentResolver().openOutputStream(Uri.parse(backupFile)))){ - GZIPOutputStream gzipOutputStream = new GZIPOutputStream(bufferedOutputStream); - OutputStreamWriter writer = new OutputStreamWriter(gzipOutputStream); - ExportParams params = new ExportParams(ExportFormat.XML); - new GncXmlExporter(params).generateExport(writer); - writer.close(); - } catch (IOException ex) { - Log.e(LOG_TAG, "Auto backup failed for book " + bookUID); - ex.printStackTrace(); - Crashlytics.logException(ex); - } - } - } } diff --git a/app/src/main/java/org/gnucash/android/ui/account/AccountsActivity.java b/app/src/main/java/org/gnucash/android/ui/account/AccountsActivity.java index 0dd960656..846330af8 100644 --- a/app/src/main/java/org/gnucash/android/ui/account/AccountsActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/account/AccountsActivity.java @@ -17,8 +17,6 @@ package org.gnucash.android.ui.account; -import android.Manifest; -import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; import android.content.ActivityNotFoundException; @@ -28,15 +26,12 @@ import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.support.design.widget.CoordinatorLayout; import android.support.design.widget.FloatingActionButton; -import android.support.design.widget.Snackbar; import android.support.design.widget.TabLayout; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; @@ -62,7 +57,6 @@ import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.db.adapter.BooksDbAdapter; -import org.gnucash.android.export.xml.GncXmlExporter; import org.gnucash.android.importer.ImportAsyncTask; import org.gnucash.android.ui.common.BaseDrawerActivity; import org.gnucash.android.ui.common.FormActivity; @@ -71,6 +65,7 @@ import org.gnucash.android.ui.transaction.TransactionsActivity; import org.gnucash.android.ui.util.TaskDelegate; import org.gnucash.android.ui.wizard.FirstRunWizardActivity; +import org.gnucash.android.util.BackupManager; import butterknife.BindView; @@ -292,7 +287,7 @@ private void handleOpenFileIntent(Intent intent) { //when someone launches the app to view a (.gnucash or .gnca) file Uri data = intent.getData(); if (data != null){ - GncXmlExporter.createBackup(); + BackupManager.backupActiveBook(); intent.setData(null); new ImportAsyncTask(this).execute(data); removeFirstRunFlag(); @@ -347,6 +342,7 @@ private void init() { showWhatsNewDialog(this); } GnuCashApplication.startScheduledActionExecutionService(this); + BackupManager.schedulePeriodicBackups(this); } @Override @@ -504,7 +500,7 @@ public static void startXmlFileChooser(Fragment fragment) { * @param onFinishTask Task to be executed when import is complete */ public static void importXmlFileFromIntent(Activity context, Intent data, TaskDelegate onFinishTask) { - GncXmlExporter.createBackup(); + BackupManager.backupActiveBook(); new ImportAsyncTask(context, onFinishTask).execute(data.getData()); } diff --git a/app/src/main/java/org/gnucash/android/ui/account/AccountsListFragment.java b/app/src/main/java/org/gnucash/android/ui/account/AccountsListFragment.java index d099027d2..a83ab10bc 100644 --- a/app/src/main/java/org/gnucash/android/ui/account/AccountsListFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/account/AccountsListFragment.java @@ -64,6 +64,7 @@ import org.gnucash.android.ui.util.AccountBalanceTask; import org.gnucash.android.ui.util.CursorRecyclerAdapter; import org.gnucash.android.ui.util.widget.EmptyRecyclerView; +import org.gnucash.android.util.BackupManager; import java.util.List; @@ -247,6 +248,7 @@ public void tryDeleteAccount(long rowId) { if (acc.getTransactionCount() > 0 || mAccountsDbAdapter.getSubAccountCount(acc.getUID()) > 0) { showConfirmationDialog(rowId); } else { + BackupManager.backupActiveBook(); // Avoid calling AccountsDbAdapter.deleteRecord(long). See #654 String uid = mAccountsDbAdapter.getUID(rowId); mAccountsDbAdapter.deleteRecord(uid); diff --git a/app/src/main/java/org/gnucash/android/ui/account/DeleteAccountDialogFragment.java b/app/src/main/java/org/gnucash/android/ui/account/DeleteAccountDialogFragment.java index bc84ac4e1..4d5f6c551 100644 --- a/app/src/main/java/org/gnucash/android/ui/account/DeleteAccountDialogFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/account/DeleteAccountDialogFragment.java @@ -39,6 +39,7 @@ import org.gnucash.android.model.AccountType; import org.gnucash.android.ui.common.Refreshable; import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; +import org.gnucash.android.util.BackupManager; import org.gnucash.android.util.QualifiedAccountNameCursorAdapter; import java.util.List; @@ -209,6 +210,7 @@ public void onClick(View v) { @Override public void onClick(View v) { + BackupManager.backupActiveBook(); AccountsDbAdapter accountsDbAdapter = AccountsDbAdapter.getInstance(); 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 aca8ba94a..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; @@ -138,6 +140,12 @@ public class ExportFormFragment extends Fragment implements @BindView(R.id.radio_ofx_format) RadioButton mOfxRadioButton; @BindView(R.id.radio_qif_format) RadioButton mQifRadioButton; @BindView(R.id.radio_xml_format) RadioButton mXmlRadioButton; + @BindView(R.id.radio_csv_transactions_format) RadioButton mCsvTransactionsRadioButton; + + @BindView(R.id.radio_separator_comma_format) RadioButton mSeparatorCommaButton; + @BindView(R.id.radio_separator_colon_format) RadioButton mSeparatorColonButton; + @BindView(R.id.radio_separator_semicolon_format) RadioButton mSeparatorSemicolonButton; + @BindView(R.id.layout_csv_options) LinearLayout mCsvOptionsLayout; @BindView(R.id.recurrence_options) View mRecurrenceOptionsView; /** @@ -169,6 +177,8 @@ public class ExportFormFragment extends Fragment implements */ private Uri mExportUri; + private char mExportCsvSeparator = ','; + /** * Flag to determine if export has been started. * Used to continue export after user has picked a destination file @@ -185,7 +195,9 @@ private void onRadioButtonClicked(View view){ } else { mExportWarningTextView.setVisibility(View.GONE); } - mExportDateLayout.setVisibility(View.VISIBLE); + + OptionsViewAnimationUtils.expand(mExportDateLayout); + OptionsViewAnimationUtils.collapse(mCsvOptionsLayout); break; case R.id.radio_qif_format: @@ -197,13 +209,33 @@ private void onRadioButtonClicked(View view){ } else { mExportWarningTextView.setVisibility(View.GONE); } - mExportDateLayout.setVisibility(View.VISIBLE); + + 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); + OptionsViewAnimationUtils.collapse(mExportDateLayout); + OptionsViewAnimationUtils.collapse(mCsvOptionsLayout); + break; + + case R.id.radio_csv_transactions_format: + mExportFormat = ExportFormat.CSVT; + mExportWarningTextView.setText(R.string.export_notice_csv); + OptionsViewAnimationUtils.expand(mExportDateLayout); + OptionsViewAnimationUtils.expand(mCsvOptionsLayout); + break; + + case R.id.radio_separator_comma_format: + mExportCsvSeparator = ','; + break; + case R.id.radio_separator_colon_format: + mExportCsvSeparator = ':'; + break; + case R.id.radio_separator_semicolon_format: + mExportCsvSeparator = ';'; break; } } @@ -289,6 +321,7 @@ private void startExport(){ exportParameters.setExportTarget(mExportTarget); exportParameters.setExportLocation(mExportUri != null ? mExportUri.toString() : null); exportParameters.setDeleteTransactionsAfterExport(mDeleteAllCheckBox.isChecked()); + exportParameters.setCsvSeparator(mExportCsvSeparator); Log.i(TAG, "Commencing async export of transactions"); new ExportAsyncTask(getActivity(), GnuCashApplication.getActiveDb()).execute(exportParameters); @@ -326,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)); @@ -344,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; @@ -354,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); @@ -368,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 } }); @@ -449,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() { @@ -465,12 +497,18 @@ public void onClick(View view) { mOfxRadioButton.setOnClickListener(radioClickListener); mQifRadioButton.setOnClickListener(radioClickListener); mXmlRadioButton.setOnClickListener(radioClickListener); + mCsvTransactionsRadioButton.setOnClickListener(radioClickListener); + + mSeparatorCommaButton.setOnClickListener(radioClickListener); + mSeparatorColonButton.setOnClickListener(radioClickListener); + mSeparatorSemicolonButton.setOnClickListener(radioClickListener); ExportFormat defaultFormat = ExportFormat.valueOf(defaultExportFormat.toUpperCase()); switch (defaultFormat){ case QIF: mQifRadioButton.performClick(); break; case OFX: mOfxRadioButton.performClick(); break; case XML: mXmlRadioButton.performClick(); break; + case CSVT: mCsvTransactionsRadioButton.performClick(); break; } if (GnuCashApplication.isDoubleEntryEnabled()){ @@ -504,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); } @@ -576,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/java/org/gnucash/android/ui/settings/AccountPreferencesFragment.java b/app/src/main/java/org/gnucash/android/ui/settings/AccountPreferencesFragment.java index 7a35d542c..977b06b57 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 @@ -27,17 +27,26 @@ import android.support.v7.preference.ListPreference; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceFragmentCompat; +import android.widget.Toast; + +import com.crashlytics.android.Crashlytics; import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.db.DatabaseSchema; +import org.gnucash.android.db.adapter.BooksDbAdapter; import org.gnucash.android.db.adapter.CommoditiesDbAdapter; +import org.gnucash.android.export.ExportAsyncTask; +import org.gnucash.android.export.ExportFormat; +import org.gnucash.android.export.ExportParams; +import org.gnucash.android.export.Exporter; import org.gnucash.android.model.Money; 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; /** * Account settings fragment inside the Settings activity @@ -48,6 +57,8 @@ public class AccountPreferencesFragment extends PreferenceFragmentCompat implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener{ + private static final int REQUEST_EXPORT_FILE = 0xC5; + List mCurrencyEntries = new ArrayList<>(); List mCurrencyEntryValues = new ArrayList<>(); @@ -91,6 +102,9 @@ public void onResume() { Preference preference = findPreference(getString(R.string.key_import_accounts)); preference.setOnPreferenceClickListener(this); + preference = findPreference(getString(R.string.key_export_accounts_csv)); + preference.setOnPreferenceClickListener(this); + preference = findPreference(getString(R.string.key_delete_all_accounts)); preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override @@ -137,9 +151,29 @@ public boolean onPreferenceClick(Preference preference) { return true; } + if (key.equals(getString(R.string.key_export_accounts_csv))){ + selectExportFile(); + return true; + } + return false; } + /** + * Open a chooser for user to pick a file to export to + */ + private void selectExportFile() { + Intent createIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + createIntent.setType("*/*").addCategory(Intent.CATEGORY_OPENABLE); + String bookName = BooksDbAdapter.getInstance().getActiveBookDisplayName(); + + String filename = Exporter.buildExportFilename(ExportFormat.CSVA, bookName); + createIntent.setType("application/text"); + + createIntent.putExtra(Intent.EXTRA_TITLE, filename); + startActivityForResult(createIntent, REQUEST_EXPORT_FILE); + } + @Override public boolean onPreferenceChange(Preference preference, Object newValue) { if (preference.getKey().equals(getString(R.string.key_default_currency))){ @@ -167,6 +201,22 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { AccountsActivity.importXmlFileFromIntent(getActivity(), data, null); } break; + + case REQUEST_EXPORT_FILE: + if (resultCode == Activity.RESULT_OK && data != null){ + ExportParams exportParams = new ExportParams(ExportFormat.CSVA); + exportParams.setExportTarget(ExportParams.ExportTarget.URI); + exportParams.setExportLocation(data.getData().toString()); + ExportAsyncTask exportTask = new ExportAsyncTask(getActivity(), GnuCashApplication.getActiveDb()); + + try { + exportTask.execute(exportParams).get(); + } catch (InterruptedException | ExecutionException e) { + Crashlytics.logException(e); + Toast.makeText(getActivity(), "An error occurred during the Accounts CSV export", + Toast.LENGTH_LONG).show(); + } + } } } } diff --git a/app/src/main/java/org/gnucash/android/ui/settings/BackupPreferenceFragment.java b/app/src/main/java/org/gnucash/android/ui/settings/BackupPreferenceFragment.java index 9bbd4081c..e92f83445 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/BackupPreferenceFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/BackupPreferenceFragment.java @@ -48,10 +48,9 @@ import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.db.adapter.BooksDbAdapter; import org.gnucash.android.export.Exporter; -import org.gnucash.android.export.xml.GncXmlExporter; import org.gnucash.android.importer.ImportAsyncTask; import org.gnucash.android.ui.settings.dialog.OwnCloudDialogFragment; -import org.gnucash.android.util.BookUtils; +import org.gnucash.android.util.BackupManager; import java.io.File; import java.text.DateFormat; @@ -151,7 +150,7 @@ public void onResume() { pref = findPreference(getString(R.string.key_backup_location)); pref.setOnPreferenceClickListener(this); - String defaultBackupLocation = BookUtils.getBookBackupFileUri(BooksDbAdapter.getInstance().getActiveBookUID()); + String defaultBackupLocation = BackupManager.getBookBackupFileUri(BooksDbAdapter.getInstance().getActiveBookUID()); if (defaultBackupLocation != null){ pref.setSummary(Uri.parse(defaultBackupLocation).getAuthority()); } @@ -193,7 +192,7 @@ public boolean onPreferenceClick(Preference preference) { } if (key.equals(getString(R.string.key_create_backup))){ - boolean result = GncXmlExporter.createBackup(); + boolean result = BackupManager.backupActiveBook(); int msg = result ? R.string.toast_backup_successful : R.string.toast_backup_failed; Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show(); } @@ -369,9 +368,9 @@ public void onConnectionFailed(ConnectionResult connectionResult) { */ private void restoreBackup() { Log.i("Settings", "Opening GnuCash XML backups for restore"); - String bookUID = BooksDbAdapter.getInstance().getActiveBookUID(); + final String bookUID = BooksDbAdapter.getInstance().getActiveBookUID(); - final String defaultBackupFile = BookUtils.getBookBackupFileUri(bookUID); + final String defaultBackupFile = BackupManager.getBookBackupFileUri(bookUID); if (defaultBackupFile != null){ android.support.v7.app.AlertDialog.Builder builder = new android.support.v7.app.AlertDialog.Builder(getActivity()) .setTitle(R.string.title_confirm_restore_backup) @@ -393,8 +392,7 @@ public void onClick(DialogInterface dialogInterface, int i) { } //If no default location was set, look in the internal SD card location - File[] backupFiles = new File(Exporter.getBackupFolderPath(bookUID)).listFiles(); - if (backupFiles == null || backupFiles.length == 0){ + if (BackupManager.getBackupList(bookUID).isEmpty()){ android.support.v7.app.AlertDialog.Builder builder = new android.support.v7.app.AlertDialog.Builder(getActivity()) .setTitle(R.string.title_no_backups_found) .setMessage(R.string.msg_no_backups_to_restore_from) @@ -408,14 +406,10 @@ public void onClick(DialogInterface dialog, int which) { return; } - Arrays.sort(backupFiles); - List backupFilesList = Arrays.asList(backupFiles); - Collections.reverse(backupFilesList); - final File[] sortedBackupFiles = (File[]) backupFilesList.toArray(); final ArrayAdapter arrayAdapter = new ArrayAdapter<>(getActivity(), android.R.layout.select_dialog_singlechoice); final DateFormat dateFormatter = SimpleDateFormat.getDateTimeInstance(); - for (File backupFile : sortedBackupFiles) { + for (File backupFile : BackupManager.getBackupList(bookUID)) { long time = Exporter.getExportTime(backupFile.getName()); if (time > 0) arrayAdapter.add(dateFormatter.format(new Date(time))); @@ -435,7 +429,7 @@ public void onClick(DialogInterface dialog, int which) { restoreDialogBuilder.setAdapter(arrayAdapter, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - File backupFile = sortedBackupFiles[which]; + File backupFile = BackupManager.getBackupList(bookUID).get(which); new ImportAsyncTask(getActivity()).execute(Uri.fromFile(backupFile)); } }); @@ -477,7 +471,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { PreferenceActivity.getActiveBookSharedPreferences() .edit() - .putString(BookUtils.KEY_BACKUP_FILE, backupFileUri.toString()) + .putString(BackupManager.KEY_BACKUP_FILE, backupFileUri.toString()) .apply(); Preference pref = findPreference(getString(R.string.key_backup_location)); diff --git a/app/src/main/java/org/gnucash/android/ui/settings/dialog/DeleteAllAccountsConfirmationDialog.java b/app/src/main/java/org/gnucash/android/ui/settings/dialog/DeleteAllAccountsConfirmationDialog.java index d45dfffcf..b01c7881e 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/dialog/DeleteAllAccountsConfirmationDialog.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/dialog/DeleteAllAccountsConfirmationDialog.java @@ -24,8 +24,8 @@ import org.gnucash.android.R; import org.gnucash.android.db.adapter.AccountsDbAdapter; -import org.gnucash.android.export.xml.GncXmlExporter; import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; +import org.gnucash.android.util.BackupManager; /** * Confirmation dialog for deleting all accounts from the system. @@ -49,7 +49,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { Context context = getDialog().getContext(); - GncXmlExporter.createBackup(); + BackupManager.backupActiveBook(); AccountsDbAdapter.getInstance().deleteAllRecords(); Toast.makeText(context, R.string.toast_all_accounts_deleted, Toast.LENGTH_SHORT).show(); WidgetConfigurationActivity.updateAllWidgets(context); diff --git a/app/src/main/java/org/gnucash/android/ui/settings/dialog/DeleteAllTransactionsConfirmationDialog.java b/app/src/main/java/org/gnucash/android/ui/settings/dialog/DeleteAllTransactionsConfirmationDialog.java index 0b37f0bd6..284038e70 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/dialog/DeleteAllTransactionsConfirmationDialog.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/dialog/DeleteAllTransactionsConfirmationDialog.java @@ -29,9 +29,9 @@ import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.db.adapter.DatabaseAdapter; import org.gnucash.android.db.adapter.TransactionsDbAdapter; -import org.gnucash.android.export.xml.GncXmlExporter; import org.gnucash.android.model.Transaction; import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; +import org.gnucash.android.util.BackupManager; import java.util.ArrayList; import java.util.List; @@ -57,7 +57,7 @@ public static DeleteAllTransactionsConfirmationDialog newInstance() { .setPositiveButton(R.string.alert_dialog_ok_delete, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { - GncXmlExporter.createBackup(); + BackupManager.backupActiveBook(); Context context = getActivity(); AccountsDbAdapter accountsDbAdapter = AccountsDbAdapter.getInstance(); diff --git a/app/src/main/java/org/gnucash/android/ui/settings/dialog/DeleteBookConfirmationDialog.java b/app/src/main/java/org/gnucash/android/ui/settings/dialog/DeleteBookConfirmationDialog.java index fe5dfccf9..2f58bcaf8 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/dialog/DeleteBookConfirmationDialog.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/dialog/DeleteBookConfirmationDialog.java @@ -24,6 +24,7 @@ import org.gnucash.android.R; import org.gnucash.android.db.adapter.BooksDbAdapter; import org.gnucash.android.ui.common.Refreshable; +import org.gnucash.android.util.BackupManager; /** * Confirmation dialog for deleting a book. @@ -52,6 +53,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { @Override public void onClick(DialogInterface dialogInterface, int which) { final String bookUID = getArguments().getString("bookUID"); + BackupManager.backupBook(bookUID); BooksDbAdapter.getInstance().deleteBook(bookUID); ((Refreshable) getTargetFragment()).refresh(); } diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledActionsListFragment.java b/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledActionsListFragment.java index ac382c53f..01f25340e 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledActionsListFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledActionsListFragment.java @@ -60,6 +60,7 @@ import org.gnucash.android.model.Transaction; import org.gnucash.android.ui.common.FormActivity; import org.gnucash.android.ui.common.UxArgument; +import org.gnucash.android.util.BackupManager; import java.text.DateFormat; import java.util.Date; @@ -116,6 +117,8 @@ public void onDestroyActionMode(ActionMode mode) { public boolean onActionItemClicked(ActionMode mode, MenuItem item) { switch (item.getItemId()) { case R.id.context_menu_delete: + BackupManager.backupActiveBook(); + for (long id : getListView().getCheckedItemIds()) { if (mActionType == ScheduledAction.ActionType.TRANSACTION) { diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsListFragment.java b/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsListFragment.java index bb5796009..9ea0847e9 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsListFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsListFragment.java @@ -60,6 +60,7 @@ import org.gnucash.android.ui.transaction.dialog.BulkMoveDialogFragment; import org.gnucash.android.ui.util.CursorRecyclerAdapter; import org.gnucash.android.ui.util.widget.EmptyRecyclerView; +import org.gnucash.android.util.BackupManager; import java.util.List; @@ -354,6 +355,7 @@ public void onClick(View v) { public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case R.id.context_menu_delete: + BackupManager.backupActiveBook(); mTransactionsDbAdapter.deleteRecord(transactionId); WidgetConfigurationActivity.updateAllWidgets(getActivity()); refresh(); diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransactionsDeleteConfirmationDialogFragment.java b/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransactionsDeleteConfirmationDialogFragment.java index 7c6f04cd0..0fcc48892 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransactionsDeleteConfirmationDialogFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransactionsDeleteConfirmationDialogFragment.java @@ -27,11 +27,11 @@ import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.db.adapter.DatabaseAdapter; import org.gnucash.android.db.adapter.TransactionsDbAdapter; -import org.gnucash.android.export.xml.GncXmlExporter; import org.gnucash.android.model.Transaction; import org.gnucash.android.ui.common.Refreshable; import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; +import org.gnucash.android.util.BackupManager; import java.util.ArrayList; import java.util.List; @@ -65,7 +65,7 @@ public static TransactionsDeleteConfirmationDialogFragment newInstance(int title public void onClick(DialogInterface dialog, int whichButton) { TransactionsDbAdapter transactionsDbAdapter = TransactionsDbAdapter.getInstance(); if (rowId == 0) { - GncXmlExporter.createBackup(); //create backup before deleting everything + BackupManager.backupActiveBook(); //create backup before deleting everything List openingBalances = new ArrayList(); boolean preserveOpeningBalances = GnuCashApplication.shouldSaveOpeningBalances(false); if (preserveOpeningBalances) { diff --git a/app/src/main/java/org/gnucash/android/ui/util/AccountBalanceTask.java b/app/src/main/java/org/gnucash/android/ui/util/AccountBalanceTask.java index e16b21da3..8bef261af 100644 --- a/app/src/main/java/org/gnucash/android/ui/util/AccountBalanceTask.java +++ b/app/src/main/java/org/gnucash/android/ui/util/AccountBalanceTask.java @@ -55,7 +55,7 @@ protected Money doInBackground(String... params) { Money balance = Money.getZeroInstance(); try { - balance = accountsDbAdapter.getAccountBalance(params[0], -1, System.currentTimeMillis()); + balance = accountsDbAdapter.getAccountBalance(params[0], -1, -1); } catch (Exception ex) { Log.e(LOG_TAG, "Error computing account balance ", ex); Crashlytics.logException(ex); diff --git a/app/src/main/java/org/gnucash/android/util/BackupJob.java b/app/src/main/java/org/gnucash/android/util/BackupJob.java new file mode 100644 index 000000000..9f68814e5 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/util/BackupJob.java @@ -0,0 +1,46 @@ +/* Copyright (c) 2018 Àlex Magaz Graça + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.util; + +import android.content.Context; +import android.content.Intent; +import android.support.annotation.NonNull; +import android.support.v4.app.JobIntentService; +import android.util.Log; + + +/** + * Job to back up books periodically. + * + *

The backups are triggered by an alarm set in + * {@link BackupManager#schedulePeriodicBackups(Context)} + * (through {@link org.gnucash.android.receivers.PeriodicJobReceiver}).

+ */ +public class BackupJob extends JobIntentService { + private static final String LOG_TAG = "BackupJob"; + private static final int JOB_ID = 1000; + + public static void enqueueWork(Context context) { + Intent intent = new Intent(context, BackupJob.class); + enqueueWork(context, BackupJob.class, JOB_ID, intent); + } + + @Override + protected void onHandleWork(@NonNull Intent intent) { + Log.i(LOG_TAG, "Doing backup of all books."); + BackupManager.backupAllBooks(); + } +} diff --git a/app/src/main/java/org/gnucash/android/util/BackupManager.java b/app/src/main/java/org/gnucash/android/util/BackupManager.java new file mode 100644 index 000000000..9969ab638 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/util/BackupManager.java @@ -0,0 +1,193 @@ +/* Copyright (c) 2018 Àlex Magaz Graça + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.util; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.SystemClock; +import android.support.annotation.Nullable; +import android.util.Log; + +import com.crashlytics.android.Crashlytics; + +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.db.adapter.BooksDbAdapter; +import org.gnucash.android.export.ExportFormat; +import org.gnucash.android.export.ExportParams; +import org.gnucash.android.export.Exporter; +import org.gnucash.android.export.xml.GncXmlExporter; +import org.gnucash.android.model.Book; +import org.gnucash.android.receivers.PeriodicJobReceiver; +import org.gnucash.android.ui.settings.PreferenceActivity; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.zip.GZIPOutputStream; + + +/** + * Deals with all backup-related tasks. + */ +public class BackupManager { + private static final String LOG_TAG = "BackupManager"; + public static final String KEY_BACKUP_FILE = "book_backup_file_key"; + + /** + * Perform an automatic backup of all books in the database. + * This method is run every time the service is executed + */ + static void backupAllBooks() { + BooksDbAdapter booksDbAdapter = BooksDbAdapter.getInstance(); + List bookUIDs = booksDbAdapter.getAllBookUIDs(); + Context context = GnuCashApplication.getAppContext(); + + for (String bookUID : bookUIDs) { + String backupFile = getBookBackupFileUri(bookUID); + if (backupFile == null){ + backupBook(bookUID); + continue; + } + + try (BufferedOutputStream bufferedOutputStream = + new BufferedOutputStream(context.getContentResolver().openOutputStream(Uri.parse(backupFile)))){ + GZIPOutputStream gzipOutputStream = new GZIPOutputStream(bufferedOutputStream); + OutputStreamWriter writer = new OutputStreamWriter(gzipOutputStream); + ExportParams params = new ExportParams(ExportFormat.XML); + new GncXmlExporter(params).generateExport(writer); + writer.close(); + } catch (IOException ex) { + Log.e(LOG_TAG, "Auto backup failed for book " + bookUID); + ex.printStackTrace(); + Crashlytics.logException(ex); + } + } + } + + /** + * Backs up the active book to the directory {@link #getBackupFolderPath(String)}. + * + * @return {@code true} if backup was successful, {@code false} otherwise + */ + public static boolean backupActiveBook() { + return backupBook(BooksDbAdapter.getInstance().getActiveBookUID()); + } + + /** + * Backs up the book with UID {@code bookUID} to the directory + * {@link #getBackupFolderPath(String)}. + * + * @param bookUID Unique ID of the book + * @return {@code true} if backup was successful, {@code false} otherwise + */ + public static boolean backupBook(String bookUID){ + OutputStream outputStream; + try { + String backupFile = getBookBackupFileUri(bookUID); + if (backupFile != null){ + outputStream = GnuCashApplication.getAppContext().getContentResolver().openOutputStream(Uri.parse(backupFile)); + } else { //no Uri set by user, use default location on SD card + backupFile = getBackupFilePath(bookUID); + outputStream = new FileOutputStream(backupFile); + } + + BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); + GZIPOutputStream gzipOutputStream = new GZIPOutputStream(bufferedOutputStream); + OutputStreamWriter writer = new OutputStreamWriter(gzipOutputStream); + + ExportParams params = new ExportParams(ExportFormat.XML); + new GncXmlExporter(params).generateExport(writer); + writer.close(); + return true; + } catch (IOException | Exporter.ExporterException e) { + Crashlytics.logException(e); + Log.e("GncXmlExporter", "Error creating XML backup", e); + return false; + } + } + + /** + * Returns the full path of a file to make database backup of the specified book. + * Backups are done in XML format and are Gzipped (with ".gnca" extension). + * @param bookUID GUID of the book + * @return the file path for backups of the database. + * @see #getBackupFolderPath(String) + */ + private static String getBackupFilePath(String bookUID){ + Book book = BooksDbAdapter.getInstance().getRecord(bookUID); + return getBackupFolderPath(book.getUID()) + + Exporter.buildExportFilename(ExportFormat.XML, book.getDisplayName()); + } + + /** + * Returns the path to the backups folder for the book with GUID {@code bookUID}. + * + *

Each book has its own backup folder.

+ * + * @return Absolute path to backup folder for the book + */ + private static String getBackupFolderPath(String bookUID){ + String baseFolderPath = GnuCashApplication.getAppContext() + .getExternalFilesDir(null) + .getAbsolutePath(); + String path = baseFolderPath + "/" + bookUID + "/backups/"; + File file = new File(path); + if (!file.exists()) + file.mkdirs(); + return path; + } + + /** + * Return the user-set backup file URI for the book with UID {@code bookUID}. + * @param bookUID Unique ID of the book + * @return DocumentFile for book backups, or null if the user hasn't set any. + */ + @Nullable + public static String getBookBackupFileUri(String bookUID){ + SharedPreferences sharedPreferences = PreferenceActivity.getBookSharedPreferences(bookUID); + return sharedPreferences.getString(KEY_BACKUP_FILE, null); + } + + public static List getBackupList(String bookUID) { + File[] backupFiles = new File(getBackupFolderPath(bookUID)).listFiles(); + Arrays.sort(backupFiles); + List backupFilesList = Arrays.asList(backupFiles); + Collections.reverse(backupFilesList); + return backupFilesList; + } + + public static void schedulePeriodicBackups(Context context) { + Log.i(LOG_TAG, "Scheduling backup job"); + Intent intent = new Intent(context, PeriodicJobReceiver.class); + intent.setAction(PeriodicJobReceiver.ACTION_BACKUP); + PendingIntent alarmIntent = PendingIntent.getBroadcast(context,0, intent, + PendingIntent.FLAG_UPDATE_CURRENT); + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_FIFTEEN_MINUTES, + AlarmManager.INTERVAL_DAY, alarmIntent); + } +} diff --git a/app/src/main/java/org/gnucash/android/util/BookUtils.java b/app/src/main/java/org/gnucash/android/util/BookUtils.java index 67bf5f4dd..dbd308d7e 100644 --- a/app/src/main/java/org/gnucash/android/util/BookUtils.java +++ b/app/src/main/java/org/gnucash/android/util/BookUtils.java @@ -1,30 +1,15 @@ package org.gnucash.android.util; -import android.content.SharedPreferences; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.ui.account.AccountsActivity; -import org.gnucash.android.ui.settings.PreferenceActivity; /** * Utility class for common operations involving books */ public class BookUtils { - public static final String KEY_BACKUP_FILE = "book_backup_file_key"; - - /** - * Return the backup file for the book - * @param bookUID Unique ID of the book - * @return DocumentFile for book backups - */ - @Nullable - public static String getBookBackupFileUri(String bookUID){ - SharedPreferences sharedPreferences = PreferenceActivity.getBookSharedPreferences(bookUID); - return sharedPreferences.getString(KEY_BACKUP_FILE, null); - } /** * Activates the book with unique identifer {@code bookUID}, and refreshes the database adapters diff --git a/app/src/main/res/layout/fragment_export_form.xml b/app/src/main/res/layout/fragment_export_form.xml index 162b3f58f..a4cc5ee71 100644 --- a/app/src/main/res/layout/fragment_export_form.xml +++ b/app/src/main/res/layout/fragment_export_form.xml @@ -75,35 +75,87 @@ android:gravity="center_vertical" android:orientation="horizontal"> - + android:text="CSV"/> - + android:text="QIF" /> + + + + + + + + + + + + + diff --git a/app/src/main/res/raw/iso_4217_currencies.xml b/app/src/main/res/raw/iso_4217_currencies.xml index 1312e8db2..27a93a61b 100644 --- a/app/src/main/res/raw/iso_4217_currencies.xml +++ b/app/src/main/res/raw/iso_4217_currencies.xml @@ -1313,7 +1313,7 @@ exchange-code="364" parts-per-unit="1" smallest-fraction="1" - local-symbol="﷼﷼" + local-symbol="﷼" /> diff --git a/app/src/main/res/values-el-rGR/strings.xml b/app/src/main/res/values-el-rGR/strings.xml index 49f40617d..cf674c732 100644 --- a/app/src/main/res/values-el-rGR/strings.xml +++ b/app/src/main/res/values-el-rGR/strings.xml @@ -206,10 +206,10 @@ Memo Spend Receive - Withdrawal - Deposit - Payment - Charge + Ανάληψη + Κατάθεση + Πληρωμή + Χρέωση Decrease Increase Income @@ -217,8 +217,8 @@ Expense Bill Invoice - Buy - Sell + Αγορά + Πώληση Αρχικά υπόλοιπα Καθαρή Θέση Enable to save the current account balance (before deleting transactions) as new opening balance after deleting transactions @@ -228,14 +228,14 @@ Generates separate QIF files per currency Imbalance: Add split - Favorite + Αγαπημένο Navigation drawer opened Navigation drawer closed - Reports + Αναφορές Pie Chart Line Chart Bar Chart - Report Preferences + Προτιμήσεις αναφοράς Account color in reports Use account color in the bar/pie chart Order by size diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 35196f9a2..e83540caf 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -38,7 +38,7 @@ ABONO Cuentas Transacciones - BORRAR + Borrar Borrar Cancelar Cuenta borrada diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml new file mode 100644 index 000000000..7fec943b9 --- /dev/null +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -0,0 +1,457 @@ + + + + + Create Account + Edit Account + Add a new transaction to an account + View account details + No accounts to display + Account name + Cancel + Save + Test + Enter Passcode + Wrong passcode, please try again + Passcode set + Please confirm your passcode + Invalid passcode confirmation. Please try again + Description + Amount + New transaction + No transactions to display + DEBIT + CREDIT + Accounts + Transactions + Delete + Delete + Cancel + Account deleted + Confirm delete + Edit Transaction + Add note + %1$d selected + Balance: + Export To: + Export Transactions + By default, only new transactions since last export will be exported. Check this option to export all transactions + Error exporting %1$s file + Export + Delete transactions after export + All exported transactions will be deleted when exporting is completed + Settings + + Save As… + Dropbox + ownCloud + Send to… + + Move + Move %1$d transaction(s) + Destination Account + Cannot move transactions.\nThe destination account uses a different currency from origin account + General + About + Choose default currency + Default currency + Default currency to assign to new accounts + Enables recording transactions in GnuCash for Android + Enables creation of accounts in GnuCash for Android + Your GnuCash data + Read and modify GnuCash data + Record transactions in GnuCash + Create accounts in GnuCash + Display account + Hide account balance in widget + Create Accounts + No accounts exist in GnuCash.\nCreate an account before adding a widget + License + Apache License v2.0. Click for details + General Preferences + Select Account + There are no transactions available to export + Passcode Preferences + Enable passcode + Change Passcode + About GnuCash + GnuCash Android %1$s export + GnuCash Android Export from + Transactions + Transaction Preferences + Account Preferences + Default Transaction Type + The type of transaction to use by default, CREDIT or DEBIT + + CREDIT + DEBIT + + Are you sure you want to delete ALL transactions? + Are you sure you want to delete this transaction? + Export + Export all transactions + Delete exported transactions + Default export email + The default email address to send exports to. You can still change this when you export. + All transactions will be a transfer from one account to another + Activate Double Entry + Balance + Enter an account name to create an account + Currency + Parent account + Use XML OFX header + Enable this option when exporting to third-party application other than GnuCash for desktop + What\'s New + + - Added ability to export to any service which supports the Storage Access Framework \n + - Added option to set the location for regular automatic backups (See backup settings)\n + - Added Bitcoin currency support\n + - Added support for renaming books\n + - Multiple bug fixes and improvements\n + + Dismiss + Enter an amount to save the transaction + An error occurred while importing the GnuCash accounts + GnuCash Accounts successfully imported + Import account structure exported from GnuCash desktop + Import GnuCash XML + Delete all accounts in the database. All transactions will be deleted as + well. + + Delete all accounts + Accounts + All accounts have been successfully deleted + Are you sure you want to delete all accounts and transactions?\n\nThis + operation cannot be undone! + + All transactions in all accounts will be deleted! + Delete all transactions + All transactions successfully deleted! + Importing accounts + Transactions + Sub-Accounts + Search + Default Export Format + File format to use by default when exporting transactions + Recurrence + + Imbalance + Exporting transactions + No recurring transactions to display. + Successfully deleted recurring transaction + Placeholder account + Default Transfer Account + + %d sub-account + %d sub-accounts + + + CASH + BANK + CREDIT CARD + ASSET + LIABILITY + INCOME + EXPENSE + PAYABLE + RECEIVABLE + EQUITY + CURRENCY + STOCK + MUTUAL FUND + TRADING + + + QIF + OFX + XML + + + Select a Color + + + Account Color & Type + Delete sub-accounts + Recent + Favorites + All + Creates default GnuCash commonly-used account structure + Create default accounts + A new book will be opened with the default accounts\n\nYour current accounts and transactions will not be modified! + Transactions + Select destination for export + Memo + Spend + Receive + Withdrawal + Deposit + Payment + Charge + Decrease + Increase + Income + Rebate + Expense + Bill + Invoice + Buy + Sell + Opening Balances + Equity + Enable to save the current account balance (before deleting transactions) as new opening balance after deleting transactions + + Save account opening balances + OFX does not support double-entry transactions + Generates separate QIF files per currency + Imbalance: + Add split + Favorite + Navigation drawer opened + Navigation drawer closed + Reports + Pie Chart + Line Chart + Bar Chart + Report Preferences + Account color in reports + Use account color in the bar/pie chart + Order by size + Show legend + Show labels + Show percentage + Show average lines + Group Smaller Slices + No chart data available + Total + Other + The percentage of selected value calculated from the total amount + The percentage of selected value calculated from the current stacked bar amount + Save as template + This account contains transactions. \nWhat would you like to do with these transactions + This account contains sub-accounts. \nWhat would you like to do with these sub-accounts + Delete transactions + Create and specify a transfer account OR disable double-entry in settings to save the transaction + Tap to create schedule + Restore Backup… + Backup & export + Enable DropBox + Enable ownCloud + Backup + Enable exporting to DropBox + Enable exporting to ownCloud + Backup Preferences + Create Backup + Create a backup of the active book + Restore most recent backup of active book + Backup successful + Backup failed + Exports all accounts and transactions + Install a file manager to select files + Select backup to restore + Favorites + Open… + Reports + Export… + Settings + User Name + Password + owncloud + https:// + OC server not found + OC username/password invalid + Invalid chars: \\ < > : \" | * ? + OC server OK + OC username/password OK + Dir name OK + + Hourly + Every %d hours + + + Daily + Every %d days + + + Weekly + Every %d weeks + + + Monthly + Every %d months + + + Yearly + Every %d years + + Enable Crash Logging + Automatically send information about app malfunction to the developers. + Format + Enter your old passcode + Enter your new passcode + Exports + No scheduled exports to display + Create export schedule + Exported to: %1$s + The legend is too long + Account description + No recent accounts + No favorite accounts + Scheduled Actions + "Ended, last executed on %1$s" + Next + Done + Default Currency + Account Setup + Select Currency + Feedback Options + Create default accounts + Import my accounts + Let me handle it + Other… + Automatically send crash reports + Disable crash reports + Back + Setup GnuCash + Welcome to GnuCash + Before you dive in, \nlet\'s setup a few things first\n\nTo continue, press Next + Split Editor + Check that all splits have valid amounts before saving! + Invalid expression! + Scheduled recurring transaction + Transfer Funds + + Select a slice to see details + Period: + From: + To: + Provide either the converted amount or exchange rate in order to transfer funds + Exchange rate + Fetch quote + Converted Amount + Sheet + Expenses for last 3 months + Total Assets + Total Liabilities + Net Worth + Assets + Liabilities + Equity + Move to: + Group By + Month + Quarter + Year + Balance Sheet + Total: + Google+ Community + Translate GnuCash + Share ideas, discuss changes or report problems + Translate or proof-read on CrowdIn + No compatible apps to receive the exported transactions! + Move… + Duplicate + Cash Flow + Budgets + Enable compact view + Enable to always use compact view for transactions list + Invalid exchange rate + e.g. 1 %1$s = x.xx %2$s + Invalid amount + + Current month + Last 3 months + Last 6 months + Last 12 months + All time + Custom range… + + + 1 + + + 2 + ABC + 3 + DEF + 4 + GHI + 5 + JKL + 6 + MNO + 7 + PQRS + 8 + TUV + 9 + WXYZ + 0 + + + Manage Books + Manage Books… + Select any part of the chart to view details + Confirm delete Book + All accounts and transactions in this book will be deleted! + Delete Book + Last Exported: + Enable Sync + New Book + The selected transaction has no splits and cannot be opened + %1$d splits + in %1$s + + %d account + %d accounts + + + %d transaction + %d transactions + + + EXPENSE + INCOME + + Connected to Google Drive + Unable to connect to Google Drive + Please enter an amount to split + external service + Updated transaction recurring schedule + Since + All time + Recommend in Play Store + until %1$s + on %1$s + for %1$d times + Compact View + Book %1$d + never + Rename Book + Rename + Rename + Select backup file + Select a file for automatic backups + Confirm restore from backup + A new book will be opened with the contents of this backup. Do you wish to proceed? + Restore + No backups found + There are no existing backup files to restore from + + gnucash_android_backup.gnca + Select the destination after export is complete + Export to \'/Apps/GnuCash Android/\' folder on Dropbox + Preferences + diff --git a/app/src/main/res/values-in-rID/strings.xml b/app/src/main/res/values-in-rID/strings.xml index 2535bed27..8f9d8d940 100644 --- a/app/src/main/res/values-in-rID/strings.xml +++ b/app/src/main/res/values-in-rID/strings.xml @@ -20,15 +20,15 @@ Ubah Akun Tambah transaksi baru ke akun Lihat rincian akun - Tidak ada akun untuk ditampilkan + Tak ada akun untuk ditampilkan Nama akun Batal Simpan - Tes - Masukkan Kode akses - Kode akses salah, silakan coba lagi - Kode akses dibuat - Harap konfirmasi kode akses Anda + Uji coba + Masukkan sandi + Sandi salah, silakan coba lagi + Sandi dibuat + Harap konfirmasi sandi Anda Konfirmasi kode akses tidak valid. Silakan cobalagi Deskripsi Jumlah diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 2f3bfca4f..37681f758 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -138,7 +138,7 @@ Todas as transações apagadas com sucesso! Importando contas Transações - Sub-Contas + Subcontas Procurar Formato de Exportação padrão Formato de arquivo a ser usado por padrão ao exportar transações @@ -151,8 +151,8 @@ Conta não editável Conta para transferências padrão - %d sub-conta - %d sub-contas + %d subconta + %d subconta DINHEIRO @@ -180,9 +180,9 @@ Cor de conta & Tipo - Apagar sub-contas + Apagar subcontas Recentes - Favoritos + Favoritas Todas Cria uma estrutura de contas GnuCash padrão Cria contas padrão @@ -222,8 +222,8 @@ Gráfico de Linhas Gráfico de Barras Preferências de relatórios - Côr da conta nos relatórios - Use côr da conta no gráfico de barras/linhas + Cor da conta nos relatórios + Use cor da conta no gráfico de barras/linhas Ordenar por tamanho Alterna visibilidade da legenda Alterna visibilidade das etiquetas @@ -428,7 +428,7 @@ Neste processo não serão recolhidas informações do utilizador! Agenda recorrente de transação atualizada Desde Desde o início - Recomendado na Play Store + Recomendar na Play Store até %1$s em %1$s por %1$d vezes diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index fed5df41b..eedcb017a 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -428,7 +428,7 @@ Neste processo não serão recolhidas informações do utilizador!
Transação atualizada agendamento recorrente Desde Todo o tempo - Recomendado na Play Store + Recomendar na Play Store desde%1$s na %1$s por %1$d vezes diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index bd31a17fe..1b625cc34 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -154,6 +154,7 @@ %d дочерний счёт %d шт. дочерних счетов + %d sub-accounts %d шт. дочерних счетов @@ -224,7 +225,7 @@ График Гистограмма Настройки отчётов - Цвет счёта в отчётых + Цвет счёта в отчётах Использовать цвет счёта в отчётах Отсортировать по размеру Показать легенду @@ -277,26 +278,31 @@ Каждый час Каждые %d часа + Every %d hours Каждые %d часов Ежедневно Каждые %d дня + Every %d days Каждые %d дней Еженедельно Каждые %d недели + Every %d weeks Каждые %d недель Ежемесячно Каждые %d месяца + Every %d months Каждые %d месяцев Ежегодно Каждые %d года + Every %d years Каждые %d лет Записывать отказы программы @@ -418,11 +424,13 @@ %d счета %d счета + %d accounts %d счетов %d транзакция %d транзакции + %d transactions %d транзакций diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 60a54c00c..40fcb6920 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -19,7 +19,7 @@ 创建科目 编辑科目 为科目增加交易 - 科目的详细资料 + 查看帐户详细信息 没有可以显示的科目 科目名称 取消 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 3e7583fe2..8b502ea05 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -18,9 +18,9 @@ 新增科目 編輯科目 - 给科目添加交易 + 在科目中新增交易 檢視帳戶詳細資訊 - 没有要显示的科目 + 沒有科目可顯示 科目名稱 取消 存檔 @@ -78,7 +78,7 @@ 建立會計科目 顯示科目名字 小工具中隱藏帳戶餘額 - 创建科目 + 建立科目 GnuCash裡還没有會計科目信息。\n使用小部件前需要添加會計科目 授權許可 Apache License v2.0,點擊查看詳细(將打開網頁)。 @@ -125,7 +125,7 @@
知道了 輸入金額才能保存交易 - 匯入GnuCash科目时發生錯誤。 + 匯入GnuCash科目時發生錯誤。 GnuCash科目資料匯入完成。 匯入從GnuCash桌面版匯出的科目設置 匯入GnuCash科目 @@ -177,7 +177,7 @@ XML - 选择一种颜色 + 選擇顏色 科目顏色和類型 @@ -187,7 +187,7 @@ 所有 建立通用的科目結構 建立預設科目 - 這將用預設科目來創建新的帳簿\n\n目前擁有的科目與交易不會受影響 + 這將用預設科目來建立新的帳簿\n\n目前擁有的科目與交易不會受影響 交易 選擇儲存的位置 描述 @@ -196,7 +196,7 @@ 提款 存款 付款 - 费用 + 費用 減少 增加 收入 @@ -220,10 +220,10 @@ 報表 圓形圖 折線圖 - 橫條圖 + 長條圖 報表設置 用不同顏色区分科目 - 在饼图中使用科目的颜色 + 在圓餅圖中使用科目的顏色 按數量排序 顯示圖例 顯示標籤 @@ -279,7 +279,7 @@ 每 %d 天 - 每 %d 周 + 每 %d 週 每 %d 月 @@ -311,13 +311,13 @@ No user-identifiable information will be collected as part of this process! 選擇幣種 回饋選項 建立預設科目 - 汇入科目 + 匯入科目 稍后处理 其他... 自動發送故障報告 禁用崩潰報告 後退 - 设置GnuCash + 設定GnuCash 歡迎來到GnuCash 在使用之前,需 \n要设置几个参数\n\n请点击“下一步”繼續 拆分交易 @@ -343,7 +343,7 @@ No user-identifiable information will be collected as part of this process! 負債 財產淨值 移動至 - 分组方式 + 分組方式 季度 @@ -352,13 +352,13 @@ No user-identifiable information will be collected as part of this process! Google+ 社群 翻譯GnuCash 在Google+上提交问题和建议 - 帮忙翻譯或校對( CrowdIn) + 在CrowdIn上協助翻譯或校對GnuCash 没有合适的应用接收汇出的文档 移動... 複製 現金流 預算 - 啟用紧凑視圖 + 啟用緊湊視圖 交易清單總是啟用緊湊視圖 匯率不正確 例如 1 %1$s = x.xx %2$s @@ -393,18 +393,18 @@ No user-identifiable information will be collected as part of this process! WXYZ 0 + - 管理帐簿 - 管理帐簿 + 管理帳簿 + 管理帳簿… 選擇該圖表以查看詳細資訊的任何部分 確認删除 - 帐簿中所有科目和交易都將被刪除 ! - 删除帐簿 + 帳簿中所有科目和交易都將被刪除 ! + 删除帳簿 最後匯出︰ 啟用同步 新建帐簿 选择的交易没有拆分 %1$d 项分割 - 于 %1$s + 於 %1$s %d 個科目 @@ -419,15 +419,15 @@ No user-identifiable information will be collected as part of this process! 無法連線到伺服器 請輸入要拆分的金額 外部服務 - 排程交易已经更新 + 排程交易已經更新 自從 全部時間 在 Play Store 推薦 直到%1$s 在%1$s %1$d 次 - 紧凑视图 - 账簿 %1$d + 緊湊視圖 + 帳簿 %1$d 從未 重新命名帳簿 重新命名 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 bbd535f73..4ef7ae17e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -117,17 +117,16 @@ Enable this option when exporting to third-party application other than GnuCash for desktop What\'s New - - Added ability to export to any service which supports the Storage Access Framework \n - - Added option to set the location for regular automatic backups (See backup settings)\n - - Added Bitcoin currency support\n - - Added support for renaming books\n - - Multiple bug fixes and improvements\n + - Adds CSV export format \n + - Improve compatibility with GnuCash desktop files\n + - Limit space usage by backups \n + - Multiple bug fixes and enhancements\n Dismiss Enter an amount to save the transaction An error occurred while importing the GnuCash accounts GnuCash Accounts successfully imported - Import account structure exported from GnuCash desktop + Import account structure from GnuCash XML Import GnuCash XML Delete all accounts in the database. All transactions will be deleted as well. @@ -176,6 +175,7 @@ TRADING + CSV QIF OFX XML @@ -463,4 +463,41 @@ Export to \'/Apps/GnuCash Android/\' folder on Dropbox Preferences Yes, I\'m sure + export_accounts_csv_key + Export all accounts (without transactions) to CSV + Export as CSV + Separator + Exports transactions as CSV + + Date + Transaction ID + Number + Description + Notes + Commodity/Currency + Void Reason + Action + Memo + Full Account Name + Account Name + Amount With Sym. + Amount Num + Reconcile + Reconcile Date + Rate/Price + + + Type + Full Name + Name + Code + Description + Color + Notes + Commoditym + Commodityn + Hidden + Tax + Placeholder + diff --git a/app/src/main/res/xml/fragment_account_preferences.xml b/app/src/main/res/xml/fragment_account_preferences.xml index ace62559e..d8299897d 100644 --- a/app/src/main/res/xml/fragment_account_preferences.xml +++ b/app/src/main/res/xml/fragment_account_preferences.xml @@ -28,6 +28,9 @@ + diff --git a/app/src/test/java/org/gnucash/android/test/unit/export/BackupTest.java b/app/src/test/java/org/gnucash/android/test/unit/export/BackupTest.java index 3b144dd2c..7cfbc30bd 100644 --- a/app/src/test/java/org/gnucash/android/test/unit/export/BackupTest.java +++ b/app/src/test/java/org/gnucash/android/test/unit/export/BackupTest.java @@ -25,6 +25,7 @@ import org.gnucash.android.importer.GncXmlImporter; import org.gnucash.android.test.unit.testutil.ShadowCrashlytics; import org.gnucash.android.test.unit.testutil.ShadowUserVoice; +import org.gnucash.android.util.BackupManager; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -52,12 +53,6 @@ public void setUp(){ loadDefaultAccounts(); } - @Test - public void shouldCreateBackup(){ - boolean backupResult = GncXmlExporter.createBackup(); - assertThat(backupResult).isTrue(); - } - @Test public void shouldCreateBackupFileName() throws Exporter.ExporterException { Exporter exporter = new GncXmlExporter(new ExportParams(ExportFormat.XML)); diff --git a/app/src/test/java/org/gnucash/android/test/unit/export/QifExporterTest.java b/app/src/test/java/org/gnucash/android/test/unit/export/QifExporterTest.java index 9f399d362..689e69b33 100644 --- a/app/src/test/java/org/gnucash/android/test/unit/export/QifExporterTest.java +++ b/app/src/test/java/org/gnucash/android/test/unit/export/QifExporterTest.java @@ -16,6 +16,7 @@ package org.gnucash.android.test.unit.export; import android.database.sqlite.SQLiteDatabase; +import android.support.annotation.NonNull; import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.db.BookDbHelper; @@ -45,6 +46,7 @@ import java.io.FileReader; import java.io.IOException; import java.util.List; +import java.util.zip.ZipFile; import static org.assertj.core.api.Assertions.assertThat; @@ -83,7 +85,7 @@ public void testWithNoTransactionsToExport_shouldNotCreateAnyFile(){ /** * Test that QIF files are generated */ - //// FIXME: 20.04.2017 Test failing with NPE + @Test public void testGenerateQIFExport(){ AccountsDbAdapter accountsDbAdapter = new AccountsDbAdapter(mDb); @@ -109,10 +111,11 @@ public void testGenerateQIFExport(){ } /** - * Test that when more than one currency is in use, multiple QIF files will be generated + * Test that when more than one currency is in use, a zip with multiple QIF files + * will be generated */ - //// FIXME: 20.04.2017 test failing with NPE - public void multiCurrencyTransactions_shouldResultInMultipleQifFiles(){ + // @Test Fails randomly. Sometimes it doesn't split the QIF. + public void multiCurrencyTransactions_shouldResultInMultipleZippedQifFiles() throws IOException { AccountsDbAdapter accountsDbAdapter = new AccountsDbAdapter(mDb); Account account = new Account("Basic Account", Commodity.getInstance("EUR")); @@ -139,25 +142,29 @@ public void multiCurrencyTransactions_shouldResultInMultipleQifFiles(){ QifExporter qifExporter = new QifExporter(exportParameters, mDb); List exportedFiles = qifExporter.generateExport(); - assertThat(exportedFiles).hasSize(2); + assertThat(exportedFiles).hasSize(1); File file = new File(exportedFiles.get(0)); - assertThat(file).exists().hasExtension("qif"); - assertThat(file.length()).isGreaterThan(0L); + assertThat(file).exists().hasExtension("zip"); + assertThat(new ZipFile(file).size()).isEqualTo(2); } - //@Test - public void description_and_memo_field_test() { - // arrange - + /** + * Test that the memo and description fields of transactions are exported. + */ + @Test + public void memoAndDescription_shouldBeExported() throws IOException { String expectedDescription = "my description"; String expectedMemo = "my memo"; AccountsDbAdapter accountsDbAdapter = new AccountsDbAdapter(mDb); + Account account = new Account("Basic Account"); Transaction transaction = new Transaction("One transaction"); + transaction.addSplit(new Split(Money.createZeroInstance("EUR"), account.getUID())); transaction.setDescription(expectedDescription); transaction.setNote(expectedMemo); account.addTransaction(transaction); + accountsDbAdapter.addRecord(account); ExportParams exportParameters = new ExportParams(ExportFormat.QIF); @@ -165,26 +172,26 @@ public void description_and_memo_field_test() { exportParameters.setExportTarget(ExportParams.ExportTarget.SD_CARD); exportParameters.setDeleteTransactionsAfterExport(false); - // act - QifExporter qifExporter = new QifExporter(exportParameters, mDb); List exportedFiles = qifExporter.generateExport(); - // assert - assertThat(exportedFiles).hasSize(1); File file = new File(exportedFiles.get(0)); + String fileContent = readFileContent(file); assertThat(file).exists().hasExtension("qif"); - StringBuilder fileContentsBuilder = new StringBuilder(); - try { - BufferedReader reader = new BufferedReader(new FileReader(file)); - fileContentsBuilder.append(reader.readLine()); - } catch (IOException e) { - e.printStackTrace(); - } - // todo: check the description & memo fields. - String fileContent = fileContentsBuilder.toString(); assertThat(fileContent.contains(expectedDescription)); assertThat(fileContent.contains(expectedMemo)); } + + @NonNull + public String readFileContent(File file) throws IOException { + StringBuilder fileContentsBuilder = new StringBuilder(); + BufferedReader reader = new BufferedReader(new FileReader(file)); + String line; + while ((line = reader.readLine()) != null) { + fileContentsBuilder.append(line).append('\n'); + } + + return fileContentsBuilder.toString(); + } } \ No newline at end of file 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 d0a974803..fd555a7f1 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 @@ -381,8 +381,8 @@ public void scheduledBackups_shouldNotRunBeforeNextScheduledExecution(){ } /** - * Tests that an scheduled backup doesn't include transactions added or modified - * previous to the last run. + * Tests that a scheduled QIF backup isn't done when no transactions have + * been added or modified after the last run. */ @Test public void scheduledBackups_shouldNotIncludeTransactionsPreviousToTheLastRun() { @@ -422,8 +422,8 @@ public void scheduledBackups_shouldNotIncludeTransactionsPreviousToTheLastRun() actions.add(scheduledBackup); ScheduledActionService.processScheduledActions(actions, mDb); - assertThat(scheduledBackup.getExecutionCount()).isEqualTo(2); - assertThat(scheduledBackup.getLastRunTime()).isGreaterThan(previousLastRun); + assertThat(scheduledBackup.getExecutionCount()).isEqualTo(1); + assertThat(scheduledBackup.getLastRunTime()).isEqualTo(previousLastRun); assertThat(backupFolder.listFiles()).hasSize(0); } @@ -481,6 +481,7 @@ public void scheduledBackups_shouldIncludeTransactionsAfterTheLastRun() { assertThat(scheduledBackup.getExecutionCount()).isEqualTo(2); assertThat(scheduledBackup.getLastRunTime()).isGreaterThan(previousLastRun); assertThat(backupFolder.listFiles()).hasSize(1); + assertThat(backupFolder.listFiles()[0].getName()).endsWith(".qif"); } @After diff --git a/app/src/test/java/org/gnucash/android/util/BackupManagerTest.java b/app/src/test/java/org/gnucash/android/util/BackupManagerTest.java new file mode 100644 index 000000000..d7038e409 --- /dev/null +++ b/app/src/test/java/org/gnucash/android/util/BackupManagerTest.java @@ -0,0 +1,83 @@ +package org.gnucash.android.util; + +import org.gnucash.android.R; +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.db.adapter.BooksDbAdapter; +import org.gnucash.android.importer.GncXmlImporter; +import org.gnucash.android.test.unit.testutil.ShadowCrashlytics; +import org.gnucash.android.test.unit.testutil.ShadowUserVoice; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.xml.sax.SAXException; + +import java.io.IOException; + +import javax.xml.parsers.ParserConfigurationException; + +import static org.assertj.core.api.Assertions.assertThat; + + +@RunWith(RobolectricTestRunner.class) //package is required so that resources can be found in dev mode +@Config(sdk = 21, packageName = "org.gnucash.android", + shadows = {ShadowCrashlytics.class, ShadowUserVoice.class}) +public class BackupManagerTest { + private BooksDbAdapter mBooksDbAdapter; + + @Before + public void setUp() throws Exception { + mBooksDbAdapter = BooksDbAdapter.getInstance(); + mBooksDbAdapter.deleteAllRecords(); + assertThat(mBooksDbAdapter.getRecordsCount()).isEqualTo(0); + } + + @Test + public void backupAllBooks() throws Exception { + String activeBookUID = createNewBookWithDefaultAccounts(); + BookUtils.activateBook(activeBookUID); + createNewBookWithDefaultAccounts(); + assertThat(mBooksDbAdapter.getRecordsCount()).isEqualTo(2); + + BackupManager.backupAllBooks(); + + for (String bookUID : mBooksDbAdapter.getAllBookUIDs()) { + assertThat(BackupManager.getBackupList(bookUID).size()).isEqualTo(1); + } + } + + @Test + public void getBackupList() throws Exception { + String bookUID = createNewBookWithDefaultAccounts(); + BookUtils.activateBook(bookUID); + + BackupManager.backupActiveBook(); + Thread.sleep(1000); // FIXME: Use Mockito to get a different date in Exporter.buildExportFilename + BackupManager.backupActiveBook(); + + assertThat(BackupManager.getBackupList(bookUID).size()).isEqualTo(2); + } + + @Test + public void whenNoBackupsHaveBeenDone_shouldReturnEmptyBackupList() { + String bookUID = createNewBookWithDefaultAccounts(); + BookUtils.activateBook(bookUID); + + assertThat(BackupManager.getBackupList(bookUID)).isEmpty(); + } + + /** + * Creates a new database with default accounts + * @return The book UID for the new database + * @throws RuntimeException if the new books could not be created + */ + private String createNewBookWithDefaultAccounts(){ + try { + return GncXmlImporter.parse(GnuCashApplication.getAppContext().getResources().openRawResource(R.raw.default_accounts)); + } catch (ParserConfigurationException | SAXException | IOException e) { + e.printStackTrace(); + throw new RuntimeException("Could not create default accounts"); + } + } +} \ No newline at end of file diff --git a/crowdin.yml b/crowdin.yml index be4fb23ca..57f6ea49d 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -5,7 +5,7 @@ # Choose file structure in crowdin # e.g. true or false # -"preserve_hierarchy": true +"preserve_hierarchy": false # # Files configuration @@ -13,19 +13,16 @@ files: [ { # - # Source files filter - # e.g. "/resources/en/*.json" - # + # Source files "source" : "/app/src/main/res/values/strings.xml", # # where translations live - # e.g. "/resources/%two_letters_code%/%original_file_name%" - # "translation" : "/app/src/main/res/values-%android_code%/%original_file_name%", # - # Often software projects have custom names for locale directories. crowdin-cli allows you to map your own languages to be understandable by Crowdin. + # Often software projects have custom names for locale directories. + # crowdin-cli allows you to map your own languages to be understandable by Crowdin. # "languages_mapping" : { "android_code": { @@ -61,66 +58,9 @@ files: [ # #"update_option" : "", - # - # Start block only for XML - # - - # - # Defines whether to translate tags attributes. - # e.g. 0 or 1 (Default is 1) - # - # "translate_attributes" : 1, - - # - # Defines whether to translate texts placed inside the tags. - # e.g. 0 or 1 (Default is 1) - # - # "translate_content" : 1, - - # - # This is an array of strings, where each item is the XPaths to DOM element that should be imported - # e.g. ["/content/text", "/content/text[@value]"] - # - # "translatable_elements" : [], - - # - # Defines whether to split long texts into smaller text segments. - # e.g. 0 or 1 (Default is 1) - # - # "content_segmentation" : 1, - - # - # End block only for XML - # - - # - # Start .properties block - # - - # - # Defines whether single quote should be escaped by another single quote or backslash in exported translations. - # e.g. 0 or 1 or 2 or 3 (Default is 3) - # 0 - do not escape single quote; - # 1 - escape single quote by another single quote; - # 2 - escape single quote by backslash; - # 3 - escape single quote by another single quote only in strings containing variables ( {0} ). - # - # "escape_quotes" : 3, - - # - # End .properties block - # - - # - # Is first line contains header? - # e.g. true or false - # - #"first_line_contains_header" : true, - - # - # for spreadsheets - # e.g. "identifier,source_phrase,context,uk,ru,fr" - # - # "scheme" : "", + }, + { + "source" : "/resources/playstore/play_store_description.txt", + "translation": "/resources/playstore/play_store_description_i18n/play_store_description_%android_code%.txt" } ] \ No newline at end of file diff --git a/play_store_description.txt b/resources/playstore/play_store_description.txt similarity index 69% rename from play_store_description.txt rename to resources/playstore/play_store_description.txt index 45e63bf94..b40f0cde8 100644 --- a/play_store_description.txt +++ b/resources/playstore/play_store_description.txt @@ -1,8 +1,9 @@ -GnuCash is a mobile finance expense tracker application for Android. +GnuCash is a mobile finance manager for Android. -It is a companion application for GnuCash for the desktop and enables flexible tracking of expenses on-the-go which can be exported to QIF or OFX formats. +It is a companion application for GnuCash on the desktop, and enables flexible tracking of expenses on-the-go. +Recorded transactions can be exported back to the desktop GnuCash via several formats (CSV / QIF / OFX). -Some of feature highlights include: +Some of feature highlights include: • An easy-to-use interface. @@ -26,10 +27,3 @@ You can also import an existing account hierarchy from GnuCash desktop.
NOTE: that the app does not offer full compatibility with GnuCash for the desktop. You cannot synchronize between the desktop app and this one. But you can import your accounts and transactions from GnuCash XML files. -
-What does the app use the requested permissions for? -• VIBRATE: Used to provide haptic feedback when entering some inputs -• WAKE_LOCK: Used for keeping device active when exporting scheduled transactions in the background service -• RECEIVE_BOOT_COMPLETED: Used to restart service for scheduled transactions or exports after device is rebooted -• INTERNET/ACCESS_NETWORK_STATE: Used when exporting accounts/transactions to 3rd-party service like DropBox or ownCloud - diff --git a/resources/playstore/play_store_description_i18n/.keep b/resources/playstore/play_store_description_i18n/.keep new file mode 100644 index 000000000..e69de29bb