diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 06e1cab88..8900c416a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,6 +1,12 @@ GnuCash Android is built by people like you! Please [join us](https://github.com/codinguser/gnucash-android). -## Git and Pull requests +## Reporting Issues +* The GitHub issue tracker is used for collecting and managing bugs, feature requests and general development planning. +* When creating a request, first search to make sure a similar one doesn't already exist in the tracker. +* Be as specific as possible when providing descriptions of the problems encountered and what the expected behaviour should be. +* It is also possible to report issues by creating tickets directly from within the app (in the Help Center) + +## Code Contributions * Contributions are submitted, reviewed, and accepted using Github pull requests. [Read this article](https://help.github.com/articles/using-pull-requests) for some details. We use the _Fork and Pull_ model, as described there. * You can maintain your stable installation of GnuCash and test with another installation. The two instances of GnuCash Android will live side-by-side on your device and not affect each other. You can install the development version by executing `gradle installDD` inside the root project directory @@ -18,8 +24,8 @@ The two instances of GnuCash Android will live side-by-side on your device and n * Try to make clean commits that are easily readable (including descriptive commit messages!) * Test before you push make sure all test pass on your machine. * Unit tests can be run with `gradle test` - * UI tests can be run with `gradle cDDAT` -* Make small pull requests that are easy to review but make sure they do add value by themselves. + * UI tests can be run with `gradle spoonDD`. This will run the tests on all connected devices/emulators. +* Make small pull requests that are easy to review but which also add value. ## Coding style * Do write comments. You don't have to comment every line, but if you come up with something thats a bit complex/weird, just leave a comment. Bear in mind that you will probably leave the project at some point and that other people will read your code. Undocumented huge amounts of code are nearly worthless! diff --git a/.travis.yml b/.travis.yml index 1b304c2bd..864c60a37 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ android: - platform-tools - tools - tools #not a typo. Needed for SDK update - - build-tools-24.0.3 + - build-tools-25.0.0 # The SDK version used to compile your project - android-24 diff --git a/CHANGELOG.md b/CHANGELOG.md index af07ac57c..cd80eab7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ Change Log =============================================================================== +Version 2.2.0 *(2017-05-05)* +---------------------------- +* Feature #646: Option to select backup file using Storage Access Framework +* Feature #565: Regular automatic backups (even when not explicitly set by user) +* Feature #656: Added Bitcoin (BTC) currency support +* Feature #634: Added support for renaming books +* Fixed #672: Crash when exporting multi-currency transactions to Google Drive +* Fixed #654: Crash when editing account if its default transfer account no longer exists +* Fixed #625: Hourly backups were being executed on a monthly basis +* Fixed #607: Widgets stop functioning after switching books +* Fixed #641: Weekday is ignored for weekly scheduled actions +* Improved #635: Improved support for BYN currency +* Improved #661: Removed need for WRITE_EXTERNAL_STORAGE permission for Android 4.4 (KitKat) and above + * This release raises the minimum API level to 19 (KitKat) + Version 2.1.7 *(2017-04-18)* ---------------------------- * Properly handle crashes during migration of backup/export files to new location diff --git a/README.md b/README.md index 5f19c01cc..402714386 100644 --- a/README.md +++ b/README.md @@ -11,28 +11,28 @@ 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 2.3.3 Gingerbread (API level 10) and above. +The application supports Android 4.4 KitKat (API level 10) and above. Features include: * An easy-to-use interface. - * Chart of Accounts: A master account can have a hierarchy of detail accounts underneath it. + * **Chart of Accounts**: A master account can have a hierarchy of detail accounts underneath it. This allows similar account types (e.g. Cash, Bank, Stock) to be grouped into one master account (e.g. Assets). - * Split Transactions: A single transaction can be split into several pieces to record taxes, fees, and other compound entries. + * **Split Transactions**: A single transaction can be split into several pieces to record taxes, fees, and other compound entries. - * Double Entry: Every transaction must debit one account and credit another by an equal amount. + * **Double Entry**: Every transaction must debit one account and credit another by an equal amount. This ensures that the "books balance": that the difference between income and outflow exactly equals the sum of all assets, be they bank, cash, stock or other. - * Income/Expense Account Types (Categories): These serve not only to categorize your cash flow, but when used properly with the double-entry feature, these can provide an accurate Profit&Loss statement. + * **Income/Expense Account Types (Categories)**: These serve not only to categorize your cash flow, but when used properly with the double-entry feature, these can provide an accurate Profit&Loss statement. - * Scheduled Transactions: GnuCash has the ability to automatically create and enter transactions. + * **Scheduled Transactions**: GnuCash has the ability to automatically create and enter transactions. - * Export to GnuCash XML, QIF or OFX. Also, scheduled exports to 3rd-party sync services like DropBox and Google Drive + * **Export to GnuCash XML**, QIF or OFX. Also, scheduled exports to 3rd-party sync services like DropBox and Google Drive - * Reports: View summary of transactions (income and expenses) as pie/bar/line charts + * **Reports**: View summary of transactions (income and expenses) as pie/bar/line charts # Installation @@ -88,17 +88,17 @@ Google+ Community: https://plus.google.com/communities/104728406764752407046 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) + * One way is providing translations for locales which are not yet available, or improving translations. Please visit [CrowdIn](https://crowdin.com/project/gnucash-android) in order to update and create new translations -* You could as well contribute code, fixing bugs, new features or automated tests. Pull requests are always welcome. -Take a look at the [bug tracker](https://github.com/codinguser/gnucash-android/issues?state=open) -for ideas where to start. Also take a look at the [contribution guidlines](https://github.com/codinguser/gnucash-android/blob/master/.github/CONTRIBUTING.md) - 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 +# Licence 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 ed424bf23..eb78b8d4d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,9 +5,9 @@ apply plugin: 'io.fabric' apply plugin: 'android-apt' def versionMajor = 2 -def versionMinor = 1 -def versionPatch = 7 -def versionBuild = 0 +def versionMinor = 2 +def versionPatch = 0 +def versionBuild = 4 def buildTime() { def df = new SimpleDateFormat("yyyyMMdd HH:mm 'UTC'") @@ -22,11 +22,11 @@ def gitSha() { android { compileSdkVersion 24 - buildToolsVersion '24.0.3' + buildToolsVersion '25.0.0' defaultConfig { applicationId "org.gnucash.android" testApplicationId 'org.gnucash.android.test' - minSdkVersion 10 + minSdkVersion 19 targetSdkVersion 23 versionCode versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild versionName "${versionMajor}.${versionMinor}.${versionPatch}" @@ -85,6 +85,7 @@ android { } debug { debuggable true + minifyEnabled false // testCoverageEnabled true signingConfig signingConfigs.debug } @@ -167,6 +168,7 @@ afterEvaluate { spoon { debug = true grantAllPermissions = true + codeCoverage = true } initCrashlyticsPropertiesIfNeeded() } @@ -190,7 +192,7 @@ android.productFlavors.all { flavour -> } -def androidSupportVersion = "24.2.1" +def androidSupportVersion = "25.3.1" def androidEspressoVersion = "2.2.2" def androidSupportTestVersion = "0.5" @@ -209,23 +211,24 @@ dependencies { 'com.android.support:cardview-v7:' + androidSupportVersion, 'com.android.support:preference-v7:' + androidSupportVersion, 'com.android.support:recyclerview-v7:' + androidSupportVersion, - 'com.code-troopers.betterpickers:library:3.0.1', + '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.0', + '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:2.1.2', - 'com.facebook.stetho:stetho:1.4.1', 'com.android.support:multidex:1.0.1' ) - compile 'com.jakewharton:butterknife:8.4.0' - apt 'com.jakewharton:butterknife-compiler:8.4.0' + debugCompile 'com.facebook.stetho:stetho:1.4.2' + + compile 'com.jakewharton:butterknife:8.5.1' + apt 'com.jakewharton:butterknife-compiler:8.5.1' compile ('com.uservoice:uservoice-android-sdk:1.2.5') { exclude module: 'commons-logging' @@ -233,7 +236,7 @@ dependencies { exclude module: 'httpclient' } - compile('com.crashlytics.sdk.android:crashlytics:2.5.2@aar') { + compile('com.crashlytics.sdk.android:crashlytics:2.6.7@aar') { transitive = true; } 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 bfa7fb14f..c0c61e22f 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 @@ -175,7 +175,6 @@ public static void preventFirstRunDialogs(Context context) { } - @TargetApi(Build.VERSION_CODES.HONEYCOMB) public void testDisplayAccountsList(){ AccountsActivity.createDefaultAccounts("EUR", mAccountsActivity); mAccountsActivity.recreate(); @@ -462,7 +461,6 @@ public void testIntentAccountCreation(){ /** * Tests that the setup wizard is displayed on first run */ - @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Test public void shouldShowWizardOnFirstRun() throws Throwable { Editor editor = PreferenceManager.getDefaultSharedPreferences(mAccountsActivity) diff --git a/app/src/androidTest/java/org/gnucash/android/test/ui/ExportTransactionsTest.java b/app/src/androidTest/java/org/gnucash/android/test/ui/ExportTransactionsTest.java index cba7a8e40..5e7117c3a 100644 --- a/app/src/androidTest/java/org/gnucash/android/test/ui/ExportTransactionsTest.java +++ b/app/src/androidTest/java/org/gnucash/android/test/ui/ExportTransactionsTest.java @@ -22,8 +22,10 @@ import android.content.pm.PackageManager; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; import android.os.Build; import android.support.test.InstrumentationRegistry; +import android.support.test.espresso.Espresso; import android.support.test.espresso.contrib.DrawerActions; import android.support.test.espresso.matcher.ViewMatchers; import android.support.test.runner.AndroidJUnit4; @@ -53,6 +55,7 @@ import org.gnucash.android.model.Transaction; import org.gnucash.android.ui.account.AccountsActivity; import org.gnucash.android.ui.settings.PreferenceActivity; +import org.gnucash.android.util.BookUtils; import org.junit.After; import org.junit.Before; import org.junit.FixMethodOrder; @@ -139,139 +142,6 @@ public void setUp() throws Exception { mAccountsDbAdapter.addRecord(account, DatabaseAdapter.UpdateMethod.insert); } - - /** - * Tests the export of an OFX file with the transactions from the application. - * The exported file name contains a timestamp with minute precision. - * If this test fails, it may be due to the file being created and tested in different minutes of the clock - * Just try rerunning it again. - */ - @Test - public void testOfxExport(){ - SharedPreferences.Editor prefsEditor = PreferenceActivity.getActiveBookSharedPreferences() - .edit(); - prefsEditor.putBoolean(mAcccountsActivity.getString(R.string.key_use_double_entry), false) - .commit(); - testExport(ExportFormat.OFX); - prefsEditor.putBoolean(mAcccountsActivity.getString(R.string.key_use_double_entry), true) - .commit(); - } - - @Test - public void whenInSingleEntry_shouldHideXmlExportOption(){ - SharedPreferences.Editor prefsEditor = PreferenceActivity.getActiveBookSharedPreferences() - .edit(); - prefsEditor.putBoolean(mAcccountsActivity.getString(R.string.key_use_double_entry), false) - .commit(); - - DrawerActions.openDrawer(R.id.drawer_layout); - onView(withText(R.string.nav_menu_export)).perform(click()); - onView(withId(R.id.radio_xml_format)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))); - - prefsEditor.putBoolean(mAcccountsActivity.getString(R.string.key_use_double_entry), true) - .commit(); - } - - /** - * Test the export of transactions in the QIF format - */ - @Test - public void testQifExport(){ - testExport(ExportFormat.QIF); - } - - @Test - public void testXmlExport(){ - testExport(ExportFormat.XML); - } - - /** - * Generates export for the specified format and tests that the file actually is created - * @param format Export format to use - */ - public void testExport(ExportFormat format){ - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (mAcccountsActivity.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - mAcccountsActivity.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}, 0x23); - - onView(withId(AlertDialog.BUTTON_POSITIVE)).perform(click()); - } - } - - File folder = new File(Exporter.getExportFolderPath(BooksDbAdapter.getInstance().getActiveBookUID())); - folder.mkdirs(); - assertThat(folder).exists(); - - for (File file : folder.listFiles()) { - file.delete(); - } - - onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()); - onView(withText(R.string.nav_menu_export)).perform(click()); - - onView(withId(R.id.spinner_export_destination)).perform(click()); - String[] destinations = getActivity().getResources().getStringArray(R.array.export_destinations); - - onView(withText(destinations[0])).perform(click()); - onView(withText(format.name())).perform(click()); - - onView(withId(R.id.menu_save)).perform(click()); - - assertThat(folder.listFiles().length).isEqualTo(1); - File exportFile = folder.listFiles()[0]; - assertThat(exportFile.getName()).endsWith(format.getExtension()); - } - - @Test - public void testDeleteTransactionsAfterExport(){ - assertThat(mTransactionsDbAdapter.getRecordsCount()).isGreaterThan(0); - - SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(getActivity()).edit(); //PreferenceActivity.getActiveBookSharedPreferences(getActivity()).edit(); - editor.putBoolean(mAcccountsActivity.getString(R.string.key_delete_transactions_after_export), true); - editor.commit(); - - PreferenceActivity.getActiveBookSharedPreferences() - .edit() - .putBoolean(mAcccountsActivity.getString(R.string.key_use_double_entry), true) - .apply(); - - testExport(ExportFormat.XML); - - assertThat(mTransactionsDbAdapter.getRecordsCount()).isEqualTo(0); - List transactions = mTransactionsDbAdapter.getAllTransactions(); - - editor.putBoolean(mAcccountsActivity.getString(R.string.key_delete_transactions_after_export), false).commit(); - } - - /** - * Test creating a scheduled export - * Does not work on Travis yet - */ - @Test - public void testShouldCreateExportSchedule(){ - onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()); - onView(withText(R.string.nav_menu_export)).perform(click()); - - onView(withText(ExportFormat.XML.name())).perform(click()); - onView(withId(R.id.input_recurrence)).perform(click()); - - //switch on recurrence dialog - onView(allOf(isAssignableFrom(CompoundButton.class), isDisplayed(), isEnabled())).perform(click()); - onView(withText("OK")).perform(click()); - - onView(withId(R.id.menu_save)).perform(click()); - ScheduledActionDbAdapter scheduledactionDbAdapter = ScheduledActionDbAdapter.getInstance(); //new ScheduledActionDbAdapter(mDb, new RecurrenceDbAdapter(mDb)); - List scheduledActions = scheduledactionDbAdapter.getAllEnabledScheduledActions(); - assertThat(scheduledActions) - .hasSize(1) - .extracting("mActionType").contains(ScheduledAction.ActionType.BACKUP); - - ScheduledAction action = scheduledActions.get(0); - assertThat(action.getRecurrence().getPeriodType()).isEqualTo(PeriodType.WEEK); - assertThat(action.getEndTime()).isEqualTo(0); - } @Test public void testCreateBackup(){ diff --git a/app/src/androidTest/java/org/gnucash/android/test/ui/MultiBookTest.java b/app/src/androidTest/java/org/gnucash/android/test/ui/MultiBookTest.java index 02809a01a..b5aeef59c 100644 --- a/app/src/androidTest/java/org/gnucash/android/test/ui/MultiBookTest.java +++ b/app/src/androidTest/java/org/gnucash/android/test/ui/MultiBookTest.java @@ -21,6 +21,7 @@ import android.support.test.runner.AndroidJUnit4; import org.gnucash.android.R; +import org.gnucash.android.db.BookDbHelper; import org.gnucash.android.db.adapter.BooksDbAdapter; import org.gnucash.android.model.Book; import org.gnucash.android.test.ui.util.DisableAnimationsRule; @@ -37,10 +38,13 @@ import static android.support.test.espresso.action.ViewActions.swipeUp; import static android.support.test.espresso.assertion.ViewAssertions.matches; import static android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent; +import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant; import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withParent; import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.allOf; /** * Test support for multiple books in the application @@ -119,6 +123,28 @@ public void testCreateNewBook(){ assertThat(mBooksDbAdapter.getRecordsCount()).isEqualTo(bookCount+1); } + //TODO: Finish implementation of this test + public void testDeleteBook(){ + long bookCount = mBooksDbAdapter.getRecordsCount(); + + Book book = new Book(); + String displayName = "To Be Deleted"; + book.setDisplayName(displayName); + mBooksDbAdapter.addRecord(book); + + assertThat(mBooksDbAdapter.getRecordsCount()).isEqualTo(bookCount + 1); + + shouldOpenBookManager(); + + onView(allOf(withParent(hasDescendant(withText(displayName))), + withId(R.id.options_menu))).perform(click()); + + onView(withText(R.string.menu_delete)).perform(click()); + onView(withText(R.string.btn_delete_book)).perform(click()); + + assertThat(mBooksDbAdapter.getRecordsCount()).isEqualTo(bookCount); + } + private static void sleep(long millis){ try { Thread.sleep(millis); diff --git a/app/src/androidTest/java/org/gnucash/android/test/ui/OwnCloudExportTest.java b/app/src/androidTest/java/org/gnucash/android/test/ui/OwnCloudExportTest.java index b37d61171..bda6f16e5 100644 --- a/app/src/androidTest/java/org/gnucash/android/test/ui/OwnCloudExportTest.java +++ b/app/src/androidTest/java/org/gnucash/android/test/ui/OwnCloudExportTest.java @@ -22,6 +22,7 @@ import android.database.sqlite.SQLiteDatabase; import android.net.ConnectivityManager; import android.net.NetworkInfo; +import android.support.test.espresso.Espresso; import android.support.test.espresso.contrib.DrawerActions; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; @@ -183,20 +184,19 @@ public void OwnCloudCredentials() { assertTrue(mPrefs.getBoolean(mAccountsActivity.getString(R.string.key_owncloud_sync), false)); } - @Test + //// FIXME: 20.04.2017 This test now fails since introduction of SAF. public void OwnCloudExport() { Assume.assumeTrue(hasActiveInternetConnection()); mPrefs.edit().putBoolean(mAccountsActivity.getString(R.string.key_owncloud_sync), true).commit(); onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()); onView(withText(R.string.nav_menu_export)).perform(click()); + Espresso.closeSoftKeyboard(); + Espresso.pressBack(); //close the SAF file picker window onView(withId(R.id.spinner_export_destination)).perform(click()); String[] destinations = mAccountsActivity.getResources().getStringArray(R.array.export_destinations); onView(withText(destinations[3])).perform(click()); onView(withId(R.id.menu_save)).perform(click()); -// onView(withSpinnerText( -// mAccountsActivity.getResources().getStringArray(R.array.export_destinations)[3])) -// .perform(click()); assertToastDisplayed(String.format(mAccountsActivity.getString(R.string.toast_exported_to), "ownCloud -> " + OC_DIR)); } diff --git a/app/src/androidTest/java/org/gnucash/android/test/ui/PieChartReportTest.java b/app/src/androidTest/java/org/gnucash/android/test/ui/PieChartReportTest.java index b0b1a15c4..df2e367ad 100644 --- a/app/src/androidTest/java/org/gnucash/android/test/ui/PieChartReportTest.java +++ b/app/src/androidTest/java/org/gnucash/android/test/ui/PieChartReportTest.java @@ -45,6 +45,7 @@ import org.gnucash.android.ui.report.ReportsActivity; import org.gnucash.android.ui.report.piechart.PieChartFragment; import org.gnucash.android.ui.settings.PreferenceActivity; +import org.gnucash.android.util.BookUtils; import org.joda.time.LocalDateTime; import org.junit.After; import org.junit.AfterClass; @@ -118,7 +119,7 @@ public static void prepareTestCase() throws Exception { oldActiveBookUID = BooksDbAdapter.getInstance().getActiveBookUID(); testBookUID = GncXmlImporter.parse(context.getResources().openRawResource(R.raw.default_accounts)); - GnuCashApplication.loadBook(testBookUID); + BookUtils.loadBook(testBookUID); mTransactionsDbAdapter = TransactionsDbAdapter.getInstance(); mAccountsDbAdapter = AccountsDbAdapter.getInstance(); diff --git a/app/src/androidTest/java/org/gnucash/android/test/ui/TransactionsActivityTest.java b/app/src/androidTest/java/org/gnucash/android/test/ui/TransactionsActivityTest.java index aa30d51a4..085ea5c93 100644 --- a/app/src/androidTest/java/org/gnucash/android/test/ui/TransactionsActivityTest.java +++ b/app/src/androidTest/java/org/gnucash/android/test/ui/TransactionsActivityTest.java @@ -365,7 +365,7 @@ public void testAutoBalanceTransactions(){ mTransactionsDbAdapter.deleteAllRecords(); assertThat(mTransactionsDbAdapter.getRecordsCount()).isEqualTo(0); - String imbalanceAcctUID = mAccountsDbAdapter.getImbalanceAccountUID(Currency.getInstance(CURRENCY_CODE)); + String imbalanceAcctUID = mAccountsDbAdapter.getImbalanceAccountUID(Commodity.getInstance(CURRENCY_CODE)); assertThat(imbalanceAcctUID).isNull(); validateTransactionListDisplayed(); @@ -382,7 +382,7 @@ public void testAutoBalanceTransactions(){ assertThat(mTransactionsDbAdapter.getRecordsCount()).isEqualTo(1); Transaction transaction = mTransactionsDbAdapter.getAllTransactions().get(0); assertThat(transaction.getSplits()).hasSize(2); - imbalanceAcctUID = mAccountsDbAdapter.getImbalanceAccountUID(Currency.getInstance(CURRENCY_CODE)); + imbalanceAcctUID = mAccountsDbAdapter.getImbalanceAccountUID(Commodity.getInstance(CURRENCY_CODE)); assertThat(imbalanceAcctUID).isNotNull(); assertThat(imbalanceAcctUID).isNotEmpty(); assertThat(mAccountsDbAdapter.isHiddenAccount(imbalanceAcctUID)).isTrue(); //imbalance account should be hidden in single entry mode @@ -403,7 +403,7 @@ public void testSplitEditor(){ mTransactionsDbAdapter.deleteAllRecords(); //when we start there should be no imbalance account in the system - String imbalanceAcctUID = mAccountsDbAdapter.getImbalanceAccountUID(Currency.getInstance(CURRENCY_CODE)); + String imbalanceAcctUID = mAccountsDbAdapter.getImbalanceAccountUID(Commodity.getInstance(CURRENCY_CODE)); assertThat(imbalanceAcctUID).isNull(); validateTransactionListDisplayed(); @@ -431,7 +431,7 @@ public void testSplitEditor(){ Transaction transaction = transactions.get(0); assertThat(transaction.getSplits()).hasSize(3); //auto-balanced - imbalanceAcctUID = mAccountsDbAdapter.getImbalanceAccountUID(Currency.getInstance(CURRENCY_CODE)); + imbalanceAcctUID = mAccountsDbAdapter.getImbalanceAccountUID(Commodity.getInstance(CURRENCY_CODE)); assertThat(imbalanceAcctUID).isNotNull(); assertThat(imbalanceAcctUID).isNotEmpty(); assertThat(mAccountsDbAdapter.isHiddenAccount(imbalanceAcctUID)).isFalse(); diff --git a/app/src/debug/java/org/gnucash/android/app/StethoUtils.java b/app/src/debug/java/org/gnucash/android/app/StethoUtils.java new file mode 100644 index 000000000..f3d87f5eb --- /dev/null +++ b/app/src/debug/java/org/gnucash/android/app/StethoUtils.java @@ -0,0 +1,39 @@ +package org.gnucash.android.app; + +import android.app.Application; +import android.os.Build; + +import com.facebook.stetho.Stetho; + +import org.gnucash.android.BuildConfig; + +/** + * Utility class for initializing Stetho in debug builds + */ + +public class StethoUtils { + + /** + * Sets up Stetho to enable remote debugging from Chrome developer tools. + * + *

Among other things, allows access to the database and preferences. + * See http://facebook.github.io/stetho/#features

+ */ + public static void install(Application application){ + //don't initialize stetho during tests + if (!BuildConfig.DEBUG || isRoboUnitTest()) + return; + + Stetho.initialize(Stetho.newInitializerBuilder(application) + .enableWebKitInspector(Stetho.defaultInspectorModulesProvider(application)) + .build()); + } + + /** + * Returns {@code true} if the app is being run by robolectric + * @return {@code true} if in unit testing, {@code false} otherwise + */ + private static boolean isRoboUnitTest(){ + return "robolectric".equals(Build.FINGERPRINT); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 45e5a6aba..c8857485a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,18 +33,17 @@ android:protectionLevel="dangerous" /> + android:name="org.gnucash.android.permission.RECORD_TRANSACTION" /> - + android:name="org.gnucash.android.permission.CREATE_ACCOUNT" /> + + android:label="Schedule repeating transactions when device is rebooted"/> + android:label="Export and backup to 3rd party hosting services" /> true if double entry is enabled in the app settings, false otherwise. * If the value is not set, the default value can be specified in the parameters. @@ -367,7 +337,7 @@ public static void startScheduledActionExecutionService(Context context){ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_FIFTEEN_MINUTES, - AlarmManager.INTERVAL_HALF_DAY, pendingIntent); + AlarmManager.INTERVAL_HOUR, pendingIntent); context.startService(alarmIntent); //run the service the first time } @@ -389,18 +359,4 @@ private void setUpUserVoice() { UserVoice.init(config, this); } - /** - * Sets up Stetho to enable remote debugging from Chrome developer tools. - * - *

Among other things, allows access to the database and preferences. - * See http://facebook.github.io/stetho/#features

- */ - private void setUpRemoteDebuggingFromChrome() { - Stetho.Initializer initializer = - Stetho.newInitializerBuilder(this) - .enableWebKitInspector( - Stetho.defaultInspectorModulesProvider(this)) - .build(); - Stetho.initialize(initializer); - } } \ No newline at end of file diff --git a/app/src/main/java/org/gnucash/android/db/DatabaseSchema.java b/app/src/main/java/org/gnucash/android/db/DatabaseSchema.java index 01f07d2d9..f3dcbeaf8 100644 --- a/app/src/main/java/org/gnucash/android/db/DatabaseSchema.java +++ b/app/src/main/java/org/gnucash/android/db/DatabaseSchema.java @@ -39,7 +39,7 @@ public class DatabaseSchema { * Version number of database containing accounts and transactions info. * With any change to the database schema, this number must increase */ - public static final int DATABASE_VERSION = 14; + public static final int DATABASE_VERSION = 15; /** * Name of the database diff --git a/app/src/main/java/org/gnucash/android/db/MigrationHelper.java b/app/src/main/java/org/gnucash/android/db/MigrationHelper.java index f386fbdba..5acc1d74a 100644 --- a/app/src/main/java/org/gnucash/android/db/MigrationHelper.java +++ b/app/src/main/java/org/gnucash/android/db/MigrationHelper.java @@ -1468,6 +1468,18 @@ static int upgradeDbToVersion13(SQLiteDatabase db){ .putBoolean(keyUseCompactView, useCompactTrnView) .apply(); + rescheduleServiceAlarm(); + + + return oldVersion; + } + + /** + * Cancel the existing alarm for the scheduled service and restarts/reschedules the service + */ + private static void rescheduleServiceAlarm() { + Context context = GnuCashApplication.getAppContext(); + //cancel the existing pending intent so that the alarm can be rescheduled Intent alarmIntent = new Intent(context, ScheduledActionService.class); PendingIntent pendingIntent = PendingIntent.getService(context, 0, alarmIntent, PendingIntent.FLAG_NO_CREATE); @@ -1477,9 +1489,7 @@ static int upgradeDbToVersion13(SQLiteDatabase db){ pendingIntent.cancel(); } - GnuCashApplication.startScheduledActionExecutionService(GnuCashApplication.getAppContext()); - - return oldVersion; + GnuCashApplication.startScheduledActionExecutionService(context); } /** @@ -1509,6 +1519,9 @@ private static void moveDirectory(File srcDir, File dstDir) throws IOException { } } + if (srcDir.listFiles() == null) //nothing to see here, move along + return; + for (File src : srcDir.listFiles()){ if (src.isDirectory()){ File dst = new File(dstDir, src.getName()); @@ -1570,4 +1583,49 @@ public void run() { return 14; } + + /** + * Upgrades the database to version 14. + *

This migration makes the following changes to the database: + *

    + *
  • Fixes accounts referencing a default transfer account that no longer + * exists (see #654)
  • + *
+ *

+ * @param db SQLite database to be upgraded + * @return New database version, 14 if migration succeeds, 13 otherwise + */ + static int upgradeDbToVersion15(SQLiteDatabase db) { + Log.i(DatabaseHelper.LOG_TAG, "Upgrading database to version 15"); + int dbVersion = 14; + + db.beginTransaction(); + try { + ContentValues contentValues = new ContentValues(); + contentValues.putNull(AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID); + db.update( + AccountEntry.TABLE_NAME, + contentValues, + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID + + " NOT IN (SELECT " + AccountEntry.COLUMN_UID + + " FROM " + AccountEntry.TABLE_NAME + ")", + null); + db.setTransactionSuccessful(); + dbVersion = 15; + } finally { + db.endTransaction(); + } + + //remove previously saved export destination index because the number of destinations has changed + //an invalid value would lead to crash on start + Context context = GnuCashApplication.getAppContext(); + android.preference.PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .remove(context.getString(R.string.key_last_export_destination)) + .apply(); + + //the default interval has been changed from daily to hourly with this release. So reschedule alarm + rescheduleServiceAlarm(); + return dbVersion; + } } diff --git a/app/src/main/java/org/gnucash/android/db/adapter/AccountsDbAdapter.java b/app/src/main/java/org/gnucash/android/db/adapter/AccountsDbAdapter.java index 273dd908b..f6b85bd88 100644 --- a/app/src/main/java/org/gnucash/android/db/adapter/AccountsDbAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/adapter/AccountsDbAdapter.java @@ -43,7 +43,6 @@ import java.math.BigDecimal; import java.sql.Timestamp; import java.util.ArrayList; -import java.util.Currency; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -440,8 +439,8 @@ private Account buildSimpleAccountInstance(Cursor c) { account.setDescription(description == null ? "" : description); account.setParentUID(c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_PARENT_ACCOUNT_UID))); account.setAccountType(AccountType.valueOf(c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_TYPE)))); - Currency currency = Currency.getInstance(c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_CURRENCY))); - account.setCommodity(mCommoditiesDbAdapter.getCommodity(currency.getCurrencyCode())); + String currencyCode = c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_CURRENCY)); + account.setCommodity(mCommoditiesDbAdapter.getCommodity(currencyCode)); account.setPlaceHolderFlag(c.getInt(c.getColumnIndexOrThrow(AccountEntry.COLUMN_PLACEHOLDER)) == 1); account.setDefaultTransferAccountUID(c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID))); String color = c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_COLOR_CODE)); @@ -599,18 +598,6 @@ public String getOrCreateImbalanceAccountUID(Commodity commodity){ return uid; } - /** - * Returns the GUID of the imbalance account for the currency - *

This method will not create the imbalance account if it doesn't exist

- * @param currency Currency for the imbalance account - * @return GUID of the account or null if the account doesn't exist yet - * @see #getOrCreateImbalanceAccountUID(Commodity) - */ - public String getImbalanceAccountUID(Currency currency){ - String imbalanceAccountName = getImbalanceAccountName(currency); - return findAccountUidByFullName(imbalanceAccountName); - } - /** * Returns the GUID of the imbalance account for the commodity * @@ -1196,15 +1183,6 @@ public static String getImbalanceAccountPrefix() { return GnuCashApplication.getAppContext().getString(R.string.imbalance_account_name) + "-"; } - /** - * Returns the imbalance account where to store transactions which are not double entry - * @param currency Currency of the transaction - * @return Imbalance account name - */ - public static String getImbalanceAccountName(Currency currency){ - return getImbalanceAccountPrefix() + currency.getCurrencyCode(); - } - /** * Returns the imbalance account where to store transactions which are not double entry. * @@ -1263,25 +1241,27 @@ public static int getActiveAccountColorResource(@NonNull String accountUID) { } /** - * Returns the list of currencies in use in the database. - *

This is not the same as the list of all available commodities

- * @return List of currencies in use + * Returns the list of commodities in use in the database. + * + *

This is not the same as the list of all available commodities.

+ * + * @return List of commodities in use */ - public List getCurrenciesInUse(){ + public List getCommoditiesInUse() { Cursor cursor = mDb.query(true, AccountEntry.TABLE_NAME, new String[]{AccountEntry.COLUMN_CURRENCY}, null, null, null, null, null, null); - List currencyList = new ArrayList<>(); + List commodityList = new ArrayList<>(); try { while (cursor.moveToNext()) { - String currencyCode = cursor.getString(cursor.getColumnIndexOrThrow(AccountEntry.COLUMN_CURRENCY)); - currencyList.add(Currency.getInstance(currencyCode)); + String currencyCode = + cursor.getString(cursor.getColumnIndexOrThrow(AccountEntry.COLUMN_CURRENCY)); + commodityList.add(mCommoditiesDbAdapter.getCommodity(currencyCode)); } } finally { cursor.close(); } - return currencyList; + return commodityList; } - /** * Deletes all accounts, transactions (and their splits) from the database. * Basically empties all 3 tables, so use with care ;) diff --git a/app/src/main/java/org/gnucash/android/db/adapter/BooksDbAdapter.java b/app/src/main/java/org/gnucash/android/db/adapter/BooksDbAdapter.java index 65e06c950..6b1054468 100644 --- a/app/src/main/java/org/gnucash/android/db/adapter/BooksDbAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/adapter/BooksDbAdapter.java @@ -31,6 +31,9 @@ import org.gnucash.android.ui.settings.PreferenceActivity; import org.gnucash.android.util.TimestampHelper; +import java.util.ArrayList; +import java.util.List; + /** * Database adapter for creating/modifying book entries */ @@ -161,6 +164,17 @@ public boolean isActive(String bookUID){ } } + public @NonNull List getAllBookUIDs(){ + List bookUIDs = new ArrayList<>(); + try (Cursor cursor = mDb.query(true, mTableName, new String[]{BookEntry.COLUMN_UID}, + null, null, null, null, null, null)) { + while (cursor.moveToNext()) { + bookUIDs.add(cursor.getString(cursor.getColumnIndexOrThrow(BookEntry.COLUMN_UID))); + } + } + + return bookUIDs; + } /** * Return the name of the currently active book. 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 0f3648599..9b7b9fb3f 100644 --- a/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java +++ b/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java @@ -17,7 +17,6 @@ package org.gnucash.android.export; -import android.annotation.TargetApi; import android.app.Activity; import android.app.ProgressDialog; import android.content.Context; @@ -27,7 +26,6 @@ import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.AsyncTask; -import android.os.Build; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.content.FileProvider; @@ -79,6 +77,8 @@ import java.util.Date; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; /** * Asynchronous task for exporting transactions. @@ -117,7 +117,6 @@ public ExportAsyncTask(Context context, SQLiteDatabase db){ } @Override - @TargetApi(11) protected void onPreExecute() { super.onPreExecute(); if (mContext instanceof Activity) { @@ -125,10 +124,9 @@ protected void onPreExecute() { mProgressDialog.setTitle(R.string.title_progress_exporting_transactions); mProgressDialog.setIndeterminate(true); mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) { - mProgressDialog.setProgressNumberFormat(null); - mProgressDialog.setProgressPercentFormat(null); - } + mProgressDialog.setProgressNumberFormat(null); + mProgressDialog.setProgressPercentFormat(null); + mProgressDialog.show(); } } @@ -261,28 +259,77 @@ private void moveToTarget() throws Exporter.ExporterException { moveExportToSDCard(); break; + case URI: + moveExportToUri(); + break; + default: throw new Exporter.ExporterException(mExportParams, "Invalid target"); } } + /** + * Move the exported files to a specified URI. + * This URI could be a Storage Access Framework file + * @throws Exporter.ExporterException + */ + private void moveExportToUri() throws Exporter.ExporterException { + Uri exportUri = Uri.parse(mExportParams.getExportLocation()); + if (exportUri == null){ + Log.w(TAG, "No URI found for export destination"); + return; + } + + if (mExportedFiles.size() > 0){ + try { + OutputStream outputStream = mContext.getContentResolver().openOutputStream(exportUri); + ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream); + byte[] buffer = new byte[1024]; + for (String exportedFile : mExportedFiles) { + File file = new File(exportedFile); + FileInputStream fileInputStream = new FileInputStream(file); + zipOutputStream.putNextEntry(new ZipEntry(file.getName())); + + int length; + while ((length = fileInputStream.read(buffer)) > 0) { + zipOutputStream.write(buffer, 0, length); + } + zipOutputStream.closeEntry(); + fileInputStream.close(); + } + zipOutputStream.close(); + } catch (IOException ex) { + Log.e(TAG, "Error when zipping QIF files for export"); + ex.printStackTrace(); + Crashlytics.logException(ex); + } + } + } + + /** + * Move the exported files to a GnuCash folder on Google Drive + * @throws Exporter.ExporterException + * @deprecated Explicit Google Drive integration is deprecated, use Storage Access Framework. See {@link #moveExportToUri()} + */ + @Deprecated private void moveExportToGoogleDrive() throws Exporter.ExporterException { Log.i(TAG, "Moving exported file to Google Drive"); final GoogleApiClient googleApiClient = BackupPreferenceFragment.getGoogleApiClient(GnuCashApplication.getAppContext()); googleApiClient.blockingConnect(); - DriveApi.DriveContentsResult driveContentsResult = - Drive.DriveApi.newDriveContents(googleApiClient).await(1, TimeUnit.MINUTES); - if (!driveContentsResult.getStatus().isSuccess()) { - throw new Exporter.ExporterException(mExportParams, - "Error while trying to create new file contents"); - } - final DriveContents driveContents = driveContentsResult.getDriveContents(); - DriveFolder.DriveFileResult driveFileResult = null; + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); + String folderId = sharedPreferences.getString(mContext.getString(R.string.key_google_drive_app_folder_id), ""); + DriveFolder folder = DriveId.decodeFromString(folderId).asDriveFolder(); try { - // write content to DriveContents - OutputStream outputStream = driveContents.getOutputStream(); for (String exportedFilePath : mExportedFiles) { + DriveApi.DriveContentsResult driveContentsResult = + Drive.DriveApi.newDriveContents(googleApiClient).await(1, TimeUnit.MINUTES); + if (!driveContentsResult.getStatus().isSuccess()) { + throw new Exporter.ExporterException(mExportParams, + "Error while trying to create new file contents"); + } + final DriveContents driveContents = driveContentsResult.getDriveContents(); + OutputStream outputStream = driveContents.getOutputStream(); File exportedFile = new File(exportedFilePath); FileInputStream fileInputStream = new FileInputStream(exportedFile); byte[] buffer = new byte[1024]; @@ -299,25 +346,18 @@ private void moveExportToGoogleDrive() throws Exporter.ExporterException { .setTitle(exportedFile.getName()) .setMimeType(mExporter.getExportMimeType()) .build(); - - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); - String folderId = sharedPreferences.getString(mContext.getString(R.string.key_google_drive_app_folder_id), ""); - DriveFolder folder = Drive.DriveApi.getFolder(googleApiClient, DriveId.decodeFromString(folderId)); // create a file on root folder - driveFileResult = folder.createFile(googleApiClient, changeSet, driveContents) + DriveFolder.DriveFileResult driveFileResult = + folder.createFile(googleApiClient, changeSet, driveContents) .await(1, TimeUnit.MINUTES); + if (!driveFileResult.getStatus().isSuccess()) + throw new Exporter.ExporterException(mExportParams, "Error creating file in Google Drive"); + + Log.i(TAG, "Created file with id: " + driveFileResult.getDriveFile().getDriveId()); } } catch (IOException e) { throw new Exporter.ExporterException(mExportParams, e); } - - if (driveFileResult == null) - throw new Exporter.ExporterException(mExportParams, "No result received"); - - if (!driveFileResult.getStatus().isSuccess()) - throw new Exporter.ExporterException(mExportParams, "Error creating file in Google Drive"); - - Log.i(TAG, "Created file with id: " + driveFileResult.getDriveFile().getDriveId()); } /** @@ -398,7 +438,9 @@ private void moveExportToOwnCloud() throws Exporter.ExporterException { * Moves the exported files from the internal storage where they are generated to * external storage, which is accessible to the user. * @return The list of files moved to the SD card. + * @deprecated Use the Storage Access Framework to save to SD card. See {@link #moveExportToUri()} */ + @Deprecated private List moveExportToSDCard() throws Exporter.ExporterException { Log.i(TAG, "Moving exported file to external storage"); new File(Exporter.getExportFolderPath(mExporter.mBookUID)); @@ -465,9 +507,8 @@ private void shareFiles(List paths) { shareIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{defaultEmail}); SimpleDateFormat formatter = (SimpleDateFormat) SimpleDateFormat.getDateTimeInstance(); - ArrayList extraText = new ArrayList<>(); - extraText.add(mContext.getString(R.string.description_export_email) - + " " + formatter.format(new Date(System.currentTimeMillis()))); + String extraText = mContext.getString(R.string.description_export_email) + + " " + formatter.format(new Date(System.currentTimeMillis())); shareIntent.putExtra(Intent.EXTRA_TEXT, extraText); if (mContext instanceof Activity) { @@ -521,6 +562,28 @@ public void moveFile(String src, String dst) throws IOException { srcFile.delete(); } + /** + * Move file from a location on disk to an outputstream. + * The outputstream could be for a URI in the Storage Access Framework + * @param src Input file (usually newly exported file) + * @param outputStream Output stream to write to + * @throws IOException if error occurred while moving the file + */ + public void moveFile(@NonNull String src, @NonNull OutputStream outputStream) throws IOException { + byte[] buffer = new byte[1024]; + int read; + try (FileInputStream inputStream = new FileInputStream(src)) { + while ((read = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, read); + } + } finally { + outputStream.flush(); + outputStream.close(); + } + Log.i(TAG, "Deleting temp export file: " + src); + new File(src).delete(); + } + private void reportSuccess() { String targetLocation; switch (mExportParams.getExportTarget()){ 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 d751cac69..90c4e4221 100644 --- a/app/src/main/java/org/gnucash/android/export/ExportParams.java +++ b/app/src/main/java/org/gnucash/android/export/ExportParams.java @@ -16,6 +16,8 @@ package org.gnucash.android.export; +import android.net.Uri; + import org.gnucash.android.ui.export.ExportFormFragment; import org.gnucash.android.util.TimestampHelper; @@ -35,7 +37,18 @@ public class ExportParams { * Options for the destination of the exported transctions file. * It could be stored on the {@link #SD_CARD} or exported through another program via {@link #SHARING} */ - public enum ExportTarget {SD_CARD, SHARING, DROPBOX, GOOGLE_DRIVE, OWNCLOUD } + public enum ExportTarget {SD_CARD("SD Card"), SHARING("External Service"), + DROPBOX("Dropbox"), GOOGLE_DRIVE("Google Drive"), OWNCLOUD("ownCloud"), + URI("Sync Service"); + private String mDescription; + ExportTarget(String description){ + mDescription = description; + } + + public String getDescription(){ + return mDescription; + } + } /** * Format to use for the exported transactions @@ -59,6 +72,12 @@ public enum ExportTarget {SD_CARD, SHARING, DROPBOX, GOOGLE_DRIVE, OWNCLOUD } */ private ExportTarget mExportTarget = ExportTarget.SHARING; + /** + * Location to save the file name being exported. + * This is typically a Uri and used for {@link ExportTarget#URI} target + */ + private String mExportLocation; + /** * Creates a new set of paramters and specifies the export format * @param format Format to use when exporting the transactions @@ -132,10 +151,28 @@ public void setExportTarget(ExportTarget mExportTarget) { this.mExportTarget = mExportTarget; } + /** + * Return the location where the file should be exported to. + * When used with {@link ExportTarget#URI}, the returned value will be a URI which can be parsed + * with {@link Uri#parse(String)} + * @return String representing export file destination. + */ + public String getExportLocation(){ + return mExportLocation; + } + + /** + * Set the location where to export the file + * @param exportLocation Destination of the export + */ + public void setExportLocation(String exportLocation){ + mExportLocation = exportLocation; + } + @Override public String toString() { return "Export all transactions created since " + TimestampHelper.getUtcStringFromTimestamp(mExportStartTime) + " UTC" - + " as "+ mExportFormat.name() + " to " + mExportTarget.name(); + + " as "+ mExportFormat.name() + " to " + mExportTarget.name() + (mExportLocation != null ? " (" + mExportLocation +")" : ""); } /** @@ -146,9 +183,11 @@ public String toString() { public String toCsv(){ String separator = ";"; - return mExportFormat.name() + separator + mExportTarget.name() + separator + return mExportFormat.name() + separator + + mExportTarget.name() + separator + TimestampHelper.getUtcStringFromTimestamp(mExportStartTime) + separator - + Boolean.toString(mDeleteTransactionsAfterExport); + + Boolean.toString(mDeleteTransactionsAfterExport) + separator + + (mExportLocation != null ? mExportLocation : ""); } /** @@ -162,7 +201,9 @@ public static ExportParams parseCsv(String csvParams){ params.setExportTarget(ExportTarget.valueOf(tokens[1])); params.setExportStartTime(TimestampHelper.getTimestampFromUtcString(tokens[2])); params.setDeleteTransactionsAfterExport(Boolean.parseBoolean(tokens[3])); - + if (tokens.length == 5){ + params.setExportLocation(tokens[4]); + } return params; } } 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 0e64df8db..f18445d46 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 @@ -24,6 +24,7 @@ import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.export.ExportParams; import org.gnucash.android.export.Exporter; +import org.gnucash.android.model.Commodity; import org.gnucash.android.util.PreferencesHelper; import org.gnucash.android.util.TimestampHelper; @@ -159,7 +160,7 @@ public List generateExport() throws ExporterException { if (decimalImbalance.compareTo(BigDecimal.ZERO) != 0) { writer.append(QifHelper.SPLIT_CATEGORY_PREFIX) .append(AccountsDbAdapter.getImbalanceAccountName( - Currency.getInstance(cursor.getString(cursor.getColumnIndexOrThrow("acct1_currency"))) + Commodity.getInstance(cursor.getString(cursor.getColumnIndexOrThrow("acct1_currency"))) )) .append(newLine); writer.append(QifHelper.SPLIT_AMOUNT_PREFIX) 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 c96420275..a8f5d3460 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 @@ -19,10 +19,12 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; import android.util.Log; import com.crashlytics.android.Crashlytics; +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; @@ -43,6 +45,7 @@ import org.gnucash.android.model.Recurrence; import org.gnucash.android.model.ScheduledAction; import org.gnucash.android.model.TransactionType; +import org.gnucash.android.util.BookUtils; import org.gnucash.android.util.TimestampHelper; import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlSerializer; @@ -50,13 +53,13 @@ import java.io.BufferedOutputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.math.BigDecimal; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collection; -import java.util.Currency; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -617,15 +620,15 @@ private void serializeDate(XmlSerializer xmlSerializer, String tag, long timeMil xmlSerializer.endTag(null, tag); } - private void exportCommodities(XmlSerializer xmlSerializer, List currencies) throws IOException { - for (Currency currency : currencies) { + private void exportCommodities(XmlSerializer xmlSerializer, List commodities) throws IOException { + for (Commodity commodity : commodities) { xmlSerializer.startTag(null, GncXmlHelper.TAG_COMMODITY); xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_VERSION, GncXmlHelper.BOOK_VERSION); xmlSerializer.startTag(null, GncXmlHelper.TAG_COMMODITY_SPACE); xmlSerializer.text("ISO4217"); xmlSerializer.endTag(null, GncXmlHelper.TAG_COMMODITY_SPACE); xmlSerializer.startTag(null, GncXmlHelper.TAG_COMMODITY_ID); - xmlSerializer.text(currency.getCurrencyCode()); + xmlSerializer.text(commodity.getCurrencyCode()); xmlSerializer.endTag(null, GncXmlHelper.TAG_COMMODITY_ID); xmlSerializer.endTag(null, GncXmlHelper.TAG_COMMODITY); } @@ -836,15 +839,15 @@ public void generateExport(Writer writer) throws ExporterException { xmlSerializer.text(BaseModel.generateUID()); xmlSerializer.endTag(null, GncXmlHelper.TAG_BOOK_ID); //commodity count - List currencies = mAccountsDbAdapter.getCurrenciesInUse(); - for (int i = 0; i < currencies.size(); i++) { - if (currencies.get(i).getCurrencyCode().equals("XXX")) { - currencies.remove(i); + List commodities = mAccountsDbAdapter.getCommoditiesInUse(); + for (int i = 0; i < commodities.size(); i++) { + if (commodities.get(i).getCurrencyCode().equals("XXX")) { + commodities.remove(i); } } xmlSerializer.startTag(null, GncXmlHelper.TAG_COUNT_DATA); xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_CD_TYPE, "commodity"); - xmlSerializer.text(currencies.size() + ""); + xmlSerializer.text(commodities.size() + ""); xmlSerializer.endTag(null, GncXmlHelper.TAG_COUNT_DATA); //account count xmlSerializer.startTag(null, GncXmlHelper.TAG_COUNT_DATA); @@ -865,7 +868,7 @@ public void generateExport(Writer writer) throws ExporterException { xmlSerializer.endTag(null, GncXmlHelper.TAG_COUNT_DATA); } // export the commodities used in the DB - exportCommodities(xmlSerializer, currencies); + exportCommodities(xmlSerializer, commodities); // prices if (priceCount > 0) { exportPrices(xmlSerializer); @@ -910,10 +913,26 @@ public String getExportMimeType(){ * @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 bookUID = BooksDbAdapter.getInstance().getActiveBookUID(); - FileOutputStream fileOutputStream = new FileOutputStream(getBackupFilePath(bookUID)); - BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream); + 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); diff --git a/app/src/main/java/org/gnucash/android/importer/ImportAsyncTask.java b/app/src/main/java/org/gnucash/android/importer/ImportAsyncTask.java index b8fdd6022..7b7c4999b 100644 --- a/app/src/main/java/org/gnucash/android/importer/ImportAsyncTask.java +++ b/app/src/main/java/org/gnucash/android/importer/ImportAsyncTask.java @@ -31,10 +31,10 @@ 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.ui.util.TaskDelegate; +import org.gnucash.android.util.BookUtils; import java.io.InputStream; @@ -58,7 +58,6 @@ public ImportAsyncTask(Activity context, TaskDelegate delegate){ this.mDelegate = delegate; } - @TargetApi(11) @Override protected void onPreExecute() { super.onPreExecute(); @@ -67,11 +66,11 @@ protected void onPreExecute() { mProgressDialog.setIndeterminate(true); mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); mProgressDialog.show(); - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB){ - //these methods must be called after progressDialog.show() - mProgressDialog.setProgressNumberFormat(null); - mProgressDialog.setProgressPercentFormat(null); - } + + //these methods must be called after progressDialog.show() + mProgressDialog.setProgressNumberFormat(null); + mProgressDialog.setProgressPercentFormat(null); + } @@ -138,7 +137,7 @@ protected void onPostExecute(Boolean importSuccess) { Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show(); if (mImportedBookUID != null) - GnuCashApplication.loadBook(mImportedBookUID); + BookUtils.loadBook(mImportedBookUID); if (mDelegate != null) mDelegate.onTaskComplete(); diff --git a/app/src/main/java/org/gnucash/android/model/Budget.java b/app/src/main/java/org/gnucash/android/model/Budget.java index add151287..3e1bdc27c 100644 --- a/app/src/main/java/org/gnucash/android/model/Budget.java +++ b/app/src/main/java/org/gnucash/android/model/Budget.java @@ -204,6 +204,9 @@ public long getStartofCurrentPeriod(){ LocalDateTime localDate = new LocalDateTime(); int interval = mRecurrence.getMultiplier(); switch (mRecurrence.getPeriodType()){ + case HOUR: + localDate = localDate.millisOfDay().withMinimumValue().plusHours(interval); + break; case DAY: localDate = localDate.millisOfDay().withMinimumValue().plusDays(interval); break; @@ -228,6 +231,9 @@ public long getEndOfCurrentPeriod(){ LocalDateTime localDate = new LocalDateTime(); int interval = mRecurrence.getMultiplier(); switch (mRecurrence.getPeriodType()){ + case HOUR: + localDate = localDate.millisOfDay().withMaximumValue().plusHours(interval); + break; case DAY: localDate = localDate.millisOfDay().withMaximumValue().plusDays(interval); break; @@ -248,6 +254,9 @@ public long getStartOfPeriod(int periodNum){ LocalDateTime localDate = new LocalDateTime(mRecurrence.getPeriodStart().getTime()); int interval = mRecurrence.getMultiplier() * periodNum; switch (mRecurrence.getPeriodType()){ + case HOUR: + localDate = localDate.millisOfDay().withMinimumValue().plusHours(interval); + break; case DAY: localDate = localDate.millisOfDay().withMinimumValue().plusDays(interval); break; @@ -273,6 +282,9 @@ public long getEndOfPeriod(int periodNum){ LocalDateTime localDate = new LocalDateTime(); int interval = mRecurrence.getMultiplier() * periodNum; switch (mRecurrence.getPeriodType()){ + case HOUR: + localDate = localDate.plusHours(interval); + break; case DAY: localDate = localDate.millisOfDay().withMaximumValue().plusDays(interval); break; diff --git a/app/src/main/java/org/gnucash/android/model/PeriodType.java b/app/src/main/java/org/gnucash/android/model/PeriodType.java index 882620be3..c7d11c478 100644 --- a/app/src/main/java/org/gnucash/android/model/PeriodType.java +++ b/app/src/main/java/org/gnucash/android/model/PeriodType.java @@ -26,7 +26,7 @@ * @see org.gnucash.android.model.ScheduledAction */ public enum PeriodType { - DAY, WEEK, MONTH, YEAR; // TODO: 22.10.2015 add support for hourly + HOUR, DAY, WEEK, MONTH, YEAR; /** @@ -36,6 +36,8 @@ public enum PeriodType { */ public String getFrequencyDescription() { switch (this) { + case HOUR: + return "HOURLY"; case DAY: return "DAILY"; case WEEK: @@ -57,17 +59,10 @@ public String getFrequencyDescription() { */ public String getByParts(long startTime){ String partString = ""; - switch (this){ - case DAY: - break; - case WEEK: - String dayOfWeek = new SimpleDateFormat("E", Locale.US).format(new Date(startTime)); - //our parser only supports two-letter day names - partString = "BYDAY=" + dayOfWeek.substring(0, dayOfWeek.length()-1).toUpperCase(); - case MONTH: - break; - case YEAR: - break; + if (this == WEEK){ + String dayOfWeek = new SimpleDateFormat("E", Locale.US).format(new Date(startTime)); + //our parser only supports two-letter day names + partString = "BYDAY=" + dayOfWeek.substring(0, dayOfWeek.length()-1).toUpperCase(); } return partString; } diff --git a/app/src/main/java/org/gnucash/android/model/Recurrence.java b/app/src/main/java/org/gnucash/android/model/Recurrence.java index 9a5f94e87..769ea04b4 100644 --- a/app/src/main/java/org/gnucash/android/model/Recurrence.java +++ b/app/src/main/java/org/gnucash/android/model/Recurrence.java @@ -24,6 +24,7 @@ import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.ui.util.RecurrenceParser; import org.joda.time.Days; +import org.joda.time.Hours; import org.joda.time.LocalDate; import org.joda.time.LocalDateTime; import org.joda.time.Months; @@ -114,6 +115,9 @@ public void setPeriodStart(Timestamp periodStart) { public long getPeriod(){ long baseMillis = 0; switch (mPeriodType){ + case HOUR: + baseMillis = RecurrenceParser.HOUR_MILLIS; + break; case DAY: baseMillis = RecurrenceParser.DAY_MILLIS; break; @@ -188,12 +192,15 @@ public String getRuleString(){ * @return Number of days left in period */ public int getDaysLeftInCurrentPeriod(){ - LocalDate startDate = new LocalDate(System.currentTimeMillis()); + LocalDateTime startDate = new LocalDateTime(System.currentTimeMillis()); int interval = mMultiplier - 1; - LocalDate endDate = null; + LocalDateTime endDate = null; switch (mPeriodType){ + case HOUR: + endDate = new LocalDateTime(System.currentTimeMillis()).plusHours(interval); + break; case DAY: - endDate = new LocalDate(System.currentTimeMillis()).plusDays(interval); + endDate = new LocalDateTime(System.currentTimeMillis()).plusDays(interval); break; case WEEK: endDate = startDate.dayOfWeek().withMaximumValue().plusWeeks(interval); @@ -216,14 +223,17 @@ public int getDaysLeftInCurrentPeriod(){ * @return Number of periods in this recurrence */ public int getNumberOfPeriods(int numberOfPeriods) { - LocalDate startDate = new LocalDate(mPeriodStart.getTime()); - LocalDate endDate; + LocalDateTime startDate = new LocalDateTime(mPeriodStart.getTime()); + LocalDateTime endDate; int interval = mMultiplier; //// TODO: 15.08.2016 Why do we add the number of periods. maybe rename method or param switch (mPeriodType){ - + case HOUR: //this is not the droid you are looking for + endDate = startDate.plusHours(numberOfPeriods); + return Hours.hoursBetween(startDate, endDate).getHours(); case DAY: - return 1; + endDate = startDate.plusDays(numberOfPeriods); + return Days.daysBetween(startDate, endDate).getDays(); case WEEK: endDate = startDate.dayOfWeek().withMaximumValue().plusWeeks(numberOfPeriods); return Weeks.weeksBetween(startDate, endDate).getWeeks() / interval; @@ -245,7 +255,9 @@ public int getNumberOfPeriods(int numberOfPeriods) { public String getTextOfCurrentPeriod(int periodNum){ LocalDate startDate = new LocalDate(mPeriodStart.getTime()); switch (mPeriodType){ - + case HOUR: + //nothing to see here. Just use default period designation + break; case DAY: return startDate.dayOfWeek().getAsText(); case WEEK: @@ -294,6 +306,9 @@ public int getCount(){ int multiple = mMultiplier; ReadablePeriod jodaPeriod; switch (mPeriodType){ + case HOUR: + jodaPeriod = Hours.hours(multiple); + break; case DAY: jodaPeriod = Days.days(multiple); break; @@ -347,6 +362,9 @@ public void setPeriodEnd(int numberOfOccurences){ LocalDateTime endDate; int occurrenceDuration = numberOfOccurences * mMultiplier; switch (mPeriodType){ + case HOUR: + endDate = localDate.plusHours(occurrenceDuration); + break; case DAY: endDate = localDate.plusDays(occurrenceDuration); break; @@ -407,8 +425,9 @@ public void setMultiplier(int multiplier){ */ private String getFrequencyRepeatString(){ Resources res = GnuCashApplication.getAppContext().getResources(); - //todo: take multiplier into account here switch (mPeriodType) { + case HOUR: + return res.getQuantityString(R.plurals.label_every_x_hours, mMultiplier, mMultiplier); case DAY: return res.getQuantityString(R.plurals.label_every_x_days, mMultiplier, mMultiplier); case WEEK: @@ -457,6 +476,14 @@ public static Recurrence fromLegacyPeriod(long period) { return recurrence; } + result = (int) (period/RecurrenceParser.HOUR_MILLIS); + if (result > 0) { + Recurrence recurrence = new Recurrence(PeriodType.HOUR); + recurrence.setMultiplier(result); + return recurrence; + } + + return new Recurrence(PeriodType.DAY); } } diff --git a/app/src/main/java/org/gnucash/android/model/ScheduledAction.java b/app/src/main/java/org/gnucash/android/model/ScheduledAction.java index b8f49219a..92fdac1a6 100644 --- a/app/src/main/java/org/gnucash/android/model/ScheduledAction.java +++ b/app/src/main/java/org/gnucash/android/model/ScheduledAction.java @@ -24,6 +24,7 @@ import java.sql.Timestamp; import java.text.SimpleDateFormat; +import java.util.Calendar; import java.util.Date; import java.util.Locale; import java.util.TimeZone; @@ -152,6 +153,9 @@ public long getTimeOfLastSchedule(){ int factor = (mExecutionCount-1) * multiplier; switch (mRecurrence.getPeriodType()){ + case HOUR: + startTime = startTime.plusHours(factor); + break; case DAY: startTime = startTime.plusDays(factor); break; @@ -214,11 +218,14 @@ private long computeNextScheduledExecutionTimeStartingAt(long startTime) { int multiplier = mRecurrence.getMultiplier(); LocalDateTime nextScheduledExecution = LocalDateTime.fromDateFields(new Date(startTime)); switch (mRecurrence.getPeriodType()) { + case HOUR: + nextScheduledExecution = nextScheduledExecution.plusHours(multiplier); + break; case DAY: nextScheduledExecution = nextScheduledExecution.plusDays(multiplier); break; case WEEK: - nextScheduledExecution = nextScheduledExecution.plusWeeks(multiplier); + nextScheduledExecution = computeNextWeeklyExecutionStartingAt(nextScheduledExecution); break; case MONTH: nextScheduledExecution = nextScheduledExecution.plusMonths(multiplier); @@ -230,6 +237,50 @@ private long computeNextScheduledExecutionTimeStartingAt(long startTime) { return nextScheduledExecution.toDate().getTime(); } + /** + * Computes the next time that this weekly scheduled action is supposed to be + * executed starting at startTime. + * + * If no weekdays have been set (GnuCash desktop allows it), it will return a + * date in the future to ensure ScheduledActionService doesn't execute it. + * + * @param startTime LocalDateTime to use as start to compute the next schedule. + * + * @return Next run time as a LocalDateTime. A date in the future, if no weekdays + * were set in the Recurrence. + */ + @NonNull + private LocalDateTime computeNextWeeklyExecutionStartingAt(LocalDateTime startTime) { + if (mRecurrence.getByDays().isEmpty()) + return LocalDateTime.now().plusDays(1); // Just a date in the future + + // Look into the week of startTime for another scheduled weekday + for (int weekDay : mRecurrence.getByDays() ) { + int jodaWeekDay = convertCalendarWeekdayToJoda(weekDay); + LocalDateTime candidateNextDueTime = startTime.withDayOfWeek(jodaWeekDay); + if (candidateNextDueTime.isAfter(startTime)) + return candidateNextDueTime; + } + + // Return the first scheduled weekday from the next due week + int firstScheduledWeekday = convertCalendarWeekdayToJoda(mRecurrence.getByDays().get(0)); + return startTime.plusWeeks(mRecurrence.getMultiplier()) + .withDayOfWeek(firstScheduledWeekday); + } + + /** + * Converts a java.util.Calendar weekday constant to the + * org.joda.time.DateTimeConstants equivalent. + * + * @param calendarWeekday weekday constant from java.util.Calendar + * @return weekday constant equivalent from org.joda.time.DateTimeConstants + */ + private int convertCalendarWeekdayToJoda(int calendarWeekday) { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.DAY_OF_WEEK, calendarWeekday); + return LocalDateTime.fromCalendarFields(cal).getDayOfWeek(); + } + /** * Set time of last execution of the scheduled action * @param nextRun Timestamp in milliseconds since Epoch 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 e792fc223..5c2470371 100644 --- a/app/src/main/java/org/gnucash/android/service/ScheduledActionService.java +++ b/app/src/main/java/org/gnucash/android/service/ScheduledActionService.java @@ -18,8 +18,10 @@ 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.VisibleForTesting; import android.util.Log; @@ -36,16 +38,23 @@ 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. @@ -70,10 +79,11 @@ protected void onHandleIntent(Intent intent) { 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) { + 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); @@ -171,16 +181,23 @@ private static int executeBackup(ScheduledAction scheduledAction, SQLiteDatabase ExportParams params = ExportParams.parseCsv(scheduledAction.getTag()); // HACK: the tag isn't updated with the new date, so set the correct by hand params.setExportStartTime(new Timestamp(scheduledAction.getLastRunTime())); + Boolean result = false; try { //wait for async task to finish before we proceed (we are holding a wake lock) - new ExportAsyncTask(GnuCashApplication.getAppContext(), db).execute(params).get(); + result = new ExportAsyncTask(GnuCashApplication.getAppContext(), db).execute(params).get(); } catch (InterruptedException | ExecutionException e) { 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"); return 1; } + /** + * Check if a scheduled action is due for execution + * @param scheduledAction Scheduled action + * @return {@code true} if execution is due, {@code false} otherwise + */ private static boolean shouldExecuteScheduledBackup(ScheduledAction scheduledAction) { long now = System.currentTimeMillis(); long endTime = scheduledAction.getEndTime(); @@ -244,4 +261,34 @@ 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 e5a3bd660..0dd960656 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 @@ -127,7 +127,6 @@ public class AccountsActivity extends BaseDrawerActivity implements OnAccountCli * Key for putting argument for tab into bundle arguments */ public static final String EXTRA_TAB_INDEX = "org.gnucash.android.extra.TAB_INDEX"; - public static final int REQUEST_PERMISSION_WRITE_SD_CARD = 0xAB; /** * Map containing fragments for the different tabs @@ -300,50 +299,6 @@ private void handleOpenFileIntent(Intent intent) { } } - /** - * Get permission for WRITING SD card - */ - @TargetApi(23) - private void getSDWritePermission(){ - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { -// if (shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)){ - Snackbar.make(mCoordinatorLayout, - "GnuCash requires permission to access the SD card for backup and restore", - Snackbar.LENGTH_INDEFINITE).setAction("GRANT", - new View.OnClickListener() { - @Override - public void onClick(View view) { - requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_PERMISSION_WRITE_SD_CARD); - } - }) - .setActionTextColor(getResources().getColor(R.color.theme_accent)) - .show(); -// } - } - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - switch (requestCode){ - case REQUEST_PERMISSION_WRITE_SD_CARD:{ - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - - //TODO: permission was granted, yay! do the - // calendar task you need to do. - - } else { - - // TODO: permission denied, boo! Disable the - // functionality that depends on this permission. - } - } return; - } - } - @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); @@ -388,8 +343,6 @@ private void init() { return; } - getSDWritePermission(); - if (hasNewFeatures()){ showWhatsNewDialog(this); } 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 5a449f461..d099027d2 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 @@ -247,7 +247,9 @@ public void tryDeleteAccount(long rowId) { if (acc.getTransactionCount() > 0 || mAccountsDbAdapter.getSubAccountCount(acc.getUID()) > 0) { showConfirmationDialog(rowId); } else { - mAccountsDbAdapter.deleteRecord(rowId); + // Avoid calling AccountsDbAdapter.deleteRecord(long). See #654 + String uid = mAccountsDbAdapter.getUID(rowId); + mAccountsDbAdapter.deleteRecord(uid); refresh(); } } @@ -494,12 +496,10 @@ public void onBindViewHolderCursor(final AccountViewHolder holder, final Cursor holder.description.setVisibility(View.GONE); // add a summary of transactions to the account view - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + // Make sure the balance task is truly multithread - new AccountBalanceTask(holder.accountBalance).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, accountUID); - } else { - new AccountBalanceTask(holder.accountBalance).execute(accountUID); - } + new AccountBalanceTask(holder.accountBalance).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, accountUID); + String accountColor = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.AccountEntry.COLUMN_COLOR_CODE)); int colorCode = accountColor == null ? Color.TRANSPARENT : Color.parseColor(accountColor); holder.colorStripView.setBackgroundColor(colorCode); diff --git a/app/src/main/java/org/gnucash/android/ui/budget/BudgetAmountEditorFragment.java b/app/src/main/java/org/gnucash/android/ui/budget/BudgetAmountEditorFragment.java index a87c40d32..fa6e6dff8 100644 --- a/app/src/main/java/org/gnucash/android/ui/budget/BudgetAmountEditorFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/budget/BudgetAmountEditorFragment.java @@ -49,7 +49,6 @@ import java.math.BigDecimal; import java.util.ArrayList; -import java.util.Currency; import java.util.List; import butterknife.BindView; @@ -250,8 +249,8 @@ public BudgetAmountViewHolder(View view){ @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { String currencyCode = mAccountsDbAdapter.getCurrencyCode(mAccountsDbAdapter.getUID(id)); - Currency currency = Currency.getInstance(currencyCode); - currencySymbolTextView.setText(currency.getSymbol()); + Commodity commodity = Commodity.getInstance(currencyCode); + currencySymbolTextView.setText(commodity.getSymbol()); } @Override diff --git a/app/src/main/java/org/gnucash/android/ui/common/BaseDrawerActivity.java b/app/src/main/java/org/gnucash/android/ui/common/BaseDrawerActivity.java index 4a5b7eb36..83c409d95 100644 --- a/app/src/main/java/org/gnucash/android/ui/common/BaseDrawerActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/common/BaseDrawerActivity.java @@ -22,7 +22,6 @@ import android.database.Cursor; import android.graphics.Color; import android.graphics.PorterDuff; -import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.LayoutRes; @@ -50,6 +49,7 @@ import org.gnucash.android.ui.report.ReportsActivity; import org.gnucash.android.ui.settings.PreferenceActivity; import org.gnucash.android.ui.transaction.ScheduledActionsActivity; +import org.gnucash.android.util.BookUtils; import butterknife.BindView; import butterknife.ButterKnife; @@ -105,7 +105,7 @@ protected void onCreate(Bundle savedInstanceState) { //if a parameter was passed to open an account within a specific book, then switch String bookUID = getIntent().getStringExtra(UxArgument.BOOK_UID); if (bookUID != null && !bookUID.equals(BooksDbAdapter.getInstance().getActiveBookUID())){ - GnuCashApplication.activateBook(bookUID); + BookUtils.activateBook(bookUID); } ButterKnife.bind(this); @@ -233,16 +233,13 @@ protected void updateActiveBookName(){ protected void onDrawerMenuItemClicked(int itemId) { switch (itemId){ case R.id.nav_item_open: { //Open... files - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){ - //use the storage access framework - Intent openDocument = new Intent(Intent.ACTION_OPEN_DOCUMENT); - openDocument.addCategory(Intent.CATEGORY_OPENABLE); - openDocument.setType("*/*"); - startActivityForResult(openDocument, REQUEST_OPEN_DOCUMENT); - - } else { - AccountsActivity.startXmlFileChooser(this); - } + //use the storage access framework + Intent openDocument = new Intent(Intent.ACTION_OPEN_DOCUMENT); + openDocument.addCategory(Intent.CATEGORY_OPENABLE); + openDocument.setType("text/*|application/*"); + String[] mimeTypes = {"text/*", "application/*"}; + openDocument.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes); + startActivityForResult(openDocument, REQUEST_OPEN_DOCUMENT); } break; @@ -328,7 +325,7 @@ public boolean onMenuItemClick(MenuItem item) { BooksDbAdapter booksDbAdapter = BooksDbAdapter.getInstance(); String bookUID = booksDbAdapter.getUID(id); if (!bookUID.equals(booksDbAdapter.getActiveBookUID())){ - GnuCashApplication.loadBook(bookUID); + BookUtils.loadBook(bookUID); finish(); } AccountsActivity.start(GnuCashApplication.getAppContext()); diff --git a/app/src/main/java/org/gnucash/android/ui/common/FormActivity.java b/app/src/main/java/org/gnucash/android/ui/common/FormActivity.java index 6d777f5f4..ba94de940 100644 --- a/app/src/main/java/org/gnucash/android/ui/common/FormActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/common/FormActivity.java @@ -38,6 +38,7 @@ import org.gnucash.android.ui.transaction.SplitEditorFragment; import org.gnucash.android.ui.transaction.TransactionFormFragment; import org.gnucash.android.ui.util.widget.CalculatorKeyboard; +import org.gnucash.android.util.BookUtils; /** * Activity for displaying forms in the application. @@ -61,7 +62,7 @@ protected void onCreate(Bundle savedInstanceState) { //if a parameter was passed to open an account within a specific book, then switch String bookUID = getIntent().getStringExtra(UxArgument.BOOK_UID); if (bookUID != null && !bookUID.equals(BooksDbAdapter.getInstance().getActiveBookUID())){ - GnuCashApplication.activateBook(bookUID); + BookUtils.activateBook(bookUID); } Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 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 7ebcb6954..90503473f 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 @@ -16,12 +16,10 @@ package org.gnucash.android.ui.export; -import android.Manifest; import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.os.Build; +import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.v4.app.Fragment; @@ -53,15 +51,16 @@ import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.db.adapter.BooksDbAdapter; import org.gnucash.android.db.adapter.DatabaseAdapter; import org.gnucash.android.db.adapter.ScheduledActionDbAdapter; import org.gnucash.android.export.DropboxHelper; 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.BaseModel; import org.gnucash.android.model.ScheduledAction; -import org.gnucash.android.ui.account.AccountsActivity; import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.settings.BackupPreferenceFragment; import org.gnucash.android.ui.settings.dialog.OwnCloudDialogFragment; @@ -91,7 +90,12 @@ public class ExportFormFragment extends Fragment implements RecurrencePickerDialogFragment.OnRecurrenceSetListener, CalendarDatePickerDialogFragment.OnDateSetListener, RadialTimePickerDialogFragment.OnTimeSetListener { - + + /** + * Request code for intent to pick export file destination + */ + private static final int REQUEST_EXPORT_FILE = 0x14; + /** * Spinner for selecting destination for the exported file. * The destination could either be SD card, or another application which @@ -109,6 +113,8 @@ public class ExportFormFragment extends Fragment implements */ @BindView(R.id.export_warning) TextView mExportWarningTextView; + @BindView(R.id.target_uri) TextView mTargetUriTextView; + /** * Recurrence text view */ @@ -132,6 +138,7 @@ public class ExportFormFragment extends Fragment implements @BindView(R.id.radio_qif_format) RadioButton mQifRadioButton; @BindView(R.id.radio_xml_format) RadioButton mXmlRadioButton; + @BindView(R.id.recurrence_options) View mRecurrenceOptionsView; /** * Event recurrence options */ @@ -156,6 +163,16 @@ public class ExportFormFragment extends Fragment implements private ExportParams.ExportTarget mExportTarget = ExportParams.ExportTarget.SD_CARD; + /** + * The Uri target for the export + */ + private Uri mExportUri; + + /** + * Flag to determine if export has been started. + * Used to continue export after user has picked a destination file + */ + private boolean mExportStarted = false; private void onRadioButtonClicked(View view){ switch (view.getId()){ @@ -232,8 +249,6 @@ public void onActivityCreated(Bundle savedInstanceState) { assert supportActionBar != null; supportActionBar.setTitle(R.string.title_export_dialog); setHasOptionsMenu(true); - - getSDWritePermission(); } @Override @@ -252,23 +267,16 @@ public void onPause() { prefs.edit().putBoolean(UxArgument.SKIP_PASSCODE_SCREEN, true).apply(); } - /** - * Get permission for WRITING SD card for Android Marshmallow and above - */ - private void getSDWritePermission(){ - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (getActivity().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - getActivity().requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}, AccountsActivity.REQUEST_PERMISSION_WRITE_SD_CARD); - } - } - } - /** * Starts the export of transactions with the specified parameters */ private void startExport(){ + if (mExportTarget == ExportParams.ExportTarget.URI && mExportUri == null){ + mExportStarted = true; + selectExportFile(); + return; + } + ExportParams exportParameters = new ExportParams(mExportFormat); if (mExportAllSwitch.isChecked()){ @@ -278,6 +286,7 @@ private void startExport(){ } exportParameters.setExportTarget(mExportTarget); + exportParameters.setExportLocation(mExportUri != null ? mExportUri.toString() : null); exportParameters.setDeleteTransactionsAfterExport(mDeleteAllCheckBox.isChecked()); Log.i(TAG, "Commencing async export of transactions"); @@ -313,14 +322,19 @@ private void bindViewListeners(){ mDestinationSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - View recurrenceOptionsView = getView().findViewById(R.id.recurrence_options); + if (view == null) //the item selection is fired twice by the Android framework. Ignore the first one + return; switch (position) { case 0: - mExportTarget = ExportParams.ExportTarget.SD_CARD; - recurrenceOptionsView.setVisibility(View.VISIBLE); + mExportTarget = ExportParams.ExportTarget.URI; + mRecurrenceOptionsView.setVisibility(View.VISIBLE); + if (mExportUri != null) + setExportUriText(mExportUri.toString()); + selectExportFile(); break; - case 1: - recurrenceOptionsView.setVisibility(View.VISIBLE); + case 1: //DROPBOX + setExportUriText(getString(R.string.label_dropbox_export_destination)); + mRecurrenceOptionsView.setVisibility(View.VISIBLE); mExportTarget = ExportParams.ExportTarget.DROPBOX; String dropboxAppKey = getString(R.string.dropbox_app_key, BackupPreferenceFragment.DROPBOX_APP_KEY); String dropboxAppSecret = getString(R.string.dropbox_app_secret, BackupPreferenceFragment.DROPBOX_APP_SECRET); @@ -330,13 +344,8 @@ public void onItemSelected(AdapterView parent, View view, int position, long } break; case 2: - recurrenceOptionsView.setVisibility(View.VISIBLE); - mExportTarget = ExportParams.ExportTarget.GOOGLE_DRIVE; - BackupPreferenceFragment.mGoogleApiClient = BackupPreferenceFragment.getGoogleApiClient(getActivity()); - BackupPreferenceFragment.mGoogleApiClient.connect(); - break; - case 3: - recurrenceOptionsView.setVisibility(View.VISIBLE); + setExportUriText(null); + mRecurrenceOptionsView.setVisibility(View.VISIBLE); mExportTarget = ExportParams.ExportTarget.OWNCLOUD; if(!(PreferenceManager.getDefaultSharedPreferences(getActivity()) .getBoolean(getString(R.string.key_owncloud_sync), false))) { @@ -344,9 +353,10 @@ public void onItemSelected(AdapterView parent, View view, int position, long ocDialog.show(getActivity().getSupportFragmentManager(), "ownCloud dialog"); } break; - case 4: + case 3: + setExportUriText(getString(R.string.label_select_destination_after_export)); mExportTarget = ExportParams.ExportTarget.SHARING; - recurrenceOptionsView.setVisibility(View.GONE); + mRecurrenceOptionsView.setVisibility(View.GONE); break; default: @@ -470,6 +480,41 @@ public void onClick(View view) { } + /** + * Display the file path of the file where the export will be saved + * @param filepath Path to export file. If {@code null}, the view will be hidden and nothing displayed + */ + private void setExportUriText(String filepath){ + if (filepath == null){ + mTargetUriTextView.setVisibility(View.GONE); + mTargetUriTextView.setText(""); + } else { + mTargetUriTextView.setText(filepath); + mTargetUriTextView.setVisibility(View.VISIBLE); + } + } + + /** + * 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("text/*").addCategory(Intent.CATEGORY_OPENABLE); + String bookName = BooksDbAdapter.getInstance().getActiveBookDisplayName(); + + if (mExportFormat == ExportFormat.XML || mExportFormat == ExportFormat.QIF) { + createIntent.setType("application/zip"); + } + + String filename = Exporter.buildExportFilename(mExportFormat, bookName); + if (mExportTarget == ExportParams.ExportTarget.URI && mExportFormat == ExportFormat.QIF){ + filename += ".zip"; + } + + createIntent.putExtra(Intent.EXTRA_TITLE, filename); + startActivityForResult(createIntent, REQUEST_EXPORT_FILE); + } + @Override public void onRecurrenceSet(String rrule) { mRecurrenceRule = rrule; @@ -488,8 +533,30 @@ public void onRecurrenceSet(String rrule) { */ @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == BackupPreferenceFragment.REQUEST_RESOLVE_CONNECTION && resultCode == Activity.RESULT_OK) { - BackupPreferenceFragment.mGoogleApiClient.connect(); + + switch (requestCode){ + case BackupPreferenceFragment.REQUEST_RESOLVE_CONNECTION: + if (resultCode == Activity.RESULT_OK) { + BackupPreferenceFragment.mGoogleApiClient.connect(); + } + break; + + case REQUEST_EXPORT_FILE: + if (resultCode == Activity.RESULT_OK){ + if (data != null){ + mExportUri = data.getData(); + } + + final int takeFlags = data.getFlags() + & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + getActivity().getContentResolver().takePersistableUriPermission(mExportUri, takeFlags); + + mTargetUriTextView.setText(mExportUri.toString()); + if (mExportStarted) + startExport(); + + } + break; } } diff --git a/app/src/main/java/org/gnucash/android/ui/passcode/PasscodeLockActivity.java b/app/src/main/java/org/gnucash/android/ui/passcode/PasscodeLockActivity.java index 0934053dc..951695868 100644 --- a/app/src/main/java/org/gnucash/android/ui/passcode/PasscodeLockActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/passcode/PasscodeLockActivity.java @@ -44,9 +44,7 @@ protected void onResume() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); boolean isPassEnabled = prefs.getBoolean(UxArgument.ENABLED_PASSCODE, false); if (isPassEnabled) { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) { - getWindow().addFlags(LayoutParams.FLAG_SECURE); - } + getWindow().addFlags(LayoutParams.FLAG_SECURE); } else { getWindow().clearFlags(LayoutParams.FLAG_SECURE); } 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 e0d618794..ac0dc24a0 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 @@ -51,6 +51,7 @@ 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 java.io.File; import java.text.DateFormat; @@ -75,6 +76,11 @@ public class BackupPreferenceFragment extends PreferenceFragmentCompat implement private static final int REQUEST_LINK_TO_DBX = 0x11; public static final int REQUEST_RESOLVE_CONNECTION = 0x12; + /** + * Request code for the backup file where to save backups + */ + private static final int REQUEST_BACKUP_FILE = 0x13; + /** * Testing app key for DropBox API */ @@ -143,13 +149,16 @@ public void onResume() { pref = findPreference(getString(R.string.key_create_backup)); pref.setOnPreferenceClickListener(this); - pref = findPreference(getString(R.string.key_dropbox_sync)); + pref = findPreference(getString(R.string.key_backup_location)); pref.setOnPreferenceClickListener(this); - toggleDropboxPreference(pref); + String defaultBackupLocation = BookUtils.getBookBackupFileUri(BooksDbAdapter.getInstance().getActiveBookUID()); + if (defaultBackupLocation != null){ + pref.setSummary(Uri.parse(defaultBackupLocation).getAuthority()); + } - pref = findPreference(getString(R.string.key_google_drive_sync)); + pref = findPreference(getString(R.string.key_dropbox_sync)); pref.setOnPreferenceClickListener(this); - toggleGoogleDrivePreference(pref); + toggleDropboxPreference(pref); pref = findPreference(getString(R.string.key_owncloud_sync)); pref.setOnPreferenceClickListener(this); @@ -164,17 +173,20 @@ public boolean onPreferenceClick(Preference preference) { restoreBackup(); } + if (key.equals(getString(R.string.key_backup_location))){ + Intent createIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + createIntent.setType("application/zip"); + createIntent.addCategory(Intent.CATEGORY_OPENABLE); + String bookName = BooksDbAdapter.getInstance().getActiveBookDisplayName(); + createIntent.putExtra(Intent.EXTRA_TITLE, Exporter.sanitizeFilename(bookName)+ "_" + getString(R.string.label_backup_filename)); + startActivityForResult(createIntent, REQUEST_BACKUP_FILE); + } if (key.equals(getString(R.string.key_dropbox_sync))){ toggleDropboxSync(); toggleDropboxPreference(preference); } - if (key.equals(getString(R.string.key_google_drive_sync))){ - toggleGoogleDriveSync(); - toggleGoogleDrivePreference(preference); - } - if (key.equals(getString(R.string.key_owncloud_sync))){ toggleOwnCloudSync(preference); toggleOwnCloudPreference(preference); @@ -358,11 +370,34 @@ public void onConnectionFailed(ConnectionResult connectionResult) { private void restoreBackup() { Log.i("Settings", "Opening GnuCash XML backups for restore"); String bookUID = BooksDbAdapter.getInstance().getActiveBookUID(); + + final String defaultBackupFile = BookUtils.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) + .setMessage(R.string.msg_confirm_restore_backup_into_new_book) + .setNegativeButton(R.string.btn_cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }) + .setPositiveButton(R.string.btn_restore, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + new ImportAsyncTask(getActivity()).execute(Uri.parse(defaultBackupFile)); + } + }); + builder.create().show(); + return; //stop here if the default backup file exists + } + + //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){ android.support.v7.app.AlertDialog.Builder builder = new android.support.v7.app.AlertDialog.Builder(getActivity()) - .setTitle("No backups found") - .setMessage("There are no existing backup files to restore from") + .setTitle(R.string.title_no_backups_found) + .setMessage(R.string.msg_no_backups_to_restore_from) .setNegativeButton(R.string.label_dismiss, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -428,6 +463,27 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { toggleDropboxPreference(pref); } break; + + case REQUEST_BACKUP_FILE: + if (resultCode == Activity.RESULT_OK){ + Uri backupFileUri = null; + if (data != null){ + backupFileUri = data.getData(); + } + + final int takeFlags = data.getFlags() + & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + getActivity().getContentResolver().takePersistableUriPermission(backupFileUri, takeFlags); + + PreferenceActivity.getActiveBookSharedPreferences() + .edit() + .putString(BookUtils.KEY_BACKUP_FILE, backupFileUri.toString()) + .apply(); + + Preference pref = findPreference(getString(R.string.key_backup_location)); + pref.setSummary(backupFileUri.getAuthority()); + } + break; } } } diff --git a/app/src/main/java/org/gnucash/android/ui/settings/BookManagerFragment.java b/app/src/main/java/org/gnucash/android/ui/settings/BookManagerFragment.java index 50df8e163..6d4f5cb97 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/BookManagerFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/BookManagerFragment.java @@ -38,6 +38,7 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.EditText; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; @@ -53,6 +54,7 @@ import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.ui.account.AccountsActivity; import org.gnucash.android.ui.common.Refreshable; +import org.gnucash.android.util.BookUtils; import org.gnucash.android.util.PreferencesHelper; import java.sql.Timestamp; @@ -164,7 +166,22 @@ public void bindView(View view, final Context context, Cursor cursor) { setLastExportedText(view, bookUID); setStatisticsText(view, bookUID); + setUpMenu(view, context, cursor, bookUID); + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + //do nothing if the active book is tapped + if (!BooksDbAdapter.getInstance().getActiveBookUID().equals(bookUID)) { + BookUtils.loadBook(bookUID); + } + } + }); + } + + private void setUpMenu(View view, final Context context, Cursor cursor, final String bookUID) { + final String bookName = cursor.getString( + cursor.getColumnIndexOrThrow(BookEntry.COLUMN_DISPLAY_NAME)); ImageView optionsMenu = (ImageView) view.findViewById(R.id.options_menu); optionsMenu.setOnClickListener(new View.OnClickListener() { @Override @@ -172,65 +189,86 @@ public void onClick(View v) { PopupMenu popupMenu = new PopupMenu(context, v); MenuInflater menuInflater = popupMenu.getMenuInflater(); menuInflater.inflate(R.menu.book_context_menu, popupMenu.getMenu()); + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { - switch (item.getItemId()){ + switch (item.getItemId()) { + case R.id.ctx_menu_rename_book: + return handleMenuRenameBook(bookName, bookUID); case R.id.ctx_menu_sync_book: //TODO implement sync return false; + case R.id.ctx_menu_delete_book: { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity()); + dialogBuilder.setTitle(getString(R.string.title_confirm_delete_book)) + .setIcon(R.drawable.ic_close_black_24dp) + .setMessage(getString(R.string.msg_all_book_data_will_be_deleted)); + dialogBuilder.setPositiveButton(getString(R.string.btn_delete_book), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + BooksDbAdapter.getInstance().deleteBook(bookUID); + refresh(); + } + }); + dialogBuilder.setNegativeButton(R.string.btn_cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }); + AlertDialog dialog = dialogBuilder.create(); + dialog.show(); //must be called before you can access buttons + dialog.getButton(AlertDialog.BUTTON_POSITIVE) + .setTextColor(ContextCompat.getColor(context, R.color.account_red)); + } + return true; default: return true; } } }); + + String activeBookUID = BooksDbAdapter.getInstance().getActiveBookUID(); + if (activeBookUID.equals(bookUID)) {//we cannot delete the active book + popupMenu.getMenu().findItem(R.id.ctx_menu_delete_book).setEnabled(false); + } popupMenu.show(); } }); + } - ImageView deleteBookBtn = (ImageView) view.findViewById(R.id.delete_book); - String activeBookUID = BooksDbAdapter.getInstance().getActiveBookUID(); - if (activeBookUID.equals(bookUID)) //we cannot delete the active book - deleteBookBtn.setVisibility(View.GONE); - else { - deleteBookBtn.setOnClickListener(new View.OnClickListener() { + /** + * Opens a dialog for renaming a book + * @param bookName Current name of the book + * @param bookUID GUID of the book + * @return {@code true} + */ + private boolean handleMenuRenameBook(String bookName, final String bookUID) { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity()); + dialogBuilder.setTitle(R.string.title_rename_book) + .setView(R.layout.dialog_rename_book) + .setPositiveButton(R.string.btn_rename, new DialogInterface.OnClickListener() { @Override - public void onClick(View v) { - //// TODO: extract strings - AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity()); - dialogBuilder.setTitle(getString(R.string.title_confirm_delete_book)) - .setIcon(R.drawable.ic_close_black_24dp) - .setMessage(getString(R.string.msg_all_book_data_will_be_deleted)); - dialogBuilder.setPositiveButton(getString(R.string.btn_delete_book), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - BooksDbAdapter.getInstance().deleteBook(bookUID); - refresh(); - } - }); - dialogBuilder.setNegativeButton(R.string.btn_cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - } - }); - AlertDialog dialog = dialogBuilder.create(); - dialog.show(); //must be called before you can access buttons - dialog.getButton(AlertDialog.BUTTON_POSITIVE) - .setTextColor(ContextCompat.getColor(context, R.color.account_red)); + public void onClick(DialogInterface dialog, int which) { + EditText bookTitle = (EditText) ((AlertDialog)dialog).findViewById(R.id.input_book_title); + BooksDbAdapter.getInstance() + .updateRecord(bookUID, + BookEntry.COLUMN_DISPLAY_NAME, + bookTitle.getText().toString().trim()); + refresh(); } - }); - } - - view.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - //do nothing if the active book is tapped - if (!BooksDbAdapter.getInstance().getActiveBookUID().equals(bookUID)) { - GnuCashApplication.loadBook(bookUID); + }) + .setNegativeButton(R.string.btn_cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); } - } - }); + }); + AlertDialog dialog = dialogBuilder.create(); + dialog.show(); + ((TextView)dialog.findViewById(R.id.input_book_title)).setText(bookName); + return true; } private void setLastExportedText(View view, String bookUID) { @@ -258,6 +296,10 @@ private void setStatisticsText(View view, String bookUID) { String stats = accountStats + ", " + transactionStats; TextView statsText = (TextView) view.findViewById(R.id.secondary_text); statsText.setText(stats); + + if (bookUID.equals(BooksDbAdapter.getInstance().getActiveBookUID())){ + ((TextView)view.findViewById(R.id.primary_text)).setTextColor(getResources().getColor(R.color.theme_primary)); + } } } diff --git a/app/src/main/java/org/gnucash/android/ui/settings/GeneralPreferenceFragment.java b/app/src/main/java/org/gnucash/android/ui/settings/GeneralPreferenceFragment.java index 26f14fad2..db07e2753 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/GeneralPreferenceFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/GeneralPreferenceFragment.java @@ -77,9 +77,6 @@ public void onResume() { final Intent intent = new Intent(getActivity(), PasscodePreferenceActivity.class); mCheckBoxPreference = (CheckBoxPreference) findPreference(getString(R.string.key_enable_passcode)); - mCheckBoxPreference.setTitle(mCheckBoxPreference.isChecked() - ? getString(R.string.title_passcode_enabled) - : getString(R.string.title_passcode_disabled)); mCheckBoxPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { @@ -145,12 +142,10 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { mEditor.putString(UxArgument.PASSCODE, data.getStringExtra(UxArgument.PASSCODE)); mEditor.putBoolean(UxArgument.ENABLED_PASSCODE, true); Toast.makeText(getActivity(), R.string.toast_passcode_set, Toast.LENGTH_SHORT).show(); - mCheckBoxPreference.setTitle(getString(R.string.title_passcode_enabled)); } if (resultCode == Activity.RESULT_CANCELED) { mEditor.putBoolean(UxArgument.ENABLED_PASSCODE, false); mCheckBoxPreference.setChecked(false); - mCheckBoxPreference.setTitle(getString(R.string.title_passcode_disabled)); } break; case REQUEST_DISABLE_PASSCODE: @@ -163,7 +158,6 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { mEditor.putString(UxArgument.PASSCODE, data.getStringExtra(UxArgument.PASSCODE)); mEditor.putBoolean(UxArgument.ENABLED_PASSCODE, true); Toast.makeText(getActivity(), R.string.toast_passcode_set, Toast.LENGTH_SHORT).show(); - mCheckBoxPreference.setTitle(getString(R.string.title_passcode_enabled)); } break; } diff --git a/app/src/main/java/org/gnucash/android/ui/settings/PreferenceActivity.java b/app/src/main/java/org/gnucash/android/ui/settings/PreferenceActivity.java index bb16e00b1..10cf12b29 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/PreferenceActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/PreferenceActivity.java @@ -93,7 +93,6 @@ public void onPanelClosed(View panel) { actionBar.setDisplayHomeAsUpEnabled(true); } - @TargetApi(Build.VERSION_CODES.KITKAT) //for one of the exceptions caught @Override public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) { String key = pref.getKey(); @@ -128,13 +127,9 @@ private void loadFragment(Fragment fragment) { public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - android.app.FragmentManager fm = getFragmentManager(); - if (fm.getBackStackEntryCount() > 0) { - fm.popBackStack(); - } else { - finish(); - } + android.app.FragmentManager fm = getFragmentManager(); + if (fm.getBackStackEntryCount() > 0) { + fm.popBackStack(); } else { finish(); } diff --git a/app/src/main/java/org/gnucash/android/ui/settings/TransactionsPreferenceFragment.java b/app/src/main/java/org/gnucash/android/ui/settings/TransactionsPreferenceFragment.java index b047430cb..a388e2632 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/TransactionsPreferenceFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/TransactionsPreferenceFragment.java @@ -28,6 +28,8 @@ import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.db.adapter.BooksDbAdapter; +import org.gnucash.android.db.adapter.CommoditiesDbAdapter; +import org.gnucash.android.model.Commodity; import org.gnucash.android.ui.settings.dialog.DeleteAllTransactionsConfirmationDialog; import java.util.Currency; @@ -122,9 +124,9 @@ public void showDeleteTransactionsDialog(){ private void setImbalanceAccountsHidden(boolean useDoubleEntry) { String isHidden = useDoubleEntry ? "0" : "1"; AccountsDbAdapter accountsDbAdapter = AccountsDbAdapter.getInstance(); - List currencies = accountsDbAdapter.getCurrenciesInUse(); - for (Currency currency : currencies) { - String uid = accountsDbAdapter.getImbalanceAccountUID(currency); + List commodities = accountsDbAdapter.getCommoditiesInUse(); + for (Commodity commodity : commodities) { + String uid = accountsDbAdapter.getImbalanceAccountUID(commodity); if (uid != null){ accountsDbAdapter.updateRecord(uid, DatabaseSchema.AccountEntry.COLUMN_HIDDEN, isHidden); } 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 198b5b817..58c4cd93f 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 @@ -36,7 +36,6 @@ * * @author Ngewi Fet */ -@TargetApi(11) public class DeleteAllAccountsConfirmationDialog extends DialogFragment { public static DeleteAllAccountsConfirmationDialog newInstance() { 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 ccff3c1bc..08379fa35 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 @@ -22,6 +22,7 @@ import android.content.res.Resources; import android.database.Cursor; import android.graphics.Rect; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.v4.app.Fragment; @@ -561,9 +562,13 @@ public void bindView(View view, Context context, Cursor cursor) { TextView primaryTextView = (TextView) view.findViewById(R.id.primary_text); ExportParams params = ExportParams.parseCsv(scheduledAction.getTag()); + String exportDestination = params.getExportTarget().getDescription(); + if (params.getExportTarget() == ExportParams.ExportTarget.URI){ + exportDestination = exportDestination + " (" + Uri.parse(params.getExportLocation()).getHost() + ")"; + } primaryTextView.setText(params.getExportFormat().name() + " " + scheduledAction.getActionType().name().toLowerCase() + " to " - + params.getExportTarget().name().toLowerCase()); + + exportDestination); view.findViewById(R.id.right_text).setVisibility(View.GONE); diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/TransactionFormFragment.java b/app/src/main/java/org/gnucash/android/ui/transaction/TransactionFormFragment.java index 14115deed..32edecb47 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/TransactionFormFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/TransactionFormFragment.java @@ -89,7 +89,6 @@ import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; -import java.util.Currency; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; @@ -273,7 +272,7 @@ private void startTransferFunds() { return; BigDecimal amountBigd = mAmountEditText.getValue(); - if (amountBigd.equals(BigDecimal.ZERO)) + if ((amountBigd == null) || amountBigd.equals(BigDecimal.ZERO)) return; Money amount = new Money(amountBigd, fromCommodity).abs(); @@ -556,7 +555,7 @@ private void initalizeViews() { if (mUseDoubleEntry){ String currentAccountUID = mAccountUID; - long defaultTransferAccountID = 0; + long defaultTransferAccountID; String rootAccountUID = mAccountsDbAdapter.getOrCreateGnuCashRootAccountUID(); do { defaultTransferAccountID = mAccountsDbAdapter.getDefaultTransferAccountID(mAccountsDbAdapter.getID(currentAccountUID)); diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsActivity.java b/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsActivity.java index 07051a11d..fb4ba91ef 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsActivity.java @@ -260,12 +260,8 @@ public void refresh(String accountUID) { if (mPagerAdapter != null) mPagerAdapter.notifyDataSetChanged(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - // make sure the account balance task is truely multi-thread - new AccountBalanceTask(mSumTextView).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mAccountUID); - } else { - new AccountBalanceTask(mSumTextView).execute(mAccountUID); - } + new AccountBalanceTask(mSumTextView).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mAccountUID); + } @Override diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransferFundsDialogFragment.java b/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransferFundsDialogFragment.java index 234dd892c..55bccd9c8 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransferFundsDialogFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransferFundsDialogFragment.java @@ -48,7 +48,6 @@ import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.ParseException; -import java.util.Currency; import butterknife.BindView; import butterknife.ButterKnife; @@ -78,7 +77,7 @@ public class TransferFundsDialogFragment extends DialogFragment { @BindView(R.id.btn_save) Button mSaveButton; @BindView(R.id.btn_cancel) Button mCancelButton; Money mOriginAmount; - String mTargetCurrencyCode; + private Commodity mTargetCommodity; Money mConvertedAmount; OnTransferFundsListener mOnTransferFundsListener; @@ -87,7 +86,7 @@ public static TransferFundsDialogFragment getInstance(Money transactionAmount, S OnTransferFundsListener transferFundsListener){ TransferFundsDialogFragment fragment = new TransferFundsDialogFragment(); fragment.mOriginAmount = transactionAmount; - fragment.mTargetCurrencyCode = Currency.getInstance(targetCurrencyCode).getCurrencyCode(); + fragment.mTargetCommodity = CommoditiesDbAdapter.getInstance().getCommodity(targetCurrencyCode); fragment.mOnTransferFundsListener = transferFundsListener; return fragment; } @@ -101,18 +100,17 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa TransactionsActivity.displayBalance(mStartAmountLabel, mOriginAmount); String fromCurrencyCode = mOriginAmount.getCommodity().getCurrencyCode(); mFromCurrencyLabel.setText(fromCurrencyCode); - mToCurrencyLabel.setText(mTargetCurrencyCode); - mConvertedAmountCurrencyLabel.setText(mTargetCurrencyCode); + mToCurrencyLabel.setText(mTargetCommodity.getCurrencyCode()); + mConvertedAmountCurrencyLabel.setText(mTargetCommodity.getCurrencyCode()); mSampleExchangeRate.setText(String.format(getString(R.string.sample_exchange_rate), fromCurrencyCode, - mTargetCurrencyCode)); + mTargetCommodity.getCurrencyCode())); final InputLayoutErrorClearer textChangeListener = new InputLayoutErrorClearer(); CommoditiesDbAdapter commoditiesDbAdapter = CommoditiesDbAdapter.getInstance(); String commodityUID = commoditiesDbAdapter.getCommodityUID(fromCurrencyCode); - Commodity currencyCommodity = commoditiesDbAdapter.getCommodity(mTargetCurrencyCode); - String currencyUID = currencyCommodity.getUID(); + String currencyUID = mTargetCommodity.getUID(); PricesDbAdapter pricesDbAdapter = PricesDbAdapter.getInstance(); Pair pricePair = pricesDbAdapter.getPrice(commodityUID, currencyUID); @@ -127,7 +125,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa BigDecimal denominator = new BigDecimal(pricePair.second); // convertedAmount = mOriginAmount * numerator / denominator BigDecimal convertedAmount = mOriginAmount.asBigDecimal().multiply(numerator) - .divide(denominator, currencyCommodity.getSmallestFractionDigits(), BigDecimal.ROUND_HALF_EVEN); + .divide(denominator, mTargetCommodity.getSmallestFractionDigits(), BigDecimal.ROUND_HALF_EVEN); DecimalFormat formatter = (DecimalFormat) NumberFormat.getNumberInstance(); mConvertedAmountInput.setText(formatter.format(convertedAmount)); } @@ -197,9 +195,8 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { private void transferFunds() { Price price = null; - CommoditiesDbAdapter commoditiesDbAdapter = CommoditiesDbAdapter.getInstance(); String originCommodityUID = mOriginAmount.getCommodity().getUID(); - String targetCommodityUID = commoditiesDbAdapter.getCommodityUID(mTargetCurrencyCode); + String targetCommodityUID = mTargetCommodity.getUID(); if (mExchangeRateRadioButton.isChecked()) { BigDecimal rate; @@ -211,8 +208,7 @@ private void transferFunds() { } price = new Price(originCommodityUID, targetCommodityUID, rate); - Commodity targetCommodity = Commodity.getInstance(mTargetCurrencyCode); - mConvertedAmount = mOriginAmount.multiply(rate).withCurrency(targetCommodity); + mConvertedAmount = mOriginAmount.multiply(rate).withCurrency(mTargetCommodity); } if (mConvertedAmountRadioButton.isChecked()) { @@ -223,7 +219,7 @@ private void transferFunds() { mConvertedAmountInputLayout.setError(getString(R.string.error_invalid_amount)); return; } - mConvertedAmount = new Money(amount, Commodity.getInstance(mTargetCurrencyCode)); + mConvertedAmount = new Money(amount, mTargetCommodity); price = new Price(originCommodityUID, targetCommodityUID); // fractions cannot be exactly represented by BigDecimal. diff --git a/app/src/main/java/org/gnucash/android/ui/util/RecurrenceParser.java b/app/src/main/java/org/gnucash/android/ui/util/RecurrenceParser.java index 2855b7115..4fbe5d8ed 100644 --- a/app/src/main/java/org/gnucash/android/ui/util/RecurrenceParser.java +++ b/app/src/main/java/org/gnucash/android/ui/util/RecurrenceParser.java @@ -38,12 +38,15 @@ * @author Ngewi Fet */ public class RecurrenceParser { + //these are time millisecond constants which are used for scheduled actions. + //they may not be calendar accurate, but they serve the purpose for scheduling approximate time for background service execution public static final long SECOND_MILLIS = 1000; - public static final long MINUTE_MILLIS = 60*SECOND_MILLIS; - public static final long DAY_MILLIS = 24*60*MINUTE_MILLIS; - public static final long WEEK_MILLIS = 7*DAY_MILLIS; - public static final long MONTH_MILLIS = 30*DAY_MILLIS; - public static final long YEAR_MILLIS = 12*MONTH_MILLIS; + public static final long MINUTE_MILLIS = 60 * SECOND_MILLIS; + public static final long HOUR_MILLIS = 60 * MINUTE_MILLIS; + public static final long DAY_MILLIS = 24 * HOUR_MILLIS; + public static final long WEEK_MILLIS = 7 * DAY_MILLIS; + public static final long MONTH_MILLIS = 30 * DAY_MILLIS; + public static final long YEAR_MILLIS = 12 * MONTH_MILLIS; /** * Parse an {@link EventRecurrence} into a {@link Recurrence} object @@ -56,6 +59,10 @@ public static Recurrence parse(EventRecurrence eventRecurrence){ PeriodType periodType; switch(eventRecurrence.freq){ + case EventRecurrence.HOURLY: + periodType = PeriodType.HOUR; + break; + case EventRecurrence.DAILY: periodType = PeriodType.DAY; break; diff --git a/app/src/main/java/org/gnucash/android/ui/util/ScrollingFABBehavior.java b/app/src/main/java/org/gnucash/android/ui/util/ScrollingFABBehavior.java index 6a71f62a3..c7953211a 100644 --- a/app/src/main/java/org/gnucash/android/ui/util/ScrollingFABBehavior.java +++ b/app/src/main/java/org/gnucash/android/ui/util/ScrollingFABBehavior.java @@ -50,10 +50,9 @@ public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionBu CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) fab.getLayoutParams(); int fabBottomMargin = lp.bottomMargin; int distanceToScroll = fab.getHeight() + fabBottomMargin; - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.GINGERBREAD_MR1) { - float ratio = (float) dependency.getY() / (float) toolbarHeight; - fab.setTranslationY(-distanceToScroll * ratio); - } + float ratio = (float) dependency.getY() / (float) toolbarHeight; + fab.setTranslationY(-distanceToScroll * ratio); + } return true; } diff --git a/app/src/main/java/org/gnucash/android/ui/util/widget/CalculatorEditText.java b/app/src/main/java/org/gnucash/android/ui/util/widget/CalculatorEditText.java index 69a778906..67f6a07f0 100644 --- a/app/src/main/java/org/gnucash/android/ui/util/widget/CalculatorEditText.java +++ b/app/src/main/java/org/gnucash/android/ui/util/widget/CalculatorEditText.java @@ -21,6 +21,7 @@ import android.inputmethodservice.KeyboardView; import android.support.annotation.Nullable; import android.support.annotation.XmlRes; +import android.support.v7.widget.AppCompatEditText; import android.text.Editable; import android.text.InputType; import android.text.TextWatcher; @@ -29,7 +30,6 @@ import android.view.MotionEvent; import android.view.View; import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; import com.crashlytics.android.Crashlytics; @@ -54,7 +54,7 @@ * with the view from your layout where the calculator keyboard should be displayed.

* @author Ngewi Fet */ -public class CalculatorEditText extends EditText { +public class CalculatorEditText extends AppCompatEditText { private CalculatorKeyboard mCalculatorKeyboard; private Commodity mCommodity = Commodity.DEFAULT_COMMODITY; diff --git a/app/src/main/java/org/gnucash/android/ui/util/widget/CheckableLinearLayout.java b/app/src/main/java/org/gnucash/android/ui/util/widget/CheckableLinearLayout.java index 5230e8be3..d7547a837 100644 --- a/app/src/main/java/org/gnucash/android/ui/util/widget/CheckableLinearLayout.java +++ b/app/src/main/java/org/gnucash/android/ui/util/widget/CheckableLinearLayout.java @@ -51,7 +51,6 @@ public CheckableLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); } - @TargetApi(11) public CheckableLinearLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } diff --git a/app/src/main/java/org/gnucash/android/ui/util/widget/ReselectSpinner.java b/app/src/main/java/org/gnucash/android/ui/util/widget/ReselectSpinner.java index dc32ef8b5..cf03b65b0 100644 --- a/app/src/main/java/org/gnucash/android/ui/util/widget/ReselectSpinner.java +++ b/app/src/main/java/org/gnucash/android/ui/util/widget/ReselectSpinner.java @@ -1,15 +1,25 @@ package org.gnucash.android.ui.util.widget; import android.content.Context; +import android.support.v7.widget.AppCompatSpinner; import android.util.AttributeSet; -import android.widget.Spinner; +import android.view.View; +import android.widget.AdapterView; + +import org.gnucash.android.ui.export.ExportFormFragment; /** * Spinner which fires OnItemSelectedListener even when an item is reselected. * Normal Spinners only fire item selected notifications when the selected item changes. - *

This is used in {@code ReportsActivity} for the time range

+ *

This is used in {@code ReportsActivity} for the time range and in the {@link ExportFormFragment}

+ *

It could happen that the selected item is fired twice especially if the item is the first in the list. + * The Android system does this internally. In order to capture the first one, check whether the view parameter + * of {@link android.widget.AdapterView.OnItemSelectedListener#onItemSelected(AdapterView, View, int, long)} is null. + * That would represent the first call during initialization of the views. This call can be ignored. + * See {@link ExportFormFragment#bindViewListeners()} for an example + *

*/ -public class ReselectSpinner extends Spinner { +public class ReselectSpinner extends AppCompatSpinner { public ReselectSpinner(Context context) { super(context); } @@ -26,9 +36,10 @@ public ReselectSpinner(Context context, AttributeSet attrs, int defStyleAttr) { public void setSelection(int position) { boolean sameSelected = getSelectedItemPosition() == position; super.setSelection(position); - if (position == 5 && sameSelected){ - getOnItemSelectedListener().onItemSelected(this, getSelectedView(), position, getSelectedItemId()); + if (sameSelected){ + OnItemSelectedListener listener = getOnItemSelectedListener(); + if (listener != null) + listener.onItemSelected(this, getSelectedView(), position, getSelectedItemId()); } - super.setSelection(position); } } diff --git a/app/src/main/java/org/gnucash/android/ui/wizard/FirstRunWizardActivity.java b/app/src/main/java/org/gnucash/android/ui/wizard/FirstRunWizardActivity.java index 9efa3c81b..39a803074 100644 --- a/app/src/main/java/org/gnucash/android/ui/wizard/FirstRunWizardActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/wizard/FirstRunWizardActivity.java @@ -160,13 +160,7 @@ public void onClick(View view) { } preferenceEditor.apply(); - - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}, AccountsActivity.REQUEST_PERMISSION_WRITE_SD_CARD); - } else { //on other version of Android, just proceed with processing. On Android M, we import when permission grant returns - createAccountsAndFinish(); - } + createAccountsAndFinish(); } else { if (mEditingAfterReview) { mPager.setCurrentItem(mPagerAdapter.getCount() - 1); @@ -282,23 +276,6 @@ public void onTaskComplete() { } } - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - switch (requestCode){ - case AccountsActivity.REQUEST_PERMISSION_WRITE_SD_CARD:{ - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - createAccountsAndFinish(); - } else { - // permission denied, boo! - // nothing to see here, move along - finish(); - AccountsActivity.removeFirstRunFlag(); - } - } - } - - } - @Override protected void onDestroy() { super.onDestroy(); diff --git a/app/src/main/java/org/gnucash/android/util/BookUtils.java b/app/src/main/java/org/gnucash/android/util/BookUtils.java new file mode 100644 index 000000000..67bf5f4dd --- /dev/null +++ b/app/src/main/java/org/gnucash/android/util/BookUtils.java @@ -0,0 +1,46 @@ +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 + * @param bookUID GUID of the book to be activated + */ + public static void activateBook(@NonNull String bookUID){ + GnuCashApplication.getBooksDbAdapter().setActive(bookUID); + GnuCashApplication.initializeDatabaseAdapters(); + } + + /** + * Loads the book with GUID {@code bookUID} and opens the AccountsActivity + * @param bookUID GUID of the book to be loaded + */ + public static void loadBook(@NonNull String bookUID){ + activateBook(bookUID); + AccountsActivity.start(GnuCashApplication.getAppContext()); + } +} diff --git a/app/src/main/java/org/gnucash/android/util/QualifiedAccountNameCursorAdapter.java b/app/src/main/java/org/gnucash/android/util/QualifiedAccountNameCursorAdapter.java index dcf17c13d..77bc86674 100644 --- a/app/src/main/java/org/gnucash/android/util/QualifiedAccountNameCursorAdapter.java +++ b/app/src/main/java/org/gnucash/android/util/QualifiedAccountNameCursorAdapter.java @@ -73,7 +73,7 @@ public void bindView(View view, Context context, Cursor cursor) { if(isFavorite == 0) { textView.setCompoundDrawablesWithIntrinsicBounds(0,0,0,0); } else { - textView.setCompoundDrawablesWithIntrinsicBounds(0,0,R.drawable.ic_star_black_24dp,0); + textView.setCompoundDrawablesWithIntrinsicBounds(0,0,R.drawable.ic_star_black_18dp,0); } } diff --git a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_focused_c.9.png b/app/src/main/res/drawable-hdpi-v14/appwidget_inner_focused_c.9.png deleted file mode 100644 index 5aafacd9c..000000000 Binary files a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_focused_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_focused_l.9.png b/app/src/main/res/drawable-hdpi-v14/appwidget_inner_focused_l.9.png deleted file mode 100644 index ab6e8f31e..000000000 Binary files a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_focused_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_focused_r.9.png b/app/src/main/res/drawable-hdpi-v14/appwidget_inner_focused_r.9.png deleted file mode 100644 index 6c1b3c624..000000000 Binary files a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_focused_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_pressed_c.9.png b/app/src/main/res/drawable-hdpi-v14/appwidget_inner_pressed_c.9.png deleted file mode 100644 index 470f5c038..000000000 Binary files a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_pressed_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_pressed_l.9.png b/app/src/main/res/drawable-hdpi-v14/appwidget_inner_pressed_l.9.png deleted file mode 100644 index e3aa8db1a..000000000 Binary files a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_pressed_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_pressed_r.9.png b/app/src/main/res/drawable-hdpi-v14/appwidget_inner_pressed_r.9.png deleted file mode 100644 index 9e27d2fdc..000000000 Binary files a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_pressed_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/action_about.png b/app/src/main/res/drawable-hdpi/action_about.png deleted file mode 100644 index 8f39c428a..000000000 Binary files a/app/src/main/res/drawable-hdpi/action_about.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/action_help.png b/app/src/main/res/drawable-hdpi/action_help.png deleted file mode 100644 index 459bed76c..000000000 Binary files a/app/src/main/res/drawable-hdpi/action_help.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_dark_bg_pressed.9.png b/app/src/main/res/drawable-hdpi/appwidget_dark_bg_pressed.9.png deleted file mode 100644 index 8df4c69cd..000000000 Binary files a/app/src/main/res/drawable-hdpi/appwidget_dark_bg_pressed.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_inner_focused_c.9.png b/app/src/main/res/drawable-hdpi/appwidget_inner_focused_c.9.png deleted file mode 100644 index a949bd2c3..000000000 Binary files a/app/src/main/res/drawable-hdpi/appwidget_inner_focused_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_inner_focused_l.9.png b/app/src/main/res/drawable-hdpi/appwidget_inner_focused_l.9.png deleted file mode 100644 index 4aaca6c50..000000000 Binary files a/app/src/main/res/drawable-hdpi/appwidget_inner_focused_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_inner_focused_r.9.png b/app/src/main/res/drawable-hdpi/appwidget_inner_focused_r.9.png deleted file mode 100644 index 1fc0f900a..000000000 Binary files a/app/src/main/res/drawable-hdpi/appwidget_inner_focused_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_inner_pressed_c.9.png b/app/src/main/res/drawable-hdpi/appwidget_inner_pressed_c.9.png deleted file mode 100644 index ca6f16cd1..000000000 Binary files a/app/src/main/res/drawable-hdpi/appwidget_inner_pressed_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_inner_pressed_l.9.png b/app/src/main/res/drawable-hdpi/appwidget_inner_pressed_l.9.png deleted file mode 100644 index 642eb3d32..000000000 Binary files a/app/src/main/res/drawable-hdpi/appwidget_inner_pressed_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_inner_pressed_r.9.png b/app/src/main/res/drawable-hdpi/appwidget_inner_pressed_r.9.png deleted file mode 100644 index 5e1f70a39..000000000 Binary files a/app/src/main/res/drawable-hdpi/appwidget_inner_pressed_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/content_new_holo_dark.png b/app/src/main/res/drawable-hdpi/content_new_holo_dark.png deleted file mode 100644 index ad8ada6bd..000000000 Binary files a/app/src/main/res/drawable-hdpi/content_new_holo_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/drawer_shadow.9.png b/app/src/main/res/drawable-hdpi/drawer_shadow.9.png deleted file mode 100644 index 236bff558..000000000 Binary files a/app/src/main/res/drawable-hdpi/drawer_shadow.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_clear_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_clear_black_24dp.png deleted file mode 100644 index 1a9cd75a0..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_clear_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_dashboard_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_dashboard_black_24dp.png deleted file mode 100644 index b832916f5..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_dashboard_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_google_drive.png b/app/src/main/res/drawable-hdpi/ic_google_drive.png deleted file mode 100644 index d885ecf8b..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_google_drive.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_star_black_18dp.png b/app/src/main/res/drawable-hdpi/ic_star_black_18dp.png new file mode 100644 index 000000000..4ea8d0cbc Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_star_black_18dp.png differ diff --git a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_focused_c.9.png b/app/src/main/res/drawable-mdpi-v14/appwidget_inner_focused_c.9.png deleted file mode 100644 index cc50fe9be..000000000 Binary files a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_focused_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_focused_l.9.png b/app/src/main/res/drawable-mdpi-v14/appwidget_inner_focused_l.9.png deleted file mode 100644 index feaa6c785..000000000 Binary files a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_focused_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_focused_r.9.png b/app/src/main/res/drawable-mdpi-v14/appwidget_inner_focused_r.9.png deleted file mode 100644 index 8d22c5617..000000000 Binary files a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_focused_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_pressed_c.9.png b/app/src/main/res/drawable-mdpi-v14/appwidget_inner_pressed_c.9.png deleted file mode 100644 index aa80a7c70..000000000 Binary files a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_pressed_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_pressed_l.9.png b/app/src/main/res/drawable-mdpi-v14/appwidget_inner_pressed_l.9.png deleted file mode 100644 index e49e8a9b8..000000000 Binary files a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_pressed_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_pressed_r.9.png b/app/src/main/res/drawable-mdpi-v14/appwidget_inner_pressed_r.9.png deleted file mode 100644 index a54ecd0da..000000000 Binary files a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_pressed_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/action_about.png b/app/src/main/res/drawable-mdpi/action_about.png deleted file mode 100644 index 7c57436fc..000000000 Binary files a/app/src/main/res/drawable-mdpi/action_about.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/action_help.png b/app/src/main/res/drawable-mdpi/action_help.png deleted file mode 100644 index 72edd5a76..000000000 Binary files a/app/src/main/res/drawable-mdpi/action_help.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/appwidget_dark_bg_pressed.9.png b/app/src/main/res/drawable-mdpi/appwidget_dark_bg_pressed.9.png deleted file mode 100644 index ca8d5ac22..000000000 Binary files a/app/src/main/res/drawable-mdpi/appwidget_dark_bg_pressed.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/appwidget_inner_focused_c.9.png b/app/src/main/res/drawable-mdpi/appwidget_inner_focused_c.9.png deleted file mode 100644 index 1450e65b1..000000000 Binary files a/app/src/main/res/drawable-mdpi/appwidget_inner_focused_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/appwidget_inner_focused_l.9.png b/app/src/main/res/drawable-mdpi/appwidget_inner_focused_l.9.png deleted file mode 100644 index 6e8f100e4..000000000 Binary files a/app/src/main/res/drawable-mdpi/appwidget_inner_focused_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/appwidget_inner_focused_r.9.png b/app/src/main/res/drawable-mdpi/appwidget_inner_focused_r.9.png deleted file mode 100644 index bc8757b88..000000000 Binary files a/app/src/main/res/drawable-mdpi/appwidget_inner_focused_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/appwidget_inner_pressed_c.9.png b/app/src/main/res/drawable-mdpi/appwidget_inner_pressed_c.9.png deleted file mode 100644 index bd542bac6..000000000 Binary files a/app/src/main/res/drawable-mdpi/appwidget_inner_pressed_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/appwidget_inner_pressed_l.9.png b/app/src/main/res/drawable-mdpi/appwidget_inner_pressed_l.9.png deleted file mode 100644 index 575ecf4e1..000000000 Binary files a/app/src/main/res/drawable-mdpi/appwidget_inner_pressed_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/appwidget_inner_pressed_r.9.png b/app/src/main/res/drawable-mdpi/appwidget_inner_pressed_r.9.png deleted file mode 100644 index 79eaea35a..000000000 Binary files a/app/src/main/res/drawable-mdpi/appwidget_inner_pressed_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/content_new_holo_dark.png b/app/src/main/res/drawable-mdpi/content_new_holo_dark.png deleted file mode 100644 index 4d5d484b3..000000000 Binary files a/app/src/main/res/drawable-mdpi/content_new_holo_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/drawer_shadow.9.png b/app/src/main/res/drawable-mdpi/drawer_shadow.9.png deleted file mode 100644 index ffe3a28d7..000000000 Binary files a/app/src/main/res/drawable-mdpi/drawer_shadow.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_clear_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_clear_black_24dp.png deleted file mode 100644 index 40a1a84e3..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_clear_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_dashboard_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_dashboard_black_24dp.png deleted file mode 100644 index c0cb8620d..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_dashboard_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_google_drive.png b/app/src/main/res/drawable-mdpi/ic_google_drive.png deleted file mode 100644 index 10db8093d..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_google_drive.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_star_black_18dp.png b/app/src/main/res/drawable-mdpi/ic_star_black_18dp.png new file mode 100644 index 000000000..b125aa041 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_star_black_18dp.png differ diff --git a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_focused_c.9.png b/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_focused_c.9.png deleted file mode 100644 index 0de253ca0..000000000 Binary files a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_focused_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_focused_l.9.png b/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_focused_l.9.png deleted file mode 100644 index ce9decd19..000000000 Binary files a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_focused_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_focused_r.9.png b/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_focused_r.9.png deleted file mode 100644 index 448cd8373..000000000 Binary files a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_focused_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_pressed_c.9.png b/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_pressed_c.9.png deleted file mode 100644 index defdbb9c0..000000000 Binary files a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_pressed_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_pressed_l.9.png b/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_pressed_l.9.png deleted file mode 100644 index 582d0e197..000000000 Binary files a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_pressed_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_pressed_r.9.png b/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_pressed_r.9.png deleted file mode 100644 index 9732dd7c5..000000000 Binary files a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_pressed_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/content_new_holo_dark.png b/app/src/main/res/drawable-xhdpi/content_new_holo_dark.png deleted file mode 100644 index 23b9a1c18..000000000 Binary files a/app/src/main/res/drawable-xhdpi/content_new_holo_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/drawer_shadow.9.png b/app/src/main/res/drawable-xhdpi/drawer_shadow.9.png deleted file mode 100644 index fabe9d965..000000000 Binary files a/app/src/main/res/drawable-xhdpi/drawer_shadow.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_clear_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_clear_black_24dp.png deleted file mode 100644 index 6bc437298..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_clear_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_dashboard_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_dashboard_black_24dp.png deleted file mode 100644 index ba2911e39..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_dashboard_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_google_drive.png b/app/src/main/res/drawable-xhdpi/ic_google_drive.png deleted file mode 100644 index 4bbb4b809..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_google_drive.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_star_black_18dp.png b/app/src/main/res/drawable-xhdpi/ic_star_black_18dp.png new file mode 100644 index 000000000..92a0f5862 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_star_black_18dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/drawer_shadow.9.png b/app/src/main/res/drawable-xxhdpi/drawer_shadow.9.png deleted file mode 100644 index b91e9d7f2..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/drawer_shadow.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_clear_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_clear_black_24dp.png deleted file mode 100644 index 51b4401ca..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_clear_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_dashboard_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_dashboard_black_24dp.png deleted file mode 100644 index ad14dfeb9..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_dashboard_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_star_black_18dp.png b/app/src/main/res/drawable-xxhdpi/ic_star_black_18dp.png new file mode 100644 index 000000000..4f67f9773 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_star_black_18dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_clear_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_clear_black_24dp.png deleted file mode 100644 index df42feecb..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_clear_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_dashboard_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_dashboard_black_24dp.png deleted file mode 100644 index 8fad114fe..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_dashboard_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_star_black_18dp.png b/app/src/main/res/drawable-xxxhdpi/ic_star_black_18dp.png new file mode 100644 index 000000000..54d306599 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_star_black_18dp.png differ diff --git a/app/src/main/res/drawable/appwidget_button_center.xml b/app/src/main/res/drawable/appwidget_button_center.xml deleted file mode 100644 index 06f5f5738..000000000 --- a/app/src/main/res/drawable/appwidget_button_center.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/appwidget_button_left.xml b/app/src/main/res/drawable/appwidget_button_left.xml deleted file mode 100644 index 7382f05ff..000000000 --- a/app/src/main/res/drawable/appwidget_button_left.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/appwidget_button_right.xml b/app/src/main/res/drawable/appwidget_button_right.xml deleted file mode 100644 index a81225917..000000000 --- a/app/src/main/res/drawable/appwidget_button_right.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout/account_spinner_dropdown_item.xml b/app/src/main/res/layout/account_spinner_dropdown_item.xml index f27051994..c32bfdc6a 100644 --- a/app/src/main/res/layout/account_spinner_dropdown_item.xml +++ b/app/src/main/res/layout/account_spinner_dropdown_item.xml @@ -17,7 +17,7 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/cardview_account.xml b/app/src/main/res/layout/cardview_account.xml index 7eb1ae8e6..cfae37339 100644 --- a/app/src/main/res/layout/cardview_account.xml +++ b/app/src/main/res/layout/cardview_account.xml @@ -95,7 +95,7 @@ android:id="@+id/account_balance" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:singleLine="true" + android:maxLines="1" android:layout_alignParentBottom="true" android:paddingBottom="4dp" android:textSize="18sp" diff --git a/app/src/main/res/layout/cardview_book.xml b/app/src/main/res/layout/cardview_book.xml index be292e8e1..e431019f5 100644 --- a/app/src/main/res/layout/cardview_book.xml +++ b/app/src/main/res/layout/cardview_book.xml @@ -38,12 +38,15 @@ android:layout_height="wrap_content" android:layout_width="match_parent" android:layout_toRightOf="@id/account_color_strip" + android:layout_toEndOf="@id/account_color_strip" android:layout_marginTop="6dp" android:layout_marginBottom="@dimen/dialog_padding" android:layout_marginLeft="@dimen/dialog_padding" - android:layout_marginRight="52dp"/> + android:layout_marginStart="@dimen/dialog_padding" + android:layout_marginRight="52dp" + android:layout_marginEnd="52dp" /> - - - + tools:text="Last export:" + android:maxLines="1" /> + tools:text="Sat, 04 July" + android:maxLines="1" /> \ No newline at end of file diff --git a/app/src/main/res/layout/cardview_budget.xml b/app/src/main/res/layout/cardview_budget.xml index 689d1da9c..e01dfeaff 100644 --- a/app/src/main/res/layout/cardview_budget.xml +++ b/app/src/main/res/layout/cardview_budget.xml @@ -48,7 +48,7 @@ android:id="@+id/budget_recurrence" android:layout_width="match_parent" android:layout_height="wrap_content" - android:singleLine="true" + android:maxLines="1" android:textSize="15sp" android:gravity="left|start" android:layout_marginLeft="@dimen/dialog_padding" @@ -83,7 +83,7 @@ android:id="@+id/budget_amount" android:layout_width="match_parent" android:layout_height="wrap_content" - android:singleLine="true" + android:maxLines="1" android:padding="5dp" android:textSize="18sp" android:gravity="right" diff --git a/app/src/main/res/layout/dialog_rename_book.xml b/app/src/main/res/layout/dialog_rename_book.xml new file mode 100644 index 000000000..7042b5c35 --- /dev/null +++ b/app/src/main/res/layout/dialog_rename_book.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/layout/drawer_section_header.xml b/app/src/main/res/layout/drawer_section_header.xml deleted file mode 100644 index 79927e90e..000000000 --- a/app/src/main/res/layout/drawer_section_header.xml +++ /dev/null @@ -1,9 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_account_detail.xml b/app/src/main/res/layout/fragment_account_detail.xml deleted file mode 100644 index 9cda56040..000000000 --- a/app/src/main/res/layout/fragment_account_detail.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - -