diff --git a/.gitignore b/.gitignore index 996ac1128..bd87d9b4f 100644 --- a/.gitignore +++ b/.gitignore @@ -21,5 +21,12 @@ out/ *.classpath # cpu profile generated by Android Studio /captures + gradle.properties + +#Crashlytics + crashlytics.properties +fabric.properties +app/fabric.properties +app/*.tap diff --git a/.travis.yml b/.travis.yml index c5d1cc4a3..7987359b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,32 +4,33 @@ android: components: - platform-tools - tools - - build-tools-22.0.1 - + - build-tools-23.0.1 + # The SDK version used to compile your project - - android-19 + - android-23 # Additional components - extra-android-support - extra-google-google_play_services - extra-google-m2repository - extra-android-m2repository - - addon-google_apis-google-19 + - addon-google_apis-google-23 # Specify at least one system image, # if you need to run emulator(s) during your tests - - sys-img-armeabi-v7a-android-19 + #- sys-img-armeabi-v7a-android-23 + # Emulator Management: Create, Start and Wait # Re-enable this when we figure out how to reliably build on Travis #before_script: # - mkdir sdcard # - mksdcard -l gnucash-sdcard 64M sdcard/gnucash-sdcard.img -# - echo no | android create avd --force -n test -t android-19 --abi armeabi-v7a +# - echo no | android create avd --force -n test -t android-23 --abi armeabi-v7a # - emulator -avd test -no-skin -no-audio -no-window -no-boot-anim -sdcard sdcard/gnucash-sdcard.img & # - android-wait-for-emulator # - adb shell input keyevent 82 & script: - ./gradlew build -# - ./gradlew connectedCheck \ No newline at end of file +# - ./gradlew connectedCheck diff --git a/CHANGELOG.md b/CHANGELOG.md index 55b0dd6af..9874ab9f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ Change Log =============================================================================== +Version 2.0.0 *(2015-11-01)* +---------------------------- +* Feature: Updated app design to use Material Design guidelines +* Feature: Setup wizard on first run of the application +* Feature: Support for multi-currency transactions +* Feature: New report summary page and more options for display/grouping reports +* Feature: Calculator keyboard when entering transactions +* Feature: Use appropriate decimal places per currency +* Feature: New help & feedback section with UserVoice +* Feature: New transaction detail view with running account balance +* Feature: Export/import commodity prices to/from GnuCash XML +* Feature: Prompt for rating the application after a number of starts +* Feature: Support for Android M permissions model +* Feature: New horizontal layout for account and transaction lists +* Feature: Automatic sending of crash reports with user permission (opt-in) +* Feature: Default transfer account setting propagates to child accounts +* Feature: Export transactions from a particular date +* Improved: Transactions are always balanced at the database layer before saving +* Improved: OFX export do not try to support double entry anymore +* Improved: Restructured the app settings categories +* Improved: Highlight active scheduled actions +* Improved: Restructured navigation drawer and added icons +* Improved: Currencies are listed sorted by currency code +* Improved: Show relative time in transaction list +* Improved: Added Portuguese translation +* Improved: Account balances are now computed faster (in parallel) +* Fixed: Data leak through app screenshot when passcode is set +* Fixed: Some inconsistencies when importing GnuCash XML +* Fixed: "Save" and "Cancel" transaction buttons not displayed in Gingerbread +* Fixed: Word-wrap on transaction type switch +* Fixed: Crash when restoring backups with poorly formatted amount strings + Version 1.6.4 *(2015-08-12)* ---------------------------- * Fixed: Crashes during backup restoration diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 45ba3eb9d..fb10c0231 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -1,16 +1,17 @@ -GnuCash for Android is a community effort which is made possible by the contributions of +GnuCash for Android is a community effort which is made possible by the contributions of several different people. -Appreciation goes to Muslim Chochlov and the to whole GnuCash community for guiding the +Appreciation goes to Muslim Chochlov and the to whole GnuCash community for guiding the project through the early phases and providing valuable feedback. -Lead Developer: +Maintainer: Ngewi Fet -Main contributors to core app experience: +Core contributors: Yongxin Wang Oleksandr Tyshkovets +Àlex Magaz Graça -The following people (in no particular order) contributed (code and translations) to GnuCash for Android: +The following people (in no particular order) contributed (patches and translations) to GnuCash Android: Christian Stimming Cristian Marchi Menelaos Maglis @@ -27,4 +28,4 @@ Alex Lei Matthew Hague Spanti Nicola Jesse Shieh -Àlex Magaz Graça \ No newline at end of file +Terry Chung diff --git a/README.md b/README.md index c95c260f0..568d49476 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ # Introduction GnuCash Android is a companion expense-tracker application for GnuCash (desktop) designed for Android. -It allows you to record transactions on-the-go and later import the data into GnuCash for the desktop. +It allows you to record transactions on-the-go and later import the data into GnuCash for the desktop. -The application supports Android 2.3.4 Gingerbread (API level 10) and above. +The application supports Android 2.3.3 Gingerbread (API level 10) and above. Features include: @@ -18,7 +18,7 @@ Features include: * 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. @@ -62,7 +62,7 @@ The app is configured to allow you to install a development and production versi ### With Android Studio The easiest way to build is to install [Android Studio](https://developer.android.com/sdk/index.html) v1.+ -with [Gradle](https://www.gradle.org/) v2.2.1. +with [Gradle](https://www.gradle.org/) v2.4. Once installed, then you can import the project into Android Studio: 1. Open `File` @@ -72,6 +72,10 @@ Once installed, then you can import the project into Android Studio: Then, Gradle will do everything for you. +## Support + +Google+ Community: https://plus.google.com/communities/104728406764752407046 + ## Contributing There are several ways you could contribute to the development. @@ -96,5 +100,5 @@ You may obtain a copy of the License at Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and +See the License for the specific language governing permissions and limitations under the License. diff --git a/app/build.gradle b/app/build.gradle index 6e7ba074f..2711a0384 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,15 +1,15 @@ import java.text.SimpleDateFormat apply plugin: 'com.android.application' -apply plugin: 'crashlytics' +apply plugin: 'io.fabric' -def versionMajor = 1 -def versionMinor = 6 -def versionPatch = 4 -def versionBuild = 0 +def versionMajor = 2 +def versionMinor = 0 +def versionPatch = 0 +def versionBuild = 7 def buildTime() { - def df = new SimpleDateFormat("yyyyMMdd") + def df = new SimpleDateFormat("yyyyMMdd HH:mm 'UTC'") df.setTimeZone(TimeZone.getTimeZone("UTC")) return df.format(new Date()) } @@ -20,18 +20,20 @@ def gitSha() { android { - compileSdkVersion 19 - buildToolsVersion "21.1.2" //maintain this version until we migrate to ActionBarCompat + compileSdkVersion 23 + buildToolsVersion "23.0.1" defaultConfig { applicationId "org.gnucash.android" testApplicationId 'org.gnucash.android.test' minSdkVersion 10 - targetSdkVersion 19 + targetSdkVersion 23 //robolectric tests only support up to API level 21 at the moment versionCode versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild versionName "${versionMajor}.${versionMinor}.${versionPatch}" resValue "string", "app_version_name", "${versionName}" resValue "string", "app_minor_version", "${versionMinor}" - buildConfigField "boolean", "USE_CRASHLYTICS", "false" + buildConfigField "boolean", "CAN_REQUEST_RATING", "false" + buildConfigField "String", "BUILD_TIME", "\"${buildTime()}\"" + if (project.hasProperty("RELEASE_DROPBOX_APP_KEY")){ resValue "string", "dropbox_app_key", RELEASE_DROPBOX_APP_KEY resValue "string", "dropbox_app_secret", RELEASE_DROPBOX_APP_SECRET @@ -108,11 +110,12 @@ android { resValue "string", "app_name", "GnuCash - beta" versionName "${versionMajor}.${versionMinor}.${versionPatch}-beta${versionBuild}" resValue "string", "app_version_name", "${versionName}" - buildConfigField "boolean", "USE_CRASHLYTICS", "true" } production { resValue "string", "app_name", "GnuCash" + buildConfigField "boolean", "CAN_REQUEST_RATING", "true" + ext.enableCrashlytics = false } } @@ -129,11 +132,18 @@ def adb = android.getAdbExe().toString() afterEvaluate { task grantAnimationPermissionDevel(type: Exec, dependsOn: 'installDevelopmentDebug') { // or install{productFlavour}{buildType} - commandLine "$adb shell pm grant $android.productFlavors.development.applicationId android.permission.SET_ANIMATION_SCALE".split(' ') + commandLine "$adb", 'devices' + standardOutput = new ByteArrayOutputStream() + + String output = standardOutput.toString() + output.eachLine { + def serial = it.split("\\s")[0] + commandLine "$adb -s $serial shell pm grant $android.productFlavors.development.applicationId android.permission.SET_ANIMATION_SCALE".split(' ') + } } task grantAnimationPermissionProduction(type: Exec, dependsOn: 'installProductionDebug'){ - commandLine "$adb shell pm grant $android.defaultConfig.applicationId android.permission.SET_ANIMATION_SCALE".split(' ') + commandLine "$adb -e shell pm grant $android.defaultConfig.applicationId android.permission.SET_ANIMATION_SCALE".split(' ') } // When launching individual tests from Android Studio, it seems that only the assemble tasks // get called directly, not the install* versions @@ -148,29 +158,55 @@ afterEvaluate { } } +def androidSupportVersion = "22.2.1" +def androidEspressoVersion = "2.2" +def androidSupportTestVersion = "0.3" dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile('com.android.support:support-v4:22.1.1', - 'com.actionbarsherlock:actionbarsherlock:4.4.0@aar', + compile('com.android.support:support-v4:' + androidSupportVersion, + 'com.android.support:appcompat-v7:' + androidSupportVersion, + 'com.android.support:design:' + androidSupportVersion, + 'com.android.support:cardview-v7:' + androidSupportVersion, + 'com.android.support:recyclerview-v7:' + androidSupportVersion, 'com.viewpagerindicator:library:2.4.1@aar', - 'com.doomonafireball.betterpickers:library:1.6.0', - 'com.commonsware.cwac:merge:1.1.+', - 'com.github.PhilJay:MPAndroidChart:v2.1.0', + 'com.code-troopers.betterpickers:library:2.0.3', + 'org.jraf:android-switch-backport:2.0.1@aar', + 'com.github.PhilJay:MPAndroidChart:v2.1.3', 'joda-time:joda-time:2.7', + 'org.ocpsoft.prettytime:prettytime:3.2.7.Final', 'com.google.android.gms:play-services-drive:7.0.0', - 'com.crashlytics.android:crashlytics:1.+' + 'com.jakewharton:butterknife:7.0.1', + 'com.kobakei:ratethisapp:0.0.3', + 'com.squareup:android-times-square:1.6.4@aar', + 'com.github.techfreak:wizardpager:1.0.0', + 'net.objecthunter:exp4j:0.4.5' ) - testCompile('org.robolectric:robolectric:3.0-rc2', + compile ('com.uservoice:uservoice-android-sdk:1.2.+') { + exclude module: 'commons-logging' + exclude module: 'httpcore' + exclude module: 'httpclient' + } + + compile('com.crashlytics.sdk.android:crashlytics:2.5.0@aar') { + transitive = true; + } + + testCompile('org.robolectric:robolectric:3.0', 'junit:junit:4.12', 'org.assertj:assertj-core:1.7.1' ) - androidTestCompile 'com.android.support.test:runner:0.2' - androidTestCompile 'com.android.support.test:rules:0.2' - androidTestCompile('com.squareup.assertj:assertj-android:1.0.0'){ + androidTestCompile ('com.android.support:support-annotations:' + androidSupportVersion, + 'com.android.support.test:runner:' + androidSupportTestVersion, + 'com.android.support.test:rules:' + androidSupportTestVersion, + 'com.android.support.test.espresso:espresso-core:' + androidEspressoVersion) + androidTestCompile ('com.android.support.test.espresso:espresso-contrib:' + androidEspressoVersion) { + exclude group: 'com.android.support', module: 'support-v4' + exclude module: 'recyclerview-v7' + } + + androidTestCompile('com.squareup.assertj:assertj-android:1.1.0'){ exclude group: 'com.android.support', module:'support-annotations' } - androidTestCompile ('com.android.support.test.espresso:espresso-core:2.1') - androidTestCompile 'com.android.support:support-annotations:22.1.1' -} \ No newline at end of file +} diff --git a/app/proguard-project.txt b/app/proguard-project.txt index 023f6a8b2..c5f28521d 100644 --- a/app/proguard-project.txt +++ b/app/proguard-project.txt @@ -23,6 +23,8 @@ protected Object[][] getContents(); } +-keep class android.support.v7.widget.SearchView { *; } + -keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable { public static final *** NULL; } diff --git a/app/project.properties b/app/project.properties index b1dd044be..216f29efc 100644 --- a/app/project.properties +++ b/app/project.properties @@ -11,7 +11,6 @@ #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt # Project target. -target=android-18 -android.library.reference.1=gen-external-apklibs/com.actionbarsherlock_actionbarsherlock_4.4.0 -android.library.reference.2=gen-external-apklibs/com.viewpagerindicator_library_2.4.1 - +target=android-21 +android.library.reference.1=build/intermediates/exploded-aar/com.android.support/support-v4/22.2.1 +android.library.reference.2=build/intermediates/exploded-aar/com.android.support/appcompat-v7/22.2.1 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 57df65076..e5723586c 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 @@ -24,11 +24,15 @@ import android.preference.PreferenceManager; import android.support.test.InstrumentationRegistry; import android.support.test.espresso.Espresso; +import android.support.test.espresso.ViewInteraction; +import android.support.test.espresso.matcher.ViewMatchers; import android.support.test.runner.AndroidJUnit4; import android.support.v4.app.Fragment; import android.test.ActivityInstrumentationTestCase2; import android.util.Log; +import com.kobakei.ratethisapp.RateThisApp; + import org.gnucash.android.R; import org.gnucash.android.db.AccountsDbAdapter; import org.gnucash.android.db.DatabaseHelper; @@ -53,9 +57,10 @@ import static android.support.test.espresso.Espresso.onData; import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; import static android.support.test.espresso.action.ViewActions.clearText; import static android.support.test.espresso.action.ViewActions.click; -import static android.support.test.espresso.action.ViewActions.longClick; +import static android.support.test.espresso.action.ViewActions.closeSoftKeyboard; import static android.support.test.espresso.action.ViewActions.scrollTo; import static android.support.test.espresso.action.ViewActions.swipeRight; import static android.support.test.espresso.action.ViewActions.typeText; @@ -63,6 +68,7 @@ import static android.support.test.espresso.matcher.ViewMatchers.isChecked; import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; import static android.support.test.espresso.matcher.ViewMatchers.isNotChecked; +import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.assertj.core.api.Assertions.assertThat; @@ -110,7 +116,7 @@ public void setUp() throws Exception { Account account = new Account(DUMMY_ACCOUNT_NAME); account.setUID(DUMMY_ACCOUNT_UID); account.setCurrency(Currency.getInstance(DUMMY_ACCOUNT_CURRENCY_CODE)); - mAccountsDbAdapter.addAccount(account); + mAccountsDbAdapter.addRecord(account); refreshAccountsList(); } @@ -119,6 +125,7 @@ public void setUp() throws Exception { * @param context Application context */ public static void preventFirstRunDialogs(Context context) { + AccountsActivity.rateAppConfig = new RateThisApp.Config(10000, 10000); Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); //do not show first run dialog @@ -132,39 +139,32 @@ public static void preventFirstRunDialogs(Context context) { editor.commit(); } - /* - public void testDisplayAccountsList(){ - final int NUMBER_OF_ACCOUNTS = 15; - for (int i = 0; i < NUMBER_OF_ACCOUNTS; i++) { - Account account = new Account("Acct " + i); - mAccountsDbAdapter.addAccount(account); - } - //there should exist a listview of accounts - refreshAccountsList(); - mSolo.waitForText("Acct"); - mSolo.scrollToBottom(); + public void testDisplayAccountsList(){ + AccountsActivity.createDefaultAccounts("EUR", mAcccountsActivity); + mAcccountsActivity.recreate(); + + refreshAccountsList(); + onView(withText("Assets")).perform(scrollTo()); + onView(withText("Expenses")).perform(click()); + onView(withText("Books")).perform(scrollTo()); + } - ListView accountsListView = (ListView) mSolo.getView(android.R.id.list); - assertNotNull(accountsListView); - assertEquals(NUMBER_OF_ACCOUNTS + 1, accountsListView.getCount()); - } - */ @Test public void testSearchAccounts(){ String SEARCH_ACCOUNT_NAME = "Search Account"; Account account = new Account(SEARCH_ACCOUNT_NAME); account.setParentUID(DUMMY_ACCOUNT_UID); - mAccountsDbAdapter.addAccount(account); + mAccountsDbAdapter.addRecord(account); //enter search query // ActionBarUtils.clickSherlockActionBarItem(mSolo, R.id.menu_search); onView(withId(R.id.menu_search)).perform(click()); - onView(withId(R.id.abs__search_src_text)).perform(typeText("Se")); + onView(withId(R.id.search_src_text)).perform(typeText("Se")); onView(withText(SEARCH_ACCOUNT_NAME)).check(matches(isDisplayed())); - onView(withId(R.id.abs__search_src_text)).perform(clearText()); + onView(withId(R.id.search_src_text)).perform(clearText()); onView(withId(R.id.primary_text)).check(matches(not(withText(SEARCH_ACCOUNT_NAME)))); } @@ -173,11 +173,11 @@ public void testSearchAccounts(){ */ @Test public void testCreateAccount(){ - onView(withId(R.id.menu_add_account)).check(matches(isDisplayed())).perform(click()); + onView(allOf(isDisplayed(), withId(R.id.fab_create_account))).perform(click()); String NEW_ACCOUNT_NAME = "A New Account"; - onView(withId(R.id.input_account_name)).perform(typeText(NEW_ACCOUNT_NAME)); - Espresso.closeSoftKeyboard(); + onView(withId(R.id.input_account_name)).perform(typeText(NEW_ACCOUNT_NAME), closeSoftKeyboard()); + sleep(1000); onView(withId(R.id.checkbox_placeholder_account)) .check(matches(isNotChecked())) .perform(click()); @@ -188,7 +188,7 @@ public void testCreateAccount(){ onView(withId(R.id.menu_save)).perform(click()); - List accounts = mAccountsDbAdapter.getAllAccounts(); + List accounts = mAccountsDbAdapter.getAllRecords(); assertThat(accounts).isNotNull(); assertThat(accounts).hasSize(2); Account newestAccount = accounts.get(0); //because of alphabetical sorting @@ -202,12 +202,13 @@ public void testCreateAccount(){ public void testChangeParentAccount() { final String accountName = "Euro Account"; Account account = new Account(accountName, Currency.getInstance("EUR")); - mAccountsDbAdapter.addAccount(account); + mAccountsDbAdapter.addRecord(account); refreshAccountsList(); - onView(withText(accountName)).perform(longClick()); - onView(withId(R.id.context_menu_edit_accounts)).perform(click()); + onView(withText(accountName)).perform(click()); + openActionBarOverflowOrOptionsMenu(mAcccountsActivity); + onView(withText(R.string.title_edit_account)).perform(click()); onView(withId(R.id.fragment_account_form)).check(matches(isDisplayed())); Espresso.closeSoftKeyboard(); onView(withId(R.id.checkbox_parent_account)).perform(scrollTo()) @@ -216,7 +217,7 @@ public void testChangeParentAccount() { onView(withId(R.id.menu_save)).perform(click()); - Account editedAccount = mAccountsDbAdapter.getAccount(account.getUID()); + Account editedAccount = mAccountsDbAdapter.getRecord(account.getUID()); String parentUID = editedAccount.getParentUID(); assertThat(parentUID).isNotNull(); @@ -232,16 +233,23 @@ public void testChangeParentAccount() { public void shouldHideParentAccountViewWhenNoParentsExist(){ onView(allOf(withText(DUMMY_ACCOUNT_NAME), isDisplayed())).perform(click()); onView(withId(R.id.fragment_transaction_list)).perform(swipeRight()); - onView(withText(R.string.label_create_account)).check(matches(isDisplayed())).perform(click()); + onView(withId(R.id.fab_create_transaction)).check(matches(isDisplayed())).perform(click()); sleep(1000); onView(withId(R.id.checkbox_parent_account)).check(matches(allOf(isChecked()))); onView(withId(R.id.input_account_name)).perform(typeText("Trading account")); + Espresso.closeSoftKeyboard(); + onView(withId(R.id.layout_parent_account)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))); + onView(withId(R.id.input_account_type_spinner)).perform(click()); + onData(allOf(is(instanceOf(String.class)), is(AccountType.TRADING.name()))).perform(click()); + + onView(withId(R.id.layout_parent_account)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))); onView(withId(R.id.layout_parent_account)).check(matches(not(isDisplayed()))); - onView(withId(R.id.menu_save)).perform(click()); + onView(withId(R.id.menu_save)).perform(click()); + sleep(1000); //no sub-accounts assertThat(mAccountsDbAdapter.getSubAccountCount(DUMMY_ACCOUNT_UID)).isEqualTo(0); assertThat(mAccountsDbAdapter.getSubAccountCount(mAccountsDbAdapter.getOrCreateGnuCashRootAccountUID())).isEqualTo(2); @@ -252,8 +260,8 @@ public void shouldHideParentAccountViewWhenNoParentsExist(){ public void testEditAccount(){ String editedAccountName = "Edited Account"; sleep(2000); - onView(withId(R.id.primary_text)).perform(longClick()); - onView(withId(R.id.context_menu_edit_accounts)).perform(click()); + onView(withId(R.id.options_menu)).perform(click()); + onView(withText(R.string.title_edit_account)).perform(click()); onView(withId(R.id.fragment_account_form)).check(matches(isDisplayed())); @@ -261,7 +269,7 @@ public void testEditAccount(){ onView(withId(R.id.menu_save)).perform(click()); - List accounts = mAccountsDbAdapter.getAllAccounts(); + List accounts = mAccountsDbAdapter.getAllRecords(); Account latest = accounts.get(0); //will be the first due to alphabetical sorting assertThat(latest.getName()).isEqualTo(editedAccountName); @@ -270,24 +278,26 @@ public void testEditAccount(){ @Test public void editingAccountShouldNotDeleteTransactions(){ - onView(allOf(withText(DUMMY_ACCOUNT_NAME), isDisplayed())) - .perform(longClick()); - Account account = new Account("Transfer Account"); + onView(allOf(withId(R.id.options_menu), isDisplayed())) + .perform(click()); + Account account = new Account("Transfer Account"); + account.setCurrency(DUMMY_ACCOUNT_CURRENCY); Transaction transaction = new Transaction("Simple trxn"); + transaction.setCurrencyCode(DUMMY_ACCOUNT_CURRENCY.getCurrencyCode()); Split split = new Split(new Money(BigDecimal.TEN, DUMMY_ACCOUNT_CURRENCY), account.getUID()); transaction.addSplit(split); transaction.addSplit(split.createPair(DUMMY_ACCOUNT_UID)); account.addTransaction(transaction); - mAccountsDbAdapter.addAccount(account); + mAccountsDbAdapter.addRecord(account); - assertThat(mAccountsDbAdapter.getAccount(DUMMY_ACCOUNT_UID).getTransactionCount()).isEqualTo(1); + assertThat(mAccountsDbAdapter.getRecord(DUMMY_ACCOUNT_UID).getTransactionCount()).isEqualTo(1); assertThat(mSplitsDbAdapter.getSplitsForTransaction(transaction.getUID())).hasSize(2); - onView(withId(R.id.context_menu_edit_accounts)).perform(click()); + onView(withText(R.string.title_edit_account)).perform(click()); onView(withId(R.id.menu_save)).perform(click()); - assertThat(mAccountsDbAdapter.getAccount(DUMMY_ACCOUNT_UID).getTransactionCount()).isEqualTo(1); + assertThat(mAccountsDbAdapter.getRecord(DUMMY_ACCOUNT_UID).getTransactionCount()).isEqualTo(1); assertThat(mSplitsDbAdapter.fetchSplitsForAccount(DUMMY_ACCOUNT_UID).getCount()).isEqualTo(1); assertThat(mSplitsDbAdapter.getSplitsForTransaction(transaction.getUID())).hasSize(2); @@ -309,8 +319,8 @@ private void sleep(long millis) { @Test(expected = IllegalArgumentException.class) public void testDeleteSimpleAccount() { sleep(2000); - onView(withId(R.id.primary_text)).perform(longClick()); - onView(withId(R.id.context_menu_delete)).perform(click()); + onView(withId(R.id.options_menu)).perform(click()); + onView(withText(R.string.menu_delete)).perform(click()); //the account has no sub-accounts // onView(withId(R.id.accounts_options)).check(matches(not(isDisplayed()))); @@ -335,13 +345,35 @@ public void testIntentAccountCreation(){ new AccountCreator().onReceive(mAcccountsActivity, intent); - Account account = mAccountsDbAdapter.getAccount("intent-account"); + Account account = mAccountsDbAdapter.getRecord("intent-account"); assertThat(account).isNotNull(); assertThat(account.getName()).isEqualTo("Intent Account"); assertThat(account.getUID()).isEqualTo("intent-account"); assertThat(account.getCurrency().getCurrencyCode()).isEqualTo("EUR"); } - + + /** + * Tests that the setup wizard is displayed on first run + */ + @Test + public void shouldShowWizardOnFirstRun() throws Throwable { + PreferenceManager.getDefaultSharedPreferences(mAcccountsActivity) + .edit() + .remove(mAcccountsActivity.getString(R.string.key_first_run)) + .commit(); + + runTestOnUiThread(new Runnable() { + @Override + public void run() { + mAcccountsActivity.recreate(); + } + }); + + //check that wizard is shown + onView(withText(mAcccountsActivity.getString(R.string.title_setup_gnucash))) + .check(matches(isDisplayed())); + } + @After public void tearDown() throws Exception { mAcccountsActivity.finish(); 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 15b191e89..1e56a232b 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 @@ -16,16 +16,22 @@ package org.gnucash.android.test.ui; +import android.Manifest; +import android.content.pm.PackageManager; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; +import android.os.Build; import android.preference.PreferenceManager; import android.support.test.InstrumentationRegistry; +import android.support.test.espresso.contrib.DrawerActions; +import android.support.test.espresso.matcher.ViewMatchers; import android.support.test.runner.AndroidJUnit4; import android.test.ActivityInstrumentationTestCase2; import android.util.Log; import android.widget.CompoundButton; import org.gnucash.android.R; +import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.db.AccountsDbAdapter; import org.gnucash.android.db.DatabaseHelper; import org.gnucash.android.db.ScheduledActionDbAdapter; @@ -51,9 +57,11 @@ import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.assertion.ViewAssertions.matches; import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; import static android.support.test.espresso.matcher.ViewMatchers.isEnabled; +import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.assertj.core.api.Assertions.assertThat; @@ -95,17 +103,18 @@ public void setUp() throws Exception { mAccountsDbAdapter = new AccountsDbAdapter(mDb, mTransactionsDbAdapter); mAccountsDbAdapter.deleteAllRecords(); - Account account = new Account("Exportable"); + Account account = new Account("Exportable"); Transaction transaction = new Transaction("Pizza"); transaction.setNote("What up?"); transaction.setTime(System.currentTimeMillis()); - Split split = new Split(new Money("8.99", "USD"), account.getUID()); + String currencyCode = GnuCashApplication.getDefaultCurrencyCode(); + Split split = new Split(new Money("8.99", currencyCode), account.getUID()); split.setMemo("Hawaii is the best!"); transaction.addSplit(split); - transaction.addSplit(split.createPair(mAccountsDbAdapter.getOrCreateImbalanceAccountUID(Currency.getInstance("USD")))); + transaction.addSplit(split.createPair(mAccountsDbAdapter.getOrCreateImbalanceAccountUID(Currency.getInstance(currencyCode)))); account.addTransaction(transaction); - mAccountsDbAdapter.addAccount(account); + mAccountsDbAdapter.addRecord(account); } @@ -117,7 +126,28 @@ public void setUp() throws Exception { */ @Test public void testOfxExport(){ + PreferenceManager.getDefaultSharedPreferences(mAcccountsActivity) + .edit().putBoolean(mAcccountsActivity.getString(R.string.key_use_double_entry), false) + .commit(); testExport(ExportFormat.OFX); + PreferenceManager.getDefaultSharedPreferences(mAcccountsActivity) + .edit().putBoolean(mAcccountsActivity.getString(R.string.key_use_double_entry), true) + .commit(); + } + + @Test + public void shouldNotOfferXmlExportInSingleEntryMode(){ + PreferenceManager.getDefaultSharedPreferences(mAcccountsActivity) + .edit().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))); + + PreferenceManager.getDefaultSharedPreferences(mAcccountsActivity) + .edit().putBoolean(mAcccountsActivity.getString(R.string.key_use_double_entry), true) + .commit(); } /** @@ -138,6 +168,16 @@ public void testXmlExport(){ * @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(android.R.id.button1)).perform(click()); + } + } + File folder = new File(Exporter.EXPORT_FOLDER_PATH); folder.mkdirs(); assertThat(folder).exists(); @@ -145,13 +185,12 @@ public void testExport(ExportFormat format){ for (File file : folder.listFiles()) { file.delete(); } - //legacy menu will be removed in the future - //onView(withId(R.id.menu_export)).perform(click()); - onView(withId(android.R.id.home)).perform(click()); + + DrawerActions.openDrawer(R.id.drawer_layout); onView(withText(R.string.nav_menu_export)).perform(click()); onView(withText(format.name())).perform(click()); - onView(withId(R.id.btn_save)).perform(click()); + onView(withId(R.id.menu_save)).perform(click()); assertThat(folder.listFiles().length).isEqualTo(1); File exportFile = folder.listFiles()[0]; @@ -165,7 +204,7 @@ public void testDeleteTransactionsAfterExport(){ PreferenceManager.getDefaultSharedPreferences(getActivity()).edit() .putBoolean(mAcccountsActivity.getString(R.string.key_delete_transactions_after_export), true).commit(); - testExport(ExportFormat.QIF); + testExport(ExportFormat.XML); assertThat(mTransactionsDbAdapter.getRecordsCount()).isEqualTo(0); PreferenceManager.getDefaultSharedPreferences(getActivity()).edit() @@ -178,7 +217,7 @@ public void testDeleteTransactionsAfterExport(){ */ @Test public void shouldCreateExportSchedule(){ - onView(withId(android.R.id.home)).perform(click()); + DrawerActions.openDrawer(R.id.drawer_layout); onView(withText(R.string.nav_menu_export)).perform(click()); onView(withText(ExportFormat.XML.name())).perform(click()); @@ -188,7 +227,7 @@ public void shouldCreateExportSchedule(){ onView(allOf(isAssignableFrom(CompoundButton.class), isDisplayed(), isEnabled())).perform(click()); onView(withText("Done")).perform(click()); - onView(withId(R.id.btn_save)).perform(click()); + onView(withId(R.id.menu_save)).perform(click()); ScheduledActionDbAdapter scheduledactionDbAdapter = new ScheduledActionDbAdapter(mDb); List scheduledActions = scheduledactionDbAdapter.getAllEnabledScheduledActions(); assertThat(scheduledActions) diff --git a/app/src/androidTest/java/org/gnucash/android/test/ui/FirstRunWizardActivityTest.java b/app/src/androidTest/java/org/gnucash/android/test/ui/FirstRunWizardActivityTest.java new file mode 100644 index 000000000..96de31a91 --- /dev/null +++ b/app/src/androidTest/java/org/gnucash/android/test/ui/FirstRunWizardActivityTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.gnucash.android.test.ui; + +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; +import android.test.ActivityInstrumentationTestCase2; +import android.util.Log; + +import org.gnucash.android.R; +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.DatabaseHelper; +import org.gnucash.android.db.SplitsDbAdapter; +import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.ui.wizard.FirstRunWizardActivity; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +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.withText; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests the first run wizard + * @author Ngewi Fet + */ +@RunWith(AndroidJUnit4.class) +public class FirstRunWizardActivityTest extends ActivityInstrumentationTestCase2{ + + private DatabaseHelper mDbHelper; + private SQLiteDatabase mDb; + private AccountsDbAdapter mAccountsDbAdapter; + private TransactionsDbAdapter mTransactionsDbAdapter; + private SplitsDbAdapter mSplitsDbAdapter; + + FirstRunWizardActivity mActivity; + public FirstRunWizardActivityTest() { + super(FirstRunWizardActivity.class); + } + + @Before + public void setUp() throws Exception { + super.setUp(); + injectInstrumentation(InstrumentationRegistry.getInstrumentation()); + + mActivity = getActivity(); + mDbHelper = new DatabaseHelper(mActivity); + try { + mDb = mDbHelper.getWritableDatabase(); + } catch (SQLException e) { + Log.e(getClass().getName(), "Error getting database: " + e.getMessage()); + mDb = mDbHelper.getReadableDatabase(); + } + mSplitsDbAdapter = new SplitsDbAdapter(mDb); + mTransactionsDbAdapter = new TransactionsDbAdapter(mDb, mSplitsDbAdapter); + mAccountsDbAdapter = new AccountsDbAdapter(mDb, mTransactionsDbAdapter); + mAccountsDbAdapter.deleteAllRecords(); + } + + + @Test + public void shouldRunWizardToEnd(){ + assertThat(mAccountsDbAdapter.getRecordsCount()).isEqualTo(0); + + onView(withId(R.id.btn_save)).perform(click()); + + onView(withText("EUR")).perform(click()); + onView(withText(R.string.btn_wizard_next)).perform(click()); + onView(withText(R.string.wizard_title_account_setup)).check(matches(isDisplayed())); + + onView(withText(R.string.wizard_option_create_default_accounts)).perform(click()); + + onView(withText(R.string.btn_wizard_next)).perform(click()); + onView(withText(R.string.wizard_option_auto_send_crash_reports)).perform(click()); + onView(withText(R.string.btn_wizard_next)).perform(click()); + + onView(withText(R.string.review)).check(matches(isDisplayed())); + + onView(withId(R.id.btn_save)).perform(click()); + + //default accounts should be created + assertThat(mAccountsDbAdapter.getRecordsCount()).isGreaterThan(60); + + boolean enableCrashlytics = GnuCashApplication.isCrashlyticsEnabled(); + assertThat(enableCrashlytics).isTrue(); + + String defaultCurrencyCode = GnuCashApplication.getDefaultCurrencyCode(); + assertThat(defaultCurrencyCode).isEqualTo("EUR"); + } + + @Test + public void shouldDisplayFullCurrencyList(){ + assertThat(mAccountsDbAdapter.getRecordsCount()).isEqualTo(0); + + onView(withId(R.id.btn_save)).perform(click()); + + onView(withText(R.string.wizard_option_currency_other)).perform(click()); + onView(withText(R.string.btn_wizard_next)).perform(click()); + onView(withText(R.string.wizard_title_select_currency)).check(matches(isDisplayed())); + +// onData(allOf(is(instanceOf(String.class)), is("CHF"))) +// .inAdapterView(withTagValue(is((Object)"currency_list_view"))).perform(click()); + onView(withText("AFA - Afghani")).perform(click()); + onView(withId(R.id.btn_save)).perform(click()); + + onView(withText(R.string.wizard_option_let_me_handle_it)).perform(click()); + + onView(withText(R.string.btn_wizard_next)).perform(click()); + onView(withText(R.string.wizard_option_disable_crash_reports)).perform(click()); + onView(withText(R.string.btn_wizard_next)).perform(click()); + + onView(withText(R.string.review)).check(matches(isDisplayed())); + onView(withId(R.id.btn_save)).perform(click()); + + //default accounts should not be created + assertThat(mAccountsDbAdapter.getRecordsCount()).isZero(); + + boolean enableCrashlytics = GnuCashApplication.isCrashlyticsEnabled(); + assertThat(enableCrashlytics).isFalse(); + + String defaultCurrencyCode = GnuCashApplication.getDefaultCurrencyCode(); + assertThat(defaultCurrencyCode).isEqualTo("AFA"); + } +} 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 new file mode 100644 index 000000000..59ef63c4f --- /dev/null +++ b/app/src/androidTest/java/org/gnucash/android/test/ui/PieChartReportTest.java @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2015 Oleksandr Tyshkovets + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.test.ui; + +import android.content.Intent; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.preference.PreferenceManager; +import android.support.test.InstrumentationRegistry; +import android.support.test.espresso.ViewAction; +import android.support.test.espresso.action.CoordinatesProvider; +import android.support.test.espresso.action.GeneralClickAction; +import android.support.test.espresso.action.Press; +import android.support.test.espresso.action.Tap; +import android.support.test.espresso.contrib.PickerActions; +import android.support.test.runner.AndroidJUnit4; +import android.test.ActivityInstrumentationTestCase2; +import android.util.Log; +import android.view.View; +import android.widget.DatePicker; + +import org.gnucash.android.R; +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.DatabaseHelper; +import org.gnucash.android.db.SplitsDbAdapter; +import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.importer.GncXmlImporter; +import org.gnucash.android.model.Account; +import org.gnucash.android.model.AccountType; +import org.gnucash.android.model.Money; +import org.gnucash.android.model.Split; +import org.gnucash.android.model.Transaction; +import org.gnucash.android.model.TransactionType; +import org.gnucash.android.ui.report.PieChartFragment; +import org.gnucash.android.ui.report.ReportsActivity; +import org.joda.time.LocalDateTime; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.math.BigDecimal; +import java.util.Currency; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.isEnabled; +import static android.support.test.espresso.matcher.ViewMatchers.withClassName; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; + +@RunWith(AndroidJUnit4.class) +public class PieChartReportTest extends ActivityInstrumentationTestCase2 { + + public static final String TAG = PieChartReportTest.class.getName(); + + private static final String TRANSACTION_NAME = "Pizza"; + private static final double TRANSACTION_AMOUNT = 9.99; + + private static final String TRANSACTION2_NAME = "1984"; + private static final double TRANSACTION2_AMOUNT = 12.49; + + private static final String TRANSACTION3_NAME = "Nice gift"; + private static final double TRANSACTION3_AMOUNT = 2000.00; + + private static final String CASH_IN_WALLET_ASSET_ACCOUNT_UID = "b687a487849470c25e0ff5aaad6a522b"; + + private static final String DINING_EXPENSE_ACCOUNT_UID = "62922c5ccb31d6198259739d27d858fe"; + private static final String DINING_EXPENSE_ACCOUNT_NAME = "Dining"; + + private static final String BOOKS_EXPENSE_ACCOUNT_UID = "a8b342435aceac7c3cac214f9385dd72"; + private static final String BOOKS_EXPENSE_ACCOUNT_NAME = "Books"; + + private static final String GIFTS_RECEIVED_INCOME_ACCOUNT_UID = "b01950c0df0890b6543209d51c8e0b0f"; + private static final String GIFTS_RECEIVED_INCOME_ACCOUNT_NAME = "Gifts Received"; + + public static final Currency CURRENCY = Currency.getInstance("USD"); + + private AccountsDbAdapter mAccountsDbAdapter; + private TransactionsDbAdapter mTransactionsDbAdapter; + + private ReportsActivity mReportsActivity; + + public PieChartReportTest() { + super(ReportsActivity.class); + } + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + injectInstrumentation(InstrumentationRegistry.getInstrumentation()); + + mReportsActivity = getActivity(); + + SQLiteDatabase db; + DatabaseHelper dbHelper = new DatabaseHelper(getInstrumentation().getTargetContext()); + try { + db = dbHelper.getWritableDatabase(); + } catch (SQLException e) { + Log.e(TAG, "Error getting database: " + e.getMessage()); + db = dbHelper.getReadableDatabase(); + } + mTransactionsDbAdapter = new TransactionsDbAdapter(db, new SplitsDbAdapter(db)); + mAccountsDbAdapter = new AccountsDbAdapter(db, mTransactionsDbAdapter); + mAccountsDbAdapter.deleteAllRecords(); + + PreferenceManager.getDefaultSharedPreferences(mReportsActivity).edit() + .putString(mReportsActivity.getString(R.string.key_default_currency), CURRENCY.getCurrencyCode()) + .commit(); + // creates default accounts + GncXmlImporter.parse(GnuCashApplication.getAppContext().getResources().openRawResource(R.raw.default_accounts)); + } + + /** + * Call this method in every tests after adding data + */ + private void getTestActivity() { + setActivityIntent(new Intent(Intent.ACTION_VIEW)); + mReportsActivity = getActivity(); + onView(withId(R.id.btn_pie_chart)).perform(click()); + } + + private void addTransactionForCurrentMonth() throws Exception { + Transaction transaction = new Transaction(TRANSACTION_NAME); + transaction.setTime(System.currentTimeMillis()); + + Split split = new Split(new Money(BigDecimal.valueOf(TRANSACTION_AMOUNT), CURRENCY), DINING_EXPENSE_ACCOUNT_UID); + split.setType(TransactionType.DEBIT); + + transaction.addSplit(split); + transaction.addSplit(split.createPair(CASH_IN_WALLET_ASSET_ACCOUNT_UID)); + + Account account = mAccountsDbAdapter.getRecord(DINING_EXPENSE_ACCOUNT_UID); + account.addTransaction(transaction); + mTransactionsDbAdapter.addRecord(transaction); + } + + private void addTransactionForPreviousMonth(int minusMonths) { + Transaction transaction = new Transaction(TRANSACTION2_NAME); + transaction.setTime(new LocalDateTime().minusMonths(minusMonths).toDate().getTime()); + + Split split = new Split(new Money(BigDecimal.valueOf(TRANSACTION2_AMOUNT), CURRENCY), BOOKS_EXPENSE_ACCOUNT_UID); + split.setType(TransactionType.DEBIT); + + transaction.addSplit(split); + transaction.addSplit(split.createPair(CASH_IN_WALLET_ASSET_ACCOUNT_UID)); + + Account account = mAccountsDbAdapter.getRecord(BOOKS_EXPENSE_ACCOUNT_UID); + account.addTransaction(transaction); + mTransactionsDbAdapter.addRecord(transaction); + } + + + @Test + public void testNoData() { + getTestActivity(); + onView(withId(R.id.pie_chart)).perform(click()); + onView(withId(R.id.selected_chart_slice)).check(matches(withText(R.string.label_select_pie_slice_to_see_details))); + } + + @Test + public void testSelectingValue() throws Exception { + addTransactionForCurrentMonth(); + addTransactionForPreviousMonth(1); + getTestActivity(); + + onView(withId(R.id.pie_chart)).perform(clickXY(Position.BEGIN, Position.MIDDLE)); + float percent = (float) (TRANSACTION_AMOUNT / (TRANSACTION_AMOUNT + TRANSACTION2_AMOUNT) * 100); + String selectedText = String.format(PieChartFragment.SELECTED_VALUE_PATTERN, DINING_EXPENSE_ACCOUNT_NAME, TRANSACTION_AMOUNT, percent); + onView(withId(R.id.selected_chart_slice)).check(matches(withText(selectedText))); + } + + @Test + public void testSpinner() throws Exception { + Split split = new Split(new Money(BigDecimal.valueOf(TRANSACTION3_AMOUNT), CURRENCY), GIFTS_RECEIVED_INCOME_ACCOUNT_UID); + Transaction transaction = new Transaction(TRANSACTION3_NAME); + transaction.addSplit(split); + transaction.addSplit(split.createPair(CASH_IN_WALLET_ASSET_ACCOUNT_UID)); + + mAccountsDbAdapter.getRecord(GIFTS_RECEIVED_INCOME_ACCOUNT_UID).addTransaction(transaction); + mTransactionsDbAdapter.addRecord(transaction); + + getTestActivity(); + + Thread.sleep(1000); + + onView(withId(R.id.report_account_type_spinner)).perform(click()); + onView(withText(AccountType.INCOME.name())).perform(click()); + + Thread.sleep(1000); + + onView(withId(R.id.pie_chart)).perform(click()); + String selectedText = String.format(PieChartFragment.SELECTED_VALUE_PATTERN, GIFTS_RECEIVED_INCOME_ACCOUNT_NAME, TRANSACTION3_AMOUNT, 100f); + onView(withId(R.id.selected_chart_slice)).check(matches(withText(selectedText))); + + onView(withId(R.id.report_account_type_spinner)).perform(click()); + onView(withText(AccountType.EXPENSE.name())).perform(click()); + + onView(withId(R.id.pie_chart)).perform(click()); + onView(withId(R.id.selected_chart_slice)).check(matches(withText(R.string.label_select_pie_slice_to_see_details))); + } + + public static ViewAction clickXY(final Position horizontal, final Position vertical){ + return new GeneralClickAction( + Tap.SINGLE, + new CoordinatesProvider() { + @Override + public float[] calculateCoordinates(View view) { + int[] xy = new int[2]; + view.getLocationOnScreen(xy); + + float x = horizontal.getPosition(xy[0], view.getWidth()); + float y = vertical.getPosition(xy[1], view.getHeight()); + return new float[]{x, y}; + } + }, + Press.FINGER); + } + + private enum Position { + BEGIN { + @Override + public float getPosition(int viewPos, int viewLength) { + return viewPos + (viewLength * 0.15f); + } + }, + MIDDLE { + @Override + public float getPosition(int viewPos, int viewLength) { + return viewPos + (viewLength * 0.5f); + } + }, + END { + @Override + public float getPosition(int viewPos, int viewLength) { + return viewPos + (viewLength * 0.85f); + } + }; + + abstract float getPosition(int widgetPos, int widgetLength); + } + + @Override + @After + public void tearDown() throws Exception { + mReportsActivity.finish(); + super.tearDown(); + } + +} 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 3934e6423..51d3a4813 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 @@ -24,6 +24,7 @@ import android.database.sqlite.SQLiteDatabase; import android.preference.PreferenceManager; import android.support.test.InstrumentationRegistry; +import android.support.test.espresso.Espresso; import android.support.test.runner.AndroidJUnit4; import android.test.ActivityInstrumentationTestCase2; import android.util.Log; @@ -40,7 +41,7 @@ import org.gnucash.android.model.Transaction; import org.gnucash.android.model.TransactionType; import org.gnucash.android.receivers.TransactionRecorder; -import org.gnucash.android.ui.UxArgument; +import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.transaction.TransactionFormFragment; import org.gnucash.android.ui.transaction.TransactionsActivity; import org.junit.After; @@ -58,7 +59,6 @@ import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.action.ViewActions.clearText; import static android.support.test.espresso.action.ViewActions.click; -import static android.support.test.espresso.action.ViewActions.longClick; import static android.support.test.espresso.action.ViewActions.typeText; import static android.support.test.espresso.assertion.ViewAssertions.matches; import static android.support.test.espresso.matcher.RootMatchers.withDecorView; @@ -127,12 +127,11 @@ public void setUp() throws Exception { account2.setUID(TRANSFER_ACCOUNT_UID); account2.setCurrency(Currency.getInstance(CURRENCY_CODE)); - long id1 = mAccountsDbAdapter.addAccount(account); - long id2 = mAccountsDbAdapter.addAccount(account2); - assertThat(id1).isGreaterThan(0); - assertThat(id2).isGreaterThan(0); + mAccountsDbAdapter.addRecord(account); + mAccountsDbAdapter.addRecord(account2); mTransaction = new Transaction(TRANSACTION_NAME); + mTransaction.setCurrencyCode(CURRENCY_CODE); mTransaction.setNote("What up?"); mTransaction.setTime(mTransactionTimeMillis); Split split = new Split(new Money(TRANSACTION_AMOUNT, CURRENCY_CODE), DUMMY_ACCOUNT_UID); @@ -142,7 +141,7 @@ public void setUp() throws Exception { mTransaction.addSplit(split.createPair(TRANSFER_ACCOUNT_UID)); account.addTransaction(mTransaction); - mTransactionsDbAdapter.addTransaction(mTransaction); + mTransactionsDbAdapter.addRecord(mTransaction); Intent intent = new Intent(Intent.ACTION_VIEW); intent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, DUMMY_ACCOUNT_UID); @@ -152,10 +151,7 @@ public void setUp() throws Exception { private void validateTransactionListDisplayed(){ - onView(withId(R.id.fragment_transaction_list)).check(matches(isDisplayed())); -// Fragment fragment = getActivity().getCurrentPagerFragment(); -// assertThat(fragment).isNotNull(); -// assertThat(fragment).isInstanceOf(TransactionsListFragment.class); + onView(withId(R.id.transaction_recycler_view)).check(matches(isDisplayed())); } private int getTransactionCount(){ @@ -175,14 +171,15 @@ public void testAddTransactionShouldRequireAmount(){ validateTransactionListDisplayed(); int beforeCount = mTransactionsDbAdapter.getTransactionsCount(DUMMY_ACCOUNT_UID); - onView(withId(R.id.menu_add_transaction)).perform(click()); + onView(withId(R.id.fab_create_transaction)).perform(click()); onView(withId(R.id.input_transaction_name)) .check(matches(isDisplayed())) .perform(typeText("Lunch")); onView(withId(R.id.menu_save)).perform(click()); - sleep(500); + onView(withText(R.string.title_add_transaction)).check(matches(isDisplayed())); + assertToastDisplayed(R.string.toast_transanction_amount_required); int afterCount = mTransactionsDbAdapter.getTransactionsCount(DUMMY_ACCOUNT_UID); @@ -204,7 +201,7 @@ private void sleep(long millis) { /** * Checks that a specific toast message is displayed - * @param toastString + * @param toastString String that should be displayed */ private void assertToastDisplayed(int toastString) { onView(withText(toastString)) @@ -236,7 +233,7 @@ public void testAddTransaction(){ setDefaultTransactionType(TransactionType.DEBIT); validateTransactionListDisplayed(); - onView(withId(R.id.menu_add_transaction)).perform(click()); + onView(withId(R.id.fab_create_transaction)).perform(click()); onView(withId(R.id.input_transaction_name)).perform(typeText("Lunch")); onView(withId(R.id.input_transaction_amount)).perform(typeText("899")); @@ -245,7 +242,7 @@ public void testAddTransaction(){ .perform(click()) .check(matches(withText(R.string.label_spend))); - String expectedValue = NumberFormat.getInstance().format(-8.99); + String expectedValue = NumberFormat.getInstance().format(-899); onView(withId(R.id.input_transaction_amount)).check(matches(withText(expectedValue))); int transactionsCount = getTransactionCount(); @@ -265,7 +262,7 @@ public void testAddTransaction(){ public void testEditTransaction(){ validateTransactionListDisplayed(); - onView(withText(TRANSACTION_NAME)).perform(click()); + onView(withId(R.id.edit_transaction)).perform(click()); validateEditTransactionFields(mTransaction); @@ -277,27 +274,27 @@ public void testEditTransaction(){ * Tests that transactions splits are automatically balanced and an imbalance account will be created * This test case assumes that single entry is used */ - @Test + //TODO: move this to the unit tests public void testAutoBalanceTransactions(){ setDoubleEntryEnabled(false); mTransactionsDbAdapter.deleteAllRecords(); - assertThat(mTransactionsDbAdapter.getTotalTransactionsCount()).isEqualTo(0); + assertThat(mTransactionsDbAdapter.getRecordsCount()).isEqualTo(0); String imbalanceAcctUID = mAccountsDbAdapter.getImbalanceAccountUID(Currency.getInstance(CURRENCY_CODE)); assertThat(imbalanceAcctUID).isNull(); validateTransactionListDisplayed(); - onView(withId(R.id.menu_add_transaction)).perform(click()); + onView(withId(R.id.fab_create_transaction)).perform(click()); onView(withId(R.id.fragment_transaction_form)).check(matches(isDisplayed())); onView(withId(R.id.input_transaction_name)).perform(typeText("Autobalance")); onView(withId(R.id.input_transaction_amount)).perform(typeText("499")); //no double entry so no split editor - onView(withId(R.id.btn_open_splits)).check(matches(not(isDisplayed()))); + //TODO: check that the split drawable is not displayed onView(withId(R.id.menu_save)).perform(click()); - assertThat(mTransactionsDbAdapter.getTotalTransactionsCount()).isEqualTo(1); + assertThat(mTransactionsDbAdapter.getRecordsCount()).isEqualTo(1); Transaction transaction = mTransactionsDbAdapter.getAllTransactions().get(0); assertThat(transaction.getSplits()).hasSize(2); imbalanceAcctUID = mAccountsDbAdapter.getImbalanceAccountUID(Currency.getInstance(CURRENCY_CODE)); @@ -312,6 +309,7 @@ public void testAutoBalanceTransactions(){ /** * Tests input of transaction splits using the split editor. * Also validates that the imbalance from the split editor will be automatically added as a split + * //FIXME: find a more reliable way to test opening of the split editor */ @Test public void testSplitEditor(){ @@ -324,23 +322,20 @@ public void testSplitEditor(){ assertThat(imbalanceAcctUID).isNull(); validateTransactionListDisplayed(); - onView(withId(R.id.menu_add_transaction)).perform(click()); + onView(withId(R.id.fab_create_transaction)).perform(click()); onView(withId(R.id.input_transaction_name)).perform(typeText("Autobalance")); onView(withId(R.id.input_transaction_amount)).perform(typeText("499")); - onView(withId(R.id.btn_open_splits)).perform(click()); + onView(withId(R.id.btn_split_editor)).perform(click()); onView(withId(R.id.split_list_layout)).check(matches(allOf(isDisplayed(), hasDescendant(withId(R.id.input_split_amount))))); - //TODO: enable this assert when we fix the sign of amounts in split editor - - onView(withId(R.id.btn_add_split)).perform(click()); + onView(withId(R.id.menu_add_split)).perform(click()); onView(allOf(withId(R.id.input_split_amount), withText(""))).perform(typeText("400")); - onView(withId(R.id.imbalance_textview)).check(matches(withText("-0.99 $"))); - onView(withId(R.id.btn_save)).perform(click()); + onView(withId(R.id.menu_save)).perform(click()); //after we use split editor, we should not be able to toggle the transaction type onView(withId(R.id.input_transaction_type)).check(matches(not(isDisplayed()))); @@ -364,7 +359,7 @@ public void testSplitEditor(){ assertThat(imbalanceSplits).hasSize(1); Split split = imbalanceSplits.get(0); - assertThat(split.getAmount().toPlainString()).isEqualTo("0.99"); + assertThat(split.getValue().asBigDecimal()).isEqualTo(new BigDecimal("99.00")); assertThat(split.getType()).isEqualTo(TransactionType.CREDIT); } @@ -380,17 +375,16 @@ private void setDoubleEntryEnabled(boolean enabled){ public void testDefaultTransactionType(){ setDefaultTransactionType(TransactionType.CREDIT); - onView(withId(R.id.menu_add_transaction)).perform(click()); + onView(withId(R.id.fab_create_transaction)).perform(click()); onView(withId(R.id.input_transaction_type)).check(matches(allOf(isChecked(), withText(R.string.label_spend)))); - onView(withId(R.id.menu_cancel)).perform(click()); - + Espresso.pressBack(); //now validate the other case setDefaultTransactionType(TransactionType.DEBIT); - onView(withId(R.id.menu_add_transaction)).perform(click()); + onView(withId(R.id.fab_create_transaction)).perform(click()); onView(withId(R.id.input_transaction_type)).check(matches(allOf(not(isChecked()), withText(R.string.label_receive)))); - onView(withId(R.id.menu_cancel)).perform(click()); + Espresso.pressBack(); } private void setDefaultTransactionType(TransactionType type) { @@ -403,12 +397,12 @@ private void setDefaultTransactionType(TransactionType type) { //FIXME: Improve on this test public void childAccountsShouldUseParentTransferAccountSetting(){ Account transferAccount = new Account("New Transfer Acct"); - mAccountsDbAdapter.addAccount(transferAccount); - mAccountsDbAdapter.addAccount(new Account("Higher account")); + mAccountsDbAdapter.addRecord(transferAccount); + mAccountsDbAdapter.addRecord(new Account("Higher account")); Account childAccount = new Account("Child Account"); childAccount.setParentUID(DUMMY_ACCOUNT_UID); - mAccountsDbAdapter.addAccount(childAccount); + mAccountsDbAdapter.addRecord(childAccount); ContentValues contentValues = new ContentValues(); contentValues.put(DatabaseSchema.AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID, transferAccount.getUID()); mAccountsDbAdapter.updateRecord(DUMMY_ACCOUNT_UID, contentValues); @@ -431,7 +425,7 @@ public void childAccountsShouldUseParentTransferAccountSetting(){ @Test public void testToggleTransactionType(){ validateTransactionListDisplayed(); - onView(withText(TRANSACTION_NAME)).perform(click()); + onView(withId(R.id.edit_transaction)).perform(click()); validateEditTransactionFields(mTransaction); @@ -454,8 +448,7 @@ public void testToggleTransactionType(){ public void testOpenTransactionEditShouldNotModifyTransaction(){ validateTransactionListDisplayed(); - onView(withText(TRANSACTION_NAME)).perform(click()); - + onView(withId(R.id.edit_transaction)).perform(click()); validateTimeInput(mTransactionTimeMillis); clickOnView(R.id.menu_save); @@ -475,36 +468,13 @@ public void testOpenTransactionEditShouldNotModifyTransaction(){ @Test public void testDeleteTransaction(){ - onView(withId(R.id.primary_text)).perform(longClick()); - clickOnView(R.id.context_menu_delete); + onView(withId(R.id.options_menu)).perform(click()); + onView(withText(R.string.menu_delete)).perform(click()); long id = mAccountsDbAdapter.getID(DUMMY_ACCOUNT_UID); assertEquals(0, mTransactionsDbAdapter.getTransactionsCount(id)); } - @Test - public void testBulkMoveTransactions(){ - String targetAccountName = "Target"; - Account account = new Account(targetAccountName); - account.setCurrency(Currency.getInstance(Locale.getDefault())); - mAccountsDbAdapter.addAccount(account); - - int beforeOriginCount = mAccountsDbAdapter.getAccount(DUMMY_ACCOUNT_UID).getTransactionCount(); - - validateTransactionListDisplayed(); - - clickOnView(R.id.checkbox_transaction); - clickOnView(R.id.context_menu_move_transactions); - - clickOnView(R.id.btn_save); - - int targetCount = mAccountsDbAdapter.getAccount(account.getUID()).getTransactionCount(); - assertThat(targetCount).isEqualTo(1); - - int afterOriginCount = mAccountsDbAdapter.getAccount(DUMMY_ACCOUNT_UID).getTransactionCount(); - assertThat(afterOriginCount).isEqualTo(beforeOriginCount-1); - } - //TODO: add normal transaction recording @Test public void testLegacyIntentTransactionRecording(){ @@ -516,12 +486,13 @@ public void testLegacyIntentTransactionRecording(){ transactionIntent.putExtra(Transaction.EXTRA_AMOUNT, new BigDecimal(4.99)); transactionIntent.putExtra(Transaction.EXTRA_ACCOUNT_UID, DUMMY_ACCOUNT_UID); transactionIntent.putExtra(Transaction.EXTRA_TRANSACTION_TYPE, TransactionType.DEBIT.name()); + transactionIntent.putExtra(Account.EXTRA_CURRENCY_CODE, "USD"); new TransactionRecorder().onReceive(mTransactionsActivity, transactionIntent); int afterCount = mTransactionsDbAdapter.getTransactionsCount(DUMMY_ACCOUNT_UID); - assertEquals(beforeCount + 1, afterCount); + assertThat(beforeCount + 1).isEqualTo(afterCount); List transactions = mTransactionsDbAdapter.getAllTransactionsForAccount(DUMMY_ACCOUNT_UID); diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3448eadb4..b35c4450d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -49,8 +49,8 @@ + android:label="@string/app_name" + android:theme="@style/Theme.GnucashTheme.NoActionBar"> @@ -62,19 +62,19 @@ - + - + - + - + - - - - + + @@ -96,10 +96,14 @@ - - - - + + + + - - \ No newline at end of file diff --git a/app/src/main/java/org/gnucash/android/app/GnuCashApplication.java b/app/src/main/java/org/gnucash/android/app/GnuCashApplication.java index d7bf90819..cadc03f7c 100644 --- a/app/src/main/java/org/gnucash/android/app/GnuCashApplication.java +++ b/app/src/main/java/org/gnucash/android/app/GnuCashApplication.java @@ -23,15 +23,20 @@ import android.content.SharedPreferences; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; -import android.os.Build; +import android.graphics.Color; import android.preference.PreferenceManager; import android.util.Log; + import com.crashlytics.android.Crashlytics; +import com.crashlytics.android.core.CrashlyticsCore; +import com.uservoice.uservoicesdk.Config; +import com.uservoice.uservoicesdk.UserVoice; -import org.gnucash.android.BuildConfig; import org.gnucash.android.R; import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.CommoditiesDbAdapter; import org.gnucash.android.db.DatabaseHelper; +import org.gnucash.android.db.PricesDbAdapter; import org.gnucash.android.db.ScheduledActionDbAdapter; import org.gnucash.android.db.SplitsDbAdapter; import org.gnucash.android.db.TransactionsDbAdapter; @@ -40,6 +45,8 @@ import java.util.Currency; import java.util.Locale; +import io.fabric.sdk.android.Fabric; + /** * An {@link Application} subclass for retrieving static context * @author Ngewi Fet @@ -71,14 +78,35 @@ public class GnuCashApplication extends Application{ private static ScheduledActionDbAdapter mScheduledActionDbAdapter; + private static CommoditiesDbAdapter mCommoditiesDbAdapter; + + private static PricesDbAdapter mPricesDbAdapter; + + /** + * Returns darker version of specified color. + * Use for theming the status bar color when setting the color of the actionBar + */ + public static int darken(int color) { + float[] hsv = new float[3]; + Color.colorToHSV(color, hsv); + hsv[2] *= 0.8f; // value component + return Color.HSVToColor(hsv); + } + @Override public void onCreate(){ super.onCreate(); GnuCashApplication.context = getApplicationContext(); - //TODO: in production, only start logging if user gave consent - if (BuildConfig.USE_CRASHLYTICS) - Crashlytics.start(this); + Fabric.with(this, new Crashlytics.Builder().core( + new CrashlyticsCore.Builder().disabled(!isCrashlyticsEnabled()).build()).build()); + + // Set this up once when your application launches + Config config = new Config("gnucash.uservoice.com"); + config.setTopicId(107400); + config.setForumId(320493); + // config.identifyUser("USER_ID", "User Name", "email@example.com"); + UserVoice.init(config, this); mDbHelper = new DatabaseHelper(getApplicationContext()); try { @@ -88,10 +116,12 @@ public void onCreate(){ Log.e(getClass().getName(), "Error getting database: " + e.getMessage()); mDb = mDbHelper.getReadableDatabase(); } - mSplitsDbAdapter = new SplitsDbAdapter(mDb); - mTransactionsDbAdapter = new TransactionsDbAdapter(mDb, mSplitsDbAdapter); - mAccountsDbAdapter = new AccountsDbAdapter(mDb, mTransactionsDbAdapter); - mScheduledActionDbAdapter = new ScheduledActionDbAdapter(mDb); + mSplitsDbAdapter = new SplitsDbAdapter(mDb); + mTransactionsDbAdapter = new TransactionsDbAdapter(mDb, mSplitsDbAdapter); + mAccountsDbAdapter = new AccountsDbAdapter(mDb, mTransactionsDbAdapter); + mScheduledActionDbAdapter = new ScheduledActionDbAdapter(mDb); + mCommoditiesDbAdapter = new CommoditiesDbAdapter(mDb); + mPricesDbAdapter = new PricesDbAdapter(mDb); } public static AccountsDbAdapter getAccountsDbAdapter() { @@ -110,6 +140,14 @@ public static ScheduledActionDbAdapter getScheduledEventDbAdapter(){ return mScheduledActionDbAdapter; } + public static CommoditiesDbAdapter getCommoditiesDbAdapter(){ + return mCommoditiesDbAdapter; + } + + public static PricesDbAdapter getPricesDbAdapter(){ + return mPricesDbAdapter; + } + /** * Returns the application context * @return Application {@link Context} object diff --git a/app/src/main/java/org/gnucash/android/db/AccountsDbAdapter.java b/app/src/main/java/org/gnucash/android/db/AccountsDbAdapter.java index d536727eb..30fc1093d 100644 --- a/app/src/main/java/org/gnucash/android/db/AccountsDbAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/AccountsDbAdapter.java @@ -22,10 +22,12 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteStatement; +import android.graphics.Color; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; + import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.model.Account; @@ -53,7 +55,7 @@ * @author Yongxin Wang * @author Oleksandr Tyshkovets */ -public class AccountsDbAdapter extends DatabaseAdapter { +public class AccountsDbAdapter extends DatabaseAdapter { /** * Separator used for account name hierarchies between parent and child accounts */ @@ -71,8 +73,6 @@ public class AccountsDbAdapter extends DatabaseAdapter { */ private final TransactionsDbAdapter mTransactionsAdapter; -// private static String mImbalanceAccountPrefix = GnuCashApplication.getAppContext().getString(R.string.imbalance_account_name) + "-"; - /** * Overloaded constructor. Creates an adapter for an already open database * @param db SQliteDatabase instance @@ -93,114 +93,117 @@ public static AccountsDbAdapter getInstance(){ /** * Adds an account to the database. - * If an account already exists in the database with the same unique ID, - * then just update that account. + * If an account already exists in the database with the same GUID, it is replaced. * @param account {@link Account} to be inserted to database * @return Database row ID of the inserted account */ - public long addAccount(Account account){ - ContentValues contentValues = getContentValues(account); - contentValues.put(AccountEntry.COLUMN_NAME, account.getName()); - contentValues.put(AccountEntry.COLUMN_TYPE, account.getAccountType().name()); - contentValues.put(AccountEntry.COLUMN_CURRENCY, account.getCurrency().getCurrencyCode()); - contentValues.put(AccountEntry.COLUMN_PLACEHOLDER, account.isPlaceholderAccount() ? 1 : 0); - contentValues.put(AccountEntry.COLUMN_HIDDEN, account.isHidden() ? 1 : 0); - if (account.getColorHexCode() != null) { - contentValues.put(AccountEntry.COLUMN_COLOR_CODE, account.getColorHexCode()); - } else { - contentValues.putNull(AccountEntry.COLUMN_COLOR_CODE); - } - contentValues.put(AccountEntry.COLUMN_FAVORITE, account.isFavorite() ? 1 : 0); - contentValues.put(AccountEntry.COLUMN_FULL_NAME, account.getFullName()); - String parentAccountUID = account.getParentUID(); - if (parentAccountUID == null && account.getAccountType() != AccountType.ROOT) { - parentAccountUID = getOrCreateGnuCashRootAccountUID(); - } - contentValues.put(AccountEntry.COLUMN_PARENT_ACCOUNT_UID, parentAccountUID); - - if (account.getDefaultTransferAccountUID() != null) { - contentValues.put(AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID, account.getDefaultTransferAccountUID()); - } else { - contentValues.putNull(AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID); - } - + @Override + public void addRecord(@NonNull Account account){ Log.d(LOG_TAG, "Replace account to db"); - long rowId = mDb.replace(AccountEntry.TABLE_NAME, null, contentValues); - + //in-case the account already existed, we want to update the templates based on it as well + List templateTransactions = mTransactionsAdapter.getScheduledTransactionsForAccount(account.getUID()); + super.addRecord(account); + String accountUID = account.getUID(); //now add transactions if there are any - if (rowId > 0 && account.getAccountType() != AccountType.ROOT){ + if (account.getAccountType() != AccountType.ROOT){ //update the fully qualified account name - updateAccount(rowId, AccountEntry.COLUMN_FULL_NAME, getFullyQualifiedAccountName(rowId)); - for (Transaction t : account.getTransactions()) { - mTransactionsAdapter.addTransaction(t); + updateRecord(accountUID, AccountEntry.COLUMN_FULL_NAME, getFullyQualifiedAccountName(accountUID)); + String commodityUID = getCommodityUID(account.getCurrency().getCurrencyCode()); + for (Transaction t : account.getTransactions()) { + t.setCommodityUID(commodityUID); + mTransactionsAdapter.addRecord(t); } - } - return rowId; + for (Transaction transaction : templateTransactions) { + mTransactionsAdapter.addRecord(transaction); + } + } } /** - * Adds some accounts to the database. - * If an account already exists in the database with the same unique ID, - * then just update that account. This function will NOT try to determine the full name + * Adds some accounts and their transactions to the database in bulk. + *

If an account already exists in the database with the same GUID, it is replaced. + * This function will NOT try to determine the full name * of the accounts inserted, full names should be generated prior to the insert. - * All or none of the accounts will be inserted; + *
All or none of the accounts will be inserted;

* @param accountList {@link Account} to be inserted to database * @return number of rows inserted */ - public long bulkAddAccounts(List accountList){ + @Override + public long bulkAddRecords(@NonNull List accountList){ + //scheduled transactions are not fetched from the database when getting account transactions + //so we retrieve those which affect this account and then re-save them later + //this is necessary because the database has ON DELETE CASCADE between accounts and splits + //and all accounts are editing via SQL REPLACE + List transactionList = new ArrayList<>(accountList.size()*2); - long nRow = 0; - try { - mDb.beginTransaction(); - SQLiteStatement replaceStatement = mDb.compileStatement("REPLACE INTO " + AccountEntry.TABLE_NAME + " ( " - + AccountEntry.COLUMN_UID + " , " - + AccountEntry.COLUMN_NAME + " , " - + AccountEntry.COLUMN_TYPE + " , " - + AccountEntry.COLUMN_CURRENCY + " , " - + AccountEntry.COLUMN_COLOR_CODE + " , " - + AccountEntry.COLUMN_FAVORITE + " , " - + AccountEntry.COLUMN_FULL_NAME + " , " - + AccountEntry.COLUMN_PLACEHOLDER + " , " - + AccountEntry.COLUMN_CREATED_AT + " , " - + AccountEntry.COLUMN_HIDDEN + " , " - + AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " , " - + AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID + " ) VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? )"); - for (Account account:accountList) { - replaceStatement.clearBindings(); - replaceStatement.bindString(1, account.getUID()); - replaceStatement.bindString(2, account.getName()); - replaceStatement.bindString(3, account.getAccountType().name()); - replaceStatement.bindString(4, account.getCurrency().getCurrencyCode()); - if (account.getColorHexCode() != null) { - replaceStatement.bindString(5, account.getColorHexCode()); - } - replaceStatement.bindLong(6, account.isFavorite() ? 1 : 0); - replaceStatement.bindString(7, account.getFullName()); - replaceStatement.bindLong(8, account.isPlaceholderAccount() ? 1 : 0); - replaceStatement.bindString(9, account.getCreatedTimestamp().toString()); - replaceStatement.bindLong(10, account.isHidden() ? 1 : 0); - if (account.getParentUID() != null) { - replaceStatement.bindString(11, account.getParentUID()); - } - if (account.getDefaultTransferAccountUID() != null) { - replaceStatement.bindString(12, account.getDefaultTransferAccountUID()); - } - //Log.d(LOG_TAG, "Replacing account in db"); - replaceStatement.execute(); - nRow ++; - transactionList.addAll(account.getTransactions()); - } - mDb.setTransactionSuccessful(); - } - finally { - mDb.endTransaction(); + for (Account account : accountList) { + transactionList.addAll(account.getTransactions()); + transactionList.addAll(mTransactionsAdapter.getScheduledTransactionsForAccount(account.getUID())); } + long nRow = super.bulkAddRecords(accountList); if (nRow > 0 && !transactionList.isEmpty()){ - mTransactionsAdapter.bulkAddTransactions(transactionList); + mTransactionsAdapter.bulkAddRecords(transactionList); } return nRow; } + + @Override + protected SQLiteStatement compileReplaceStatement(@NonNull final Account account) { + if (mReplaceStatement == null){ + mReplaceStatement = mDb.compileStatement("REPLACE INTO " + AccountEntry.TABLE_NAME + " ( " + + AccountEntry.COLUMN_UID + " , " + + AccountEntry.COLUMN_NAME + " , " + + AccountEntry.COLUMN_DESCRIPTION + " , " + + AccountEntry.COLUMN_TYPE + " , " + + AccountEntry.COLUMN_CURRENCY + " , " + + AccountEntry.COLUMN_COLOR_CODE + " , " + + AccountEntry.COLUMN_FAVORITE + " , " + + AccountEntry.COLUMN_FULL_NAME + " , " + + AccountEntry.COLUMN_PLACEHOLDER + " , " + + AccountEntry.COLUMN_CREATED_AT + " , " + + AccountEntry.COLUMN_HIDDEN + " , " + + AccountEntry.COLUMN_COMMODITY_UID + " , " + + AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " , " + + AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID + " ) VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ?, ?)"); + //commodity_uid is not forgotten. It will be inserted by a database trigger + } + + mReplaceStatement.clearBindings(); + mReplaceStatement.bindString(1, account.getUID()); + mReplaceStatement.bindString(2, account.getName()); + if (account.getDescription() != null) + mReplaceStatement.bindString(3, account.getDescription()); + mReplaceStatement.bindString(4, account.getAccountType().name()); + mReplaceStatement.bindString(5, account.getCurrency().getCurrencyCode()); + if (account.getColorHexCode() != null) { + mReplaceStatement.bindString(6, account.getColorHexCode()); + } + mReplaceStatement.bindLong(7, account.isFavorite() ? 1 : 0); + mReplaceStatement.bindString(8, account.getFullName()); + mReplaceStatement.bindLong(9, account.isPlaceholderAccount() ? 1 : 0); + mReplaceStatement.bindString(10, account.getCreatedTimestamp().toString()); + mReplaceStatement.bindLong(11, account.isHidden() ? 1 : 0); + String commodityUID = account.getCommodityUID(); + if (commodityUID == null) + commodityUID = CommoditiesDbAdapter.getInstance().getCommodityUID(account.getCurrency().getCurrencyCode()); + + mReplaceStatement.bindString(12, commodityUID); + + String parentAccountUID = account.getParentUID(); + if (parentAccountUID == null && account.getAccountType() != AccountType.ROOT) { + parentAccountUID = getOrCreateGnuCashRootAccountUID(); + } + if (parentAccountUID != null) { + mReplaceStatement.bindString(13, parentAccountUID); + } + if (account.getDefaultTransferAccountUID() != null) { + mReplaceStatement.bindString(14, account.getDefaultTransferAccountUID()); + } + + return mReplaceStatement; + } + /** * Marks all transactions for a given account as exported * @param accountUID Unique ID of the record to be marked as exported @@ -222,7 +225,7 @@ public int markAsExported(String accountUID){ AccountEntry.COLUMN_UID + " WHERE " + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_UID + " = ? " + " ) ", - new String[] {accountUID} + new String[]{accountUID} ); } @@ -370,7 +373,8 @@ public boolean recursiveDeleteAccount(long accountId){ * @param c Cursor pointing to account record in database * @return {@link Account} object constructed from database record */ - public Account buildAccountInstance(Cursor c){ + @Override + public Account buildModelInstance(@NonNull final Cursor c){ Account account = buildSimpleAccountInstance(c); account.setTransactions(mTransactionsAdapter.getAllTransactionsForAccount(account.getUID())); @@ -381,15 +385,16 @@ public Account buildAccountInstance(Cursor c){ * Builds an account instance with the provided cursor and loads its corresponding transactions. *

The method will not move the cursor position, so the cursor should already be pointing * to the account record in the database
- * Note Unlike {@link #buildAccountInstance(android.database.Cursor)} this method will not load transactions

+ * Note Unlike {@link #buildModelInstance(android.database.Cursor)} this method will not load transactions

* * @param c Cursor pointing to account record in database * @return {@link Account} object constructed from database record */ private Account buildSimpleAccountInstance(Cursor c) { Account account = new Account(c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_NAME))); - populateModel(c, account); + populateBaseModelAttributes(c, account); + account.setDescription(c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_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))); @@ -409,15 +414,15 @@ private Account buildSimpleAccountInstance(Cursor c) { * @param uid Unique Identifier of account whose parent is to be returned. Should not be null * @return DB record UID of the parent account, null if the account has no parent */ - public String getParentAccountUID(String uid){ + public String getParentAccountUID(@NonNull String uid){ Cursor cursor = mDb.query(AccountEntry.TABLE_NAME, - new String[] {AccountEntry._ID, AccountEntry.COLUMN_PARENT_ACCOUNT_UID}, + new String[]{AccountEntry.COLUMN_PARENT_ACCOUNT_UID}, AccountEntry.COLUMN_UID + " = ?", new String[]{uid}, null, null, null, null); try { if (cursor.moveToFirst()) { - Log.d(LOG_TAG, "Account already exists. Returning existing id"); + Log.d(LOG_TAG, "Found parent account UID, returning value"); return cursor.getString(cursor.getColumnIndexOrThrow(AccountEntry.COLUMN_PARENT_ACCOUNT_UID)); } else { return null; @@ -427,46 +432,6 @@ public String getParentAccountUID(String uid){ } } - /** - * Returns the unique ID of the parent account of the account with database ID id - * If the account has no parent, null is returned. - * @param id DB record ID of account . Should not be null - * @return DB record UID of the parent account, null if the account has no parent - * @see #getParentAccountUID(String) - */ - public String getParentAccountUID(long id){ - return getParentAccountUID(getUID(id)); - } - - /** - * Retrieves an account object from a database with database ID rowId - * @param rowId Identifier of the account record to be retrieved - * @return {@link Account} object corresponding to database record - */ - public Account getAccount(long rowId){ - Log.v(LOG_TAG, "Fetching account with id " + rowId); - Cursor c = fetchRecord(rowId); - try { - if (c.moveToFirst()) { - return buildAccountInstance(c); - } else { - throw new IllegalArgumentException(String.format("rowId %d does not exist", rowId)); - } - } finally { - c.close(); - } - } - - /** - * Returns the {@link Account} object populated with data from the database - * for the record with UID uid - * @param uid Unique ID of the account to be retrieved - * @return {@link Account} object for unique ID uid - */ - public Account getAccount(String uid){ - return getAccount(getID(uid)); - } - /** * Returns the color code for the account in format #rrggbb * @param accountId Database row ID of the account @@ -498,41 +463,6 @@ public AccountType getAccountType(long accountId){ return getAccountType(getUID(accountId)); } - /** - * Returns the name of the account with id accountID - * @param accountID Database ID of the account record - * @return Name of the account - */ - public String getName(long accountID) { - Cursor c = fetchRecord(accountID); - try { - if (c.moveToFirst()) { - return c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_NAME)); - } else { - throw new IllegalArgumentException("account " + accountID + " does not exist"); - } - } finally { - c.close(); - } - } - - /** - * Returns a list of all account objects in the system - * @return List of {@link Account}s in the database - */ - public List getAllAccounts(){ - LinkedList accounts = new LinkedList<>(); - Cursor c = fetchAllRecords(); - try { - while (c.moveToNext()) { - accounts.add(buildAccountInstance(c)); - } - } finally { - c.close(); - } - return accounts; - } - /** * Returns a list of all account entries in the system (includes root account) * No transactions are loaded, just the accounts @@ -574,7 +504,9 @@ public List getSimpleAccountList(String where, String[] whereArgs, Stri /** * Returns a list of accounts which have transactions that have not been exported yet * @return List of {@link Account}s with unexported transactions + * @deprecated This uses the exported flag in the database which is no longer supported. */ + @Deprecated public List getExportableAccounts(){ LinkedList accountsList = new LinkedList(); Cursor cursor = mDb.query( @@ -593,7 +525,7 @@ public List getExportableAccounts(){ ); try { while (cursor.moveToNext()) { - accountsList.add(buildAccountInstance(cursor)); + accountsList.add(buildModelInstance(cursor)); } } finally { @@ -616,7 +548,8 @@ public String getOrCreateImbalanceAccountUID(Currency currency){ account.setAccountType(AccountType.BANK); account.setParentUID(getOrCreateGnuCashRootAccountUID()); account.setHidden(!GnuCashApplication.isDoubleEntryEnabled()); - addAccount(account); + account.setColorCode("#964B00"); + addRecord(account); uid = account.getUID(); } return uid; @@ -624,6 +557,7 @@ public String getOrCreateImbalanceAccountUID(Currency currency){ /** * 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(java.util.Currency) @@ -665,7 +599,7 @@ public String createAccountHierarchy(String fullName, AccountType accountType) { parentName += ACCOUNT_NAME_SEPARATOR; } if (accountsList.size() > 0) { - bulkAddAccounts(accountsList); + bulkAddRecords(accountsList); } // if fullName is not empty, loop will be entered and then uid will never be null //noinspection ConstantConditions @@ -789,25 +723,66 @@ public Money getAccountBalance(String accountUID, long startTimestamp, long endT return computeBalance(accountUID, startTimestamp, endTimestamp); } + /** + * Compute the account balance for all accounts with the specified type within a specific duration + * @param accountType Account Type for which to compute balance + * @param startTimestamp Begin time for the duration in milliseconds + * @param endTimestamp End time for duration in milliseconds + * @return Account balance + */ + public Money getAccountBalance(AccountType accountType, long startTimestamp, long endTimestamp){ + Cursor cursor = fetchAccounts(AccountEntry.COLUMN_TYPE + "= ?", + new String[]{accountType.name()}, null); + List accountUidList = new ArrayList<>(); + while (cursor.moveToNext()){ + String accountUID = cursor.getString(cursor.getColumnIndexOrThrow(AccountEntry.COLUMN_UID)); + accountUidList.add(accountUID); + } + cursor.close(); + + boolean hasDebitNormalBalance = accountType.hasDebitNormalBalance(); + String currencyCode = GnuCashApplication.getDefaultCurrencyCode(); + + Log.d(LOG_TAG, "all account list : " + accountUidList.size()); + SplitsDbAdapter splitsDbAdapter = SplitsDbAdapter.getInstance(); + Money splitSum = (startTimestamp == -1 && endTimestamp == -1) + ? splitsDbAdapter.computeSplitBalance(accountUidList, currencyCode, hasDebitNormalBalance) + : splitsDbAdapter.computeSplitBalance(accountUidList, currencyCode, hasDebitNormalBalance, startTimestamp, endTimestamp); + + return splitSum; + } + + /** + * Returns the account balance for all accounts types specified + * @param accountTypes List of account types + * @param start Begin timestamp for transactions + * @param end End timestamp of transactions + * @return Money balance of the account types + */ + public Money getAccountBalance(List accountTypes, long start, long end){ + Money balance = Money.createZeroInstance(GnuCashApplication.getDefaultCurrencyCode()); + for (AccountType accountType : accountTypes) { + balance = balance.add(getAccountBalance(accountType, start, end)); + } + return balance; + } + private Money computeBalance(String accountUID, long startTimestamp, long endTimestamp) { Log.d(LOG_TAG, "Computing account balance for account ID " + accountUID); String currencyCode = mTransactionsAdapter.getAccountCurrencyCode(accountUID); boolean hasDebitNormalBalance = getAccountType(accountUID).hasDebitNormalBalance(); - Money balance = Money.createZeroInstance(currencyCode); List accountsList = getDescendantAccountUIDs(accountUID, - AccountEntry.COLUMN_CURRENCY + " = ? ", - new String[]{currencyCode}); + null, null); accountsList.add(0, accountUID); Log.d(LOG_TAG, "all account list : " + accountsList.size()); - SplitsDbAdapter splitsDbAdapter = SplitsDbAdapter.getInstance(); - Money splitSum = (startTimestamp == -1 && endTimestamp == -1) + SplitsDbAdapter splitsDbAdapter = SplitsDbAdapter.getInstance(); + return (startTimestamp == -1 && endTimestamp == -1) ? splitsDbAdapter.computeSplitBalance(accountsList, currencyCode, hasDebitNormalBalance) : splitsDbAdapter.computeSplitBalance(accountsList, currencyCode, hasDebitNormalBalance, startTimestamp, endTimestamp); - return balance.add(splitSum); } /** @@ -955,7 +930,17 @@ public String getOrCreateGnuCashRootAccountUID() { rootAccount.setAccountType(AccountType.ROOT); rootAccount.setFullName(ROOT_ACCOUNT_FULL_NAME); rootAccount.setHidden(true); - addAccount(rootAccount); + ContentValues contentValues = new ContentValues(); + contentValues.put(AccountEntry.COLUMN_UID, rootAccount.getUID()); + contentValues.put(AccountEntry.COLUMN_NAME, rootAccount.getName()); + contentValues.put(AccountEntry.COLUMN_FULL_NAME, rootAccount.getFullName()); + contentValues.put(AccountEntry.COLUMN_TYPE, rootAccount.getAccountType().name()); + contentValues.put(AccountEntry.COLUMN_HIDDEN, rootAccount.isHidden() ? 1 : 0); + String defaultCurrencyCode = GnuCashApplication.getDefaultCurrencyCode(); + contentValues.put(AccountEntry.COLUMN_CURRENCY, defaultCurrencyCode); + contentValues.put(AccountEntry.COLUMN_COMMODITY_UID, getCommodityUID(defaultCurrencyCode)); + Log.i(LOG_TAG, "Creating ROOT account"); + mDb.insert(AccountEntry.TABLE_NAME, null, contentValues); return rootAccount.getUID(); } @@ -993,19 +978,7 @@ public String getCurrencyCode(String uid){ * @see #getFullyQualifiedAccountName(String) */ public String getAccountName(String accountUID){ - Cursor cursor = mDb.query(AccountEntry.TABLE_NAME, - new String[]{AccountEntry._ID, AccountEntry.COLUMN_NAME}, - AccountEntry.COLUMN_UID + " = ?", - new String[]{accountUID}, null, null, null); - try { - if (cursor.moveToNext()) { - return cursor.getString(cursor.getColumnIndexOrThrow(AccountEntry.COLUMN_NAME)); - } else { - throw new IllegalArgumentException("account " + accountUID + " does not exist"); - } - } finally { - cursor.close(); - } + return getAttribute(accountUID, AccountEntry.COLUMN_NAME); } /** @@ -1072,15 +1045,6 @@ public String getAccountFullName(String accountUID) { throw new IllegalArgumentException("account UID: " + accountUID + " does not exist"); } - /** - * Overloaded convenience method. - * Simply resolves the account UID and calls {@link #getFullyQualifiedAccountName(String)} - * @param accountId Database record ID of account - * @return Fully qualified (with parent hierarchy) account name - */ - public String getFullyQualifiedAccountName(long accountId){ - return getFullyQualifiedAccountName(getUID(accountId)); - } /** * Returns true if the account with unique ID accountUID is a placeholder account. @@ -1132,7 +1096,7 @@ public List getAllOpeningBalanceTransactions(){ continue; Transaction transaction = new Transaction(GnuCashApplication.getAppContext().getString(R.string.account_name_opening_balances)); - transaction.setNote(getName(id)); + transaction.setNote(getAccountName(accountUID)); transaction.setCurrencyCode(currencyCode); TransactionType transactionType = Transaction.getTypeForBalance(getAccountType(accountUID), balance.isNegative()); @@ -1179,13 +1143,45 @@ public static String getOpeningBalanceAccountFullName(){ } /** - * Returns the list of currencies in the database - * @return List of currencies in the database + * Returns the account color for the active account as an Android resource ID. + *

+ * Basically, if we are in a top level account, use the default title color. + * but propagate a parent account's title color to children who don't have own color + *

+ * @param accountUID GUID of the account + * @return Android resource ID representing the color which can be directly set to a view + */ + public static int getActiveAccountColorResource(@NonNull String accountUID) { + AccountsDbAdapter accountsDbAdapter = getInstance(); + + String colorCode = null; + int iColor = -1; + String parentAccountUID = accountUID; + while (parentAccountUID != null ) { + colorCode = accountsDbAdapter.getAccountColorCode(accountsDbAdapter.getID(parentAccountUID)); + if (colorCode != null) { + iColor = Color.parseColor(colorCode); + break; + } + parentAccountUID = accountsDbAdapter.getParentAccountUID(parentAccountUID); + } + + if (colorCode == null) { + iColor = GnuCashApplication.getAppContext().getResources().getColor(R.color.theme_primary); + } + + return iColor; + } + + /** + * 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 */ - public List getCurrencies(){ + public List getCurrenciesInUse(){ Cursor cursor = mDb.query(true, AccountEntry.TABLE_NAME, new String[]{AccountEntry.COLUMN_CURRENCY}, null, null, null, null, null, null); - List currencyList = new ArrayList(); + List currencyList = new ArrayList<>(); try { while (cursor.moveToNext()) { String currencyCode = cursor.getString(cursor.getColumnIndexOrThrow(AccountEntry.COLUMN_CURRENCY)); @@ -1202,11 +1198,17 @@ public List getCurrencies(){ * Basically empties all 3 tables, so use with care ;) */ @Override - public int deleteAllRecords(){ - mDb.delete(TransactionEntry.TABLE_NAME, null, null); //this will take the splits along with it + public int deleteAllRecords() { + mDb.delete(DatabaseSchema.PriceEntry.TABLE_NAME, null, null); + // Relies "ON DELETE CASCADE" takes too much time + // It take more than 300s to complete the deletion on my dataset without + // clearing the split table first, but only needs a little more that 1s + // if the split table is cleared first. + mDb.delete(SplitEntry.TABLE_NAME, null, null); + mDb.delete(TransactionEntry.TABLE_NAME, null, null); mDb.delete(DatabaseSchema.ScheduledActionEntry.TABLE_NAME, null, null); return mDb.delete(AccountEntry.TABLE_NAME, null, null); - } + } public int getTransactionMaxSplitNum(@NonNull String accountUID) { Cursor cursor = mDb.query("trans_extra_info", diff --git a/app/src/main/java/org/gnucash/android/db/CommoditiesDbAdapter.java b/app/src/main/java/org/gnucash/android/db/CommoditiesDbAdapter.java new file mode 100644 index 000000000..3a62142e2 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/db/CommoditiesDbAdapter.java @@ -0,0 +1,123 @@ +package org.gnucash.android.db; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; +import android.support.annotation.NonNull; + +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.model.Commodity; + +import static org.gnucash.android.db.DatabaseSchema.CommodityEntry; + +/** + * Database adapter for {@link org.gnucash.android.model.Commodity} + */ +public class CommoditiesDbAdapter extends DatabaseAdapter { + /** + * Opens the database adapter with an existing database + * + * @param db SQLiteDatabase object + */ + public CommoditiesDbAdapter(SQLiteDatabase db) { + super(db, CommodityEntry.TABLE_NAME); + } + + public static CommoditiesDbAdapter getInstance(){ + return GnuCashApplication.getCommoditiesDbAdapter(); + } + + @Override + protected SQLiteStatement compileReplaceStatement(@NonNull final Commodity commodity) { + if (mReplaceStatement == null) { + mReplaceStatement = mDb.compileStatement("REPLACE INTO " + CommodityEntry.TABLE_NAME + " ( " + + CommodityEntry.COLUMN_UID + " , " + + CommodityEntry.COLUMN_FULLNAME + " , " + + CommodityEntry.COLUMN_NAMESPACE + " , " + + CommodityEntry.COLUMN_MNEMONIC + " , " + + CommodityEntry.COLUMN_LOCAL_SYMBOL + " , " + + CommodityEntry.COLUMN_CUSIP + " , " + + CommodityEntry.COLUMN_FRACTION + " , " + + CommodityEntry.COLUMN_QUOTE_FLAG + " ) VALUES ( ? , ? , ? , ? , ? , ? , ? , ? ) "); + } + + mReplaceStatement.clearBindings(); + mReplaceStatement.bindString(1, commodity.getUID()); + mReplaceStatement.bindString(2, commodity.getFullname()); + mReplaceStatement.bindString(3, commodity.getNamespace().name()); + mReplaceStatement.bindString(4, commodity.getMnemonic()); + mReplaceStatement.bindString(5, commodity.getLocalSymbol()); + mReplaceStatement.bindString(6, commodity.getCusip()); + mReplaceStatement.bindLong(7, commodity.getFraction()); + mReplaceStatement.bindLong(8, commodity.getQuoteFlag()); + + return mReplaceStatement; + } + + @Override + public Commodity buildModelInstance(@NonNull final Cursor cursor) { + String fullname = cursor.getString(cursor.getColumnIndexOrThrow(CommodityEntry.COLUMN_FULLNAME)); + String mnemonic = cursor.getString(cursor.getColumnIndexOrThrow(CommodityEntry.COLUMN_MNEMONIC)); + String namespace = cursor.getString(cursor.getColumnIndexOrThrow(CommodityEntry.COLUMN_NAMESPACE)); + String cusip = cursor.getString(cursor.getColumnIndexOrThrow(CommodityEntry.COLUMN_CUSIP)); + String localSymbol = cursor.getString(cursor.getColumnIndexOrThrow(CommodityEntry.COLUMN_LOCAL_SYMBOL)); + + int fraction = cursor.getInt(cursor.getColumnIndexOrThrow(CommodityEntry.COLUMN_FRACTION)); + int quoteFlag = cursor.getInt(cursor.getColumnIndexOrThrow(CommodityEntry.COLUMN_QUOTE_FLAG)); + + Commodity commodity = new Commodity(fullname, mnemonic, fraction); + commodity.setNamespace(Commodity.Namespace.valueOf(namespace)); + commodity.setCusip(cusip); + commodity.setQuoteFlag(quoteFlag); + commodity.setLocalSymbol(localSymbol); + populateBaseModelAttributes(cursor, commodity); + + return commodity; + } + + @Override + public Cursor fetchAllRecords() { + return mDb.query(mTableName, null, null, null, null, null, + CommodityEntry.COLUMN_FULLNAME + " ASC"); + } + + /** + * Fetches all commodities in the database sorted in the specified order + * @param orderBy SQL statement for orderBy without the ORDER_BY itself + * @return Cursor holding all commodity records + */ + public Cursor fetchAllRecords(String orderBy) { + return mDb.query(mTableName, null, null, null, null, null, + orderBy); + } + + /** + * Returns the commodity associated with the ISO4217 currency code + * @param currencyCode 3-letter currency code + * @return Commodity associated with code or null if none is found + */ + public Commodity getCommodity(String currencyCode){ + Cursor cursor = fetchAllRecords(CommodityEntry.COLUMN_MNEMONIC + "=?", new String[]{currencyCode}); + Commodity commodity = null; + if (cursor.moveToNext()){ + commodity = buildModelInstance(cursor); + } + cursor.close(); + return commodity; + } + + public String getCurrencyCode(@NonNull String guid) { + Cursor cursor = mDb.query(mTableName, new String[]{CommodityEntry.COLUMN_MNEMONIC}, + DatabaseSchema.CommonColumns.COLUMN_UID + " = ?", new String[]{guid}, + null, null, null); + try { + if (cursor.moveToNext()) { + return cursor.getString(cursor.getColumnIndexOrThrow(CommodityEntry.COLUMN_MNEMONIC)); + } else { + throw new IllegalArgumentException("guid " + guid + " not exits in commodity db"); + } + } finally { + cursor.close(); + } + } +} diff --git a/app/src/main/java/org/gnucash/android/db/DatabaseAdapter.java b/app/src/main/java/org/gnucash/android/db/DatabaseAdapter.java index 5fdd2558e..5281404ff 100644 --- a/app/src/main/java/org/gnucash/android/db/DatabaseAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/DatabaseAdapter.java @@ -31,6 +31,8 @@ import org.gnucash.android.model.BaseModel; import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; /** * Adapter to be used for creating and opening the database for read/write operations. @@ -39,7 +41,7 @@ * @author Ngewi Fet * */ -public abstract class DatabaseAdapter { +public abstract class DatabaseAdapter { /** * Tag for logging */ @@ -52,6 +54,8 @@ public abstract class DatabaseAdapter { protected final String mTableName; + protected SQLiteStatement mReplaceStatement; + /** * Opens the database adapter with an existing database * @param db SQLiteDatabase object @@ -62,12 +66,14 @@ public DatabaseAdapter(SQLiteDatabase db, @NonNull String tableName) { if (!db.isOpen() || db.isReadOnly()) throw new IllegalArgumentException("Database not open or is read-only. Require writeable database"); - if (mDb.getVersion() >= DatabaseSchema.SPLITS_DB_VERSION) { + if (mDb.getVersion() >= 9) { createTempView(); } } private void createTempView() { + //the multiplication by 1.0 is to cause sqlite to handle the value as REAL and not to round off + // Create some temporary views. Temporary views only exists in one DB session, and will not // be saved in the DB // @@ -76,6 +82,8 @@ private void createTempView() { // create a temporary view, combining accounts, transactions and splits, as this is often used // in the queries mDb.execSQL("CREATE TEMP VIEW IF NOT EXISTS trans_split_acct AS SELECT " + + TransactionEntry.TABLE_NAME + "." + CommonColumns.COLUMN_MODIFIED_AT + " AS " + + TransactionEntry.TABLE_NAME + "_" + CommonColumns.COLUMN_MODIFIED_AT + " , " + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_UID + " AS " + TransactionEntry.TABLE_NAME + "_" + TransactionEntry.COLUMN_UID + " , " + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_DESCRIPTION + " AS " @@ -94,8 +102,14 @@ private void createTempView() { + SplitEntry.TABLE_NAME + "_" + SplitEntry.COLUMN_UID + " , " + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_TYPE + " AS " + SplitEntry.TABLE_NAME + "_" + SplitEntry.COLUMN_TYPE + " , " - + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_AMOUNT + " AS " - + SplitEntry.TABLE_NAME + "_" + SplitEntry.COLUMN_AMOUNT + " , " + + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_VALUE_NUM + " AS " + + SplitEntry.TABLE_NAME + "_" + SplitEntry.COLUMN_VALUE_NUM + " , " + + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_VALUE_DENOM + " AS " + + SplitEntry.TABLE_NAME + "_" + SplitEntry.COLUMN_VALUE_DENOM + " , " + + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_QUANTITY_NUM + " AS " + + SplitEntry.TABLE_NAME + "_" + SplitEntry.COLUMN_QUANTITY_NUM + " , " + + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_QUANTITY_DENOM + " AS " + + SplitEntry.TABLE_NAME + "_" + SplitEntry.COLUMN_QUANTITY_DENOM + " , " + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_MEMO + " AS " + SplitEntry.TABLE_NAME + "_" + SplitEntry.COLUMN_MEMO + " , " + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_UID + " AS " @@ -146,14 +160,16 @@ private void createTempView() { // if not, attach a 'b' to the split account uid // pick the minimal value of the modified account uid (one of the ones begins with 'a', if exists) // use substr to get account uid + mDb.execSQL("CREATE TEMP VIEW IF NOT EXISTS trans_extra_info AS SELECT " + TransactionEntry.TABLE_NAME + "_" + TransactionEntry.COLUMN_UID + " AS trans_acct_t_uid , SUBSTR ( MIN ( ( CASE WHEN IFNULL ( " + SplitEntry.TABLE_NAME + "_" + SplitEntry.COLUMN_MEMO + " , '' ) == '' THEN 'a' ELSE 'b' END ) || " + AccountEntry.TABLE_NAME + "_" + AccountEntry.COLUMN_UID + " ) , 2 ) AS trans_acct_a_uid , TOTAL ( CASE WHEN " + SplitEntry.TABLE_NAME + "_" + SplitEntry.COLUMN_TYPE + " = 'DEBIT' THEN "+ SplitEntry.TABLE_NAME + "_" + - SplitEntry.COLUMN_AMOUNT + " ELSE - " + SplitEntry.TABLE_NAME + "_" + - SplitEntry.COLUMN_AMOUNT + " END ) AS trans_acct_balance , COUNT ( DISTINCT " + + SplitEntry.COLUMN_VALUE_NUM + " ELSE - " + SplitEntry.TABLE_NAME + "_" + + SplitEntry.COLUMN_VALUE_NUM + " END ) * 1.0 / " + SplitEntry.TABLE_NAME + "_" + + SplitEntry.COLUMN_VALUE_DENOM + " AS trans_acct_balance , COUNT ( DISTINCT " + AccountEntry.TABLE_NAME + "_" + AccountEntry.COLUMN_CURRENCY + " ) AS trans_currency_count , COUNT (*) AS trans_split_count FROM trans_split_acct " + " GROUP BY " + TransactionEntry.TABLE_NAME + "_" + TransactionEntry.COLUMN_UID @@ -169,12 +185,121 @@ public boolean isOpen(){ } /** - * Returns a ContentValues object which has the data of the base model + * Adds a record to the database with the data contained in the model. + *

This method uses the SQL REPLACE instructions to replace any record with a matching GUID. + * So beware of any foreign keys with cascade dependencies which might need to be re-added

+ * @param model Model to be saved to the database + */ + public void addRecord(@NonNull final Model model){ + Log.d(LOG_TAG, String.format("Adding %s record to database: ", model.getClass().getSimpleName())); + compileReplaceStatement(model).execute(); + } + + /** + * Add multiple records to the database at once + *

Either all or none of the records will be inserted/updated into the database.

+ * @param modelList List of model records + * @return Number of rows inserted + */ + public long bulkAddRecords(@NonNull List modelList) { + if (modelList.isEmpty()) { + Log.d(LOG_TAG, "Empty model list. Cannot bulk add records, returning 0"); + return 0; + } + + Log.i(LOG_TAG, String.format("Bulk adding %d %s records to the database", modelList.size(), + modelList.size() == 0 ? "null": modelList.get(0).getClass().getSimpleName())); + long nRow = 0; + try { + mDb.beginTransaction(); + for (Model model : modelList) { + compileReplaceStatement(model).execute(); + nRow++; + } + mDb.setTransactionSuccessful(); + } + finally { + mDb.endTransaction(); + } + + return nRow; + } + + /** + * Builds an instance of the model from the database record entry + *

This method should not modify the cursor in any way

+ * @param cursor Cursor pointing to the record + * @return + */ + protected abstract Model buildModelInstance(@NonNull final Cursor cursor); + + /** + * Generates an {@link SQLiteStatement} with values from the {@code model}. + * This statement can be executed to replace a record in the database. + *

If the {@link #mReplaceStatement} is null, subclasses should create a new statement and return.
+ * If it is not null, the previous bindings will be cleared and replaced with those from the model

+ * @param model Model whose attributes will be used as bindings + * @return SQLiteStatement for replacing a record in the database + */ + protected abstract SQLiteStatement compileReplaceStatement(@NonNull final Model model); + + /** + * Returns a model instance populated with data from the record with GUID {@code uid} + *

Sub-classes which require special handling should override this method

+ * @param uid GUID of the record + * @return BaseModel instance of the record + * @throws IllegalArgumentException if the record UID does not exist in thd database + */ + public Model getRecord(@NonNull String uid){ + Log.v(LOG_TAG, "Fetching record with GUID " + uid); + + Cursor cursor = fetchRecord(uid); + try { + if (cursor.moveToFirst()) { + return buildModelInstance(cursor); + } + else { + throw new IllegalArgumentException("Record with " + uid + " does not exist"); + } + } finally { + cursor.close(); + } + } + + /** + * Overload of {@link #getRecord(String)} + * Simply converts the record ID to a GUID and calls {@link #getRecord(String)} + * @param id Database record ID + * @return Subclass of {@link BaseModel} containing record info + */ + public Model getRecord(long id){ + return getRecord(getUID(id)); + } + + /** + * Returns all the records in the database + * @return List of records in the database + */ + public List getAllRecords(){ + List modelRecords = new ArrayList<>(); + Cursor c = fetchAllRecords(); + try { + while (c.moveToNext()) { + modelRecords.add(buildModelInstance(c)); + } + } finally { + c.close(); + } + return modelRecords; + } + + /** + * Adds the attributes of the base model to the ContentValues object provided + * @param contentValues Content values to which to add attributes * @param model {@link org.gnucash.android.model.BaseModel} from which to extract values * @return {@link android.content.ContentValues} with the data to be inserted into the db */ - protected ContentValues getContentValues(BaseModel model){ - ContentValues contentValues = new ContentValues(); + protected ContentValues populateBaseModelAttributes(@NonNull ContentValues contentValues, @NonNull Model model){ contentValues.put(CommonColumns.COLUMN_UID, model.getUID()); contentValues.put(CommonColumns.COLUMN_CREATED_AT, model.getCreatedTimestamp().toString()); //there is a trigger in the database for updated the modified_at column @@ -190,7 +315,7 @@ protected ContentValues getContentValues(BaseModel model){ * @param cursor Cursor pointing to database record * @param model Model instance to be initialized */ - protected static void populateModel(Cursor cursor, BaseModel model){ + protected void populateBaseModelAttributes(Cursor cursor, BaseModel model){ String uid = cursor.getString(cursor.getColumnIndexOrThrow(CommonColumns.COLUMN_UID)); String created = cursor.getString(cursor.getColumnIndexOrThrow(CommonColumns.COLUMN_CREATED_AT)); String modified= cursor.getString(cursor.getColumnIndexOrThrow(CommonColumns.COLUMN_MODIFIED_AT)); @@ -271,10 +396,9 @@ public long getID(@NonNull String uid){ long result = -1; try{ if (cursor.moveToFirst()) { - Log.d(LOG_TAG, "Transaction already exists. Returning existing id"); result = cursor.getLong(cursor.getColumnIndexOrThrow(DatabaseSchema.CommonColumns._ID)); } else { - throw new IllegalArgumentException("Account UID " + uid + " does not exist in the db"); + throw new IllegalArgumentException("GUID " + uid + " does not exist in the db"); } } finally { cursor.close(); @@ -330,6 +454,30 @@ public String getAccountCurrencyCode(@NonNull String accountUID) { } } + + /** + * Returns the commodity GUID for the given ISO 4217 currency code + * @param currencyCode ISO 4217 currency code + * @return GUID of commodity + */ + public String getCommodityUID(String currencyCode){ + String where = DatabaseSchema.CommodityEntry.COLUMN_MNEMONIC + "= ?"; + String[] whereArgs = new String[]{currencyCode}; + + Cursor cursor = mDb.query(DatabaseSchema.CommodityEntry.TABLE_NAME, + new String[]{DatabaseSchema.CommodityEntry.COLUMN_UID}, + where, whereArgs, null, null, null); + try { + if (cursor.moveToNext()) { + return cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.CommodityEntry.COLUMN_UID)); + } else { + throw new IllegalArgumentException("Currency code not found in commodities"); + } + } finally { + cursor.close(); + } + } + /** * Returns the {@link org.gnucash.android.model.AccountType} of the account with unique ID uid * @param accountUID Unique ID of the account diff --git a/app/src/main/java/org/gnucash/android/db/DatabaseHelper.java b/app/src/main/java/org/gnucash/android/db/DatabaseHelper.java index a46486730..968b4c094 100644 --- a/app/src/main/java/org/gnucash/android/db/DatabaseHelper.java +++ b/app/src/main/java/org/gnucash/android/db/DatabaseHelper.java @@ -16,44 +16,38 @@ package org.gnucash.android.db; -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.ContentValues; import android.content.Context; -import android.content.Intent; -import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; +import android.widget.Toast; + +import com.crashlytics.android.Crashlytics; -import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.export.Exporter; -import org.gnucash.android.model.AccountType; -import org.gnucash.android.model.Money; -import org.gnucash.android.model.Transaction; - -import java.io.File; -import java.math.BigDecimal; -import java.sql.Timestamp; - -import static org.gnucash.android.db.DatabaseSchema.AccountEntry; -import static org.gnucash.android.db.DatabaseSchema.ScheduledActionEntry; -import static org.gnucash.android.db.DatabaseSchema.SplitEntry; -import static org.gnucash.android.db.DatabaseSchema.TransactionEntry; +import org.gnucash.android.model.Commodity; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Locale; + +import javax.xml.parsers.ParserConfigurationException; + +import static org.gnucash.android.db.DatabaseSchema.*; /** * Helper class for managing the SQLite database. * Creates the database and handles upgrades * @author Ngewi Fet * */ -@SuppressWarnings("deprecation") public class DatabaseHelper extends SQLiteOpenHelper { /** * Tag for logging */ - private static final String LOG_TAG = DatabaseHelper.class.getName(); + public static final String LOG_TAG = DatabaseHelper.class.getName(); /** * Name of the database @@ -69,6 +63,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { + AccountEntry.COLUMN_NAME + " varchar(255) not null, " + AccountEntry.COLUMN_TYPE + " varchar(255) not null, " + AccountEntry.COLUMN_CURRENCY + " varchar(255) not null, " + + AccountEntry.COLUMN_COMMODITY_UID + " varchar(255) not null, " + AccountEntry.COLUMN_DESCRIPTION + " varchar(255), " + AccountEntry.COLUMN_COLOR_CODE + " varchar(255), " + AccountEntry.COLUMN_FAVORITE + " tinyint default 0, " @@ -78,7 +73,8 @@ public class DatabaseHelper extends SQLiteOpenHelper { + AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " varchar(255), " + AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID + " varchar(255), " + AccountEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " - + AccountEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP " + + AccountEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + "FOREIGN KEY (" + AccountEntry.COLUMN_COMMODITY_UID + ") REFERENCES " + CommodityEntry.TABLE_NAME + " (" + CommodityEntry.COLUMN_UID + ") " + ");" + createUpdatedAtTrigger(AccountEntry.TABLE_NAME); /** @@ -93,10 +89,12 @@ public class DatabaseHelper extends SQLiteOpenHelper { + TransactionEntry.COLUMN_EXPORTED + " tinyint default 0, " + TransactionEntry.COLUMN_TEMPLATE + " tinyint default 0, " + TransactionEntry.COLUMN_CURRENCY + " varchar(255) not null, " + + TransactionEntry.COLUMN_COMMODITY_UID + " varchar(255) not null, " + TransactionEntry.COLUMN_SCHEDX_ACTION_UID + " varchar(255), " + TransactionEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + TransactionEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " - + "FOREIGN KEY (" + TransactionEntry.COLUMN_SCHEDX_ACTION_UID + ") REFERENCES " + ScheduledActionEntry.TABLE_NAME + " (" + ScheduledActionEntry.COLUMN_UID + ") ON DELETE SET NULL " + + "FOREIGN KEY (" + TransactionEntry.COLUMN_SCHEDX_ACTION_UID + ") REFERENCES " + ScheduledActionEntry.TABLE_NAME + " (" + ScheduledActionEntry.COLUMN_UID + ") ON DELETE SET NULL, " + + "FOREIGN KEY (" + TransactionEntry.COLUMN_COMMODITY_UID + ") REFERENCES " + CommodityEntry.TABLE_NAME + " (" + CommodityEntry.COLUMN_UID + ") " + ");" + createUpdatedAtTrigger(TransactionEntry.TABLE_NAME); /** @@ -107,7 +105,10 @@ public class DatabaseHelper extends SQLiteOpenHelper { + SplitEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + SplitEntry.COLUMN_MEMO + " text, " + SplitEntry.COLUMN_TYPE + " varchar(255) not null, " - + SplitEntry.COLUMN_AMOUNT + " varchar(255) not null, " + + SplitEntry.COLUMN_VALUE_NUM + " integer not null, " + + SplitEntry.COLUMN_VALUE_DENOM + " integer not null, " + + SplitEntry.COLUMN_QUANTITY_NUM + " integer not null, " + + SplitEntry.COLUMN_QUANTITY_DENOM + " integer not null, " + SplitEntry.COLUMN_ACCOUNT_UID + " varchar(255) not null, " + SplitEntry.COLUMN_TRANSACTION_UID + " varchar(255) not null, " + SplitEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " @@ -120,7 +121,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { public static final String SCHEDULED_ACTIONS_TABLE_CREATE = "CREATE TABLE " + ScheduledActionEntry.TABLE_NAME + " (" + ScheduledActionEntry._ID + " integer primary key autoincrement, " + ScheduledActionEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " - + ScheduledActionEntry.COLUMN_ACTION_UID + " varchar(255) not null, " + + ScheduledActionEntry.COLUMN_ACTION_UID + " varchar(255) not null, " + ScheduledActionEntry.COLUMN_TYPE + " varchar(255) not null, " + ScheduledActionEntry.COLUMN_PERIOD + " integer not null, " + ScheduledActionEntry.COLUMN_LAST_RUN + " integer default 0, " @@ -134,6 +135,39 @@ public class DatabaseHelper extends SQLiteOpenHelper { + ScheduledActionEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP " + ");" + createUpdatedAtTrigger(ScheduledActionEntry.TABLE_NAME); + public static final String COMMODITIES_TABLE_CREATE = "CREATE TABLE " + DatabaseSchema.CommodityEntry.TABLE_NAME + " (" + + CommodityEntry._ID + " integer primary key autoincrement, " + + CommodityEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + + CommodityEntry.COLUMN_NAMESPACE + " varchar(255) not null default " + Commodity.Namespace.ISO4217.name() + ", " + + CommodityEntry.COLUMN_FULLNAME + " varchar(255) not null, " + + CommodityEntry.COLUMN_MNEMONIC + " varchar(255) not null, " + + CommodityEntry.COLUMN_LOCAL_SYMBOL+ " varchar(255) not null default '', " + + CommodityEntry.COLUMN_CUSIP + " varchar(255), " + + CommodityEntry.COLUMN_FRACTION + " integer not null, " + + CommodityEntry.COLUMN_QUOTE_FLAG + " integer not null, " + + CommodityEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + CommodityEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP " + + ");" + createUpdatedAtTrigger(CommodityEntry.TABLE_NAME); + + /** + * SQL statement to create the commodity prices table + */ + private static final String PRICES_TABLE_CREATE = "CREATE TABLE " + PriceEntry.TABLE_NAME + " (" + + PriceEntry._ID + " integer primary key autoincrement, " + + PriceEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + + PriceEntry.COLUMN_COMMODITY_UID + " varchar(255) not null, " + + PriceEntry.COLUMN_CURRENCY_UID + " varchar(255) not null, " + + PriceEntry.COLUMN_TYPE + " varchar(255), " + + PriceEntry.COLUMN_DATE + " TIMESTAMP not null, " + + PriceEntry.COLUMN_SOURCE + " text, " + + PriceEntry.COLUMN_VALUE_NUM + " integer not null, " + + PriceEntry.COLUMN_VALUE_DENOM + " integer not null, " + + PriceEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + PriceEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + "UNIQUE (" + PriceEntry.COLUMN_COMMODITY_UID + ", " + PriceEntry.COLUMN_CURRENCY_UID + ") ON CONFLICT REPLACE, " + + "FOREIGN KEY (" + PriceEntry.COLUMN_COMMODITY_UID + ") REFERENCES " + CommodityEntry.TABLE_NAME + " (" + CommodityEntry.COLUMN_UID + ") ON DELETE CASCADE, " + + "FOREIGN KEY (" + PriceEntry.COLUMN_CURRENCY_UID + ") REFERENCES " + CommodityEntry.TABLE_NAME + " (" + CommodityEntry.COLUMN_UID + ") ON DELETE CASCADE " + + ");" + createUpdatedAtTrigger(PriceEntry.TABLE_NAME); /** * Constructor @@ -154,7 +188,8 @@ static String createUpdatedAtTrigger(String tableName){ return "CREATE TRIGGER update_time_trigger " + " AFTER UPDATE ON " + tableName + " FOR EACH ROW" + " BEGIN " + "UPDATE " + tableName - + " SET " + DatabaseSchema.CommonColumns.COLUMN_MODIFIED_AT + " = CURRENT_TIMESTAMP;" + + " SET " + CommonColumns.COLUMN_MODIFIED_AT + " = CURRENT_TIMESTAMP" + + " WHERE OLD." + CommonColumns.COLUMN_UID + " = NEW." + CommonColumns.COLUMN_UID + ";" + " END;"; } @@ -170,615 +205,46 @@ public void onOpen(SQLiteDatabase db) { } @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){ Log.i(LOG_TAG, "Upgrading database from version " + oldVersion + " to " + newVersion); - if (oldVersion < newVersion){ - //introducing double entry accounting - Log.i(LOG_TAG, "Upgrading database to version " + newVersion); - if (oldVersion == 1 && newVersion >= 2){ - Log.i(LOG_TAG, "Adding column for double-entry transactions"); - String addColumnSql = "ALTER TABLE " + TransactionEntry.TABLE_NAME + - " ADD COLUMN double_account_uid varchar(255)"; - - //introducing sub accounts - Log.i(LOG_TAG, "Adding column for parent accounts"); - String addParentAccountSql = "ALTER TABLE " + AccountEntry.TABLE_NAME + - " ADD COLUMN " + AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " varchar(255)"; - - db.execSQL(addColumnSql); - db.execSQL(addParentAccountSql); - - //update account types to GnuCash account types - //since all were previously CHECKING, now all will be CASH - Log.i(LOG_TAG, "Converting account types to GnuCash compatible types"); - ContentValues cv = new ContentValues(); - cv.put(SplitEntry.COLUMN_TYPE, AccountType.CASH.toString()); - db.update(AccountEntry.TABLE_NAME, cv, null, null); - - oldVersion = 2; - } - - - if (oldVersion == 2 && newVersion >= 3){ - Log.i(LOG_TAG, "Adding flag for placeholder accounts"); - String addPlaceHolderAccountFlagSql = "ALTER TABLE " + AccountEntry.TABLE_NAME + - " ADD COLUMN " + AccountEntry.COLUMN_PLACEHOLDER + " tinyint default 0"; - - db.execSQL(addPlaceHolderAccountFlagSql); - oldVersion = 3; - } - - if (oldVersion == 3 && newVersion >= 4){ - Log.i(LOG_TAG, "Updating database to version 4"); - String addRecurrencePeriod = "ALTER TABLE " + TransactionEntry.TABLE_NAME + - " ADD COLUMN recurrence_period integer default 0"; - - String addDefaultTransferAccount = "ALTER TABLE " + AccountEntry.TABLE_NAME - + " ADD COLUMN " + AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID + " varchar(255)"; - - String addAccountColor = " ALTER TABLE " + AccountEntry.TABLE_NAME - + " ADD COLUMN " + AccountEntry.COLUMN_COLOR_CODE + " varchar(255)"; - - db.execSQL(addRecurrencePeriod); - db.execSQL(addDefaultTransferAccount); - db.execSQL(addAccountColor); - - oldVersion = 4; - } - - if (oldVersion == 4 && newVersion >= 5){ - Log.i(LOG_TAG, "Upgrading database to version 5"); - String addAccountFavorite = " ALTER TABLE " + AccountEntry.TABLE_NAME - + " ADD COLUMN " + AccountEntry.COLUMN_FAVORITE + " tinyint default 0"; - db.execSQL(addAccountFavorite); - - oldVersion = 5; - } - - if (oldVersion == 5 && newVersion >= 6){ - Log.i(LOG_TAG, "Upgrading database to version 6"); - oldVersion = upgradeDbToVersion6(db); - } - - if (oldVersion == 6 && newVersion >= DatabaseSchema.SPLITS_DB_VERSION){ - Log.i(LOG_TAG, "Upgrading database to version 7"); - oldVersion = upgradeDbToVersion7(db); - } - - if (oldVersion == 7 && newVersion >= 8){ - Log.i(LOG_TAG, "Upgrading database to version 8"); - oldVersion = upgradeDbToVersion8(db); - } - } - - if (oldVersion != newVersion) { - Log.w(LOG_TAG, "Upgrade for the database failed. The Database is currently at version " + oldVersion); + Toast.makeText(GnuCashApplication.getAppContext(), "Upgrading GnuCash database", Toast.LENGTH_SHORT).show(); + /* + * NOTE: In order to modify the database, create a new static method in the MigrationHelper class + * called upgradeDbToVersion<#>, e.g. int upgradeDbToVersion10(SQLiteDatabase) in order to upgrade to version 10. + * The upgrade method should return the current database version as the return value. + * Then all you need to do is incremend the DatabaseSchema.DATABASE_VERSION to the appropriate number. + */ + if (oldVersion > newVersion) { + throw new IllegalArgumentException("Database downgrades are not supported at the moment"); } - } - - /** - * Upgrades the database from version 7 to version 8. - *

This migration accomplishes the following: - *

    - *
  • Added created_at and modified_at columns to all tables (including triggers for updating the columns).
  • - *
  • New table for scheduled actions and migrate all existing recurring transactions
  • - *
  • Auto-balancing of all existing splits
  • - *
  • Added "hidden" flag to accounts table
  • - *
  • Add flag for transaction templates
  • - *
- *

- * @param db SQLite Database to be upgraded - * @return New database version (8) if upgrade successful, old version (7) if unsuccessful - */ - private int upgradeDbToVersion8(SQLiteDatabase db) { - Log.i(LOG_TAG, "Upgrading database to version 8"); - int oldVersion = 7; - new File(Exporter.BACKUP_FOLDER_PATH).mkdirs(); - new File(Exporter.EXPORT_FOLDER_PATH).mkdirs(); - //start moving the files in background thread before we do the database stuff - new Thread(MigrationHelper.moveExportedFilesToNewDefaultLocation).start(); - - db.beginTransaction(); - try { - Log.i(LOG_TAG, "Creating scheduled actions table"); - db.execSQL("CREATE TABLE " + ScheduledActionEntry.TABLE_NAME + " (" - + ScheduledActionEntry._ID + " integer primary key autoincrement, " - + ScheduledActionEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " - + ScheduledActionEntry.COLUMN_ACTION_UID + " varchar(255) not null, " - + ScheduledActionEntry.COLUMN_TYPE + " varchar(255) not null, " - + ScheduledActionEntry.COLUMN_PERIOD + " integer not null, " - + ScheduledActionEntry.COLUMN_LAST_RUN + " integer default 0, " - + ScheduledActionEntry.COLUMN_START_TIME + " integer not null, " - + ScheduledActionEntry.COLUMN_END_TIME + " integer default 0, " - + ScheduledActionEntry.COLUMN_TAG + " text, " - + ScheduledActionEntry.COLUMN_ENABLED + " tinyint default 1, " //enabled by default - + ScheduledActionEntry.COLUMN_TOTAL_FREQUENCY + " integer default 0, " - + ScheduledActionEntry.COLUMN_EXECUTION_COUNT+ " integer default 0, " - + ScheduledActionEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " - + ScheduledActionEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP " - + ");" + createUpdatedAtTrigger(ScheduledActionEntry.TABLE_NAME)); - - - //==============================BEGIN TABLE MIGRATIONS ======================================== - Log.i(LOG_TAG, "Migrating accounts table"); - // backup transaction table - db.execSQL("ALTER TABLE " + AccountEntry.TABLE_NAME + " RENAME TO " + AccountEntry.TABLE_NAME + "_bak"); - // create new transaction table - db.execSQL("CREATE TABLE " + AccountEntry.TABLE_NAME + " (" - + AccountEntry._ID + " integer primary key autoincrement, " - + AccountEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " - + AccountEntry.COLUMN_NAME + " varchar(255) not null, " - + AccountEntry.COLUMN_TYPE + " varchar(255) not null, " - + AccountEntry.COLUMN_CURRENCY + " varchar(255) not null, " - + AccountEntry.COLUMN_DESCRIPTION + " varchar(255), " - + AccountEntry.COLUMN_COLOR_CODE + " varchar(255), " - + AccountEntry.COLUMN_FAVORITE + " tinyint default 0, " - + AccountEntry.COLUMN_HIDDEN + " tinyint default 0, " - + AccountEntry.COLUMN_FULL_NAME + " varchar(255), " - + AccountEntry.COLUMN_PLACEHOLDER + " tinyint default 0, " - + AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " varchar(255), " - + AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID + " varchar(255), " - + AccountEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " - + AccountEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP " - + ");" + createUpdatedAtTrigger(AccountEntry.TABLE_NAME)); - - // initialize new account table with data from old table - db.execSQL("INSERT INTO " + AccountEntry.TABLE_NAME + " ( " - + AccountEntry._ID + "," - + AccountEntry.COLUMN_UID + " , " - + AccountEntry.COLUMN_NAME + " , " - + AccountEntry.COLUMN_TYPE + " , " - + AccountEntry.COLUMN_CURRENCY + " , " - + AccountEntry.COLUMN_COLOR_CODE + " , " - + AccountEntry.COLUMN_FAVORITE + " , " - + AccountEntry.COLUMN_FULL_NAME + " , " - + AccountEntry.COLUMN_PLACEHOLDER + " , " - + AccountEntry.COLUMN_HIDDEN + " , " - + AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " , " - + AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID - + ") SELECT " - + AccountEntry.TABLE_NAME + "_bak." + AccountEntry._ID + " , " - + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_UID + " , " - + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_NAME + " , " - + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_TYPE + " , " - + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_CURRENCY + " , " - + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_COLOR_CODE + " , " - + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_FAVORITE + " , " - + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_FULL_NAME + " , " - + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_PLACEHOLDER + " , " - + " CASE WHEN " + AccountEntry.TABLE_NAME + "_bak.type = 'ROOT' THEN 1 ELSE 0 END, " - + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " , " - + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID - + " FROM " + AccountEntry.TABLE_NAME + "_bak;" - ); - - Log.i(LOG_TAG, "Migrating transactions table"); - // backup transaction table - db.execSQL("ALTER TABLE " + TransactionEntry.TABLE_NAME + " RENAME TO " + TransactionEntry.TABLE_NAME + "_bak"); - // create new transaction table - db.execSQL("CREATE TABLE " + TransactionEntry.TABLE_NAME + " (" - + TransactionEntry._ID + " integer primary key autoincrement, " - + TransactionEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " - + TransactionEntry.COLUMN_DESCRIPTION + " varchar(255), " - + TransactionEntry.COLUMN_NOTES + " text, " - + TransactionEntry.COLUMN_TIMESTAMP + " integer not null, " - + TransactionEntry.COLUMN_EXPORTED + " tinyint default 0, " - + TransactionEntry.COLUMN_TEMPLATE + " tinyint default 0, " - + TransactionEntry.COLUMN_CURRENCY + " varchar(255) not null, " - + TransactionEntry.COLUMN_SCHEDX_ACTION_UID + " varchar(255), " - + TransactionEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " - + TransactionEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " - + "FOREIGN KEY (" + TransactionEntry.COLUMN_SCHEDX_ACTION_UID + ") REFERENCES " + ScheduledActionEntry.TABLE_NAME + " (" + ScheduledActionEntry.COLUMN_UID + ") ON DELETE SET NULL " - + ");" + createUpdatedAtTrigger(TransactionEntry.TABLE_NAME)); - - // initialize new transaction table with data from old table - db.execSQL("INSERT INTO " + TransactionEntry.TABLE_NAME + " ( " - + TransactionEntry._ID + " , " - + TransactionEntry.COLUMN_UID + " , " - + TransactionEntry.COLUMN_DESCRIPTION + " , " - + TransactionEntry.COLUMN_NOTES + " , " - + TransactionEntry.COLUMN_TIMESTAMP + " , " - + TransactionEntry.COLUMN_EXPORTED + " , " - + TransactionEntry.COLUMN_CURRENCY + " , " - + TransactionEntry.COLUMN_TEMPLATE - + ") SELECT " - + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry._ID + " , " - + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_UID + " , " - + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_DESCRIPTION + " , " - + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_NOTES + " , " - + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_TIMESTAMP + " , " - + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_EXPORTED + " , " - + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_CURRENCY + " , " - + " CASE WHEN " + TransactionEntry.TABLE_NAME + "_bak.recurrence_period > 0 THEN 1 ELSE 0 END " - + " FROM " + TransactionEntry.TABLE_NAME + "_bak;" - ); - - Log.i(LOG_TAG, "Migrating splits table"); - // backup transaction table - db.execSQL("ALTER TABLE " + SplitEntry.TABLE_NAME + " RENAME TO " + SplitEntry.TABLE_NAME + "_bak"); - // create new split table - db.execSQL("CREATE TABLE " + SplitEntry.TABLE_NAME + " (" - + SplitEntry._ID + " integer primary key autoincrement, " - + SplitEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " - + SplitEntry.COLUMN_MEMO + " text, " - + SplitEntry.COLUMN_TYPE + " varchar(255) not null, " - + SplitEntry.COLUMN_AMOUNT + " varchar(255) not null, " - + SplitEntry.COLUMN_ACCOUNT_UID + " varchar(255) not null, " - + SplitEntry.COLUMN_TRANSACTION_UID + " varchar(255) not null, " - + SplitEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " - + SplitEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " - + "FOREIGN KEY (" + SplitEntry.COLUMN_ACCOUNT_UID + ") REFERENCES " + AccountEntry.TABLE_NAME + " (" + AccountEntry.COLUMN_UID + ") ON DELETE CASCADE, " - + "FOREIGN KEY (" + SplitEntry.COLUMN_TRANSACTION_UID + ") REFERENCES " + TransactionEntry.TABLE_NAME + " (" + TransactionEntry.COLUMN_UID + ") ON DELETE CASCADE " - + ");" + createUpdatedAtTrigger(SplitEntry.TABLE_NAME)); - - // initialize new split table with data from old table - db.execSQL("INSERT INTO " + SplitEntry.TABLE_NAME + " ( " - + SplitEntry._ID + " , " - + SplitEntry.COLUMN_UID + " , " - + SplitEntry.COLUMN_MEMO + " , " - + SplitEntry.COLUMN_TYPE + " , " - + SplitEntry.COLUMN_AMOUNT + " , " - + SplitEntry.COLUMN_ACCOUNT_UID + " , " - + SplitEntry.COLUMN_TRANSACTION_UID - + ") SELECT " - + SplitEntry.TABLE_NAME + "_bak." + SplitEntry._ID + " , " - + SplitEntry.TABLE_NAME + "_bak." + SplitEntry.COLUMN_UID + " , " - + SplitEntry.TABLE_NAME + "_bak." + SplitEntry.COLUMN_MEMO + " , " - + SplitEntry.TABLE_NAME + "_bak." + SplitEntry.COLUMN_TYPE + " , " - + SplitEntry.TABLE_NAME + "_bak." + SplitEntry.COLUMN_AMOUNT + " , " - + SplitEntry.TABLE_NAME + "_bak." + SplitEntry.COLUMN_ACCOUNT_UID + " , " - + SplitEntry.TABLE_NAME + "_bak." + SplitEntry.COLUMN_TRANSACTION_UID - + " FROM " + SplitEntry.TABLE_NAME + "_bak;" - ); - - - - //================================ END TABLE MIGRATIONS ================================ - - // String timestamp to be used for all new created entities in migration - String timestamp = (new Timestamp(System.currentTimeMillis())).toString(); - - //ScheduledActionDbAdapter scheduledActionDbAdapter = new ScheduledActionDbAdapter(db); - //SplitsDbAdapter splitsDbAdapter = new SplitsDbAdapter(db); - //TransactionsDbAdapter transactionsDbAdapter = new TransactionsDbAdapter(db, splitsDbAdapter); - //AccountsDbAdapter accountsDbAdapter = new AccountsDbAdapter(db,transactionsDbAdapter); - - Log.i(LOG_TAG, "Creating default root account if none exists"); - ContentValues contentValues = new ContentValues(); - //assign a root account to all accounts which had null as parent except ROOT (top-level accounts) - String rootAccountUID; - Cursor cursor = db.query(AccountEntry.TABLE_NAME, - new String[]{AccountEntry.COLUMN_UID}, - AccountEntry.COLUMN_TYPE + "= ?", - new String[]{AccountType.ROOT.name()}, null, null, null); - try { - if (cursor.moveToFirst()) { - rootAccountUID = cursor.getString(cursor.getColumnIndexOrThrow(AccountEntry.COLUMN_UID)); - } - else - { - rootAccountUID = MigrationHelper.generateUUID(); - contentValues.clear(); - contentValues.put(DatabaseSchema.CommonColumns.COLUMN_UID, rootAccountUID); - contentValues.put(DatabaseSchema.CommonColumns.COLUMN_CREATED_AT, timestamp); - contentValues.put(AccountEntry.COLUMN_NAME, "ROOT"); - contentValues.put(AccountEntry.COLUMN_TYPE, "ROOT"); - contentValues.put(AccountEntry.COLUMN_CURRENCY, Money.DEFAULT_CURRENCY_CODE); - contentValues.put(AccountEntry.COLUMN_PLACEHOLDER, 0); - contentValues.put(AccountEntry.COLUMN_HIDDEN, 1); - contentValues.putNull(AccountEntry.COLUMN_COLOR_CODE); - contentValues.put(AccountEntry.COLUMN_FAVORITE, 0); - contentValues.put(AccountEntry.COLUMN_FULL_NAME, " "); - contentValues.putNull(AccountEntry.COLUMN_PARENT_ACCOUNT_UID); - contentValues.putNull(AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID); - db.insert(AccountEntry.TABLE_NAME, null, contentValues); - } - } finally { - cursor.close(); - } - //String rootAccountUID = accountsDbAdapter.getOrCreateGnuCashRootAccountUID(); - contentValues.clear(); - contentValues.put(AccountEntry.COLUMN_PARENT_ACCOUNT_UID, rootAccountUID); - db.update(AccountEntry.TABLE_NAME, contentValues, AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " IS NULL AND " + AccountEntry.COLUMN_TYPE + " != ?", new String[]{"ROOT"}); - - Log.i(LOG_TAG, "Migrating existing recurring transactions"); - cursor = db.query(TransactionEntry.TABLE_NAME + "_bak", null, "recurrence_period > 0", null, null, null, null); - long lastRun = System.currentTimeMillis(); - while (cursor.moveToNext()){ - contentValues.clear(); - Timestamp timestampT = new Timestamp(cursor.getLong(cursor.getColumnIndexOrThrow(TransactionEntry.COLUMN_TIMESTAMP))); - contentValues.put(TransactionEntry.COLUMN_CREATED_AT, timestampT.toString()); - long transactionId = cursor.getLong(cursor.getColumnIndexOrThrow(TransactionEntry._ID)); - db.update(TransactionEntry.TABLE_NAME, contentValues, TransactionEntry._ID + "=" + transactionId, null); - - //ScheduledAction scheduledAction = new ScheduledAction(ScheduledAction.ActionType.TRANSACTION); - //scheduledAction.setActionUID(cursor.getString(cursor.getColumnIndexOrThrow(TransactionEntry.COLUMN_UID))); - //long period = cursor.getLong(cursor.getColumnIndexOrThrow("recurrence_period")); - //scheduledAction.setPeriod(period); - //scheduledAction.setStartTime(timestampT.getTime()); //the start time is when the transaction was created - //scheduledAction.setLastRun(System.currentTimeMillis()); //prevent this from being executed at the end of migration - - contentValues.clear(); - contentValues.put(DatabaseSchema.CommonColumns.COLUMN_UID, MigrationHelper.generateUUID()); - contentValues.put(DatabaseSchema.CommonColumns.COLUMN_CREATED_AT, timestamp); - contentValues.put(ScheduledActionEntry.COLUMN_ACTION_UID, cursor.getString(cursor.getColumnIndexOrThrow(TransactionEntry.COLUMN_UID))); - contentValues.put(ScheduledActionEntry.COLUMN_PERIOD, cursor.getLong(cursor.getColumnIndexOrThrow("recurrence_period"))); - contentValues.put(ScheduledActionEntry.COLUMN_START_TIME, timestampT.getTime()); - contentValues.put(ScheduledActionEntry.COLUMN_END_TIME, 0); - contentValues.put(ScheduledActionEntry.COLUMN_LAST_RUN, lastRun); - contentValues.put(ScheduledActionEntry.COLUMN_TYPE, "TRANSACTION"); - contentValues.put(ScheduledActionEntry.COLUMN_TAG, ""); - contentValues.put(ScheduledActionEntry.COLUMN_ENABLED, 1); - contentValues.put(ScheduledActionEntry.COLUMN_TOTAL_FREQUENCY, 0); - contentValues.put(ScheduledActionEntry.COLUMN_EXECUTION_COUNT, 0); - //scheduledActionDbAdapter.addScheduledAction(scheduledAction); - db.insert(ScheduledActionEntry.TABLE_NAME, null, contentValues); - - //build intent for recurring transactions in the database - Intent intent = new Intent(Intent.ACTION_INSERT); - intent.setType(Transaction.MIME_TYPE); - - //cancel existing pending intent - Context context = GnuCashApplication.getAppContext(); - PendingIntent recurringPendingIntent = PendingIntent.getBroadcast(context, - (int)transactionId, intent, PendingIntent.FLAG_CANCEL_CURRENT); - AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - alarmManager.cancel(recurringPendingIntent); - } - cursor.close(); - - //auto-balance existing splits - Log.i(LOG_TAG, "Auto-balancing existing transaction splits"); -// cursor = transactionsDbAdapter.fetchAllRecords(); -// while (cursor.moveToNext()){ -// Transaction transaction = transactionsDbAdapter.buildTransactionInstance(cursor); -// if (transaction.isTemplate()) -// continue; -// Money imbalance = transaction.getImbalance(); -// if (!imbalance.isAmountZero()){ -// Split split = new Split(imbalance.negate(), -// accountsDbAdapter.getOrCreateImbalanceAccountUID(imbalance.getCurrency())); -// split.setTransactionUID(transaction.getUID()); -// splitsDbAdapter.addSplit(split); -// } -// } -// cursor.close(); - cursor = db.query( - TransactionEntry.TABLE_NAME + " , " + SplitEntry.TABLE_NAME + " ON " - + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_UID + "=" + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_TRANSACTION_UID - + " , " + AccountEntry.TABLE_NAME + " ON " - + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_ACCOUNT_UID + "=" + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_UID, - new String[]{ - TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_UID + " AS trans_uid", - TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_CURRENCY + " AS trans_currency", - "TOTAL ( CASE WHEN " + - SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_TYPE + " = 'DEBIT' THEN " + - SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_AMOUNT + " ELSE - " + - SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_AMOUNT + " END ) AS trans_acct_balance", - "COUNT ( DISTINCT " + - AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_CURRENCY + - " ) AS trans_currency_count" - }, - TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_TEMPLATE + " == 0", - null, - TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_UID, - "trans_acct_balance != 0 AND trans_currency_count = 1", - null); + while(oldVersion < newVersion){ try { - while (cursor.moveToNext()){ - double imbalance = cursor.getDouble(cursor.getColumnIndexOrThrow("trans_acct_balance")); - BigDecimal decimalImbalance = BigDecimal.valueOf(imbalance).setScale(2, BigDecimal.ROUND_HALF_UP); - if (decimalImbalance.compareTo(BigDecimal.ZERO) != 0) { - String currencyCode = cursor.getString(cursor.getColumnIndexOrThrow("trans_currency")); - String imbalanceAccountName = GnuCashApplication.getAppContext().getString(R.string.imbalance_account_name) + "-" + currencyCode; - String imbalanceAccountUID; - Cursor c = db.query(AccountEntry.TABLE_NAME, new String[]{AccountEntry.COLUMN_UID}, - AccountEntry.COLUMN_FULL_NAME + "= ?", new String[]{imbalanceAccountName}, - null, null, null); - try { - if (c.moveToFirst()) { - imbalanceAccountUID = c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_UID)); - } - else { - imbalanceAccountUID = MigrationHelper.generateUUID(); - contentValues.clear(); - contentValues.put(DatabaseSchema.CommonColumns.COLUMN_UID, imbalanceAccountUID); - contentValues.put(DatabaseSchema.CommonColumns.COLUMN_CREATED_AT, timestamp); - contentValues.put(AccountEntry.COLUMN_NAME, imbalanceAccountName); - contentValues.put(AccountEntry.COLUMN_TYPE, "BANK"); - contentValues.put(AccountEntry.COLUMN_CURRENCY, currencyCode); - contentValues.put(AccountEntry.COLUMN_PLACEHOLDER, 0); - contentValues.put(AccountEntry.COLUMN_HIDDEN, GnuCashApplication.isDoubleEntryEnabled() ? 0 : 1); - contentValues.putNull(AccountEntry.COLUMN_COLOR_CODE); - contentValues.put(AccountEntry.COLUMN_FAVORITE, 0); - contentValues.put(AccountEntry.COLUMN_FULL_NAME, imbalanceAccountName); - contentValues.put(AccountEntry.COLUMN_PARENT_ACCOUNT_UID, rootAccountUID); - contentValues.putNull(AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID); - db.insert(AccountEntry.TABLE_NAME, null, contentValues); - } - } finally { - c.close(); - } - String TransactionUID = cursor.getString(cursor.getColumnIndexOrThrow("trans_uid")); - contentValues.clear(); - contentValues.put(DatabaseSchema.CommonColumns.COLUMN_UID, MigrationHelper.generateUUID()); - contentValues.put(DatabaseSchema.CommonColumns.COLUMN_CREATED_AT, timestamp); - contentValues.put(SplitEntry.COLUMN_AMOUNT, decimalImbalance.abs().toPlainString()); - contentValues.put(SplitEntry.COLUMN_TYPE, decimalImbalance.compareTo(BigDecimal.ZERO) < 0 ? "DEBIT" : "CREDIT"); - contentValues.put(SplitEntry.COLUMN_MEMO, ""); - contentValues.put(SplitEntry.COLUMN_ACCOUNT_UID, imbalanceAccountUID); - contentValues.put(SplitEntry.COLUMN_TRANSACTION_UID, TransactionUID); - db.insert(SplitEntry.TABLE_NAME, null, contentValues); - contentValues.clear(); - contentValues.put(TransactionEntry.COLUMN_MODIFIED_AT, timestamp); - db.update(TransactionEntry.TABLE_NAME, contentValues, TransactionEntry.COLUMN_UID + " == ?", - new String[]{TransactionUID}); - } - } - } finally { - cursor.close(); + Method method = MigrationHelper.class.getDeclaredMethod("upgradeDbToVersion" + (oldVersion+1), SQLiteDatabase.class); + Object result = method.invoke(null, db); + oldVersion = Integer.parseInt(result.toString()); + + } catch (NoSuchMethodException e) { + String msg = String.format("Database upgrade method upgradeToVersion%d(SQLiteDatabase) definition not found ", newVersion); + Log.e(LOG_TAG, msg, e); + Crashlytics.log(msg); + Crashlytics.logException(e); + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + String msg = String.format("Database upgrade to version %d failed. The upgrade method is inaccessible ", newVersion); + Log.e(LOG_TAG, msg, e); + Crashlytics.log(msg); + Crashlytics.logException(e); + throw new RuntimeException(e); + } catch (InvocationTargetException e){ + Crashlytics.logException(e); + throw new RuntimeException(e); } - - Log.i(LOG_TAG, "Dropping temporary migration tables"); - db.execSQL("DROP TABLE " + SplitEntry.TABLE_NAME + "_bak"); - db.execSQL("DROP TABLE " + AccountEntry.TABLE_NAME + "_bak"); - db.execSQL("DROP TABLE " + TransactionEntry.TABLE_NAME + "_bak"); - - db.setTransactionSuccessful(); - oldVersion = 8; - } finally { - db.endTransaction(); - } - - GnuCashApplication.startScheduledActionExecutionService(GnuCashApplication.getAppContext()); - - return oldVersion; - } - - /** - * Code for upgrading the database to the {@link DatabaseSchema#SPLITS_DB_VERSION} from version 6.
- * Tasks accomplished in migration: - *
    - *
  • Added new splits table for transaction splits
  • - *
  • Extract existing info from transactions table to populate split table
  • - *
- * @param db SQLite Database - * @return The new database version if upgrade was successful, or the old db version if it failed - */ - private int upgradeDbToVersion7(SQLiteDatabase db) { - int oldVersion = 6; - db.beginTransaction(); - try { - // backup transaction table - db.execSQL("ALTER TABLE " + TransactionEntry.TABLE_NAME + " RENAME TO " + TransactionEntry.TABLE_NAME + "_bak"); - // create new transaction table - db.execSQL("create table " + TransactionEntry.TABLE_NAME + " (" - + TransactionEntry._ID + " integer primary key autoincrement, " - + TransactionEntry.COLUMN_UID + " varchar(255) not null, " - + TransactionEntry.COLUMN_DESCRIPTION + " varchar(255), " - + TransactionEntry.COLUMN_NOTES + " text, " - + TransactionEntry.COLUMN_TIMESTAMP + " integer not null, " - + TransactionEntry.COLUMN_EXPORTED + " tinyint default 0, " - + TransactionEntry.COLUMN_CURRENCY + " varchar(255) not null, " - + "recurrence_period integer default 0, " - + "UNIQUE (" + TransactionEntry.COLUMN_UID + ") " - + ");"); - // initialize new transaction table wiht data from old table - db.execSQL("INSERT INTO " + TransactionEntry.TABLE_NAME + " ( " - + TransactionEntry._ID + " , " - + TransactionEntry.COLUMN_UID + " , " - + TransactionEntry.COLUMN_DESCRIPTION + " , " - + TransactionEntry.COLUMN_NOTES + " , " - + TransactionEntry.COLUMN_TIMESTAMP + " , " - + TransactionEntry.COLUMN_EXPORTED + " , " - + TransactionEntry.COLUMN_CURRENCY + " , " - + "recurrence_period ) SELECT " - + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry._ID + " , " - + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_UID + " , " - + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_DESCRIPTION + " , " - + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_NOTES + " , " - + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_TIMESTAMP + " , " - + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_EXPORTED + " , " - + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_CURRENCY + " , " - + TransactionEntry.TABLE_NAME + "_bak.recurrence_period" - + " FROM " + TransactionEntry.TABLE_NAME + "_bak , " + AccountEntry.TABLE_NAME - + " ON " + TransactionEntry.TABLE_NAME + "_bak.account_uid == " + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_UID - ); - // create split table - db.execSQL("CREATE TABLE " + SplitEntry.TABLE_NAME + " (" - + SplitEntry._ID + " integer primary key autoincrement, " - + SplitEntry.COLUMN_UID + " varchar(255) not null, " - + SplitEntry.COLUMN_MEMO + " text, " - + SplitEntry.COLUMN_TYPE + " varchar(255) not null, " - + SplitEntry.COLUMN_AMOUNT + " varchar(255) not null, " - + SplitEntry.COLUMN_ACCOUNT_UID + " varchar(255) not null, " - + SplitEntry.COLUMN_TRANSACTION_UID + " varchar(255) not null, " - + "FOREIGN KEY (" + SplitEntry.COLUMN_ACCOUNT_UID + ") REFERENCES " + AccountEntry.TABLE_NAME + " (" + AccountEntry.COLUMN_UID + "), " - + "FOREIGN KEY (" + SplitEntry.COLUMN_TRANSACTION_UID + ") REFERENCES " + TransactionEntry.TABLE_NAME + " (" + TransactionEntry.COLUMN_UID + "), " - + "UNIQUE (" + SplitEntry.COLUMN_UID + ") " - + ");"); - // Initialize split table with data from backup transaction table - // New split table is initialized after the new transaction table as the - // foreign key constraint will stop any data from being inserted - // If new split table is created before the backup is made, the foreign key - // constraint will be rewritten to refer to the backup transaction table - db.execSQL("INSERT INTO " + SplitEntry.TABLE_NAME + " ( " - + SplitEntry.COLUMN_UID + " , " - + SplitEntry.COLUMN_TYPE + " , " - + SplitEntry.COLUMN_AMOUNT + " , " - + SplitEntry.COLUMN_ACCOUNT_UID + " , " - + SplitEntry.COLUMN_TRANSACTION_UID + " ) SELECT " - + "LOWER(HEX(RANDOMBLOB(16))) , " - + "CASE WHEN " + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_TYPE + " IN ( 'CASH' , 'BANK', 'ASSET', 'EXPENSE', 'RECEIVABLE', 'STOCK', 'MUTUAL' ) THEN CASE WHEN " - + SplitEntry.COLUMN_AMOUNT + " < 0 THEN 'CREDIT' ELSE 'DEBIT' END ELSE CASE WHEN " - + SplitEntry.COLUMN_AMOUNT + " < 0 THEN 'DEBIT' ELSE 'CREDIT' END END , " - + "ABS ( " + TransactionEntry.TABLE_NAME + "_bak.amount ) , " - + TransactionEntry.TABLE_NAME + "_bak.account_uid , " - + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_UID - + " FROM " + TransactionEntry.TABLE_NAME + "_bak , " + AccountEntry.TABLE_NAME - + " ON " + TransactionEntry.TABLE_NAME + "_bak.account_uid = " + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_UID - + " UNION SELECT " - + "LOWER(HEX(RANDOMBLOB(16))) AS " + SplitEntry.COLUMN_UID + " , " - + "CASE WHEN " + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_TYPE + " IN ( 'CASH' , 'BANK', 'ASSET', 'EXPENSE', 'RECEIVABLE', 'STOCK', 'MUTUAL' ) THEN CASE WHEN " - + SplitEntry.COLUMN_AMOUNT + " < 0 THEN 'DEBIT' ELSE 'CREDIT' END ELSE CASE WHEN " - + SplitEntry.COLUMN_AMOUNT + " < 0 THEN 'CREDIT' ELSE 'DEBIT' END END , " - + "ABS ( " + TransactionEntry.TABLE_NAME + "_bak.amount ) , " - + TransactionEntry.TABLE_NAME + "_bak.double_account_uid , " - + TransactionEntry.TABLE_NAME + "_baK." + TransactionEntry.COLUMN_UID - + " FROM " + TransactionEntry.TABLE_NAME + "_bak , " + AccountEntry.TABLE_NAME - + " ON " + TransactionEntry.TABLE_NAME + "_bak.account_uid = " + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_UID - + " WHERE " + TransactionEntry.TABLE_NAME + "_bak.double_account_uid IS NOT NULL" - ); - // drop backup transaction table - db.execSQL("DROP TABLE " + TransactionEntry.TABLE_NAME + "_bak"); - db.setTransactionSuccessful(); - oldVersion = DatabaseSchema.SPLITS_DB_VERSION; - } finally { - db.endTransaction(); - } - return oldVersion; - } - - /** - * Upgrades the database from version 5 to version 6.
- * This migration adds support for fully qualified account names and updates existing accounts. - * @param db SQLite Database to be upgraded - * @return New database version (6) if upgrade successful, old version (5) if unsuccessful - */ - private int upgradeDbToVersion6(SQLiteDatabase db) { - int oldVersion = 5; - String addFullAccountNameQuery = " ALTER TABLE " + AccountEntry.TABLE_NAME - + " ADD COLUMN " + AccountEntry.COLUMN_FULL_NAME + " varchar(255) "; - db.execSQL(addFullAccountNameQuery); - - //update all existing accounts with their fully qualified name - Cursor cursor = db.query(AccountEntry.TABLE_NAME, - new String[]{AccountEntry._ID, AccountEntry.COLUMN_UID}, - null, null, null, null, null); - while(cursor != null && cursor.moveToNext()){ - String uid = cursor.getString(cursor.getColumnIndexOrThrow(AccountEntry.COLUMN_UID)); - String fullName = MigrationHelper.getFullyQualifiedAccountName(db, uid); - - if (fullName == null) - continue; - - ContentValues contentValues = new ContentValues(); - contentValues.put(AccountEntry.COLUMN_FULL_NAME, fullName); - - long id = cursor.getLong(cursor.getColumnIndexOrThrow(AccountEntry._ID)); - db.update(AccountEntry.TABLE_NAME, contentValues, AccountEntry._ID + " = " + id, null); - } - - if (cursor != null) { - cursor.close(); } + } - oldVersion = 6; - return oldVersion; - } /** * Creates the tables in the database @@ -790,24 +256,40 @@ private void createDatabaseTables(SQLiteDatabase db) { db.execSQL(TRANSACTIONS_TABLE_CREATE); db.execSQL(SPLITS_TABLE_CREATE); db.execSQL(SCHEDULED_ACTIONS_TABLE_CREATE); + db.execSQL(COMMODITIES_TABLE_CREATE); + db.execSQL(PRICES_TABLE_CREATE); String createAccountUidIndex = "CREATE UNIQUE INDEX '" + AccountEntry.INDEX_UID + "' ON " + AccountEntry.TABLE_NAME + "(" + AccountEntry.COLUMN_UID + ")"; - String createTransactionUidIndex = "CREATE UNIQUE INDEX '"+ TransactionEntry.INDEX_UID +"' ON " + String createTransactionUidIndex = "CREATE UNIQUE INDEX '" + TransactionEntry.INDEX_UID + "' ON " + TransactionEntry.TABLE_NAME + "(" + TransactionEntry.COLUMN_UID + ")"; - String createSplitUidIndex = "CREATE UNIQUE INDEX '" + SplitEntry.INDEX_UID +"' ON " + String createSplitUidIndex = "CREATE UNIQUE INDEX '" + SplitEntry.INDEX_UID + "' ON " + SplitEntry.TABLE_NAME + "(" + SplitEntry.COLUMN_UID + ")"; String createScheduledEventUidIndex = "CREATE UNIQUE INDEX '" + ScheduledActionEntry.INDEX_UID - +"' ON " + ScheduledActionEntry.TABLE_NAME + "(" + ScheduledActionEntry.COLUMN_UID + ")"; + + "' ON " + ScheduledActionEntry.TABLE_NAME + "(" + ScheduledActionEntry.COLUMN_UID + ")"; + + String createCommodityUidIndex = "CREATE UNIQUE INDEX '" + CommodityEntry.INDEX_UID + + "' ON " + CommodityEntry.TABLE_NAME + "(" + CommodityEntry.COLUMN_UID + ")"; + + String createPriceUidIndex = "CREATE UNIQUE INDEX '" + PriceEntry.INDEX_UID + + "' ON " + PriceEntry.TABLE_NAME + "(" + PriceEntry.COLUMN_UID + ")"; db.execSQL(createAccountUidIndex); db.execSQL(createTransactionUidIndex); db.execSQL(createSplitUidIndex); db.execSQL(createScheduledEventUidIndex); - } - + db.execSQL(createCommodityUidIndex); + db.execSQL(createPriceUidIndex); + try { + MigrationHelper.importCommodities(db); + } catch (SAXException | ParserConfigurationException | IOException e) { + Log.e(LOG_TAG, "Error loading currencies into the database"); + e.printStackTrace(); + throw new RuntimeException(e); + } + } } 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 080738194..de4b3e72b 100644 --- a/app/src/main/java/org/gnucash/android/db/DatabaseSchema.java +++ b/app/src/main/java/org/gnucash/android/db/DatabaseSchema.java @@ -28,7 +28,7 @@ public class DatabaseSchema { * Database version. * With any change to the database schema, this number must increase */ - static final int DATABASE_VERSION = 8; + public static final int DATABASE_VERSION = 9; /** * Database version where Splits were introduced @@ -53,7 +53,8 @@ public static abstract class AccountEntry implements CommonColumns { public static final String COLUMN_NAME = "name"; public static final String COLUMN_CURRENCY = "currency_code"; - public static final String COLUMN_DESCRIPTION = "description"; //TODO: Use me. Just added it because we are migrating the whole table anyway + public static final String COLUMN_COMMODITY_UID = "commodity_uid"; + public static final String COLUMN_DESCRIPTION = "description"; public static final String COLUMN_PARENT_ACCOUNT_UID = "parent_account_uid"; public static final String COLUMN_PLACEHOLDER = "is_placeholder"; public static final String COLUMN_COLOR_CODE = "color_code"; @@ -77,7 +78,14 @@ public static abstract class TransactionEntry implements CommonColumns { public static final String COLUMN_DESCRIPTION = "name"; public static final String COLUMN_NOTES = "description"; public static final String COLUMN_CURRENCY = "currency_code"; + public static final String COLUMN_COMMODITY_UID = "commodity_uid"; public static final String COLUMN_TIMESTAMP = "timestamp"; + + /** + * Flag for marking transactions which have been exported + * @deprecated Transactions are exported based on last modified timestamp + */ + @Deprecated public static final String COLUMN_EXPORTED = "is_exported"; public static final String COLUMN_TEMPLATE = "is_template"; public static final String COLUMN_SCHEDX_ACTION_UID = "scheduled_action_uid"; @@ -93,7 +101,17 @@ public static abstract class SplitEntry implements CommonColumns { public static final String TABLE_NAME = "splits"; public static final String COLUMN_TYPE = "type"; - public static final String COLUMN_AMOUNT = "amount"; + + /** + * The value columns are in the currency of the transaction containing the split + */ + public static final String COLUMN_VALUE_NUM = "value_num"; + public static final String COLUMN_VALUE_DENOM = "value_denom"; + /** + * The quantity columns are in the currency of the account to which the split belongs + */ + public static final String COLUMN_QUANTITY_NUM = "quantity_num"; + public static final String COLUMN_QUANTITY_DENOM = "quantity_denom"; public static final String COLUMN_MEMO = "memo"; public static final String COLUMN_ACCOUNT_UID = "account_uid"; public static final String COLUMN_TRANSACTION_UID = "transaction_uid"; @@ -117,4 +135,60 @@ public static abstract class ScheduledActionEntry implements CommonColumns { public static final String INDEX_UID = "scheduled_action_uid_index"; } + + public static abstract class CommodityEntry implements CommonColumns { + public static final String TABLE_NAME = "commodities"; + + /** + * The namespace field denotes the namespace for this commodity, + * either a currency or symbol from a quote source + */ + public static final String COLUMN_NAMESPACE = "namespace"; + + /** + * The fullname is the official full name of the currency + */ + public static final String COLUMN_FULLNAME = "fullname"; + + /** + * The mnemonic is the official abbreviated designation for the currency + */ + public static final String COLUMN_MNEMONIC = "mnemonic"; + + public static final String COLUMN_LOCAL_SYMBOL = "local_symbol"; + + /** + * The fraction is the number of sub-units that the basic commodity can be divided into + */ + public static final String COLUMN_FRACTION = "fraction"; + + /** + * A CUSIP is a nine-character alphanumeric code that identifies a North American financial security + * for the purposes of facilitating clearing and settlement of trades + */ + public static final String COLUMN_CUSIP = "cusip"; + + /** + * TRUE if prices are to be downloaded for this commodity from a quote source + */ + public static final String COLUMN_QUOTE_FLAG = "quote_flag"; + + public static final String INDEX_UID = "commodities_uid_index"; + } + + + public static abstract class PriceEntry implements CommonColumns { + public static final String TABLE_NAME = "prices"; + + public static final String COLUMN_COMMODITY_UID = "commodity_guid"; + public static final String COLUMN_CURRENCY_UID = "currency_guid"; + public static final String COLUMN_DATE = "date"; + public static final String COLUMN_SOURCE = "source"; + public static final String COLUMN_TYPE = "type"; + public static final String COLUMN_VALUE_NUM = "value_num"; + public static final String COLUMN_VALUE_DENOM = "value_denom"; + + public static final String INDEX_UID = "prices_uid_index"; + + } } 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 5b90ebca2..943087c0e 100644 --- a/app/src/main/java/org/gnucash/android/db/MigrationHelper.java +++ b/app/src/main/java/org/gnucash/android/db/MigrationHelper.java @@ -16,32 +16,63 @@ package org.gnucash.android.db; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.os.Environment; +import android.text.TextUtils; import android.util.Log; import com.crashlytics.android.Crashlytics; +import org.gnucash.android.R; +import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.export.Exporter; -import org.gnucash.android.importer.GncXmlImporter; +import org.gnucash.android.importer.CommoditiesXmlHandler; import org.gnucash.android.model.AccountType; +import org.gnucash.android.model.BaseModel; +import org.gnucash.android.model.Commodity; +import org.gnucash.android.model.Money; +import org.gnucash.android.model.Transaction; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.IOError; import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; import java.nio.channels.FileChannel; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + import static org.gnucash.android.db.DatabaseSchema.AccountEntry; +import static org.gnucash.android.db.DatabaseSchema.CommodityEntry; +import static org.gnucash.android.db.DatabaseSchema.CommonColumns; +import static org.gnucash.android.db.DatabaseSchema.PriceEntry; +import static org.gnucash.android.db.DatabaseSchema.ScheduledActionEntry; +import static org.gnucash.android.db.DatabaseSchema.SplitEntry; +import static org.gnucash.android.db.DatabaseSchema.TransactionEntry; /** * Collection of helper methods which are used during database migrations * * @author Ngewi Fet */ +@SuppressWarnings("unused") public class MigrationHelper { public static final String LOG_TAG = "MigrationHelper"; @@ -102,7 +133,7 @@ static String getFullyQualifiedAccountName(SQLiteDatabase db, String accountUID) */ private static String getGnuCashRootAccountUID(SQLiteDatabase db){ String condition = AccountEntry.COLUMN_TYPE + "= '" + AccountType.ROOT.name() + "'"; - Cursor cursor = db.query(DatabaseSchema.AccountEntry.TABLE_NAME, + Cursor cursor = db.query(AccountEntry.TABLE_NAME, null, condition, null, null, null, AccountEntry.COLUMN_NAME + " ASC"); String rootUID = null; @@ -113,16 +144,6 @@ private static String getGnuCashRootAccountUID(SQLiteDatabase db){ return rootUID; } - /** - * Imports GnuCash XML into the database from file - * @param filepath Path to GnuCash XML file - */ - static void importGnucashXML(SQLiteDatabase db, String filepath) throws Exception { - Log.i(LOG_TAG, "Importing GnuCash XML"); - FileInputStream inputStream = new FileInputStream(filepath); - GncXmlImporter.parse(db, inputStream); - } - /** * Copies the contents of the file in {@code src} to {@code dst} and then deletes the {@code src} if copy was successful. * If the file copy was unsuccessful, the src file will not be deleted. @@ -189,8 +210,853 @@ public void run() { } }; - public static String generateUUID() - { - return UUID.randomUUID().toString().replaceAll("-", ""); + + /** + * Imports commodities into the database from XML resource file + */ + static void importCommodities(SQLiteDatabase db) throws SAXException, ParserConfigurationException, IOException { + SAXParserFactory spf = SAXParserFactory.newInstance(); + SAXParser sp = spf.newSAXParser(); + XMLReader xr = sp.getXMLReader(); + + InputStream commoditiesInputStream = GnuCashApplication.getAppContext().getResources() + .openRawResource(R.raw.iso_4217_currencies); + BufferedInputStream bos = new BufferedInputStream(commoditiesInputStream); + + /** Create handler to handle XML Tags ( extends DefaultHandler ) */ + + CommoditiesXmlHandler handler = new CommoditiesXmlHandler(db); + + xr.setContentHandler(handler); + xr.parse(new InputSource(bos)); + } + + + /** + * Upgrades the database from version 1 to 2 + * @param db SQLiteDatabase + * @return Version number: 2 if upgrade successful, 1 otherwise + */ + public static int upgradeDbToVersion2(SQLiteDatabase db) { + int oldVersion; + String addColumnSql = "ALTER TABLE " + TransactionEntry.TABLE_NAME + + " ADD COLUMN double_account_uid varchar(255)"; + + //introducing sub accounts + Log.i(DatabaseHelper.LOG_TAG, "Adding column for parent accounts"); + String addParentAccountSql = "ALTER TABLE " + AccountEntry.TABLE_NAME + + " ADD COLUMN " + AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " varchar(255)"; + + db.execSQL(addColumnSql); + db.execSQL(addParentAccountSql); + + //update account types to GnuCash account types + //since all were previously CHECKING, now all will be CASH + Log.i(DatabaseHelper.LOG_TAG, "Converting account types to GnuCash compatible types"); + ContentValues cv = new ContentValues(); + cv.put(SplitEntry.COLUMN_TYPE, AccountType.CASH.toString()); + db.update(AccountEntry.TABLE_NAME, cv, null, null); + + oldVersion = 2; + return oldVersion; + } + + /** + * Upgrades the database from version 2 to 3 + * @param db SQLiteDatabase to upgrade + * @return Version number: 3 if upgrade successful, 2 otherwise + */ + static int upgradeDbToVersion3(SQLiteDatabase db) { + int oldVersion; + String addPlaceHolderAccountFlagSql = "ALTER TABLE " + AccountEntry.TABLE_NAME + + " ADD COLUMN " + AccountEntry.COLUMN_PLACEHOLDER + " tinyint default 0"; + + db.execSQL(addPlaceHolderAccountFlagSql); + oldVersion = 3; + return oldVersion; + } + + /** + * Upgrades the database from version 3 to 4 + * @param db SQLiteDatabase + * @return Version number: 4 if upgrade successful, 3 otherwise + */ + static int upgradeDbToVersion4(SQLiteDatabase db) { + int oldVersion; + String addRecurrencePeriod = "ALTER TABLE " + TransactionEntry.TABLE_NAME + + " ADD COLUMN recurrence_period integer default 0"; + + String addDefaultTransferAccount = "ALTER TABLE " + AccountEntry.TABLE_NAME + + " ADD COLUMN " + AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID + " varchar(255)"; + + String addAccountColor = " ALTER TABLE " + AccountEntry.TABLE_NAME + + " ADD COLUMN " + AccountEntry.COLUMN_COLOR_CODE + " varchar(255)"; + + db.execSQL(addRecurrencePeriod); + db.execSQL(addDefaultTransferAccount); + db.execSQL(addAccountColor); + + oldVersion = 4; + return oldVersion; + } + + /** + * Upgrades the database from version 4 to 5 + *

Adds favorites column to accounts

+ * @param db SQLiteDatabase + * @return Version number: 5 if upgrade successful, 4 otherwise + */ + static int upgradeDbToVersion5(SQLiteDatabase db) { + int oldVersion; + String addAccountFavorite = " ALTER TABLE " + AccountEntry.TABLE_NAME + + " ADD COLUMN " + AccountEntry.COLUMN_FAVORITE + " tinyint default 0"; + db.execSQL(addAccountFavorite); + + oldVersion = 5; + return oldVersion; + } + + /** + * Upgrades the database from version 5 to version 6.
+ * This migration adds support for fully qualified account names and updates existing accounts. + * @param db SQLite Database to be upgraded + * @return New database version (6) if upgrade successful, old version (5) if unsuccessful + */ + static int upgradeDbToVersion6(SQLiteDatabase db) { + int oldVersion = 5; + String addFullAccountNameQuery = " ALTER TABLE " + AccountEntry.TABLE_NAME + + " ADD COLUMN " + AccountEntry.COLUMN_FULL_NAME + " varchar(255) "; + db.execSQL(addFullAccountNameQuery); + + //update all existing accounts with their fully qualified name + Cursor cursor = db.query(AccountEntry.TABLE_NAME, + new String[]{AccountEntry._ID, AccountEntry.COLUMN_UID}, + null, null, null, null, null); + while(cursor != null && cursor.moveToNext()){ + String uid = cursor.getString(cursor.getColumnIndexOrThrow(AccountEntry.COLUMN_UID)); + String fullName = getFullyQualifiedAccountName(db, uid); + + if (fullName == null) + continue; + + ContentValues contentValues = new ContentValues(); + contentValues.put(AccountEntry.COLUMN_FULL_NAME, fullName); + + long id = cursor.getLong(cursor.getColumnIndexOrThrow(AccountEntry._ID)); + db.update(AccountEntry.TABLE_NAME, contentValues, AccountEntry._ID + " = " + id, null); + } + + if (cursor != null) { + cursor.close(); + } + + oldVersion = 6; + return oldVersion; + } + + + /** + * Code for upgrading the database to version 7 from version 6.
+ * Tasks accomplished in migration: + *
    + *
  • Added new splits table for transaction splits
  • + *
  • Extract existing info from transactions table to populate split table
  • + *
+ * @param db SQLite Database + * @return The new database version if upgrade was successful, or the old db version if it failed + */ + static int upgradeDbToVersion7(SQLiteDatabase db) { + int oldVersion = 6; + db.beginTransaction(); + try { + // backup transaction table + db.execSQL("ALTER TABLE " + TransactionEntry.TABLE_NAME + " RENAME TO " + TransactionEntry.TABLE_NAME + "_bak"); + // create new transaction table + db.execSQL("create table " + TransactionEntry.TABLE_NAME + " (" + + TransactionEntry._ID + " integer primary key autoincrement, " + + TransactionEntry.COLUMN_UID + " varchar(255) not null, " + + TransactionEntry.COLUMN_DESCRIPTION + " varchar(255), " + + TransactionEntry.COLUMN_NOTES + " text, " + + TransactionEntry.COLUMN_TIMESTAMP + " integer not null, " + + TransactionEntry.COLUMN_EXPORTED + " tinyint default 0, " + + TransactionEntry.COLUMN_CURRENCY + " varchar(255) not null, " + + "recurrence_period integer default 0, " + + "UNIQUE (" + TransactionEntry.COLUMN_UID + ") " + + ");"); + // initialize new transaction table wiht data from old table + db.execSQL("INSERT INTO " + TransactionEntry.TABLE_NAME + " ( " + + TransactionEntry._ID + " , " + + TransactionEntry.COLUMN_UID + " , " + + TransactionEntry.COLUMN_DESCRIPTION + " , " + + TransactionEntry.COLUMN_NOTES + " , " + + TransactionEntry.COLUMN_TIMESTAMP + " , " + + TransactionEntry.COLUMN_EXPORTED + " , " + + TransactionEntry.COLUMN_CURRENCY + " , " + + "recurrence_period ) SELECT " + + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry._ID + " , " + + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_UID + " , " + + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_DESCRIPTION + " , " + + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_NOTES + " , " + + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_TIMESTAMP + " , " + + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_EXPORTED + " , " + + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_CURRENCY + " , " + + TransactionEntry.TABLE_NAME + "_bak.recurrence_period" + + " FROM " + TransactionEntry.TABLE_NAME + "_bak , " + AccountEntry.TABLE_NAME + + " ON " + TransactionEntry.TABLE_NAME + "_bak.account_uid == " + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_UID + ); + // create split table + db.execSQL("CREATE TABLE " + SplitEntry.TABLE_NAME + " (" + + SplitEntry._ID + " integer primary key autoincrement, " + + SplitEntry.COLUMN_UID + " varchar(255) not null, " + + SplitEntry.COLUMN_MEMO + " text, " + + SplitEntry.COLUMN_TYPE + " varchar(255) not null, " + + "amount" + " varchar(255) not null, " + + SplitEntry.COLUMN_ACCOUNT_UID + " varchar(255) not null, " + + SplitEntry.COLUMN_TRANSACTION_UID + " varchar(255) not null, " + + "FOREIGN KEY (" + SplitEntry.COLUMN_ACCOUNT_UID + ") REFERENCES " + AccountEntry.TABLE_NAME + " (" + AccountEntry.COLUMN_UID + "), " + + "FOREIGN KEY (" + SplitEntry.COLUMN_TRANSACTION_UID + ") REFERENCES " + TransactionEntry.TABLE_NAME + " (" + TransactionEntry.COLUMN_UID + "), " + + "UNIQUE (" + SplitEntry.COLUMN_UID + ") " + + ");"); + // Initialize split table with data from backup transaction table + // New split table is initialized after the new transaction table as the + // foreign key constraint will stop any data from being inserted + // If new split table is created before the backup is made, the foreign key + // constraint will be rewritten to refer to the backup transaction table + db.execSQL("INSERT INTO " + SplitEntry.TABLE_NAME + " ( " + + SplitEntry.COLUMN_UID + " , " + + SplitEntry.COLUMN_TYPE + " , " + + "amount" + " , " + + SplitEntry.COLUMN_ACCOUNT_UID + " , " + + SplitEntry.COLUMN_TRANSACTION_UID + " ) SELECT " + + "LOWER(HEX(RANDOMBLOB(16))) , " + + "CASE WHEN " + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_TYPE + " IN ( 'CASH' , 'BANK', 'ASSET', 'EXPENSE', 'RECEIVABLE', 'STOCK', 'MUTUAL' ) THEN CASE WHEN " + + "amount" + " < 0 THEN 'CREDIT' ELSE 'DEBIT' END ELSE CASE WHEN " + + "amount" + " < 0 THEN 'DEBIT' ELSE 'CREDIT' END END , " + + "ABS ( " + TransactionEntry.TABLE_NAME + "_bak.amount ) , " + + TransactionEntry.TABLE_NAME + "_bak.account_uid , " + + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_UID + + " FROM " + TransactionEntry.TABLE_NAME + "_bak , " + AccountEntry.TABLE_NAME + + " ON " + TransactionEntry.TABLE_NAME + "_bak.account_uid = " + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_UID + + " UNION SELECT " + + "LOWER(HEX(RANDOMBLOB(16))) AS " + SplitEntry.COLUMN_UID + " , " + + "CASE WHEN " + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_TYPE + " IN ( 'CASH' , 'BANK', 'ASSET', 'EXPENSE', 'RECEIVABLE', 'STOCK', 'MUTUAL' ) THEN CASE WHEN " + + "amount" + " < 0 THEN 'DEBIT' ELSE 'CREDIT' END ELSE CASE WHEN " + + "amount" + " < 0 THEN 'CREDIT' ELSE 'DEBIT' END END , " + + "ABS ( " + TransactionEntry.TABLE_NAME + "_bak.amount ) , " + + TransactionEntry.TABLE_NAME + "_bak.double_account_uid , " + + TransactionEntry.TABLE_NAME + "_baK." + TransactionEntry.COLUMN_UID + + " FROM " + TransactionEntry.TABLE_NAME + "_bak , " + AccountEntry.TABLE_NAME + + " ON " + TransactionEntry.TABLE_NAME + "_bak.account_uid = " + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_UID + + " WHERE " + TransactionEntry.TABLE_NAME + "_bak.double_account_uid IS NOT NULL" + ); + // drop backup transaction table + db.execSQL("DROP TABLE " + TransactionEntry.TABLE_NAME + "_bak"); + db.setTransactionSuccessful(); + oldVersion = 7; + } finally { + db.endTransaction(); + } + return oldVersion; + } + + /** + * Upgrades the database from version 7 to version 8. + *

This migration accomplishes the following: + *

    + *
  • Added created_at and modified_at columns to all tables (including triggers for updating the columns).
  • + *
  • New table for scheduled actions and migrate all existing recurring transactions
  • + *
  • Auto-balancing of all existing splits
  • + *
  • Added "hidden" flag to accounts table
  • + *
  • Add flag for transaction templates
  • + *
+ *

+ * @param db SQLite Database to be upgraded + * @return New database version (8) if upgrade successful, old version (7) if unsuccessful + */ + static int upgradeDbToVersion8(SQLiteDatabase db) { + Log.i(DatabaseHelper.LOG_TAG, "Upgrading database to version 8"); + int oldVersion = 7; + new File(Exporter.BACKUP_FOLDER_PATH).mkdirs(); + new File(Exporter.EXPORT_FOLDER_PATH).mkdirs(); + //start moving the files in background thread before we do the database stuff + new Thread(moveExportedFilesToNewDefaultLocation).start(); + + db.beginTransaction(); + try { + + Log.i(DatabaseHelper.LOG_TAG, "Creating scheduled actions table"); + db.execSQL("CREATE TABLE " + ScheduledActionEntry.TABLE_NAME + " (" + + ScheduledActionEntry._ID + " integer primary key autoincrement, " + + ScheduledActionEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + + ScheduledActionEntry.COLUMN_ACTION_UID + " varchar(255) not null, " + + ScheduledActionEntry.COLUMN_TYPE + " varchar(255) not null, " + + ScheduledActionEntry.COLUMN_PERIOD + " integer not null, " + + ScheduledActionEntry.COLUMN_LAST_RUN + " integer default 0, " + + ScheduledActionEntry.COLUMN_START_TIME + " integer not null, " + + ScheduledActionEntry.COLUMN_END_TIME + " integer default 0, " + + ScheduledActionEntry.COLUMN_TAG + " text, " + + ScheduledActionEntry.COLUMN_ENABLED + " tinyint default 1, " //enabled by default + + ScheduledActionEntry.COLUMN_TOTAL_FREQUENCY + " integer default 0, " + + ScheduledActionEntry.COLUMN_EXECUTION_COUNT+ " integer default 0, " + + ScheduledActionEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + ScheduledActionEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP " + + ");" + DatabaseHelper.createUpdatedAtTrigger(ScheduledActionEntry.TABLE_NAME)); + + + //==============================BEGIN TABLE MIGRATIONS ======================================== + Log.i(DatabaseHelper.LOG_TAG, "Migrating accounts table"); + // backup transaction table + db.execSQL("ALTER TABLE " + AccountEntry.TABLE_NAME + " RENAME TO " + AccountEntry.TABLE_NAME + "_bak"); + // create new transaction table + db.execSQL("CREATE TABLE " + AccountEntry.TABLE_NAME + " (" + + AccountEntry._ID + " integer primary key autoincrement, " + + AccountEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + + AccountEntry.COLUMN_NAME + " varchar(255) not null, " + + AccountEntry.COLUMN_TYPE + " varchar(255) not null, " + + AccountEntry.COLUMN_CURRENCY + " varchar(255) not null, " + + AccountEntry.COLUMN_DESCRIPTION + " varchar(255), " + + AccountEntry.COLUMN_COLOR_CODE + " varchar(255), " + + AccountEntry.COLUMN_FAVORITE + " tinyint default 0, " + + AccountEntry.COLUMN_HIDDEN + " tinyint default 0, " + + AccountEntry.COLUMN_FULL_NAME + " varchar(255), " + + AccountEntry.COLUMN_PLACEHOLDER + " tinyint default 0, " + + AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " varchar(255), " + + AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID + " varchar(255), " + + AccountEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + AccountEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP " + + ");" + DatabaseHelper.createUpdatedAtTrigger(AccountEntry.TABLE_NAME)); + + // initialize new account table with data from old table + db.execSQL("INSERT INTO " + AccountEntry.TABLE_NAME + " ( " + + AccountEntry._ID + "," + + AccountEntry.COLUMN_UID + " , " + + AccountEntry.COLUMN_NAME + " , " + + AccountEntry.COLUMN_TYPE + " , " + + AccountEntry.COLUMN_CURRENCY + " , " + + AccountEntry.COLUMN_COLOR_CODE + " , " + + AccountEntry.COLUMN_FAVORITE + " , " + + AccountEntry.COLUMN_FULL_NAME + " , " + + AccountEntry.COLUMN_PLACEHOLDER + " , " + + AccountEntry.COLUMN_HIDDEN + " , " + + AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " , " + + AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID + + ") SELECT " + + AccountEntry.TABLE_NAME + "_bak." + AccountEntry._ID + " , " + + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_UID + " , " + + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_NAME + " , " + + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_TYPE + " , " + + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_CURRENCY + " , " + + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_COLOR_CODE + " , " + + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_FAVORITE + " , " + + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_FULL_NAME + " , " + + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_PLACEHOLDER + " , " + + " CASE WHEN " + AccountEntry.TABLE_NAME + "_bak.type = 'ROOT' THEN 1 ELSE 0 END, " + + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " , " + + AccountEntry.TABLE_NAME + "_bak." + AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID + + " FROM " + AccountEntry.TABLE_NAME + "_bak;" + ); + + Log.i(DatabaseHelper.LOG_TAG, "Migrating transactions table"); + // backup transaction table + db.execSQL("ALTER TABLE " + TransactionEntry.TABLE_NAME + " RENAME TO " + TransactionEntry.TABLE_NAME + "_bak"); + // create new transaction table + db.execSQL("CREATE TABLE " + TransactionEntry.TABLE_NAME + " (" + + TransactionEntry._ID + " integer primary key autoincrement, " + + TransactionEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + + TransactionEntry.COLUMN_DESCRIPTION + " varchar(255), " + + TransactionEntry.COLUMN_NOTES + " text, " + + TransactionEntry.COLUMN_TIMESTAMP + " integer not null, " + + TransactionEntry.COLUMN_EXPORTED + " tinyint default 0, " + + TransactionEntry.COLUMN_TEMPLATE + " tinyint default 0, " + + TransactionEntry.COLUMN_CURRENCY + " varchar(255) not null, " + + TransactionEntry.COLUMN_SCHEDX_ACTION_UID + " varchar(255), " + + TransactionEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + TransactionEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + "FOREIGN KEY (" + TransactionEntry.COLUMN_SCHEDX_ACTION_UID + ") REFERENCES " + ScheduledActionEntry.TABLE_NAME + " (" + ScheduledActionEntry.COLUMN_UID + ") ON DELETE SET NULL " + + ");" + DatabaseHelper.createUpdatedAtTrigger(TransactionEntry.TABLE_NAME)); + + // initialize new transaction table with data from old table + db.execSQL("INSERT INTO " + TransactionEntry.TABLE_NAME + " ( " + + TransactionEntry._ID + " , " + + TransactionEntry.COLUMN_UID + " , " + + TransactionEntry.COLUMN_DESCRIPTION + " , " + + TransactionEntry.COLUMN_NOTES + " , " + + TransactionEntry.COLUMN_TIMESTAMP + " , " + + TransactionEntry.COLUMN_EXPORTED + " , " + + TransactionEntry.COLUMN_CURRENCY + " , " + + TransactionEntry.COLUMN_TEMPLATE + + ") SELECT " + + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry._ID + " , " + + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_UID + " , " + + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_DESCRIPTION + " , " + + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_NOTES + " , " + + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_TIMESTAMP + " , " + + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_EXPORTED + " , " + + TransactionEntry.TABLE_NAME + "_bak." + TransactionEntry.COLUMN_CURRENCY + " , " + + " CASE WHEN " + TransactionEntry.TABLE_NAME + "_bak.recurrence_period > 0 THEN 1 ELSE 0 END " + + " FROM " + TransactionEntry.TABLE_NAME + "_bak;" + ); + + Log.i(DatabaseHelper.LOG_TAG, "Migrating splits table"); + // backup split table + db.execSQL("ALTER TABLE " + SplitEntry.TABLE_NAME + " RENAME TO " + SplitEntry.TABLE_NAME + "_bak"); + // create new split table + db.execSQL("CREATE TABLE " + SplitEntry.TABLE_NAME + " (" + + SplitEntry._ID + " integer primary key autoincrement, " + + SplitEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + + SplitEntry.COLUMN_MEMO + " text, " + + SplitEntry.COLUMN_TYPE + " varchar(255) not null, " + + "amount" + " varchar(255) not null, " + + SplitEntry.COLUMN_ACCOUNT_UID + " varchar(255) not null, " + + SplitEntry.COLUMN_TRANSACTION_UID + " varchar(255) not null, " + + SplitEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + SplitEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + "FOREIGN KEY (" + SplitEntry.COLUMN_ACCOUNT_UID + ") REFERENCES " + AccountEntry.TABLE_NAME + " (" + AccountEntry.COLUMN_UID + ") ON DELETE CASCADE, " + + "FOREIGN KEY (" + SplitEntry.COLUMN_TRANSACTION_UID + ") REFERENCES " + TransactionEntry.TABLE_NAME + " (" + TransactionEntry.COLUMN_UID + ") ON DELETE CASCADE " + + ");" + DatabaseHelper.createUpdatedAtTrigger(SplitEntry.TABLE_NAME)); + + // initialize new split table with data from old table + db.execSQL("INSERT INTO " + SplitEntry.TABLE_NAME + " ( " + + SplitEntry._ID + " , " + + SplitEntry.COLUMN_UID + " , " + + SplitEntry.COLUMN_MEMO + " , " + + SplitEntry.COLUMN_TYPE + " , " + + "amount" + " , " + + SplitEntry.COLUMN_ACCOUNT_UID + " , " + + SplitEntry.COLUMN_TRANSACTION_UID + + ") SELECT " + + SplitEntry.TABLE_NAME + "_bak." + SplitEntry._ID + " , " + + SplitEntry.TABLE_NAME + "_bak." + SplitEntry.COLUMN_UID + " , " + + SplitEntry.TABLE_NAME + "_bak." + SplitEntry.COLUMN_MEMO + " , " + + SplitEntry.TABLE_NAME + "_bak." + SplitEntry.COLUMN_TYPE + " , " + + SplitEntry.TABLE_NAME + "_bak." + "amount" + " , " + + SplitEntry.TABLE_NAME + "_bak." + SplitEntry.COLUMN_ACCOUNT_UID + " , " + + SplitEntry.TABLE_NAME + "_bak." + SplitEntry.COLUMN_TRANSACTION_UID + + " FROM " + SplitEntry.TABLE_NAME + "_bak;" + ); + + + + //================================ END TABLE MIGRATIONS ================================ + + // String timestamp to be used for all new created entities in migration + String timestamp = (new Timestamp(System.currentTimeMillis())).toString(); + + //ScheduledActionDbAdapter scheduledActionDbAdapter = new ScheduledActionDbAdapter(db); + //SplitsDbAdapter splitsDbAdapter = new SplitsDbAdapter(db); + //TransactionsDbAdapter transactionsDbAdapter = new TransactionsDbAdapter(db, splitsDbAdapter); + //AccountsDbAdapter accountsDbAdapter = new AccountsDbAdapter(db,transactionsDbAdapter); + + Log.i(DatabaseHelper.LOG_TAG, "Creating default root account if none exists"); + ContentValues contentValues = new ContentValues(); + //assign a root account to all accounts which had null as parent except ROOT (top-level accounts) + String rootAccountUID; + Cursor cursor = db.query(AccountEntry.TABLE_NAME, + new String[]{AccountEntry.COLUMN_UID}, + AccountEntry.COLUMN_TYPE + "= ?", + new String[]{AccountType.ROOT.name()}, null, null, null); + try { + if (cursor.moveToFirst()) { + rootAccountUID = cursor.getString(cursor.getColumnIndexOrThrow(AccountEntry.COLUMN_UID)); + } + else + { + rootAccountUID = BaseModel.generateUID(); + contentValues.clear(); + contentValues.put(CommonColumns.COLUMN_UID, rootAccountUID); + contentValues.put(CommonColumns.COLUMN_CREATED_AT, timestamp); + contentValues.put(AccountEntry.COLUMN_NAME, "ROOT"); + contentValues.put(AccountEntry.COLUMN_TYPE, "ROOT"); + contentValues.put(AccountEntry.COLUMN_CURRENCY, Money.DEFAULT_CURRENCY_CODE); + contentValues.put(AccountEntry.COLUMN_PLACEHOLDER, 0); + contentValues.put(AccountEntry.COLUMN_HIDDEN, 1); + contentValues.putNull(AccountEntry.COLUMN_COLOR_CODE); + contentValues.put(AccountEntry.COLUMN_FAVORITE, 0); + contentValues.put(AccountEntry.COLUMN_FULL_NAME, " "); + contentValues.putNull(AccountEntry.COLUMN_PARENT_ACCOUNT_UID); + contentValues.putNull(AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID); + db.insert(AccountEntry.TABLE_NAME, null, contentValues); + } + } finally { + cursor.close(); + } + //String rootAccountUID = accountsDbAdapter.getOrCreateGnuCashRootAccountUID(); + contentValues.clear(); + contentValues.put(AccountEntry.COLUMN_PARENT_ACCOUNT_UID, rootAccountUID); + db.update(AccountEntry.TABLE_NAME, contentValues, AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " IS NULL AND " + AccountEntry.COLUMN_TYPE + " != ?", new String[]{"ROOT"}); + + Log.i(DatabaseHelper.LOG_TAG, "Migrating existing recurring transactions"); + cursor = db.query(TransactionEntry.TABLE_NAME + "_bak", null, "recurrence_period > 0", null, null, null, null); + long lastRun = System.currentTimeMillis(); + while (cursor.moveToNext()){ + contentValues.clear(); + Timestamp timestampT = new Timestamp(cursor.getLong(cursor.getColumnIndexOrThrow(TransactionEntry.COLUMN_TIMESTAMP))); + contentValues.put(TransactionEntry.COLUMN_CREATED_AT, timestampT.toString()); + long transactionId = cursor.getLong(cursor.getColumnIndexOrThrow(TransactionEntry._ID)); + db.update(TransactionEntry.TABLE_NAME, contentValues, TransactionEntry._ID + "=" + transactionId, null); + + //ScheduledAction scheduledAction = new ScheduledAction(ScheduledAction.ActionType.TRANSACTION); + //scheduledAction.setActionUID(cursor.getString(cursor.getColumnIndexOrThrow(TransactionEntry.COLUMN_UID))); + //long period = cursor.getLong(cursor.getColumnIndexOrThrow("recurrence_period")); + //scheduledAction.setPeriod(period); + //scheduledAction.setStartTime(timestampT.getTime()); //the start time is when the transaction was created + //scheduledAction.setLastRun(System.currentTimeMillis()); //prevent this from being executed at the end of migration + + contentValues.clear(); + contentValues.put(CommonColumns.COLUMN_UID, BaseModel.generateUID()); + contentValues.put(CommonColumns.COLUMN_CREATED_AT, timestamp); + contentValues.put(ScheduledActionEntry.COLUMN_ACTION_UID, cursor.getString(cursor.getColumnIndexOrThrow(TransactionEntry.COLUMN_UID))); + contentValues.put(ScheduledActionEntry.COLUMN_PERIOD, cursor.getLong(cursor.getColumnIndexOrThrow("recurrence_period"))); + contentValues.put(ScheduledActionEntry.COLUMN_START_TIME, timestampT.getTime()); + contentValues.put(ScheduledActionEntry.COLUMN_END_TIME, 0); + contentValues.put(ScheduledActionEntry.COLUMN_LAST_RUN, lastRun); + contentValues.put(ScheduledActionEntry.COLUMN_TYPE, "TRANSACTION"); + contentValues.put(ScheduledActionEntry.COLUMN_TAG, ""); + contentValues.put(ScheduledActionEntry.COLUMN_ENABLED, 1); + contentValues.put(ScheduledActionEntry.COLUMN_TOTAL_FREQUENCY, 0); + contentValues.put(ScheduledActionEntry.COLUMN_EXECUTION_COUNT, 0); + //scheduledActionDbAdapter.addRecord(scheduledAction); + db.insert(ScheduledActionEntry.TABLE_NAME, null, contentValues); + + //build intent for recurring transactions in the database + Intent intent = new Intent(Intent.ACTION_INSERT); + intent.setType(Transaction.MIME_TYPE); + + //cancel existing pending intent + Context context = GnuCashApplication.getAppContext(); + PendingIntent recurringPendingIntent = PendingIntent.getBroadcast(context, + (int)transactionId, intent, PendingIntent.FLAG_CANCEL_CURRENT); + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + alarmManager.cancel(recurringPendingIntent); + } + cursor.close(); + + //auto-balance existing splits + Log.i(DatabaseHelper.LOG_TAG, "Auto-balancing existing transaction splits"); + cursor = db.query( + TransactionEntry.TABLE_NAME + " , " + SplitEntry.TABLE_NAME + " ON " + + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_UID + "=" + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_TRANSACTION_UID + + " , " + AccountEntry.TABLE_NAME + " ON " + + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_ACCOUNT_UID + "=" + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_UID, + new String[]{ + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_UID + " AS trans_uid", + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_CURRENCY + " AS trans_currency", + "TOTAL ( CASE WHEN " + + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_TYPE + " = 'DEBIT' THEN " + + SplitEntry.TABLE_NAME + "." + "amount" + " ELSE - " + + SplitEntry.TABLE_NAME + "." + "amount" + " END ) AS trans_acct_balance", + "COUNT ( DISTINCT " + + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_CURRENCY + + " ) AS trans_currency_count" + }, + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_TEMPLATE + " == 0", + null, + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_UID, + "trans_acct_balance != 0 AND trans_currency_count = 1", + null); + try { + while (cursor.moveToNext()){ + double imbalance = cursor.getDouble(cursor.getColumnIndexOrThrow("trans_acct_balance")); + BigDecimal decimalImbalance = BigDecimal.valueOf(imbalance).setScale(2, BigDecimal.ROUND_HALF_UP); + if (decimalImbalance.compareTo(BigDecimal.ZERO) != 0) { + String currencyCode = cursor.getString(cursor.getColumnIndexOrThrow("trans_currency")); + String imbalanceAccountName = GnuCashApplication.getAppContext().getString(R.string.imbalance_account_name) + "-" + currencyCode; + String imbalanceAccountUID; + Cursor c = db.query(AccountEntry.TABLE_NAME, new String[]{AccountEntry.COLUMN_UID}, + AccountEntry.COLUMN_FULL_NAME + "= ?", new String[]{imbalanceAccountName}, + null, null, null); + try { + if (c.moveToFirst()) { + imbalanceAccountUID = c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_UID)); + } + else { + imbalanceAccountUID = BaseModel.generateUID(); + contentValues.clear(); + contentValues.put(CommonColumns.COLUMN_UID, imbalanceAccountUID); + contentValues.put(CommonColumns.COLUMN_CREATED_AT, timestamp); + contentValues.put(AccountEntry.COLUMN_NAME, imbalanceAccountName); + contentValues.put(AccountEntry.COLUMN_TYPE, "BANK"); + contentValues.put(AccountEntry.COLUMN_CURRENCY, currencyCode); + contentValues.put(AccountEntry.COLUMN_PLACEHOLDER, 0); + contentValues.put(AccountEntry.COLUMN_HIDDEN, GnuCashApplication.isDoubleEntryEnabled() ? 0 : 1); + contentValues.putNull(AccountEntry.COLUMN_COLOR_CODE); + contentValues.put(AccountEntry.COLUMN_FAVORITE, 0); + contentValues.put(AccountEntry.COLUMN_FULL_NAME, imbalanceAccountName); + contentValues.put(AccountEntry.COLUMN_PARENT_ACCOUNT_UID, rootAccountUID); + contentValues.putNull(AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID); + db.insert(AccountEntry.TABLE_NAME, null, contentValues); + } + } finally { + c.close(); + } + String TransactionUID = cursor.getString(cursor.getColumnIndexOrThrow("trans_uid")); + contentValues.clear(); + contentValues.put(CommonColumns.COLUMN_UID, BaseModel.generateUID()); + contentValues.put(CommonColumns.COLUMN_CREATED_AT, timestamp); + contentValues.put("amount", decimalImbalance.abs().toPlainString()); + contentValues.put(SplitEntry.COLUMN_TYPE, decimalImbalance.compareTo(BigDecimal.ZERO) < 0 ? "DEBIT" : "CREDIT"); + contentValues.put(SplitEntry.COLUMN_MEMO, ""); + contentValues.put(SplitEntry.COLUMN_ACCOUNT_UID, imbalanceAccountUID); + contentValues.put(SplitEntry.COLUMN_TRANSACTION_UID, TransactionUID); + db.insert(SplitEntry.TABLE_NAME, null, contentValues); + contentValues.clear(); + contentValues.put(TransactionEntry.COLUMN_MODIFIED_AT, timestamp); + db.update(TransactionEntry.TABLE_NAME, contentValues, TransactionEntry.COLUMN_UID + " == ?", + new String[]{TransactionUID}); + } + } + } finally { + cursor.close(); + } + + Log.i(DatabaseHelper.LOG_TAG, "Dropping temporary migration tables"); + db.execSQL("DROP TABLE " + SplitEntry.TABLE_NAME + "_bak"); + db.execSQL("DROP TABLE " + AccountEntry.TABLE_NAME + "_bak"); + db.execSQL("DROP TABLE " + TransactionEntry.TABLE_NAME + "_bak"); + + db.setTransactionSuccessful(); + oldVersion = 8; + } finally { + db.endTransaction(); + } + + GnuCashApplication.startScheduledActionExecutionService(GnuCashApplication.getAppContext()); + + return oldVersion; + } + + /** + * Upgrades the database from version 8 to version 9. + *

This migration accomplishes the following: + *

    + *
  • Adds a commodities table to the database
  • + *
  • Adds prices table to the database
  • + *
  • Add separate columns for split value and quantity
  • + *
  • Migrate amounts to use the correct denominations for the currency
  • + *
+ *

+ * @param db SQLite Database to be upgraded + * @return New database version (9) if upgrade successful, old version (8) if unsuccessful + * @throws RuntimeException if the default commodities could not be imported + */ + static int upgradeDbToVersion9(SQLiteDatabase db){ + Log.i(DatabaseHelper.LOG_TAG, "Upgrading database to version 9"); + int oldVersion = 8; + + db.beginTransaction(); + try { + String createCommoditiesSql = "CREATE TABLE " + CommodityEntry.TABLE_NAME + " (" + + CommodityEntry._ID + " integer primary key autoincrement, " + + CommodityEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + + CommodityEntry.COLUMN_NAMESPACE + " varchar(255) not null default " + Commodity.Namespace.ISO4217.name() + ", " + + CommodityEntry.COLUMN_FULLNAME + " varchar(255) not null, " + + CommodityEntry.COLUMN_MNEMONIC + " varchar(255) not null, " + + CommodityEntry.COLUMN_LOCAL_SYMBOL+ " varchar(255) not null default '', " + + CommodityEntry.COLUMN_CUSIP + " varchar(255), " + + CommodityEntry.COLUMN_FRACTION + " integer not null, " + + CommodityEntry.COLUMN_QUOTE_FLAG + " integer not null, " + + CommodityEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + CommodityEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP " + + ");" + DatabaseHelper.createUpdatedAtTrigger(CommodityEntry.TABLE_NAME); + db.execSQL(createCommoditiesSql); + try { + importCommodities(db); + } catch (SAXException | ParserConfigurationException | IOException e) { + Log.e(DatabaseHelper.LOG_TAG, "Error loading currencies into the database", e); + Crashlytics.logException(e); + throw new RuntimeException(e); + } + + db.execSQL(" ALTER TABLE " + AccountEntry.TABLE_NAME + + " ADD COLUMN " + AccountEntry.COLUMN_COMMODITY_UID + " varchar(255) " + + " REFERENCES " + CommodityEntry.TABLE_NAME + " (" + CommodityEntry.COLUMN_UID + ") "); + + db.execSQL(" ALTER TABLE " + TransactionEntry.TABLE_NAME + + " ADD COLUMN " + TransactionEntry.COLUMN_COMMODITY_UID + " varchar(255) " + + " REFERENCES " + CommodityEntry.TABLE_NAME + " (" + CommodityEntry.COLUMN_UID + ") "); + + db.execSQL("UPDATE " + AccountEntry.TABLE_NAME + " SET " + AccountEntry.COLUMN_COMMODITY_UID + " = " + + " (SELECT " + CommodityEntry.COLUMN_UID + + " FROM " + CommodityEntry.TABLE_NAME + + " WHERE " + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_COMMODITY_UID + " = " + CommodityEntry.TABLE_NAME + "." + CommodityEntry.COLUMN_UID + + ")"); + + db.execSQL("UPDATE " + TransactionEntry.TABLE_NAME + " SET " + TransactionEntry.COLUMN_COMMODITY_UID + " = " + + " (SELECT " + CommodityEntry.COLUMN_UID + + " FROM " + CommodityEntry.TABLE_NAME + + " WHERE " + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_COMMODITY_UID + " = " + CommodityEntry.TABLE_NAME + "." + CommodityEntry.COLUMN_UID + + ")"); + + String createPricesSql = "CREATE TABLE " + PriceEntry.TABLE_NAME + " (" + + PriceEntry._ID + " integer primary key autoincrement, " + + PriceEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + + PriceEntry.COLUMN_COMMODITY_UID + " varchar(255) not null, " + + PriceEntry.COLUMN_CURRENCY_UID + " varchar(255) not null, " + + PriceEntry.COLUMN_TYPE + " varchar(255), " + + PriceEntry.COLUMN_DATE + " TIMESTAMP not null, " + + PriceEntry.COLUMN_SOURCE + " text, " + + PriceEntry.COLUMN_VALUE_NUM + " integer not null, " + + PriceEntry.COLUMN_VALUE_DENOM + " integer not null, " + + PriceEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + PriceEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + "UNIQUE (" + PriceEntry.COLUMN_COMMODITY_UID + ", " + PriceEntry.COLUMN_CURRENCY_UID + ") ON CONFLICT REPLACE, " + + "FOREIGN KEY (" + PriceEntry.COLUMN_COMMODITY_UID + ") REFERENCES " + CommodityEntry.TABLE_NAME + " (" + CommodityEntry.COLUMN_UID + ") ON DELETE CASCADE, " + + "FOREIGN KEY (" + PriceEntry.COLUMN_CURRENCY_UID + ") REFERENCES " + CommodityEntry.TABLE_NAME + " (" + CommodityEntry.COLUMN_UID + ") ON DELETE CASCADE " + + ");" + DatabaseHelper.createUpdatedAtTrigger(PriceEntry.TABLE_NAME); + db.execSQL(createPricesSql); + + + //store split amounts as integer components numerator and denominator + + db.execSQL("ALTER TABLE " + SplitEntry.TABLE_NAME + " RENAME TO " + SplitEntry.TABLE_NAME + "_bak"); + // create new split table + db.execSQL("CREATE TABLE " + SplitEntry.TABLE_NAME + " (" + + SplitEntry._ID + " integer primary key autoincrement, " + + SplitEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + + SplitEntry.COLUMN_MEMO + " text, " + + SplitEntry.COLUMN_TYPE + " varchar(255) not null, " + + SplitEntry.COLUMN_VALUE_NUM + " integer not null, " + + SplitEntry.COLUMN_VALUE_DENOM + " integer not null, " + + SplitEntry.COLUMN_QUANTITY_NUM + " integer not null, " + + SplitEntry.COLUMN_QUANTITY_DENOM + " integer not null, " + + SplitEntry.COLUMN_ACCOUNT_UID + " varchar(255) not null, " + + SplitEntry.COLUMN_TRANSACTION_UID + " varchar(255) not null, " + + SplitEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + SplitEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + "FOREIGN KEY (" + SplitEntry.COLUMN_ACCOUNT_UID + ") REFERENCES " + AccountEntry.TABLE_NAME + " (" + AccountEntry.COLUMN_UID + ") ON DELETE CASCADE, " + + "FOREIGN KEY (" + SplitEntry.COLUMN_TRANSACTION_UID + ") REFERENCES " + TransactionEntry.TABLE_NAME + " (" + TransactionEntry.COLUMN_UID + ") ON DELETE CASCADE " + + ");" + DatabaseHelper.createUpdatedAtTrigger(SplitEntry.TABLE_NAME)); + + // initialize new split table with data from old table + db.execSQL("INSERT INTO " + SplitEntry.TABLE_NAME + " ( " + + SplitEntry._ID + " , " + + SplitEntry.COLUMN_UID + " , " + + SplitEntry.COLUMN_MEMO + " , " + + SplitEntry.COLUMN_TYPE + " , " + + SplitEntry.COLUMN_VALUE_NUM + " , " + + SplitEntry.COLUMN_VALUE_DENOM + " , " + + SplitEntry.COLUMN_QUANTITY_NUM + " , " + + SplitEntry.COLUMN_QUANTITY_DENOM + " , " + + SplitEntry.COLUMN_ACCOUNT_UID + " , " + + SplitEntry.COLUMN_TRANSACTION_UID + + ") SELECT " + + SplitEntry.TABLE_NAME + "_bak." + SplitEntry._ID + " , " + + SplitEntry.TABLE_NAME + "_bak." + SplitEntry.COLUMN_UID + " , " + + SplitEntry.TABLE_NAME + "_bak." + SplitEntry.COLUMN_MEMO + " , " + + SplitEntry.TABLE_NAME + "_bak." + SplitEntry.COLUMN_TYPE + " , " + + SplitEntry.TABLE_NAME + "_bak.amount * 100, " //we will update this value in the next steps + + "100, " + + SplitEntry.TABLE_NAME + "_bak.amount * 100, " //default units of 2 decimal places were assumed until now + + "100, " + + SplitEntry.TABLE_NAME + "_bak." + SplitEntry.COLUMN_ACCOUNT_UID + " , " + + SplitEntry.TABLE_NAME + "_bak." + SplitEntry.COLUMN_TRANSACTION_UID + + " FROM " + SplitEntry.TABLE_NAME + "_bak;"); + + + //************** UPDATE SPLITS WHOSE CURRENCIES HAVE NO DECIMAL PLACES ***************** + //get all account UIDs which have currencies with fraction digits of 0 + String query = "SELECT " + "A." + AccountEntry.COLUMN_UID + " AS account_uid " + + " FROM " + AccountEntry.TABLE_NAME + " AS A, " + CommodityEntry.TABLE_NAME + " AS C " + + " WHERE A." + AccountEntry.COLUMN_CURRENCY + " = C." + CommodityEntry.COLUMN_MNEMONIC + + " AND C." + CommodityEntry.COLUMN_FRACTION + "= 1"; + + Cursor cursor = db.rawQuery(query, null); + + List accountUIDs = new ArrayList<>(); + try { + while (cursor.moveToNext()) { + String accountUID = cursor.getString(cursor.getColumnIndexOrThrow("account_uid")); + accountUIDs.add(accountUID); + } + } finally { + cursor.close(); + } + + String accounts = TextUtils.join("' , '", accountUIDs); + db.execSQL("REPLACE INTO " + SplitEntry.TABLE_NAME + " ( " + + SplitEntry.COLUMN_UID + " , " + + SplitEntry.COLUMN_MEMO + " , " + + SplitEntry.COLUMN_TYPE + " , " + + SplitEntry.COLUMN_ACCOUNT_UID + " , " + + SplitEntry.COLUMN_TRANSACTION_UID + " , " + + SplitEntry.COLUMN_CREATED_AT + " , " + + SplitEntry.COLUMN_MODIFIED_AT + " , " + + SplitEntry.COLUMN_VALUE_NUM + " , " + + SplitEntry.COLUMN_VALUE_DENOM + " , " + + SplitEntry.COLUMN_QUANTITY_NUM + " , " + + SplitEntry.COLUMN_QUANTITY_DENOM + + ") SELECT " + + SplitEntry.COLUMN_UID + " , " + + SplitEntry.COLUMN_MEMO + " , " + + SplitEntry.COLUMN_TYPE + " , " + + SplitEntry.COLUMN_ACCOUNT_UID + " , " + + SplitEntry.COLUMN_TRANSACTION_UID + " , " + + SplitEntry.COLUMN_CREATED_AT + " , " + + SplitEntry.COLUMN_MODIFIED_AT + " , " + + " ROUND (" + SplitEntry.COLUMN_VALUE_NUM + "/ 100), " + + "1, " + + " ROUND (" + SplitEntry.COLUMN_QUANTITY_NUM + "/ 100), " + + "1 " + + " FROM " + SplitEntry.TABLE_NAME + + " WHERE " + SplitEntry.COLUMN_ACCOUNT_UID + " IN ('" + accounts + "')" + + ";"); + + + + //************ UPDATE SPLITS WITH CURRENCIES HAVING 3 DECIMAL PLACES ******************* + query = "SELECT " + "A." + AccountEntry.COLUMN_UID + " AS account_uid " + + " FROM " + AccountEntry.TABLE_NAME + " AS A, " + CommodityEntry.TABLE_NAME + " AS C " + + " WHERE A." + AccountEntry.COLUMN_CURRENCY + " = C." + CommodityEntry.COLUMN_MNEMONIC + + " AND C." + CommodityEntry.COLUMN_FRACTION + "= 1000"; + + cursor = db.rawQuery(query, null); + + accountUIDs.clear(); + try { + while (cursor.moveToNext()) { + String accountUID = cursor.getString(cursor.getColumnIndexOrThrow("account_uid")); + accountUIDs.add(accountUID); + } + } finally { + cursor.close(); + } + + accounts = TextUtils.join("' , '", accountUIDs); + db.execSQL("REPLACE INTO " + SplitEntry.TABLE_NAME + " ( " + + SplitEntry.COLUMN_UID + " , " + + SplitEntry.COLUMN_MEMO + " , " + + SplitEntry.COLUMN_TYPE + " , " + + SplitEntry.COLUMN_ACCOUNT_UID + " , " + + SplitEntry.COLUMN_TRANSACTION_UID + " , " + + SplitEntry.COLUMN_CREATED_AT + " , " + + SplitEntry.COLUMN_MODIFIED_AT + " , " + + SplitEntry.COLUMN_VALUE_NUM + " , " + + SplitEntry.COLUMN_VALUE_DENOM + " , " + + SplitEntry.COLUMN_QUANTITY_NUM + " , " + + SplitEntry.COLUMN_QUANTITY_DENOM + + ") SELECT " + + SplitEntry.COLUMN_UID + " , " + + SplitEntry.COLUMN_MEMO + " , " + + SplitEntry.COLUMN_TYPE + " , " + + SplitEntry.COLUMN_ACCOUNT_UID + " , " + + SplitEntry.COLUMN_TRANSACTION_UID + " , " + + SplitEntry.COLUMN_CREATED_AT + " , " + + SplitEntry.COLUMN_MODIFIED_AT + " , " + + SplitEntry.COLUMN_VALUE_NUM + "* 10, " //add an extra zero because we used only 2 digits before + + "1000, " + + SplitEntry.COLUMN_QUANTITY_NUM + "* 10, " + + "1000 " + + " FROM " + SplitEntry.TABLE_NAME + + " WHERE " + SplitEntry.COLUMN_ACCOUNT_UID + " IN ('" + accounts + "')" + + ";"); + + db.execSQL("DROP TABLE " + SplitEntry.TABLE_NAME + "_bak"); + + db.setTransactionSuccessful(); + oldVersion = 9; + } finally { + db.endTransaction(); + } + return oldVersion; } } diff --git a/app/src/main/java/org/gnucash/android/db/PricesDbAdapter.java b/app/src/main/java/org/gnucash/android/db/PricesDbAdapter.java new file mode 100644 index 000000000..83645f4f5 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/db/PricesDbAdapter.java @@ -0,0 +1,130 @@ +package org.gnucash.android.db; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; +import android.support.annotation.NonNull; +import android.util.Log; +import android.util.Pair; + +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.model.Price; + +import java.sql.Timestamp; + +import static org.gnucash.android.db.DatabaseSchema.PriceEntry; + +/** + * Database adapter for prices + */ +public class PricesDbAdapter extends DatabaseAdapter { + /** + * Opens the database adapter with an existing database + * @param db SQLiteDatabase object + */ + public PricesDbAdapter(SQLiteDatabase db) { + super(db, PriceEntry.TABLE_NAME); + } + + public static PricesDbAdapter getInstance(){ + return GnuCashApplication.getPricesDbAdapter(); + } + + @Override + protected SQLiteStatement compileReplaceStatement(@NonNull final Price price) { + if (mReplaceStatement == null) { + mReplaceStatement = mDb.compileStatement("REPLACE INTO " + PriceEntry.TABLE_NAME + " ( " + + PriceEntry.COLUMN_UID + " , " + + PriceEntry.COLUMN_COMMODITY_UID + " , " + + PriceEntry.COLUMN_CURRENCY_UID + " , " + + PriceEntry.COLUMN_DATE + " , " + + PriceEntry.COLUMN_SOURCE + " , " + + PriceEntry.COLUMN_TYPE + " , " + + PriceEntry.COLUMN_VALUE_NUM + " , " + + PriceEntry.COLUMN_VALUE_DENOM + " ) VALUES ( ? , ? , ? , ? , ? , ? , ? , ? ) "); + } + + mReplaceStatement.clearBindings(); + mReplaceStatement.bindString(1, price.getUID()); + mReplaceStatement.bindString(2, price.getCommodityUID()); + mReplaceStatement.bindString(3, price.getCurrencyUID()); + mReplaceStatement.bindString(4, price.getDate().toString()); + if (price.getSource() != null) { + mReplaceStatement.bindString(5, price.getSource()); + } + if (price.getType() != null) { + mReplaceStatement.bindString(6, price.getType()); + } + mReplaceStatement.bindLong(7, price.getValueNum()); + mReplaceStatement.bindLong(8, price.getValueDenom()); + + return mReplaceStatement; + } + + @Override + public Price buildModelInstance(@NonNull final Cursor cursor) { + String commodityUID = cursor.getString(cursor.getColumnIndexOrThrow(PriceEntry.COLUMN_COMMODITY_UID)); + String currencyUID = cursor.getString(cursor.getColumnIndexOrThrow(PriceEntry.COLUMN_CURRENCY_UID)); + String dateString = cursor.getString(cursor.getColumnIndexOrThrow(PriceEntry.COLUMN_DATE)); + String source = cursor.getString(cursor.getColumnIndexOrThrow(PriceEntry.COLUMN_SOURCE)); + String type = cursor.getString(cursor.getColumnIndexOrThrow(PriceEntry.COLUMN_TYPE)); + long valueNum = cursor.getLong(cursor.getColumnIndexOrThrow(PriceEntry.COLUMN_VALUE_NUM)); + long valueDenom = cursor.getLong(cursor.getColumnIndexOrThrow(PriceEntry.COLUMN_VALUE_DENOM)); + + Price price = new Price(commodityUID, currencyUID); + price.setDate(Timestamp.valueOf(dateString)); + price.setSource(source); + price.setType(type); + price.setValueNum(valueNum); + price.setValueDenom(valueDenom); + + populateBaseModelAttributes(cursor, price); + return price; + } + + /** + * get the price for commodity / currency pair + * + * Pair is used instead of Price because we must sometimes invert the commodity/currency in DB, + * rendering the Price UID invalid. + * + * @return The numerator/denominator pair for commodity / currency pair + */ + public Pair getPrice(@NonNull String commodityUID, @NonNull String currencyUID) { + Pair pairZero = new Pair<>(0L, 0L); + if (commodityUID.equals(currencyUID)) + { + return new Pair(1L, 1L); + } + Cursor cursor = mDb.query(PriceEntry.TABLE_NAME, null, + // the commodity and currency can be swapped + "( " + PriceEntry.COLUMN_COMMODITY_UID + " = ? AND " + PriceEntry.COLUMN_CURRENCY_UID + " = ? ) OR ( " + + PriceEntry.COLUMN_COMMODITY_UID + " = ? AND " + PriceEntry.COLUMN_CURRENCY_UID + " = ? )", + new String[]{commodityUID, currencyUID, currencyUID, commodityUID}, null, null, + // only get the latest price + PriceEntry.COLUMN_DATE + " DESC", "1"); + try { + if (cursor.moveToNext()) { + String commodityUIDdb = cursor.getString(cursor.getColumnIndexOrThrow(PriceEntry.COLUMN_COMMODITY_UID)); + long valueNum = cursor.getLong(cursor.getColumnIndexOrThrow(PriceEntry.COLUMN_VALUE_NUM)); + long valueDenom = cursor.getLong(cursor.getColumnIndexOrThrow(PriceEntry.COLUMN_VALUE_DENOM)); + if (valueNum < 0 || valueDenom < 0) { + // this should not happen + return pairZero; + } + if (!commodityUIDdb.equals(commodityUID)) { + // swap Num and denom + long t = valueNum; + valueNum = valueDenom; + valueDenom = t; + } + return new Pair(valueNum, valueDenom); + } else { + return pairZero; + } + } finally { + cursor.close(); + } + } +} diff --git a/app/src/main/java/org/gnucash/android/db/ScheduledActionDbAdapter.java b/app/src/main/java/org/gnucash/android/db/ScheduledActionDbAdapter.java index 3cb26df05..8a98500d2 100644 --- a/app/src/main/java/org/gnucash/android/db/ScheduledActionDbAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/ScheduledActionDbAdapter.java @@ -35,7 +35,7 @@ * * @author Ngewi Fet */ -public class ScheduledActionDbAdapter extends DatabaseAdapter { +public class ScheduledActionDbAdapter extends DatabaseAdapter { public ScheduledActionDbAdapter(SQLiteDatabase db){ super(db, ScheduledActionEntry.TABLE_NAME); @@ -50,28 +50,6 @@ public static ScheduledActionDbAdapter getInstance(){ return GnuCashApplication.getScheduledEventDbAdapter(); } - /** - * Adds a scheduled event to the database or replaces the existing entry if one with the same GUID exists - * @param scheduledAction {@link ScheduledAction} to be added - * @return Database row ID of the newly created/replaced instance - */ - public long addScheduledAction(ScheduledAction scheduledAction){ - ContentValues contentValues = getContentValues(scheduledAction); - contentValues.put(ScheduledActionEntry.COLUMN_ACTION_UID, scheduledAction.getActionUID()); - contentValues.put(ScheduledActionEntry.COLUMN_PERIOD, scheduledAction.getPeriod()); - contentValues.put(ScheduledActionEntry.COLUMN_START_TIME, scheduledAction.getStartTime()); - contentValues.put(ScheduledActionEntry.COLUMN_END_TIME, scheduledAction.getEndTime()); - contentValues.put(ScheduledActionEntry.COLUMN_LAST_RUN, scheduledAction.getLastRun()); - contentValues.put(ScheduledActionEntry.COLUMN_TYPE, scheduledAction.getActionType().name()); - contentValues.put(ScheduledActionEntry.COLUMN_TAG, scheduledAction.getTag()); - contentValues.put(ScheduledActionEntry.COLUMN_ENABLED, scheduledAction.isEnabled() ? "1":"0"); - contentValues.put(ScheduledActionEntry.COLUMN_TOTAL_FREQUENCY, scheduledAction.getTotalFrequency()); - contentValues.put(ScheduledActionEntry.COLUMN_EXECUTION_COUNT, scheduledAction.getExecutionCount()); - - Log.d(LOG_TAG, "Replace scheduled event in the db"); - return mDb.replace(ScheduledActionEntry.TABLE_NAME, null, contentValues); - } - /** * Updates only the recurrence attributes of the scheduled action. * The recurrence attributes are the period, start time, end time and/or total frequency. @@ -82,7 +60,8 @@ public long addScheduledAction(ScheduledAction scheduledAction){ * @return Database record ID of the edited scheduled action */ public long updateRecurrenceAttributes(ScheduledAction scheduledAction){ - ContentValues contentValues = getContentValues(scheduledAction); + ContentValues contentValues = new ContentValues(); + populateBaseModelAttributes(contentValues, scheduledAction); contentValues.put(ScheduledActionEntry.COLUMN_PERIOD, scheduledAction.getPeriod()); contentValues.put(ScheduledActionEntry.COLUMN_START_TIME, scheduledAction.getStartTime()); contentValues.put(ScheduledActionEntry.COLUMN_END_TIME, scheduledAction.getEndTime()); @@ -95,17 +74,11 @@ public long updateRecurrenceAttributes(ScheduledAction scheduledAction){ return mDb.update(ScheduledActionEntry.TABLE_NAME, contentValues, where, whereArgs); } - /** - * Adds a multiple scheduled actions to the database in one transaction. - * @param scheduledActionList List of ScheduledActions - * @return Returns the number of rows inserted - */ - public int bulkAddScheduledActions(List scheduledActionList){ - Log.d(LOG_TAG, "Bulk adding scheduled actions to the database"); - int nRow = 0; - try { - mDb.beginTransaction(); - SQLiteStatement replaceStatement = mDb.compileStatement("REPLACE INTO " + ScheduledActionEntry.TABLE_NAME + " ( " + + @Override + protected SQLiteStatement compileReplaceStatement(@NonNull final ScheduledAction schedxAction) { + if (mReplaceStatement == null) { + mReplaceStatement = mDb.compileStatement("REPLACE INTO " + ScheduledActionEntry.TABLE_NAME + " ( " + ScheduledActionEntry.COLUMN_UID + " , " + ScheduledActionEntry.COLUMN_ACTION_UID + " , " + ScheduledActionEntry.COLUMN_TYPE + " , " @@ -118,41 +91,36 @@ public int bulkAddScheduledActions(List scheduledActionList){ + ScheduledActionEntry.COLUMN_TAG + " , " + ScheduledActionEntry.COLUMN_TOTAL_FREQUENCY + " , " + ScheduledActionEntry.COLUMN_EXECUTION_COUNT + " ) VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? )"); - for (ScheduledAction schedxAction:scheduledActionList) { - replaceStatement.clearBindings(); - replaceStatement.bindString(1, schedxAction.getUID()); - replaceStatement.bindString(2, schedxAction.getActionUID()); - replaceStatement.bindString(3, schedxAction.getActionType().name()); - replaceStatement.bindLong(4, schedxAction.getStartTime()); - replaceStatement.bindLong(5, schedxAction.getEndTime()); - replaceStatement.bindLong(6, schedxAction.getLastRun()); - replaceStatement.bindLong(7, schedxAction.getPeriod()); - replaceStatement.bindLong(8, schedxAction.isEnabled() ? 1 : 0); - replaceStatement.bindString(9, schedxAction.getCreatedTimestamp().toString()); - if (schedxAction.getTag() == null) - replaceStatement.bindNull(10); - else - replaceStatement.bindString(10, schedxAction.getTag()); - replaceStatement.bindString(11, Integer.toString(schedxAction.getTotalFrequency())); - replaceStatement.bindString(12, Integer.toString(schedxAction.getExecutionCount())); - - replaceStatement.execute(); - nRow ++; - } - mDb.setTransactionSuccessful(); } - finally { - mDb.endTransaction(); - } - return nRow; + + mReplaceStatement.clearBindings(); + mReplaceStatement.bindString(1, schedxAction.getUID()); + mReplaceStatement.bindString(2, schedxAction.getActionUID()); + mReplaceStatement.bindString(3, schedxAction.getActionType().name()); + mReplaceStatement.bindLong(4, schedxAction.getStartTime()); + mReplaceStatement.bindLong(5, schedxAction.getEndTime()); + mReplaceStatement.bindLong(6, schedxAction.getLastRun()); + mReplaceStatement.bindLong(7, schedxAction.getPeriod()); + mReplaceStatement.bindLong(8, schedxAction.isEnabled() ? 1 : 0); + mReplaceStatement.bindString(9, schedxAction.getCreatedTimestamp().toString()); + if (schedxAction.getTag() == null) + mReplaceStatement.bindNull(10); + else + mReplaceStatement.bindString(10, schedxAction.getTag()); + mReplaceStatement.bindString(11, Integer.toString(schedxAction.getTotalFrequency())); + mReplaceStatement.bindString(12, Integer.toString(schedxAction.getExecutionCount())); + + return mReplaceStatement; } + /** * Builds a {@link org.gnucash.android.model.ScheduledAction} instance from a row to cursor in the database. * The cursor should be already pointing to the right entry in the data set. It will not be modified in any way * @param cursor Cursor pointing to data set * @return ScheduledEvent object instance */ - public ScheduledAction buildScheduledActionInstance(final Cursor cursor){ + @Override + public ScheduledAction buildModelInstance(@NonNull final Cursor cursor){ String actionUid = cursor.getString(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_ACTION_UID)); long period = cursor.getLong(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_PERIOD)); long startTime = cursor.getLong(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_START_TIME)); @@ -165,7 +133,7 @@ public ScheduledAction buildScheduledActionInstance(final Cursor cursor){ int execCount = cursor.getInt(cursor.getColumnIndexOrThrow(ScheduledActionEntry.COLUMN_EXECUTION_COUNT)); ScheduledAction event = new ScheduledAction(ScheduledAction.ActionType.valueOf(typeString)); - populateModel(cursor, event); + populateBaseModelAttributes(cursor, event); event.setPeriod(period); event.setStartTime(startTime); event.setEndTime(endTime); @@ -179,24 +147,6 @@ public ScheduledAction buildScheduledActionInstance(final Cursor cursor){ return event; } - /** - * Returns an instance of {@link org.gnucash.android.model.ScheduledAction} from the database record - * @param uid GUID of event - * @return ScheduledEvent object instance - */ - public ScheduledAction getScheduledAction(String uid){ - Cursor cursor = fetchRecord(getID(uid)); - - ScheduledAction scheduledAction = null; - if (cursor != null) { - if (cursor.moveToFirst()) { - scheduledAction = buildScheduledActionInstance(cursor); - } - cursor.close(); - } - return scheduledAction; - } - /** * Returns all {@link org.gnucash.android.model.ScheduledAction}s from the database with the specified action UID. * Note that the parameter is not of the the scheduled action record, but from the action table @@ -211,7 +161,7 @@ public List getScheduledActionsWithUID(@NonNull String actionUI List scheduledActions = new ArrayList(); try { while (cursor.moveToNext()) { - scheduledActions.add(buildScheduledActionInstance(cursor)); + scheduledActions.add(buildModelInstance(cursor)); } } finally { cursor.close(); @@ -219,19 +169,6 @@ public List getScheduledActionsWithUID(@NonNull String actionUI return scheduledActions; } - /** - * Returns all scheduled events in the database - * @return List with all scheduled events - */ - public List getAllScheduledActions(){ - Cursor cursor = fetchAllRecords(); - List scheduledActions = new ArrayList(); - while (cursor.moveToNext()){ - scheduledActions.add(buildScheduledActionInstance(cursor)); - } - return scheduledActions; - } - /** * Returns all enabled scheduled actions in the database * @return List of enalbed scheduled actions @@ -241,7 +178,7 @@ public List getAllEnabledScheduledActions(){ null, ScheduledActionEntry.COLUMN_ENABLED + "=1", null, null, null, null); List scheduledActions = new ArrayList(); while (cursor.moveToNext()){ - scheduledActions.add(buildScheduledActionInstance(cursor)); + scheduledActions.add(buildModelInstance(cursor)); } return scheduledActions; } diff --git a/app/src/main/java/org/gnucash/android/db/SplitsDbAdapter.java b/app/src/main/java/org/gnucash/android/db/SplitsDbAdapter.java index 5ce9eab78..b96b85fa1 100644 --- a/app/src/main/java/org/gnucash/android/db/SplitsDbAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/SplitsDbAdapter.java @@ -17,13 +17,14 @@ package org.gnucash.android.db; -import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteQueryBuilder; import android.database.sqlite.SQLiteStatement; +import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; +import android.util.Pair; import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.model.AccountType; @@ -32,6 +33,7 @@ import org.gnucash.android.model.TransactionType; import java.math.BigDecimal; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.Currency; import java.util.List; @@ -46,7 +48,7 @@ * @author Yongxin Wang * @author Oleksandr Tyshkovets */ -public class SplitsDbAdapter extends DatabaseAdapter { +public class SplitsDbAdapter extends DatabaseAdapter { public SplitsDbAdapter(SQLiteDatabase db) { super(db, SplitEntry.TABLE_NAME); @@ -63,76 +65,54 @@ public static SplitsDbAdapter getInstance(){ /** * Adds a split to the database. - * If the split (with same unique ID) already exists, then it is simply updated + * The transactions belonging to the split are marked as exported * @param split {@link org.gnucash.android.model.Split} to be recorded in DB - * @return Record ID of the newly saved split */ - public long addSplit(Split split){ - ContentValues contentValues = getContentValues(split); - contentValues.put(SplitEntry.COLUMN_AMOUNT, split.getAmount().absolute().toPlainString()); - contentValues.put(SplitEntry.COLUMN_TYPE, split.getType().name()); - contentValues.put(SplitEntry.COLUMN_MEMO, split.getMemo()); - contentValues.put(SplitEntry.COLUMN_ACCOUNT_UID, split.getAccountUID()); - contentValues.put(SplitEntry.COLUMN_TRANSACTION_UID, split.getTransactionUID()); - + public void addRecord(@NonNull final Split split){ Log.d(LOG_TAG, "Replace transaction split in db"); - long rowId = mDb.replace(SplitEntry.TABLE_NAME, null, contentValues); + super.addRecord(split); long transactionId = getTransactionID(split.getTransactionUID()); //when a split is updated, we want mark the transaction as not exported updateRecord(TransactionEntry.TABLE_NAME, transactionId, - TransactionEntry.COLUMN_EXPORTED, String.valueOf(rowId > 0 ? 0 : 1)); + TransactionEntry.COLUMN_EXPORTED, String.valueOf(0)); //modifying a split means modifying the accompanying transaction as well updateRecord(TransactionEntry.TABLE_NAME, transactionId, - TransactionEntry.COLUMN_MODIFIED_AT, Long.toString(System.currentTimeMillis())); - return rowId; + TransactionEntry.COLUMN_MODIFIED_AT, new Timestamp(System.currentTimeMillis()).toString()); } - /** - * Adds some splits to the database. - * If the split already exists, then it is simply updated. - * This function will NOT update the exported status of corresponding transactions. - * All or none of the splits will be inserted/updated into the database. - * @param splitList {@link org.gnucash.android.model.Split} to be recorded in DB. The amount - * of all splits should be non-negative - * @return Number of records of the newly saved split - */ - public long bulkAddSplits(List splitList) { - long nRow = 0; - try { - mDb.beginTransaction(); - SQLiteStatement replaceStatement = mDb.compileStatement("REPLACE INTO " + SplitEntry.TABLE_NAME + " ( " - + SplitEntry.COLUMN_UID + " , " - + SplitEntry.COLUMN_MEMO + " , " - + SplitEntry.COLUMN_TYPE + " , " - + SplitEntry.COLUMN_AMOUNT + " , " - + SplitEntry.COLUMN_CREATED_AT + " , " - + SplitEntry.COLUMN_ACCOUNT_UID + " , " - + SplitEntry.COLUMN_TRANSACTION_UID + " ) VALUES ( ? , ? , ? , ? , ? , ? , ? ) "); - for (Split split : splitList) { - replaceStatement.clearBindings(); - replaceStatement.bindString(1, split.getUID()); - if (split.getMemo() != null) { - replaceStatement.bindString(2, split.getMemo()); - } - replaceStatement.bindString(3, split.getType().name()); - replaceStatement.bindString(4, split.getAmount().toPlainString()); - replaceStatement.bindString(5, split.getCreatedTimestamp().toString()); - replaceStatement.bindString(6, split.getAccountUID()); - replaceStatement.bindString(7, split.getTransactionUID()); - - //Log.d(TAG, "Replacing transaction split in db"); - replaceStatement.execute(); - nRow++; - } - mDb.setTransactionSuccessful(); - } - finally { - mDb.endTransaction(); + @Override + protected SQLiteStatement compileReplaceStatement(@NonNull final Split split) { + if (mReplaceStatement == null) { + mReplaceStatement = mDb.compileStatement("REPLACE INTO " + SplitEntry.TABLE_NAME + " ( " + + SplitEntry.COLUMN_UID + " , " + + SplitEntry.COLUMN_MEMO + " , " + + SplitEntry.COLUMN_TYPE + " , " + + SplitEntry.COLUMN_VALUE_NUM + " , " + + SplitEntry.COLUMN_VALUE_DENOM + " , " + + SplitEntry.COLUMN_QUANTITY_NUM + " , " + + SplitEntry.COLUMN_QUANTITY_DENOM + " , " + + SplitEntry.COLUMN_CREATED_AT + " , " + + SplitEntry.COLUMN_ACCOUNT_UID + " , " + + SplitEntry.COLUMN_TRANSACTION_UID + " ) VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? ) "); } - return nRow; + mReplaceStatement.clearBindings(); + mReplaceStatement.bindString(1, split.getUID()); + if (split.getMemo() != null) { + mReplaceStatement.bindString(2, split.getMemo()); + } + mReplaceStatement.bindString(3, split.getType().name()); + mReplaceStatement.bindLong(4, split.getValue().getNumerator()); + mReplaceStatement.bindLong(5, split.getValue().getDenominator()); + mReplaceStatement.bindLong(6, split.getQuantity().getNumerator()); + mReplaceStatement.bindLong(7, split.getQuantity().getDenominator()); + mReplaceStatement.bindString(8, split.getCreatedTimestamp().toString()); + mReplaceStatement.bindString(9, split.getAccountUID()); + mReplaceStatement.bindString(10, split.getTransactionUID()); + + return mReplaceStatement; } /** @@ -141,18 +121,24 @@ public long bulkAddSplits(List splitList) { * @param cursor Cursor pointing to transaction record in database * @return {@link org.gnucash.android.model.Split} instance */ - public Split buildSplitInstance(Cursor cursor){ - String amountString = cursor.getString(cursor.getColumnIndexOrThrow(SplitEntry.COLUMN_AMOUNT)); + public Split buildModelInstance(@NonNull final Cursor cursor){ + long valueNum = cursor.getLong(cursor.getColumnIndexOrThrow(SplitEntry.COLUMN_VALUE_NUM)); + long valueDenom = cursor.getLong(cursor.getColumnIndexOrThrow(SplitEntry.COLUMN_VALUE_DENOM)); + long quantityNum = cursor.getLong(cursor.getColumnIndexOrThrow(SplitEntry.COLUMN_QUANTITY_NUM)); + long quantityDenom = cursor.getLong(cursor.getColumnIndexOrThrow(SplitEntry.COLUMN_QUANTITY_DENOM)); String typeName = cursor.getString(cursor.getColumnIndexOrThrow(SplitEntry.COLUMN_TYPE)); String accountUID = cursor.getString(cursor.getColumnIndexOrThrow(SplitEntry.COLUMN_ACCOUNT_UID)); String transxUID = cursor.getString(cursor.getColumnIndexOrThrow(SplitEntry.COLUMN_TRANSACTION_UID)); String memo = cursor.getString(cursor.getColumnIndexOrThrow(SplitEntry.COLUMN_MEMO)); + String transactionCurrency = TransactionsDbAdapter.getInstance().getAttribute(transxUID, TransactionEntry.COLUMN_CURRENCY); + Money value = new Money(valueNum, valueDenom, transactionCurrency); String currencyCode = getAccountCurrencyCode(accountUID); - Money amount = new Money(amountString, currencyCode); + Money quantity = new Money(quantityNum, quantityDenom, currencyCode); - Split split = new Split(amount, accountUID); - populateModel(cursor, split); + Split split = new Split(value, accountUID); + split.setQuantity(quantity); + populateBaseModelAttributes(cursor, split); split.setTransactionUID(transxUID); split.setType(TransactionType.valueOf(typeName)); split.setMemo(memo); @@ -160,81 +146,6 @@ public Split buildSplitInstance(Cursor cursor){ return split; } - - /** - * Retrieves a split from the database - * @param uid Unique Identifier String of the split transaction - * @return {@link org.gnucash.android.model.Split} instance - */ - public Split getSplit(String uid){ - return getSplit(getID(uid)); - } - - /** - * Returns the Split instance given the database id - * @param id Database record ID of the split - * @return {@link org.gnucash.android.model.Split} instance - */ - public Split getSplit(long id){ - Cursor cursor = fetchRecord(id); - try { - if (cursor.moveToFirst()) { - return buildSplitInstance(cursor); - } - else { - throw new IllegalArgumentException("split " + id + " does not exist"); - } - } finally { - cursor.close(); - } - } - - /** - * Returns the sum of the splits for a given account. - * This takes into account the kind of movement caused by the split in the account (which also depends on account type) - * @param accountUID String unique ID of account - * @return Balance of the splits for this account - */ - public Money computeSplitBalance(String accountUID) { - Cursor cursor = fetchSplitsForAccount(accountUID); - String currencyCode = getAccountCurrencyCode(accountUID); - Money splitSum = new Money("0", currencyCode); - AccountType accountType = getAccountType(accountUID); - - try { - while (cursor.moveToNext()) { - String amountString = cursor.getString(cursor.getColumnIndexOrThrow(SplitEntry.COLUMN_AMOUNT)); - String typeString = cursor.getString(cursor.getColumnIndexOrThrow(SplitEntry.COLUMN_TYPE)); - - TransactionType transactionType = TransactionType.valueOf(typeString); - Money amount = new Money(amountString, currencyCode); - - if (accountType.hasDebitNormalBalance()) { - switch (transactionType) { - case DEBIT: - splitSum = splitSum.add(amount); - break; - case CREDIT: - splitSum = splitSum.subtract(amount); - break; - } - } else { - switch (transactionType) { - case DEBIT: - splitSum = splitSum.subtract(amount); - break; - case CREDIT: - splitSum = splitSum.add(amount); - break; - } - } - } - } finally { - cursor.close(); - } - return splitSum; - } - /** * Returns the sum of the splits for given set of accounts. * This takes into account the kind of movement caused by the split in the account (which also depends on account type) @@ -264,6 +175,7 @@ public Money computeSplitBalance(List accountUIDList, String currencyCod return calculateSplitBalance(accountUIDList, currencyCode, hasDebitNormalBalance, startTimestamp, endTimestamp); } + private Money calculateSplitBalance(List accountUIDList, String currencyCode, boolean hasDebitNormalBalance, long startTimestamp, long endTimestamp){ if (accountUIDList.size() == 0){ @@ -272,40 +184,76 @@ private Money calculateSplitBalance(List accountUIDList, String currency Cursor cursor; String[] selectionArgs = null; - String selection = SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_ACCOUNT_UID + " in ( '" + TextUtils.join("' , '", accountUIDList) + "' ) AND " + - SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_TRANSACTION_UID + " = " + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_UID + " AND " + - TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_TEMPLATE + " = 0"; + String selection = DatabaseSchema.AccountEntry.TABLE_NAME + "_" + DatabaseSchema.CommonColumns.COLUMN_UID + " in ( '" + TextUtils.join("' , '", accountUIDList) + "' ) AND " + + TransactionEntry.TABLE_NAME + "_" + TransactionEntry.COLUMN_TEMPLATE + " = 0"; if (startTimestamp != -1 && endTimestamp != -1) { - selection += " AND " + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_TIMESTAMP + " BETWEEN ? AND ? "; + selection += " AND " + TransactionEntry.TABLE_NAME + "_" + TransactionEntry.COLUMN_TIMESTAMP + " BETWEEN ? AND ? "; selectionArgs = new String[]{String.valueOf(startTimestamp), String.valueOf(endTimestamp)}; } else if (startTimestamp == -1 && endTimestamp != -1) { - selection += " AND " + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_TIMESTAMP + " <= ?"; + selection += " AND " + TransactionEntry.TABLE_NAME + "_" + TransactionEntry.COLUMN_TIMESTAMP + " <= ?"; selectionArgs = new String[]{String.valueOf(endTimestamp)}; - } else if (startTimestamp != -1 && endTimestamp == -1) { - selection += " AND " + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_TIMESTAMP + " >= ?"; + } else if (startTimestamp != -1/* && endTimestamp == -1*/) { + selection += " AND " + TransactionEntry.TABLE_NAME + "_" + TransactionEntry.COLUMN_TIMESTAMP + " >= ?"; selectionArgs = new String[]{String.valueOf(startTimestamp)}; } - cursor = mDb.query(SplitEntry.TABLE_NAME + " , " + TransactionEntry.TABLE_NAME, - new String[]{"TOTAL ( CASE WHEN " + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_TYPE + " = 'DEBIT' THEN " + - SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_AMOUNT + " ELSE - " + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_AMOUNT + " END )"}, - selection, selectionArgs, null, null, null); + cursor = mDb.query("trans_split_acct", + new String[]{"TOTAL ( CASE WHEN " + SplitEntry.TABLE_NAME + "_" + SplitEntry.COLUMN_TYPE + " = 'DEBIT' THEN " + + SplitEntry.TABLE_NAME + "_" + SplitEntry.COLUMN_QUANTITY_NUM + " ELSE - " + + SplitEntry.TABLE_NAME + "_" + SplitEntry.COLUMN_QUANTITY_NUM + " END )", + SplitEntry.TABLE_NAME + "_" + SplitEntry.COLUMN_QUANTITY_DENOM, + DatabaseSchema.AccountEntry.TABLE_NAME + "_" + DatabaseSchema.AccountEntry.COLUMN_CURRENCY}, + selection, selectionArgs, DatabaseSchema.AccountEntry.TABLE_NAME + "_" + DatabaseSchema.AccountEntry.COLUMN_CURRENCY, null, null); try { - if (cursor.moveToFirst()) { - double amount = cursor.getDouble(0); - cursor.close(); - Log.d(LOG_TAG, "amount return " + amount); + Money total = Money.createZeroInstance(currencyCode); + CommoditiesDbAdapter commoditiesDbAdapter = null; + PricesDbAdapter pricesDbAdapter = null; + Currency currency = null; + String currencyUID = null; + while (cursor.moveToNext()) { + long amount_num = cursor.getLong(0); + long amount_denom = cursor.getLong(1); + String commodity = cursor.getString(2); + //Log.d(getClass().getName(), commodity + " " + amount_num + "/" + amount_denom); + if (commodity.equals("XXX") || amount_num == 0) { + // ignore custom currency + continue; + } if (!hasDebitNormalBalance) { - amount = -amount; + amount_num = -amount_num; + } + if (commodity.equals(currencyCode)) { + // currency matches + total = total.add(new Money(amount_num, amount_denom, currencyCode)); + //Log.d(getClass().getName(), "currency " + commodity + " sub - total " + total); + } else { + // there is a second currency involved + if (commoditiesDbAdapter == null) { + commoditiesDbAdapter = new CommoditiesDbAdapter(mDb); + pricesDbAdapter = new PricesDbAdapter(mDb); + currency = Currency.getInstance(currencyCode); + currencyUID = commoditiesDbAdapter.getCommodityUID(currencyCode); + } + // get price + String commodityUID = commoditiesDbAdapter.getCommodityUID(commodity); + Pair price = pricesDbAdapter.getPrice(commodityUID, currencyUID); + if (price.first <= 0 || price.second <= 0) { + // no price exists, just ignore it + continue; + } + BigDecimal amount = Money.getBigDecimal(amount_num, amount_denom); + BigDecimal amountConverted = amount.multiply(new BigDecimal(price.first)) + .divide(new BigDecimal(price.second), currency.getDefaultFractionDigits(), BigDecimal.ROUND_HALF_EVEN); + total = total.add(new Money(amountConverted, currency)); + //Log.d(getClass().getName(), "currency " + commodity + " sub - total " + total); } - return new Money(BigDecimal.valueOf(amount).setScale(2, BigDecimal.ROUND_HALF_UP), Currency.getInstance(currencyCode)); } + return total; } finally { cursor.close(); } - return new Money("0", currencyCode); } /** @@ -318,7 +266,7 @@ public List getSplitsForTransaction(String transactionUID){ List splitList = new ArrayList(); try { while (cursor.moveToNext()) { - splitList.add(buildSplitInstance(cursor)); + splitList.add(buildModelInstance(cursor)); } } finally { cursor.close(); @@ -348,7 +296,7 @@ public List getSplitsForTransactionInAccount(String transactionUID, Strin List splitList = new ArrayList(); if (cursor != null){ while (cursor.moveToNext()){ - splitList.add(buildSplitInstance(cursor)); + splitList.add(buildModelInstance(cursor)); } cursor.close(); } @@ -422,7 +370,7 @@ public Cursor fetchSplitsForTransactionAndAccount(String transactionUID, String null, SplitEntry.COLUMN_TRANSACTION_UID + " = ? AND " + SplitEntry.COLUMN_ACCOUNT_UID + " = ?", new String[]{transactionUID, accountUID}, - null, null, SplitEntry.COLUMN_AMOUNT + " ASC"); + null, null, SplitEntry.COLUMN_VALUE_NUM + " ASC"); } /** @@ -449,7 +397,7 @@ public String getTransactionUID(long transactionId){ @Override public boolean deleteRecord(long rowId) { - Split split = getSplit(rowId); + Split split = getRecord(rowId); String transactionUID = split.getTransactionUID(); boolean result = mDb.delete(SplitEntry.TABLE_NAME, SplitEntry._ID + "=" + rowId, null) > 0; diff --git a/app/src/main/java/org/gnucash/android/db/TransactionsDbAdapter.java b/app/src/main/java/org/gnucash/android/db/TransactionsDbAdapter.java index 5fa0b2a97..c235b75b4 100644 --- a/app/src/main/java/org/gnucash/android/db/TransactionsDbAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/TransactionsDbAdapter.java @@ -23,6 +23,7 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteQueryBuilder; import android.database.sqlite.SQLiteStatement; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; @@ -50,7 +51,7 @@ * @author Yongxin Wang * @author Oleksandr Tyshkovets */ -public class TransactionsDbAdapter extends DatabaseAdapter { +public class TransactionsDbAdapter extends DatabaseAdapter { private final SplitsDbAdapter mSplitsDbAdapter; @@ -81,37 +82,25 @@ public SplitsDbAdapter getSplitDbAdapter() { * If a transaction already exists in the database with the same unique ID, * then the record will just be updated instead * @param transaction {@link Transaction} to be inserted to database - * @return Database row ID of the inserted transaction */ - public long addTransaction(Transaction transaction){ - ContentValues contentValues = getContentValues(transaction); - contentValues.put(TransactionEntry.COLUMN_DESCRIPTION, transaction.getDescription()); - contentValues.put(TransactionEntry.COLUMN_TIMESTAMP, transaction.getTimeMillis()); - contentValues.put(TransactionEntry.COLUMN_NOTES, transaction.getNote()); - contentValues.put(TransactionEntry.COLUMN_EXPORTED, transaction.isExported() ? 1 : 0); - contentValues.put(TransactionEntry.COLUMN_TEMPLATE, transaction.isTemplate() ? 1 : 0); - contentValues.put(TransactionEntry.COLUMN_CURRENCY, transaction.getCurrencyCode()); - contentValues.put(TransactionEntry.COLUMN_SCHEDX_ACTION_UID, transaction.getScheduledActionUID()); - + @Override + public void addRecord(@NonNull Transaction transaction){ Log.d(LOG_TAG, "Replacing transaction in db"); - long rowId = -1; mDb.beginTransaction(); try { - rowId = mDb.replaceOrThrow(TransactionEntry.TABLE_NAME, null, contentValues); + Split imbalanceSplit = transaction.getAutoBalanceSplit(); + if (imbalanceSplit != null){ + String imbalanceAccountUID = AccountsDbAdapter.getInstance().getOrCreateImbalanceAccountUID(transaction.getCurrency()); + imbalanceSplit.setAccountUID(imbalanceAccountUID); + } + super.addRecord(transaction); Log.d(LOG_TAG, "Adding splits for transaction"); - ArrayList splitUIDs = new ArrayList(transaction.getSplits().size()); + ArrayList splitUIDs = new ArrayList<>(transaction.getSplits().size()); for (Split split : transaction.getSplits()) { - contentValues = getContentValues(split); - contentValues.put(SplitEntry.COLUMN_AMOUNT, split.getAmount().absolute().toPlainString()); - contentValues.put(SplitEntry.COLUMN_TYPE, split.getType().name()); - contentValues.put(SplitEntry.COLUMN_MEMO, split.getMemo()); - contentValues.put(SplitEntry.COLUMN_ACCOUNT_UID, split.getAccountUID()); - contentValues.put(SplitEntry.COLUMN_TRANSACTION_UID, split.getTransactionUID()); - splitUIDs.add(split.getUID()); - Log.d(LOG_TAG, "Replace transaction split in db"); - mDb.replaceOrThrow(SplitEntry.TABLE_NAME, null, contentValues); + mSplitsDbAdapter.addRecord(split); + splitUIDs.add(split.getUID()); } Log.d(LOG_TAG, transaction.getSplits().size() + " splits added"); @@ -120,14 +109,14 @@ public long addTransaction(Transaction transaction){ + SplitEntry.COLUMN_UID + " NOT IN ('" + TextUtils.join("' , '", splitUIDs) + "')", new String[]{transaction.getUID()}); Log.d(LOG_TAG, deleted + " splits deleted"); + mDb.setTransactionSuccessful(); - } catch (SQLException sqle) { - Log.e(LOG_TAG, sqle.getMessage()); - Crashlytics.logException(sqle); + } catch (SQLException sqlEx) { + Log.e(LOG_TAG, sqlEx.getMessage()); + Crashlytics.logException(sqlEx); } finally { mDb.endTransaction(); } - return rowId; } /** @@ -139,49 +128,21 @@ public long addTransaction(Transaction transaction){ * @param transactionList {@link Transaction} transactions to be inserted to database * @return Number of transactions inserted */ - public long bulkAddTransactions(List transactionList){ + @Override + public long bulkAddRecords(@NonNull List transactionList){ + long start = System.nanoTime(); + long rowInserted = super.bulkAddRecords(transactionList); + long end = System.nanoTime(); + Log.d(getClass().getSimpleName(), String.format("bulk add transaction time %d ", end - start)); List splitList = new ArrayList<>(transactionList.size()*3); - long rowInserted = 0; - try { - mDb.beginTransaction(); - SQLiteStatement replaceStatement = mDb.compileStatement("REPLACE INTO " + TransactionEntry.TABLE_NAME + " ( " - + TransactionEntry.COLUMN_UID + " , " - + TransactionEntry.COLUMN_DESCRIPTION + " , " - + TransactionEntry.COLUMN_NOTES + " , " - + TransactionEntry.COLUMN_TIMESTAMP + " , " - + TransactionEntry.COLUMN_EXPORTED + " , " - + TransactionEntry.COLUMN_CURRENCY + " , " - + TransactionEntry.COLUMN_CREATED_AT + " , " - + TransactionEntry.COLUMN_SCHEDX_ACTION_UID + " , " - + TransactionEntry.COLUMN_TEMPLATE + " ) VALUES ( ? , ? , ? , ?, ? , ? , ? , ? , ?)"); - for (Transaction transaction : transactionList) { - //Log.d(TAG, "Replacing transaction in db"); - replaceStatement.clearBindings(); - replaceStatement.bindString(1, transaction.getUID()); - replaceStatement.bindString(2, transaction.getDescription()); - replaceStatement.bindString(3, transaction.getNote()); - replaceStatement.bindLong(4, transaction.getTimeMillis()); - replaceStatement.bindLong(5, transaction.isExported() ? 1 : 0); - replaceStatement.bindString(6, transaction.getCurrencyCode()); - replaceStatement.bindString(7, transaction.getCreatedTimestamp().toString()); - if (transaction.getScheduledActionUID() == null) - replaceStatement.bindNull(8); - else - replaceStatement.bindString(8, transaction.getScheduledActionUID()); - replaceStatement.bindLong(9, transaction.isTemplate() ? 1 : 0); - replaceStatement.execute(); - rowInserted ++; - splitList.addAll(transaction.getSplits()); - } - mDb.setTransactionSuccessful(); - } - finally { - mDb.endTransaction(); + for (Transaction transaction : transactionList) { + splitList.addAll(transaction.getSplits()); } if (rowInserted != 0 && !splitList.isEmpty()) { try { - long nSplits = mSplitsDbAdapter.bulkAddSplits(splitList); - Log.d(LOG_TAG, String.format("%d splits inserted", nSplits)); + start = System.nanoTime(); + long nSplits = mSplitsDbAdapter.bulkAddRecords(splitList); + Log.d(LOG_TAG, String.format("%d splits inserted in %d ns", splitList.size(), System.nanoTime()-start)); } finally { SQLiteStatement deleteEmptyTransaction = mDb.compileStatement("DELETE FROM " + @@ -195,26 +156,47 @@ public long bulkAddTransactions(List transactionList){ return rowInserted; } - /** - * Retrieves a transaction object from a database with database ID rowId - * @param rowId Identifier of the transaction record to be retrieved - * @return {@link Transaction} object corresponding to database record - */ - public Transaction getTransaction(long rowId) { - Log.v(LOG_TAG, "Fetching transaction with id " + rowId); - Cursor c = fetchRecord(rowId); - try { - if (c.moveToFirst()) { - return buildTransactionInstance(c); - } else { - throw new IllegalArgumentException("row " + rowId + " does not exist"); - } - } finally { - c.close(); + @Override + protected SQLiteStatement compileReplaceStatement(@NonNull final Transaction transaction) { + if (mReplaceStatement == null) { + mReplaceStatement = mDb.compileStatement("REPLACE INTO " + TransactionEntry.TABLE_NAME + " ( " + + TransactionEntry.COLUMN_UID + " , " + + TransactionEntry.COLUMN_DESCRIPTION + " , " + + TransactionEntry.COLUMN_NOTES + " , " + + TransactionEntry.COLUMN_TIMESTAMP + " , " + + TransactionEntry.COLUMN_EXPORTED + " , " + + TransactionEntry.COLUMN_CURRENCY + " , " + + TransactionEntry.COLUMN_COMMODITY_UID + " , " + + TransactionEntry.COLUMN_CREATED_AT + " , " + + TransactionEntry.COLUMN_SCHEDX_ACTION_UID + " , " + + TransactionEntry.COLUMN_TEMPLATE + " ) VALUES ( ? , ? , ? , ?, ? , ? , ? , ?, ? , ?)"); } + + mReplaceStatement.clearBindings(); + mReplaceStatement.bindString(1, transaction.getUID()); + mReplaceStatement.bindString(2, transaction.getDescription()); + mReplaceStatement.bindString(3, transaction.getNote()); + mReplaceStatement.bindLong(4, transaction.getTimeMillis()); + mReplaceStatement.bindLong(5, transaction.isExported() ? 1 : 0); + mReplaceStatement.bindString(6, transaction.getCurrencyCode()); + + String commodityUID = transaction.getCommodityUID(); + if (commodityUID == null) + commodityUID = getCommodityUID(transaction.getCurrency().getCurrencyCode()); + + mReplaceStatement.bindString(7, commodityUID); + mReplaceStatement.bindString(8, transaction.getCreatedTimestamp().toString()); + + if (transaction.getScheduledActionUID() == null) + mReplaceStatement.bindNull(9); + else + mReplaceStatement.bindString(9, transaction.getScheduledActionUID()); + mReplaceStatement.bindLong(10, transaction.isTemplate() ? 1 : 0); + + return mReplaceStatement; } - - /** + + /** * Returns a cursor to a set of all transactions which have a split belonging to the accound with unique ID * accountUID. * @param accountUID UID of the account whose transactions are to be retrieved @@ -237,6 +219,28 @@ public Cursor fetchAllTransactionsForAccount(String accountUID){ return queryBuilder.query(mDb, projectionIn, selection, selectionArgs, null, null, sortOrder); } + /** + * Returns a cursor to all scheduled transactions which have at least one split in the account + *

This is basically a set of all template transactions for this account

+ * @param accountUID GUID of account + * @return Cursor with set of transactions + */ + public Cursor fetchScheduledTransactionsForAccount(String accountUID){ + SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); + queryBuilder.setTables(TransactionEntry.TABLE_NAME + + " INNER JOIN " + SplitEntry.TABLE_NAME + " ON " + + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_UID + " = " + + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_TRANSACTION_UID); + queryBuilder.setDistinct(true); + String[] projectionIn = new String[]{TransactionEntry.TABLE_NAME + ".*"}; + String selection = SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_ACCOUNT_UID + " = ?" + + " AND " + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_TEMPLATE + " = 1"; + String[] selectionArgs = new String[]{accountUID}; + String sortOrder = TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_TIMESTAMP + " DESC"; + + return queryBuilder.query(mDb, projectionIn, selection, selectionArgs, null, null, sortOrder); + } + /** * Deletes all transactions which contain a split in the account. *

Note:As long as the transaction has one split which belongs to the account {@code accountUID}, @@ -304,7 +308,7 @@ public List getAllTransactionsForAccount(String accountUID){ ArrayList transactionsList = new ArrayList<>(); try { while (c.moveToNext()) { - transactionsList.add(buildTransactionInstance(c)); + transactionsList.add(buildModelInstance(c)); } } finally { c.close(); @@ -321,7 +325,7 @@ public List getAllTransactions(){ List transactions = new ArrayList(); try { while (cursor.moveToNext()) { - transactions.add(buildTransactionInstance(cursor)); + transactions.add(buildModelInstance(cursor)); } } finally { cursor.close(); @@ -361,19 +365,19 @@ public Cursor fetchTransactionsWithSplitsWithTransactionAccount(String [] column * Return number of transactions in the database which are non recurring * @return Number of transactions */ - public int getTotalTransactionsCount() { + public long getRecordsCount() { String queryCount = "SELECT COUNT(*) FROM " + TransactionEntry.TABLE_NAME + " WHERE " + TransactionEntry.COLUMN_TEMPLATE + " =0"; Cursor cursor = mDb.rawQuery(queryCount, null); try { cursor.moveToFirst(); - return cursor.getInt(0); + return cursor.getLong(0); } finally { cursor.close(); } } - public int getTotalTransactionsCount(@Nullable String where, @Nullable String[] whereArgs) { + public long getRecordsCount(@Nullable String where, @Nullable String[] whereArgs) { Cursor cursor = mDb.query(true, TransactionEntry.TABLE_NAME + " , trans_extra_info ON " + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_UID + " = trans_extra_info.trans_acct_t_uid", @@ -386,7 +390,7 @@ public int getTotalTransactionsCount(@Nullable String where, @Nullable String[] null); try{ cursor.moveToFirst(); - return cursor.getInt(0); + return cursor.getLong(0); } finally { cursor.close(); } @@ -398,10 +402,11 @@ public int getTotalTransactionsCount(@Nullable String where, @Nullable String[] * @param c Cursor pointing to transaction record in database * @return {@link Transaction} object constructed from database record */ - public Transaction buildTransactionInstance(Cursor c){ + @Override + public Transaction buildModelInstance(@NonNull final Cursor c){ String name = c.getString(c.getColumnIndexOrThrow(TransactionEntry.COLUMN_DESCRIPTION)); Transaction transaction = new Transaction(name); - populateModel(c, transaction); + populateBaseModelAttributes(c, transaction); transaction.setTime(c.getLong(c.getColumnIndexOrThrow(TransactionEntry.COLUMN_TIMESTAMP))); transaction.setNote(c.getString(c.getColumnIndexOrThrow(TransactionEntry.COLUMN_NOTES))); @@ -456,7 +461,7 @@ public int moveTransaction(String transactionUID, String srcAccountUID, String d for (Split split : splits) { split.setAccountUID(dstAccountUID); } - mSplitsDbAdapter.bulkAddSplits(splits); + mSplitsDbAdapter.bulkAddRecords(splits); return splits.size(); } @@ -502,6 +507,37 @@ public long getTemplateTransactionsCount(){ return statement.simpleQueryForLong(); } + /** + * Returns a list of all scheduled transactions in the database + * @return List of all scheduled transactions + */ + public List getScheduledTransactionsForAccount(String accountUID){ + Cursor cursor = fetchScheduledTransactionsForAccount(accountUID); + List scheduledTransactions = new ArrayList<>(); + try { + while (cursor.moveToNext()) { + scheduledTransactions.add(buildModelInstance(cursor)); + } + return scheduledTransactions; + } finally { + cursor.close(); + } + } + + /** + * Returns the number of splits for the transaction in the database + * @param transactionUID GUID of the transaction + * @return Number of splits belonging to the transaction + */ + public long getSplitCount(@NonNull String transactionUID){ + if (transactionUID == null) + return 0; + String sql = "SELECT COUNT(*) FROM " + SplitEntry.TABLE_NAME + + " WHERE " + SplitEntry.COLUMN_TRANSACTION_UID + "= '" + transactionUID + "'"; + SQLiteStatement statement = mDb.compileStatement(sql); + return statement.simpleQueryForLong(); + } + /** * Returns a cursor to transactions whose name (UI: description) start with the prefix *

This method is used for autocomplete suggestions when creating new transactions.
@@ -540,15 +576,6 @@ public int updateTransaction(ContentValues contentValues, String whereClause, St return mDb.update(TransactionEntry.TABLE_NAME, contentValues, whereClause, whereArgs); } - /** - * Returns a transaction for the given transaction GUID - * @param transactionUID GUID of the transaction - * @return Retrieves a transaction from the database - */ - public Transaction getTransaction(String transactionUID) { - return getTransaction(getID(transactionUID)); - } - /** * Return the number of currencies used in the transaction. * For example if there are different splits with different currencies 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 601070ceb..94391f824 100644 --- a/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java +++ b/app/src/main/java/org/gnucash/android/export/ExportAsyncTask.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; @@ -51,7 +52,6 @@ import org.gnucash.android.db.TransactionsDbAdapter; import org.gnucash.android.export.ofx.OfxExporter; import org.gnucash.android.export.qif.QifExporter; -import org.gnucash.android.export.qif.QifHelper; import org.gnucash.android.export.xml.GncXmlExporter; import org.gnucash.android.model.Transaction; import org.gnucash.android.ui.account.AccountsActivity; @@ -59,13 +59,10 @@ import org.gnucash.android.ui.settings.SettingsActivity; import org.gnucash.android.ui.transaction.TransactionsActivity; -import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.FileWriter; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; @@ -92,7 +89,7 @@ public class ExportAsyncTask extends AsyncTask { /** * Log tag */ - public static final String TAG = "ExporterAsyncTask"; + public static final String TAG = "ExportAsyncTask"; /** * Export parameters @@ -149,6 +146,7 @@ protected Boolean doInBackground(ExportParams... params) { File file = new File(mExportParams.getTargetFilepath()); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8")); try { + // FIXME: detect if there aren't transactions to export and inform the user mExporter.generateExport(writer); writer.flush(); } @@ -176,7 +174,8 @@ public void run() { switch (mExportParams.getExportTarget()) { case SHARING: - shareFile(mExportParams.getTargetFilepath()); + File output = moveExportToSDCard(); + shareFile(output.getAbsolutePath()); return true; case DROPBOX: @@ -188,7 +187,7 @@ public void run() { return true; case SD_CARD: - copyExportToSDCard(); + moveExportToSDCard(); return true; } @@ -197,6 +196,7 @@ public void run() { /** * Transmits the exported transactions to the designated location, either SD card or third-party application + * Finishes the activity if the export was starting in the context of an activity * @param exportResult Result of background export execution */ @Override @@ -246,6 +246,7 @@ protected void onPostExecute(Boolean exportResult) { if (mContext instanceof Activity) { if (mProgressDialog != null && mProgressDialog.isShowing()) mProgressDialog.dismiss(); + ((Activity) mContext).finish(); } } @@ -366,7 +367,7 @@ private List getExportedFiles() throws IOException { List exportedFilePaths; if (mExportParams.getExportFormat() == ExportFormat.QIF) { String path = mExportParams.getTargetFilepath(); - exportedFilePaths = splitQIF(new File(path), new File(path)); + exportedFilePaths = QifExporter.splitQIF(new File(path)); } else { exportedFilePaths = new ArrayList<>(); exportedFilePaths.add(mExportParams.getTargetFilepath()); @@ -375,10 +376,11 @@ private List getExportedFiles() throws IOException { } /** - * Copies the exported file from the internal storage where it is generated to external storage - * which is accessible to the user + * Moves the exported file from the internal storage where it is generated to external storage + * which is accessible to the user. + * @return File to which the export was moved. */ - private void copyExportToSDCard() { + private File moveExportToSDCard() { Log.i(TAG, "Moving exported file to external storage"); File src = new File(mExportParams.getTargetFilepath()); File dst = Exporter.createExportFile(mExportParams.getExportFormat()); @@ -386,6 +388,7 @@ private void copyExportToSDCard() { try { copyFile(src, dst); src.delete(); + return dst; } catch (IOException e) { Crashlytics.logException(e); Log.e(TAG, e.getMessage()); @@ -409,7 +412,7 @@ private void backupAndDeleteTransactions(){ transactionsDbAdapter.deleteAllNonTemplateTransactions(); if (preserveOpeningBalances) { - transactionsDbAdapter.bulkAddTransactions(openingBalances); + transactionsDbAdapter.bulkAddRecords(openingBalances); } } @@ -426,7 +429,7 @@ private void shareFile(String path) { ArrayList exportFiles = new ArrayList<>(); if (mExportParams.getExportFormat() == ExportFormat.QIF) { try { - List splitFiles = splitQIF(new File(path), new File(path)); + List splitFiles = QifExporter.splitQIF(new File(path)); for (String file : splitFiles) { exportFiles.add(Uri.parse("file://" + file)); } @@ -446,34 +449,18 @@ private void shareFile(String path) { } SimpleDateFormat formatter = (SimpleDateFormat) SimpleDateFormat.getDateTimeInstance(); - ArrayList extraText = new ArrayList(); + ArrayList extraText = new ArrayList<>(); extraText.add(mContext.getString(R.string.description_export_email) + " " + formatter.format(new Date(System.currentTimeMillis()))); shareIntent.putExtra(Intent.EXTRA_TEXT, extraText); - if (mContext instanceof Activity) - mContext.startActivity(Intent.createChooser(shareIntent, mContext.getString(R.string.title_select_export_destination))); - } - - /** - * Copies a file from src to dst - * @param src Absolute path to the source file - * @param dst Absolute path to the destination file - * @throws IOException if the file could not be copied - */ - public void copyFile(File src, File dst) throws IOException { - //TODO: Make this asynchronous at some time, t in the future. - if (mExportParams.getExportFormat() == ExportFormat.QIF) { - splitQIF(src, dst); - } else { - FileChannel inChannel = new FileInputStream(src).getChannel(); - FileChannel outChannel = new FileOutputStream(dst).getChannel(); - try { - inChannel.transferTo(0, inChannel.size(), outChannel); - } finally { - if (inChannel != null) - inChannel.close(); - outChannel.close(); + if (mContext instanceof Activity) { + List activities = mContext.getPackageManager().queryIntentActivities(shareIntent, 0); + if (activities != null && !activities.isEmpty()) { + mContext.startActivity(Intent.createChooser(shareIntent, mContext.getString(R.string.title_select_export_destination))); + } else { + Toast.makeText(mContext, R.string.toast_no_compatible_apps_to_receive_export, + Toast.LENGTH_LONG).show(); } } } @@ -484,37 +471,17 @@ public void copyFile(File src, File dst) throws IOException { * @param dst Absolute path to the destination file * @throws IOException if the file could not be copied */ - private static List splitQIF(File src, File dst) throws IOException { - // split only at the last dot - String[] pathParts = dst.getPath().split("(?=\\.[^\\.]+$)"); - ArrayList splitFiles = new ArrayList<>(); - String line; - BufferedReader in = new BufferedReader(new FileReader(src)); - BufferedWriter out = null; + public void copyFile(File src, File dst) throws IOException { + //TODO: Make this asynchronous at some time, t in the future. + FileChannel inChannel = new FileInputStream(src).getChannel(); + FileChannel outChannel = new FileOutputStream(dst).getChannel(); try { - while ((line = in.readLine()) != null) { - if (line.startsWith(QifHelper.INTERNAL_CURRENCY_PREFIX)) { - String currencyCode = line.substring(1); - if (out != null) { - out.close(); - } - String newFileName = pathParts[0] + "_" + currencyCode + pathParts[1]; - splitFiles.add(newFileName); - out = new BufferedWriter(new FileWriter(newFileName)); - } else { - if (out == null) { - throw new IllegalArgumentException(src.getPath() + " format is not correct"); - } - out.append(line).append('\n'); - } - } + inChannel.transferTo(0, inChannel.size(), outChannel); } finally { - in.close(); - if (out != null) { - out.close(); - } + if (inChannel != null) + inChannel.close(); + outChannel.close(); } - return splitFiles; } } 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 120d052b3..6b23ede19 100644 --- a/app/src/main/java/org/gnucash/android/export/ExportParams.java +++ b/app/src/main/java/org/gnucash/android/export/ExportParams.java @@ -17,13 +17,13 @@ package org.gnucash.android.export; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.ui.export.ExportDialogFragment; +import org.gnucash.android.ui.export.ExportFormFragment; /** * Encapsulation of the parameters used for exporting transactions. * The parameters are determined by the user in the export dialog and are then transmitted to the asynchronous task which * actually performs the export. - * @see ExportDialogFragment + * @see ExportFormFragment * @see ExportAsyncTask * * @author Ngewi Fet @@ -85,7 +85,8 @@ public ExportFormat getExportFormat() { */ public void setExportFormat(ExportFormat exportFormat) { this.mExportFormat = exportFormat; - mTargetFilepath = GnuCashApplication.getAppContext().getExternalFilesDir(null) + "/" + Exporter.buildExportFilename(mExportFormat); + this.mTargetFilepath = GnuCashApplication.getAppContext().getFilesDir() + "/" + + Exporter.buildExportFilename(mExportFormat); } /** @@ -145,14 +146,6 @@ public String getTargetFilepath() { return mTargetFilepath; } - /** - * Sets target file path for transactions in private application storage - * @param mTargetFilepath String path to file - */ - public void setTargetFilepath(String mTargetFilepath) { - this.mTargetFilepath = mTargetFilepath; - } - @Override public String toString() { return "Export " + mExportFormat.name() + " to " + mExportTarget.name() + " at " diff --git a/app/src/main/java/org/gnucash/android/export/Exporter.java b/app/src/main/java/org/gnucash/android/export/Exporter.java index 29ce1e791..41f041661 100644 --- a/app/src/main/java/org/gnucash/android/export/Exporter.java +++ b/app/src/main/java/org/gnucash/android/export/Exporter.java @@ -20,6 +20,7 @@ import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.os.Environment; +import android.support.annotation.NonNull; import android.util.Log; import com.crashlytics.android.Crashlytics; @@ -27,6 +28,8 @@ import org.gnucash.android.BuildConfig; import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.CommoditiesDbAdapter; +import org.gnucash.android.db.PricesDbAdapter; import org.gnucash.android.db.ScheduledActionDbAdapter; import org.gnucash.android.db.SplitsDbAdapter; import org.gnucash.android.db.TransactionsDbAdapter; @@ -34,6 +37,7 @@ import java.io.File; import java.io.FileFilter; import java.io.Writer; +import java.sql.Timestamp; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; @@ -73,6 +77,13 @@ public abstract class Exporter { protected ExportParams mParameters; private static final SimpleDateFormat EXPORT_FILENAME_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); + + /** + * last export time in preferences + */ + public static final String PREF_LAST_EXPORT_TIME = "last_export_time"; + + public static final String TIMESTAMP_ZERO = new Timestamp(0).toString(); /** * Adapter for retrieving accounts to export * Subclasses should close this object when they are done with exporting @@ -81,6 +92,8 @@ public abstract class Exporter { protected TransactionsDbAdapter mTransactionsDbAdapter; protected SplitsDbAdapter mSplitsDbAdapter; protected ScheduledActionDbAdapter mScheduledActionDbAdapter; + protected PricesDbAdapter mPricesDbAdpater; + protected CommoditiesDbAdapter mCommoditiesDbAdapter; protected Context mContext; public Exporter(ExportParams params, SQLiteDatabase db) { @@ -91,11 +104,15 @@ public Exporter(ExportParams params, SQLiteDatabase db) { mTransactionsDbAdapter = TransactionsDbAdapter.getInstance(); mSplitsDbAdapter = SplitsDbAdapter.getInstance(); mScheduledActionDbAdapter = ScheduledActionDbAdapter.getInstance(); + mPricesDbAdpater = PricesDbAdapter.getInstance(); + mCommoditiesDbAdapter = CommoditiesDbAdapter.getInstance(); } else { mSplitsDbAdapter = new SplitsDbAdapter(db); mTransactionsDbAdapter = new TransactionsDbAdapter(db, mSplitsDbAdapter); mAccountsDbAdapter = new AccountsDbAdapter(db, mTransactionsDbAdapter); mScheduledActionDbAdapter = new ScheduledActionDbAdapter(db); + mPricesDbAdpater = new PricesDbAdapter(db); + mCommoditiesDbAdapter = new CommoditiesDbAdapter(db); } } @@ -192,6 +209,10 @@ public ExporterException(ExportParams params){ super("Failed to generate " + params.getExportFormat().toString()); } + public ExporterException(@NonNull ExportParams params, @NonNull String msg) { + super("Failed to generate " + params.getExportFormat().toString() + "-" + msg); + } + public ExporterException(ExportParams params, Throwable throwable){ super("Failed to generate " + params.getExportFormat().toString() +"-"+ throwable.getMessage(), throwable); diff --git a/app/src/main/java/org/gnucash/android/export/ofx/OfxExporter.java b/app/src/main/java/org/gnucash/android/export/ofx/OfxExporter.java index f7eb81e0c..ff9b144ea 100644 --- a/app/src/main/java/org/gnucash/android/export/ofx/OfxExporter.java +++ b/app/src/main/java/org/gnucash/android/export/ofx/OfxExporter.java @@ -23,6 +23,7 @@ import com.crashlytics.android.Crashlytics; import org.gnucash.android.R; +import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.db.AccountsDbAdapter; import org.gnucash.android.export.ExportParams; import org.gnucash.android.export.Exporter; @@ -36,6 +37,7 @@ import java.io.IOException; import java.io.StringWriter; import java.io.Writer; +import java.sql.Timestamp; import java.util.List; import javax.xml.parsers.DocumentBuilder; @@ -43,7 +45,6 @@ import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; @@ -91,7 +92,12 @@ private void generateOfx(Document doc, Element parent){ for (Account account : mAccountsList) { if (account.getTransactionCount() == 0) continue; - + + //do not export imbalance accounts for OFX transactions and double-entry disabled + if (!GnuCashApplication.isDoubleEntryEnabled() && account.getName().contains(mContext.getString(R.string.imbalance_account_name))) + continue; + + //add account details (transactions) to the XML document account.toOfx(doc, statementTransactionResponse, mParameters.shouldExportAllTransactions()); @@ -103,7 +109,7 @@ private void generateOfx(Document doc, Element parent){ public String generateExport() throws ExporterException { mAccountsList = mParameters.shouldExportAllTransactions() ? - mAccountsDbAdapter.getAllAccounts() : mAccountsDbAdapter.getExportableAccounts(); + mAccountsDbAdapter.getAllRecords() : mAccountsDbAdapter.getExportableAccounts(); DocumentBuilderFactory docFactory = DocumentBuilderFactory .newInstance(); @@ -126,10 +132,13 @@ public String generateExport() throws ExporterException { boolean useXmlHeader = PreferenceManager.getDefaultSharedPreferences(mContext) .getBoolean(mContext.getString(R.string.key_xml_ofx_header), false); + String timeStamp = new Timestamp(System.currentTimeMillis()).toString(); + StringWriter stringWriter = new StringWriter(); //if we want SGML OFX headers, write first to string and then prepend header if (useXmlHeader){ write(document, stringWriter, false); + PreferenceManager.getDefaultSharedPreferences(mContext).edit().putString(Exporter.PREF_LAST_EXPORT_TIME, timeStamp).apply(); return stringWriter.toString(); } else { Node ofxNode = document.getElementsByTagName("OFX").item(0); @@ -139,6 +148,7 @@ public String generateExport() throws ExporterException { StringBuffer stringBuffer = new StringBuffer(OfxHelper.OFX_SGML_HEADER); stringBuffer.append('\n'); stringBuffer.append(stringWriter.toString()); + PreferenceManager.getDefaultSharedPreferences(mContext).edit().putString(Exporter.PREF_LAST_EXPORT_TIME, timeStamp).apply(); return stringBuffer.toString(); } } 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 db8349ea4..27f362719 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 @@ -18,16 +18,27 @@ import android.content.ContentValues; import android.database.Cursor; +import android.preference.PreferenceManager; import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.db.TransactionsDbAdapter; import org.gnucash.android.export.ExportParams; import org.gnucash.android.export.Exporter; +import org.gnucash.android.model.Transaction; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; import java.io.IOException; import java.io.Writer; import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.ArrayList; import java.util.Currency; +import java.util.List; import static org.gnucash.android.db.DatabaseSchema.AccountEntry; import static org.gnucash.android.db.DatabaseSchema.SplitEntry; @@ -50,12 +61,14 @@ public void generateExport(Writer writer) throws ExporterException { final String newLine = "\n"; TransactionsDbAdapter transactionsDbAdapter = mTransactionsDbAdapter; try { + String lastExportTimeStamp = PreferenceManager.getDefaultSharedPreferences(mContext).getString(Exporter.PREF_LAST_EXPORT_TIME, Exporter.TIMESTAMP_ZERO); Cursor cursor = transactionsDbAdapter.fetchTransactionsWithSplitsWithTransactionAccount( new String[]{ TransactionEntry.TABLE_NAME + "_" + TransactionEntry.COLUMN_UID + " AS trans_uid", TransactionEntry.TABLE_NAME + "_" + TransactionEntry.COLUMN_TIMESTAMP + " AS trans_time", TransactionEntry.TABLE_NAME + "_" + TransactionEntry.COLUMN_DESCRIPTION + " AS trans_desc", - SplitEntry.TABLE_NAME + "_" + SplitEntry.COLUMN_AMOUNT + " AS split_amount", + SplitEntry.TABLE_NAME + "_" + SplitEntry.COLUMN_QUANTITY_NUM + " AS split_quantity_num", + SplitEntry.TABLE_NAME + "_" + SplitEntry.COLUMN_QUANTITY_DENOM + " AS split_quantity_denom", SplitEntry.TABLE_NAME + "_" + SplitEntry.COLUMN_TYPE + " AS split_type", SplitEntry.TABLE_NAME + "_" + SplitEntry.COLUMN_MEMO + " AS split_memo", "trans_extra_info.trans_acct_balance AS trans_acct_balance", @@ -76,7 +89,8 @@ public void generateExport(Writer writer) throws ExporterException { "trans_split_count == 1 )" + ( mParameters.shouldExportAllTransactions() ? - "" : " AND " + TransactionEntry.TABLE_NAME + "_" + TransactionEntry.COLUMN_EXPORTED + "== 0" + //"" : " AND " + TransactionEntry.TABLE_NAME + "_" + TransactionEntry.COLUMN_EXPORTED + "== 0" + "" : " AND " + TransactionEntry.TABLE_NAME + "_" + DatabaseSchema.CommonColumns.COLUMN_MODIFIED_AT + " > \"" + lastExportTimeStamp + "\"" ), null, // trans_time ASC : put transactions in time order @@ -156,9 +170,34 @@ public void generateExport(Writer writer) throws ExporterException { .append(newLine); } String splitType = cursor.getString(cursor.getColumnIndexOrThrow("split_type")); + Double quantity_num = cursor.getDouble(cursor.getColumnIndexOrThrow("split_quantity_num")); + int quantity_denom = cursor.getInt(cursor.getColumnIndexOrThrow("split_quantity_denom")); + int precision = 0; + switch (quantity_denom) { + case 0: // will sometimes happen for zero values + break; + case 1: + precision = 0; + break; + case 10: + precision = 1; + break; + case 100: + precision = 2; + break; + case 1000: + precision = 3; + break; + default: + throw new ExporterException(mParameters, "split quantity has illegal denominator: "+ quantity_denom); + } + Double quantity = 0.0; + if (quantity_denom != 0) { + quantity = quantity_num / quantity_denom; + } writer.append(QifHelper.SPLIT_AMOUNT_PREFIX) .append(splitType.equals("DEBIT") ? "-" : "") - .append(cursor.getString(cursor.getColumnIndexOrThrow("split_amount"))) + .append(String.format("%." + precision + "f", quantity)) .append(newLine); } if (!currentTransactionUID.equals("")) { @@ -177,5 +216,49 @@ public void generateExport(Writer writer) throws ExporterException { { throw new ExporterException(mParameters, e); } + + /// export successful + String timeStamp = new Timestamp(System.currentTimeMillis()).toString(); + PreferenceManager.getDefaultSharedPreferences(mContext).edit().putString(Exporter.PREF_LAST_EXPORT_TIME, timeStamp).apply(); + } + + /** + * Splits a Qif file into several ones for each currency. + * + * @param file File object of the Qif file to split. + * @return a list of paths of the newly created Qif files. + * @throws IOException if something went wrong while splitting the file. + */ + public static List splitQIF(File file) throws IOException { + // split only at the last dot + String[] pathParts = file.getPath().split("(?=\\.[^\\.]+$)"); + ArrayList splitFiles = new ArrayList<>(); + String line; + BufferedReader in = new BufferedReader(new FileReader(file)); + BufferedWriter out = null; + try { + while ((line = in.readLine()) != null) { + if (line.startsWith(QifHelper.INTERNAL_CURRENCY_PREFIX)) { + String currencyCode = line.substring(1); + if (out != null) { + out.close(); + } + String newFileName = pathParts[0] + "_" + currencyCode + pathParts[1]; + splitFiles.add(newFileName); + out = new BufferedWriter(new FileWriter(newFileName)); + } else { + if (out == null) { + throw new IllegalArgumentException(file.getPath() + " format is not correct"); + } + out.append(line).append('\n'); + } + } + } finally { + in.close(); + if (out != null) { + out.close(); + } + } + return splitFiles; } } 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 fd3fef1e9..8577781f7 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 @@ -30,6 +30,8 @@ import org.gnucash.android.export.Exporter; import org.gnucash.android.model.Account; import org.gnucash.android.model.AccountType; +import org.gnucash.android.model.BaseModel; +import org.gnucash.android.model.Money; import org.gnucash.android.model.PeriodType; import org.gnucash.android.model.ScheduledAction; import org.gnucash.android.model.TransactionType; @@ -113,6 +115,8 @@ private void exportSlots(XmlSerializer xmlSerializer, } private void exportAccounts(XmlSerializer xmlSerializer) throws IOException { + // gnucash desktop requires that parent account appears before its descendants. + // sort by full-name to fulfill the request Cursor cursor = mAccountsDbAdapter.fetchAccounts(null, null, DatabaseSchema.AccountEntry.COLUMN_FULL_NAME + " ASC"); while (cursor.moveToNext()) { // write account @@ -147,9 +151,12 @@ private void exportAccounts(XmlSerializer xmlSerializer) throws IOException { xmlSerializer.text(Integer.toString((int) Math.pow(10, Currency.getInstance(acctCurrencyCode).getDefaultFractionDigits()))); xmlSerializer.endTag(null, GncXmlHelper.TAG_COMMODITY_SCU); // account description - // this is optional in Gnc XML, and currently not in the db, so description node - // is omitted - // + String description = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.AccountEntry.COLUMN_DESCRIPTION)); + if (description != null && !description.equals("")) { + xmlSerializer.startTag(null, GncXmlHelper.TAG_ACCT_DESCRIPTION); + xmlSerializer.text(description); + xmlSerializer.endTag(null, GncXmlHelper.TAG_ACCT_DESCRIPTION); + } // account slots, color, placeholder, default transfer account, favorite ArrayList slotKey = new ArrayList<>(); ArrayList slotType = new ArrayList<>(); @@ -268,8 +275,10 @@ private void exportTransactions(XmlSerializer xmlSerializer, boolean exportTempl SplitEntry.TABLE_NAME+"."+ SplitEntry.COLUMN_UID + " AS split_uid", SplitEntry.TABLE_NAME+"."+ SplitEntry.COLUMN_MEMO + " AS split_memo", SplitEntry.TABLE_NAME+"."+ SplitEntry.COLUMN_TYPE + " AS split_type", - SplitEntry.TABLE_NAME+"."+ SplitEntry.COLUMN_AMOUNT + " AS split_amount", - SplitEntry.TABLE_NAME+"."+ SplitEntry.COLUMN_ACCOUNT_UID + " AS split_acct_uid"}, + SplitEntry.TABLE_NAME+"."+ SplitEntry.COLUMN_VALUE_NUM + " AS split_value_num", + SplitEntry.TABLE_NAME+"."+ SplitEntry.COLUMN_VALUE_DENOM + " AS split_value_denom", + SplitEntry.TABLE_NAME+"."+ SplitEntry.COLUMN_QUANTITY_NUM + " AS split_quantity_num", + SplitEntry.TABLE_NAME+"."+ SplitEntry.COLUMN_QUANTITY_DENOM + " AS split_quantity_denom", SplitEntry.TABLE_NAME+"."+ SplitEntry.COLUMN_ACCOUNT_UID + " AS split_acct_uid"}, where, null, TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_TIMESTAMP + " ASC , " + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_UID + " ASC "); @@ -282,7 +291,7 @@ private void exportTransactions(XmlSerializer xmlSerializer, boolean exportTempl mRootTemplateAccount.setAccountType(AccountType.ROOT); mTransactionToTemplateAccountMap.put(" ", mRootTemplateAccount); while (cursor.moveToNext()) { - Account account = new Account(UUID.randomUUID().toString().replaceAll("-", "")); + Account account = new Account(BaseModel.generateUID()); account.setAccountType(AccountType.BANK); String trnUID = cursor.getString(cursor.getColumnIndexOrThrow("trans_uid")); mTransactionToTemplateAccountMap.put(trnUID, account); @@ -392,16 +401,22 @@ private void exportTransactions(XmlSerializer xmlSerializer, boolean exportTempl xmlSerializer.endTag(null, GncXmlHelper.TAG_RECONCILED_STATE); // value, in the transaction's currency String trxType = cursor.getString(cursor.getColumnIndexOrThrow("split_type")); - BigDecimal splitAmount = new BigDecimal(cursor.getString(cursor.getColumnIndexOrThrow("split_amount"))); + int splitValueNum = cursor.getInt(cursor.getColumnIndexOrThrow("split_value_num")); + int splitValueDenom = cursor.getInt(cursor.getColumnIndexOrThrow("split_value_denom")); + BigDecimal splitAmount = Money.getBigDecimal(splitValueNum, splitValueDenom); String strValue = "0/" + denomString; if (!exportTemplates) { //when doing normal transaction export - strValue = (trxType.equals("CREDIT") ? "-" : "") + GncXmlHelper.formatSplitAmount(splitAmount, trxCurrency); + strValue = (trxType.equals("CREDIT") ? "-" : "") + splitValueNum + "/" + splitValueDenom; } xmlSerializer.startTag(null, GncXmlHelper.TAG_SPLIT_VALUE); xmlSerializer.text(strValue); xmlSerializer.endTag(null, GncXmlHelper.TAG_SPLIT_VALUE); // quantity, in the split account's currency - // TODO: multi currency support. + String splitQuantityNum = cursor.getString(cursor.getColumnIndexOrThrow("split_quantity_num")); + String splitQuantityDenom = cursor.getString(cursor.getColumnIndexOrThrow("split_quantity_denom")); + if (!exportTemplates) { + strValue = (trxType.equals("CREDIT") ? "-" : "") + splitQuantityNum + "/" + splitQuantityDenom; + } xmlSerializer.startTag(null, GncXmlHelper.TAG_SPLIT_QUANTITY); xmlSerializer.text(strValue); xmlSerializer.endTag(null, GncXmlHelper.TAG_SPLIT_QUANTITY); @@ -615,6 +630,67 @@ private void exportCommodity(XmlSerializer xmlSerializer, List currenc } } + private void exportPrices(XmlSerializer xmlSerializer) throws IOException { + xmlSerializer.startTag(null, GncXmlHelper.TAG_PRICEDB); + xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_VERSION, "1"); + Cursor cursor = mPricesDbAdpater.fetchAllRecords(); + try { + while(cursor.moveToNext()) { + xmlSerializer.startTag(null, GncXmlHelper.TAG_PRICE); + // GUID + xmlSerializer.startTag(null, GncXmlHelper.TAG_PRICE_ID); + xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_TYPE, GncXmlHelper.ATTR_VALUE_GUID); + xmlSerializer.text(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.CommonColumns.COLUMN_UID))); + xmlSerializer.endTag(null, GncXmlHelper.TAG_PRICE_ID); + // commodity + xmlSerializer.startTag(null, GncXmlHelper.TAG_PRICE_COMMODITY); + 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(mCommoditiesDbAdapter.getCurrencyCode(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.PriceEntry.COLUMN_COMMODITY_UID)))); + xmlSerializer.endTag(null, GncXmlHelper.TAG_COMMODITY_ID); + xmlSerializer.endTag(null, GncXmlHelper.TAG_PRICE_COMMODITY); + // currency + xmlSerializer.startTag(null, GncXmlHelper.TAG_PRICE_CURRENCY); + 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(mCommoditiesDbAdapter.getCurrencyCode(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.PriceEntry.COLUMN_CURRENCY_UID)))); + xmlSerializer.endTag(null, GncXmlHelper.TAG_COMMODITY_ID); + xmlSerializer.endTag(null, GncXmlHelper.TAG_PRICE_CURRENCY); + // time + String strDate = GncXmlHelper.formatDate(Timestamp.valueOf(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.PriceEntry.COLUMN_DATE))).getTime()); + xmlSerializer.startTag(null, GncXmlHelper.TAG_PRICE_TIME); + xmlSerializer.startTag(null, GncXmlHelper.TAG_TS_DATE); + xmlSerializer.text(strDate); + xmlSerializer.endTag(null, GncXmlHelper.TAG_TS_DATE); + xmlSerializer.endTag(null, GncXmlHelper.TAG_PRICE_TIME); + // source + xmlSerializer.startTag(null, GncXmlHelper.TAG_PRICE_SOURCE); + xmlSerializer.text(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.PriceEntry.COLUMN_SOURCE))); + xmlSerializer.endTag(null, GncXmlHelper.TAG_PRICE_SOURCE); + // type, optional + String type = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.PriceEntry.COLUMN_TYPE)); + if (type != null && !type.equals("")) { + xmlSerializer.startTag(null, GncXmlHelper.TAG_PRICE_TYPE); + xmlSerializer.text(type); + xmlSerializer.endTag(null, GncXmlHelper.TAG_PRICE_TYPE); + } + // value + xmlSerializer.startTag(null, GncXmlHelper.TAG_PRICE_VALUE); + xmlSerializer.text(cursor.getLong(cursor.getColumnIndexOrThrow(DatabaseSchema.PriceEntry.COLUMN_VALUE_NUM)) + + "/" + cursor.getLong(cursor.getColumnIndexOrThrow(DatabaseSchema.PriceEntry.COLUMN_VALUE_DENOM))); + xmlSerializer.endTag(null, GncXmlHelper.TAG_PRICE_VALUE); + xmlSerializer.endTag(null, GncXmlHelper.TAG_PRICE); + } + } finally { + cursor.close(); + } + xmlSerializer.endTag(null, GncXmlHelper.TAG_PRICEDB); + } + @Override public void generateExport(Writer writer) throws ExporterException{ try { @@ -639,10 +715,10 @@ public void generateExport(Writer writer) throws ExporterException{ // book_id xmlSerializer.startTag(null, GncXmlHelper.TAG_BOOK_ID); xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_TYPE, GncXmlHelper.ATTR_VALUE_GUID); - xmlSerializer.text(UUID.randomUUID().toString().replaceAll("-", "")); + xmlSerializer.text(BaseModel.generateUID()); xmlSerializer.endTag(null, GncXmlHelper.TAG_BOOK_ID); //commodity count - List currencies = mAccountsDbAdapter.getCurrencies(); + List currencies = mAccountsDbAdapter.getCurrenciesInUse(); for (int i = 0; i< currencies.size();i++) { if (currencies.get(i).getCurrencyCode().equals("XXX")) { currencies.remove(i); @@ -660,12 +736,23 @@ public void generateExport(Writer writer) throws ExporterException{ //transaction count xmlSerializer.startTag(null, GncXmlHelper.TAG_COUNT_DATA); xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_CD_TYPE, "transaction"); - xmlSerializer.text(mTransactionsDbAdapter.getTotalTransactionsCount() + ""); + xmlSerializer.text(mTransactionsDbAdapter.getRecordsCount() + ""); xmlSerializer.endTag(null, GncXmlHelper.TAG_COUNT_DATA); + //price count + long priceCount = mPricesDbAdpater.getRecordsCount(); + if (priceCount > 0) { + xmlSerializer.startTag(null, GncXmlHelper.TAG_COUNT_DATA); + xmlSerializer.attribute(null, GncXmlHelper.ATTR_KEY_CD_TYPE, "price"); + xmlSerializer.text(priceCount + ""); + xmlSerializer.endTag(null, GncXmlHelper.TAG_COUNT_DATA); + } // export the commodities used in the DB exportCommodity(xmlSerializer, currencies); - // accounts. bulk import does not rely on account order - // the cursor gather account in arbitrary order + // prices + if (priceCount > 0) { + exportPrices(xmlSerializer); + } + // accounts. exportAccounts(xmlSerializer); // transactions. exportTransactions(xmlSerializer, false); diff --git a/app/src/main/java/org/gnucash/android/export/xml/GncXmlHelper.java b/app/src/main/java/org/gnucash/android/export/xml/GncXmlHelper.java index 53f479e3d..92de9512e 100644 --- a/app/src/main/java/org/gnucash/android/export/xml/GncXmlHelper.java +++ b/app/src/main/java/org/gnucash/android/export/xml/GncXmlHelper.java @@ -72,7 +72,7 @@ public abstract class GncXmlHelper { public static final String TAG_SLOT_VALUE = "slot:value"; public static final String TAG_ACT_SLOTS = "act:slots"; public static final String TAG_SLOT = "slot"; - public static final String TAG_ACCT_DESCRIPTION = "act:description"; //TODO: Use this when we add descriptions to the database + public static final String TAG_ACCT_DESCRIPTION = "act:description"; public static final String TAG_TRANSACTION = "gnc:transaction"; public static final String TAG_TRX_ID = "trn:id"; @@ -94,6 +94,16 @@ public abstract class GncXmlHelper { public static final String TAG_SPLIT_QUANTITY = "split:quantity"; public static final String TAG_SPLIT_SLOTS = "split:slots"; + public static final String TAG_PRICEDB = "gnc:pricedb"; + public static final String TAG_PRICE = "price"; + public static final String TAG_PRICE_ID = "price:id"; + public static final String TAG_PRICE_COMMODITY = "price:commodity"; + public static final String TAG_PRICE_CURRENCY = "price:currency"; + public static final String TAG_PRICE_TIME = "price:time"; + public static final String TAG_PRICE_SOURCE = "price:source"; + public static final String TAG_PRICE_TYPE = "price:type"; + public static final String TAG_PRICE_VALUE = "price:value"; + @Deprecated public static final String TAG_RECURRENCE_PERIOD = "trn:recurrence_period"; @@ -161,7 +171,7 @@ public static long parseDate(String dateString) throws ParseException { /** * Parses amount strings from GnuCash XML into {@link java.math.BigDecimal}s. - * The amounts are formatted as 12345/4100 + * The amounts are formatted as 12345/100 * @param amountString String containing the amount * @return BigDecimal with numerical value * @throws ParseException if the amount could not be parsed @@ -174,7 +184,9 @@ public static BigDecimal parseSplitAmount(String amountString) throws ParseExcep } int scale = amountString.length() - pos - 2; //do this before, because we could modify the string - String numerator = TransactionFormFragment.stripCurrencyFormatting(amountString.substring(0, pos)); + //String numerator = TransactionFormFragment.stripCurrencyFormatting(amountString.substring(0, pos)); + String numerator = amountString.substring(0,pos); + numerator = TransactionFormFragment.stripCurrencyFormatting(numerator); BigInteger numeratorInt = new BigInteger(numerator); return new BigDecimal(numeratorInt, scale); } @@ -201,7 +213,6 @@ public static String formatSplitAmount(BigDecimal amount, Currency trxCurrency){ * So we will use the device locale here and hope that the user has the same locale on the desktop GnuCash

* @param amount Amount to be formatted * @return String representation of amount - * @see #parseTemplateSplitAmount(String) */ public static String formatTemplateSplitAmount(BigDecimal amount){ //TODO: If we ever implement an application-specific locale setting, use it here as well diff --git a/app/src/main/java/org/gnucash/android/importer/CommoditiesXmlHandler.java b/app/src/main/java/org/gnucash/android/importer/CommoditiesXmlHandler.java new file mode 100644 index 000000000..6b4ce3a1c --- /dev/null +++ b/app/src/main/java/org/gnucash/android/importer/CommoditiesXmlHandler.java @@ -0,0 +1,71 @@ +package org.gnucash.android.importer; + +import android.database.sqlite.SQLiteDatabase; + +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.db.CommoditiesDbAdapter; +import org.gnucash.android.model.Commodity; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import java.util.ArrayList; +import java.util.List; + +/** + * XML stream handler for parsing currencies to add to the database + */ +public class CommoditiesXmlHandler extends DefaultHandler { + + public static final String TAG_CURRENCY = "currency"; + public static final String ATTR_ISO_CODE = "isocode"; + public static final String ATTR_FULL_NAME = "fullname"; + public static final String ATTR_NAMESPACE = "namespace"; + public static final String ATTR_EXCHANGE_CODE = "exchange-code"; + public static final String ATTR_SMALLEST_FRACTION = "smallest-fraction"; + public static final String ATTR_LOCAL_SYMBOL = "local-symbol"; + /** + * List of commodities parsed from the XML file. + * They will be all added to db at once at the end of the document + */ + private List mCommodities; + + private CommoditiesDbAdapter mCommoditiesDbAdapter; + + public CommoditiesXmlHandler(SQLiteDatabase db){ + if (db == null){ + mCommoditiesDbAdapter = GnuCashApplication.getCommoditiesDbAdapter(); + } else { + mCommoditiesDbAdapter = new CommoditiesDbAdapter(db); + } + mCommodities = new ArrayList<>(); + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + if (qName.equals(TAG_CURRENCY)) { + String isoCode = attributes.getValue(ATTR_ISO_CODE); + String fullname = attributes.getValue(ATTR_FULL_NAME); + String namespace = attributes.getValue(ATTR_NAMESPACE); + String cusip = attributes.getValue(ATTR_EXCHANGE_CODE); + //TODO: investigate how up-to-date the currency XML list is and use of parts-per-unit vs smallest-fraction. + //some currencies like XAF have smallest fraction 100, but parts-per-unit of 1. + // However java.util.Currency agrees only with the parts-per-unit although we use smallest-fraction in the app + // This could lead to inconsistencies over time + String smallestFraction = attributes.getValue(ATTR_SMALLEST_FRACTION); + String localSymbol = attributes.getValue(ATTR_LOCAL_SYMBOL); + + Commodity commodity = new Commodity(fullname, isoCode, Integer.parseInt(smallestFraction)); + commodity.setNamespace(Commodity.Namespace.valueOf(namespace)); + commodity.setCusip(cusip); + commodity.setLocalSymbol(localSymbol); + + mCommodities.add(commodity); + } + } + + @Override + public void endDocument() throws SAXException { + mCommoditiesDbAdapter.bulkAddRecords(mCommodities); + } +} diff --git a/app/src/main/java/org/gnucash/android/importer/GncXmlHandler.java b/app/src/main/java/org/gnucash/android/importer/GncXmlHandler.java index 75b5dd9ac..fec03cfa7 100644 --- a/app/src/main/java/org/gnucash/android/importer/GncXmlHandler.java +++ b/app/src/main/java/org/gnucash/android/importer/GncXmlHandler.java @@ -24,14 +24,18 @@ import com.crashlytics.android.Crashlytics; import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.CommoditiesDbAdapter; +import org.gnucash.android.db.PricesDbAdapter; import org.gnucash.android.db.ScheduledActionDbAdapter; import org.gnucash.android.db.SplitsDbAdapter; import org.gnucash.android.db.TransactionsDbAdapter; import org.gnucash.android.export.xml.GncXmlHelper; import org.gnucash.android.model.Account; import org.gnucash.android.model.AccountType; +import org.gnucash.android.model.BaseModel; import org.gnucash.android.model.Money; import org.gnucash.android.model.PeriodType; +import org.gnucash.android.model.Price; import org.gnucash.android.model.ScheduledAction; import org.gnucash.android.model.Split; import org.gnucash.android.model.Transaction; @@ -132,10 +136,25 @@ public class GncXmlHandler extends DefaultHandler { Split mSplit; /** - * (Absolute) quantity of the split + * (Absolute) quantity of the split, which uses split account currency */ BigDecimal mQuantity; + /** + * (Absolute) value of the split, which uses transaction currency + */ + BigDecimal mValue; + + /** + * price table entry + */ + Price mPrice; + + boolean mPriceCommodity; + boolean mPriceCurrency; + + List mPriceList; + /** * Whether the quantity is negative */ @@ -208,6 +227,10 @@ public class GncXmlHandler extends DefaultHandler { private ScheduledActionDbAdapter mScheduledActionsDbAdapter; + private CommoditiesDbAdapter mCommoditiesDbAdapter; + + private PricesDbAdapter mPricesDbAdapter; + /** * Creates a handler for handling XML stream events when parsing the XML backup file */ @@ -229,10 +252,14 @@ private void init(@Nullable SQLiteDatabase db) { mAccountsDbAdapter = AccountsDbAdapter.getInstance(); mTransactionsDbAdapter = TransactionsDbAdapter.getInstance(); mScheduledActionsDbAdapter = ScheduledActionDbAdapter.getInstance(); + mCommoditiesDbAdapter = CommoditiesDbAdapter.getInstance(); + mPricesDbAdapter = PricesDbAdapter.getInstance(); } else { mTransactionsDbAdapter = new TransactionsDbAdapter(db, new SplitsDbAdapter(db)); mAccountsDbAdapter = new AccountsDbAdapter(db, mTransactionsDbAdapter); mScheduledActionsDbAdapter = new ScheduledActionDbAdapter(db); + mCommoditiesDbAdapter = new CommoditiesDbAdapter(db); + mPricesDbAdapter = new PricesDbAdapter(db); } mContent = new StringBuilder(); @@ -247,6 +274,8 @@ private void init(@Nullable SQLiteDatabase db) { mTemplateAccountToTransactionMap = new HashMap<>(); mAutoBalanceSplits = new ArrayList<>(); + + mPriceList = new ArrayList<>(); } @Override @@ -263,7 +292,7 @@ public void startElement(String uri, String localName, mISO4217Currency = false; break; case GncXmlHelper.TAG_TRN_SPLIT: - mSplit = new Split(Money.getZeroInstance(),""); + mSplit = new Split(Money.getZeroInstance(), ""); break; case GncXmlHelper.TAG_DATE_POSTED: mIsDatePosted = true; @@ -290,6 +319,19 @@ public void startElement(String uri, String localName, case GncXmlHelper.TAG_RX_START: mIsRecurrenceStart = true; break; + case GncXmlHelper.TAG_PRICE: + mPrice = new Price(); + break; + case GncXmlHelper.TAG_PRICE_CURRENCY: + mPriceCurrency = true; + mPriceCommodity = false; + mISO4217Currency = false; + break; + case GncXmlHelper.TAG_PRICE_COMMODITY: + mPriceCurrency = false; + mPriceCommodity = true; + mISO4217Currency = false; + break; } } @@ -322,6 +364,9 @@ public void endElement(String uri, String localName, String qualifiedName) throw case GncXmlHelper.TAG_COMMODITY_SPACE: if (characterString.equals("ISO4217")) { mISO4217Currency = true; + } else { + // price of non-ISO4217 commodities cannot be handled + mPrice = null; } break; case GncXmlHelper.TAG_COMMODITY_ID: @@ -332,6 +377,19 @@ public void endElement(String uri, String localName, String qualifiedName) throw if (mTransaction != null) { mTransaction.setCurrencyCode(currencyCode); } + if (mPrice != null) { + if (mPriceCommodity) { + mPrice.setCommodityUID(mCommoditiesDbAdapter.getCommodityUID(currencyCode)); + mPriceCommodity = false; + } + if (mPriceCurrency) { + mPrice.setCurrencyUID(mCommoditiesDbAdapter.getCommodityUID(currencyCode)); + mPriceCurrency = false; + } + } + break; + case GncXmlHelper.TAG_ACCT_DESCRIPTION: + mAccount.setDescription(characterString); break; case GncXmlHelper.TAG_PARENT_UID: mAccount.setParentUID(characterString); @@ -387,11 +445,11 @@ public void endElement(String uri, String localName, String qualifiedName) throw break; case GncXmlHelper.TAG_SLOT_VALUE: if (mInPlaceHolderSlot) { - Log.v(LOG_TAG, "Setting account placeholder flag"); + //Log.v(LOG_TAG, "Setting account placeholder flag"); mAccount.setPlaceHolderFlag(Boolean.parseBoolean(characterString)); mInPlaceHolderSlot = false; } else if (mInColorSlot) { - Log.d(LOG_TAG, "Parsing color code: " + characterString); + //Log.d(LOG_TAG, "Parsing color code: " + characterString); String color = characterString.trim(); //Gnucash exports the account color in format #rrrgggbbb, but we need only #rrggbb. //so we trim the last digit in each block, doesn't affect the color much @@ -452,6 +510,9 @@ public void endElement(String uri, String localName, String qualifiedName) throw mTransaction.setCreatedTimestamp(timestamp); mIsDateEntered = false; } + if (mPrice != null) { + mPrice.setDate(new Timestamp(GncXmlHelper.parseDate(characterString))); + } } catch (ParseException e) { Crashlytics.logException(e); String message = "Unable to parse transaction time - " + characterString; @@ -470,9 +531,10 @@ public void endElement(String uri, String localName, String qualifiedName) throw case GncXmlHelper.TAG_SPLIT_MEMO: mSplit.setMemo(characterString); break; - case GncXmlHelper.TAG_SPLIT_QUANTITY: - // delay the assignment of currency when the split account is seen + case GncXmlHelper.TAG_SPLIT_VALUE: try { + // The value and quantity can have different sign for custom currency(stock). + // Use the sign of value for split, as it would not be custom currency String q = characterString; if (q.charAt(0) == '-') { mNegativeQuantity = true; @@ -480,7 +542,18 @@ public void endElement(String uri, String localName, String qualifiedName) throw } else { mNegativeQuantity = false; } - mQuantity = GncXmlHelper.parseSplitAmount(q); + mValue = GncXmlHelper.parseSplitAmount(characterString).abs(); // use sign from quantity + } catch (ParseException e) { + String msg = "Error parsing split quantity - " + characterString; + Crashlytics.log(msg); + Crashlytics.logException(e); + throw new SAXException(msg, e); + } + break; + case GncXmlHelper.TAG_SPLIT_QUANTITY: + // delay the assignment of currency when the split account is seen + try { + mQuantity = GncXmlHelper.parseSplitAmount(characterString).abs(); } catch (ParseException e) { String msg = "Error parsing split quantity - " + characterString; Crashlytics.log(msg); @@ -490,11 +563,12 @@ public void endElement(String uri, String localName, String qualifiedName) throw break; case GncXmlHelper.TAG_SPLIT_ACCOUNT: if (!mInTemplates) { - //the split amount uses the account currency - Money amount = new Money(mQuantity, getCurrencyForAccount(characterString)); //this is intentional: GnuCash XML formats split amounts, credits are negative, debits are positive. mSplit.setType(mNegativeQuantity ? TransactionType.CREDIT : TransactionType.DEBIT); - mSplit.setAmount(amount); + //the split amount uses the account currency + mSplit.setQuantity(new Money(mQuantity, getCurrencyForAccount(characterString))); + //the split value uses the transaction currency + mSplit.setValue(new Money(mValue, mTransaction.getCurrency())); mSplit.setAccountUID(characterString); } else { if (!mIgnoreTemplateTransaction) @@ -598,7 +672,7 @@ public void endElement(String uri, String localName, String qualifiedName) throw if (mScheduledAction.getActionType() == ScheduledAction.ActionType.TRANSACTION) { mScheduledAction.setActionUID(mTemplateAccountToTransactionMap.get(characterString)); } else { - mScheduledAction.setActionUID(UUID.randomUUID().toString().replaceAll("-","")); + mScheduledAction.setActionUID(BaseModel.generateUID()); } break; case GncXmlHelper.TAG_SCHEDULED_ACTION: @@ -610,6 +684,42 @@ public void endElement(String uri, String localName, String qualifiedName) throw mRecurrenceMultiplier = 1; //reset it, even though it will be parsed from XML each time mIgnoreScheduledAction = false; break; + // price table + case GncXmlHelper.TAG_PRICE_ID: + mPrice.setUID(characterString); + break; + case GncXmlHelper.TAG_PRICE_SOURCE: + if (mPrice != null) { + mPrice.setSource(characterString); + } + break; + case GncXmlHelper.TAG_PRICE_VALUE: + if (mPrice != null) { + String[] parts = characterString.split("/"); + if (parts.length != 2) { + String message = "Illegal price - " + characterString; + Log.e(LOG_TAG, message); + Crashlytics.log(message); + throw new SAXException(message); + } else { + mPrice.setValueNum(Long.valueOf(parts[0])); + mPrice.setValueDenom(Long.valueOf(parts[1])); + Log.d(getClass().getName(), "price " + characterString + + " .. " + mPrice.getValueNum() + "/" + mPrice.getValueDenom()); + } + } + break; + case GncXmlHelper.TAG_PRICE_TYPE: + if (mPrice != null) { + mPrice.setType(characterString); + } + break; + case GncXmlHelper.TAG_PRICE: + if (mPrice != null) { + mPriceList.add(mPrice); + mPrice = null; + } + break; } //reset the accumulated characters @@ -707,24 +817,29 @@ public void endDocument() throws SAXException { } long startTime = System.nanoTime(); mAccountsDbAdapter.beginTransaction(); + Log.d(getClass().getSimpleName(), "bulk insert starts"); try { + Log.d(getClass().getSimpleName(), "before clean up db"); mAccountsDbAdapter.deleteAllRecords(); - - long nAccounts = mAccountsDbAdapter.bulkAddAccounts(mAccountList); + Log.d(getClass().getSimpleName(), String.format("deb clean up done %d ns", System.nanoTime()-startTime)); + long nAccounts = mAccountsDbAdapter.bulkAddRecords(mAccountList); Log.d("Handler:", String.format("%d accounts inserted", nAccounts)); //We need to add scheduled actions first because there is a foreign key constraint on transactions //which are generated from scheduled actions (we do auto-create some transactions during import) - int nSchedActions = mScheduledActionsDbAdapter.bulkAddScheduledActions(mScheduledActionsList); + long nSchedActions = mScheduledActionsDbAdapter.bulkAddRecords(mScheduledActionsList); Log.d("Handler:", String.format("%d scheduled actions inserted", nSchedActions)); - long nTempTransactions = mTransactionsDbAdapter.bulkAddTransactions(mTemplateTransactions); + long nTempTransactions = mTransactionsDbAdapter.bulkAddRecords(mTemplateTransactions); Log.d("Handler:", String.format("%d template transactions inserted", nTempTransactions)); - long nTransactions = mTransactionsDbAdapter.bulkAddTransactions(mTransactionList); + long nTransactions = mTransactionsDbAdapter.bulkAddRecords(mTransactionList); Log.d("Handler:", String.format("%d transactions inserted", nTransactions)); + long nPrices = mPricesDbAdapter.bulkAddRecords(mPriceList); + Log.d(getClass().getSimpleName(), String.format("%d prices inserted", nPrices)); + long endTime = System.nanoTime(); - Log.d("Handler:", String.format(" bulk insert time: %d", endTime - startTime)); + Log.d(getClass().getSimpleName(), String.format("bulk insert time: %d", endTime - startTime)); mAccountsDbAdapter.setTransactionSuccessful(); } finally { @@ -756,7 +871,7 @@ private void handleEndOfTemplateNumericSlot(String characterString, TransactionT try { BigDecimal amountBigD = GncXmlHelper.parseSplitAmount(characterString); Money amount = new Money(amountBigD, getCurrencyForAccount(mSplit.getAccountUID())); - mSplit.setAmount(amount.absolute()); + mSplit.setValue(amount.absolute()); mSplit.setType(splitType); mIgnoreTemplateTransaction = false; //we have successfully parsed an amount } catch (NumberFormatException | ParseException e) { diff --git a/app/src/main/java/org/gnucash/android/importer/GncXmlImporter.java b/app/src/main/java/org/gnucash/android/importer/GncXmlImporter.java index 0ebefd85b..43ea7c614 100644 --- a/app/src/main/java/org/gnucash/android/importer/GncXmlImporter.java +++ b/app/src/main/java/org/gnucash/android/importer/GncXmlImporter.java @@ -17,8 +17,11 @@ package org.gnucash.android.importer; import android.database.sqlite.SQLiteDatabase; +import android.preference.PreferenceManager; import android.util.Log; +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.export.Exporter; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; @@ -27,6 +30,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; +import java.sql.Timestamp; import java.util.zip.GZIPInputStream; import javax.xml.parsers.ParserConfigurationException; @@ -82,12 +86,16 @@ public static void parse(InputStream gncXmlInputStream) throws ParserConfigurati bos = new BufferedInputStream(pb); //TODO: Set an error handler which can log errors - + Log.d(GncXmlImporter.class.getSimpleName(), "Start import"); GncXmlHandler handler = new GncXmlHandler(); xr.setContentHandler(handler); long startTime = System.nanoTime(); xr.parse(new InputSource(bos)); long endTime = System.nanoTime(); - Log.d("Import", String.format("%d ns spent on importing the file", endTime-startTime)); + + String timeStamp = new Timestamp(System.currentTimeMillis()).toString(); + PreferenceManager.getDefaultSharedPreferences(GnuCashApplication.getAppContext()).edit().putString(Exporter.PREF_LAST_EXPORT_TIME, timeStamp).apply(); + + Log.d(GncXmlImporter.class.getSimpleName(), String.format("%d ns spent on importing the file", endTime-startTime)); } } 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 55c6f138b..a0232003a 100644 --- a/app/src/main/java/org/gnucash/android/importer/ImportAsyncTask.java +++ b/app/src/main/java/org/gnucash/android/importer/ImportAsyncTask.java @@ -73,6 +73,7 @@ protected Boolean doInBackground(InputStream... inputStreams) { } catch (Exception exception){ Log.e(ImportAsyncTask.class.getName(), "" + exception.getMessage()); Crashlytics.logException(exception); + exception.printStackTrace(); final String err_msg = exception.getLocalizedMessage(); context.runOnUiThread(new Runnable() { diff --git a/app/src/main/java/org/gnucash/android/model/Account.java b/app/src/main/java/org/gnucash/android/model/Account.java index 3da83bf07..42ca52790 100644 --- a/app/src/main/java/org/gnucash/android/model/Account.java +++ b/app/src/main/java/org/gnucash/android/model/Account.java @@ -17,11 +17,16 @@ package org.gnucash.android.model; +import android.preference.PreferenceManager; + import org.gnucash.android.BuildConfig; +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.export.Exporter; import org.gnucash.android.export.ofx.OfxHelper; import org.w3c.dom.Document; import org.w3c.dom.Element; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.Currency; import java.util.List; @@ -80,11 +85,18 @@ public enum OfxAccountType {CHECKING, SAVINGS, MONEYMRKT, CREDITLINE } */ private String mFullName; + /** + * Account description + */ + private String mDescription; + /** * Currency used by transactions in this account */ private Currency mCurrency; - + + private String mCommodityUID; + /** * Type of account * Defaults to {@link AccountType#CASH} @@ -94,7 +106,7 @@ public enum OfxAccountType {CHECKING, SAVINGS, MONEYMRKT, CREDITLINE } /** * List of transactions in this account */ - private List mTransactionsList = new ArrayList(); + private List mTransactionsList = new ArrayList<>(); /** * Account UID of the parent account. Can be null @@ -194,6 +206,22 @@ public void setFullName(String fullName) { this.mFullName = fullName; } + /** + * Returns the account mDescription + * @return String with mDescription + */ + public String getDescription() { + return mDescription; + } + + /** + * Sets the account mDescription + * @param description String mDescription + */ + public void setDescription(String description) { + this.mDescription = description; + } + /** * Get the type of account * @return {@link AccountType} type of account @@ -308,14 +336,30 @@ public Currency getCurrency() { /** * Sets the currency to be used by this account - * @param mCurrency the mCurrency to set + * @param currency the mCurrency to set */ - public void setCurrency(Currency mCurrency) { - this.mCurrency = mCurrency; + public void setCurrency(Currency currency) { + this.mCurrency = currency; //TODO: Maybe at some time t, this method should convert all //transaction values to the corresponding value in the new currency } + /** + * Returns the commodity GUID for this account + * @return String GUID of commodity + */ + public String getCommodityUID() { + return mCommodityUID; + } + + /** + * Sets the commodity GUID for this account + * @param commodityUID String commodity GUID + */ + public void setCommodityUID(String commodityUID) { + this.mCommodityUID = commodityUID; + } + /** * Sets the Unique Account Identifier of the parent account * @param parentUID String Unique ID of parent account @@ -480,9 +524,10 @@ public void toOfx(Document doc, Element parent, boolean exportAllTransactions){ Element bankTransactionsList = doc.createElement(OfxHelper.TAG_BANK_TRANSACTION_LIST); bankTransactionsList.appendChild(dtstart); bankTransactionsList.appendChild(dtend); - + + Timestamp lastExportedTimestamp = Timestamp.valueOf(PreferenceManager.getDefaultSharedPreferences(GnuCashApplication.getAppContext()).getString(Exporter.PREF_LAST_EXPORT_TIME, Exporter.TIMESTAMP_ZERO)); for (Transaction transaction : mTransactionsList) { - if (!exportAllTransactions && transaction.isExported()) + if (!exportAllTransactions && /*transaction.isExported()*/ transaction.getModifiedTimestamp().before(lastExportedTimestamp)) continue; bankTransactionsList.appendChild(transaction.toOFX(doc, getUID())); } diff --git a/app/src/main/java/org/gnucash/android/model/BaseModel.java b/app/src/main/java/org/gnucash/android/model/BaseModel.java index eff68b1fe..eefde82a5 100644 --- a/app/src/main/java/org/gnucash/android/model/BaseModel.java +++ b/app/src/main/java/org/gnucash/android/model/BaseModel.java @@ -45,12 +45,11 @@ public BaseModel(){ } /** - * Method for generating the Global Unique ID for the object and sets the internal variable which can be retrieved with {@link #getUID()}. - * Subclasses can override this method to provide a different implementation + * Method for generating the Global Unique ID for the model object * @return Random GUID for the model object */ - protected String generateUID(){ - return mUID = UUID.randomUUID().toString().replaceAll("-", ""); + public static String generateUID(){ + return UUID.randomUUID().toString().replaceAll("-", ""); } /** @@ -74,7 +73,7 @@ public void setUID(String uid) { this.mUID = uid; } - /** + /**8 * Returns the timestamp when this model entry was created in the database * @return Timestamp of creation of model */ diff --git a/app/src/main/java/org/gnucash/android/model/Commodity.java b/app/src/main/java/org/gnucash/android/model/Commodity.java new file mode 100644 index 000000000..169e2d15b --- /dev/null +++ b/app/src/main/java/org/gnucash/android/model/Commodity.java @@ -0,0 +1,106 @@ +package org.gnucash.android.model; + +/** + * Commodities are the currencies used in the application. + * At the moment only ISO4217 currencies are supported + */ +public class Commodity extends BaseModel { + public enum Namespace { ISO4217 } //Namespace for commodities + + private Namespace mNamespace = Namespace.ISO4217; + + /** + * This is the currency code for ISO4217 currencies + */ + private String mMnemonic; + private String mFullname; + private String mCusip; + private String mLocalSymbol = ""; + private int mFraction; + private int mQuoteFlag; + + /** + * Create a new commodity + * @param fullname Official full name of the currency + * @param mnemonic Official abbreviated designation for the currency + * @param fraction Number of sub-units that the basic commodity can be divided into + */ + public Commodity(String fullname, String mnemonic, int fraction){ + this.mFullname = fullname; + this.mMnemonic = mnemonic; + this.mFraction = fraction; + } + + public Namespace getNamespace() { + return mNamespace; + } + + public void setNamespace(Namespace namespace) { + this.mNamespace = namespace; + } + + /** + * Returns the mnemonic, or currency code for ISO4217 currencies + * @return Mnemonic of the commodity + */ + public String getMnemonic() { + return mMnemonic; + } + + public void setMnemonic(String mMnemonic) { + this.mMnemonic = mMnemonic; + } + + public String getFullname() { + return mFullname; + } + + public void setFullname(String mFullname) { + this.mFullname = mFullname; + } + + public String getCusip() { + return mCusip; + } + + public void setCusip(String mCusip) { + this.mCusip = mCusip; + } + + public String getLocalSymbol() { + return mLocalSymbol; + } + + /** + * Returns the symbol for this commodity. + *

Normally this would be the local symbol, but in it's absence, the mnemonic (currency code) + * is returned.

+ * @return + */ + public String getSymbol(){ + if (mLocalSymbol == null || mLocalSymbol.isEmpty()){ + return mMnemonic; + } + return mLocalSymbol; + } + + public void setLocalSymbol(String localSymbol) { + this.mLocalSymbol = localSymbol; + } + + public int getFraction() { + return mFraction; + } + + public void setFraction(int fraction) { + this.mFraction = fraction; + } + + public int getQuoteFlag() { + return mQuoteFlag; + } + + public void setQuoteFlag(int quoteFlag) { + this.mQuoteFlag = quoteFlag; + } +} diff --git a/app/src/main/java/org/gnucash/android/model/Money.java b/app/src/main/java/org/gnucash/android/model/Money.java index 7013dd309..20024b1c3 100644 --- a/app/src/main/java/org/gnucash/android/model/Money.java +++ b/app/src/main/java/org/gnucash/android/model/Money.java @@ -18,14 +18,16 @@ import android.support.annotation.NonNull; +import android.util.Log; import com.crashlytics.android.Crashlytics; import org.gnucash.android.app.GnuCashApplication; import java.math.BigDecimal; -import java.math.MathContext; +import java.math.BigInteger; import java.math.RoundingMode; +import java.security.InvalidParameterException; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; @@ -74,12 +76,6 @@ public final class Money implements Comparable{ * Defaults to {@link #DEFAULT_ROUNDING_MODE} */ protected RoundingMode ROUNDING_MODE = DEFAULT_ROUNDING_MODE; - - /** - * Number of decimal places to limit fractions to in arithmetic operations - * Defaults to {@link #DEFAULT_DECIMAL_PLACES} - */ - protected int DECIMAL_PLACES = DEFAULT_DECIMAL_PLACES; /** * Default currency code (according ISO 4217) @@ -113,7 +109,23 @@ public static Money getZeroInstance(){ public Money() { init(); } - + + public static BigDecimal getBigDecimal(long numerator, long denominator) { + int scale; + if (numerator == 0 && denominator == 0) { + denominator = 1; + } + switch ((int)denominator) { + case 1: scale = 0; break; + case 10: scale = 1; break; + case 100: scale = 2; break; + case 1000: scale = 3; break; + default: + throw new InvalidParameterException("invalid denominator " + denominator); + } + return new BigDecimal(BigInteger.valueOf(numerator), scale); + } + /** * Overloaded constructor * @param amount {@link BigDecimal} value of the money instance @@ -121,7 +133,7 @@ public Money() { */ public Money(BigDecimal amount, Currency currency){ this.mAmount = amount; - this.mCurrency = currency; + setCurrency(currency); } /** @@ -131,24 +143,22 @@ public Money(BigDecimal amount, Currency currency){ * @param currencyCode Currency code as specified by ISO 4217 */ public Money(String amount, String currencyCode){ - setAmount(amount); setCurrency(Currency.getInstance(currencyCode)); + setAmount(amount); } - + /** - * Overloaded constructor - * Accepts context options for rounding mode during operations on this money object - * @param amount {@link BigDecimal} value of the money instance - * @param currency {@link Currency} associated with the amount - * @param context {@link MathContext} specifying rounding mode during operations + * Constructs a new money amount given the numerator and denominator of the amount. + * The rounding mode used for the division is {@link BigDecimal#ROUND_HALF_EVEN} + * @param numerator Numerator as integer + * @param denominator Denominator as integer + * @param currencyCode 3-character currency code string */ - public Money(BigDecimal amount, Currency currency, MathContext context){ - setAmount(amount); - setCurrency(currency); - ROUNDING_MODE = context.getRoundingMode(); - DECIMAL_PLACES = context.getPrecision(); + public Money(long numerator, long denominator, String currencyCode){ + mAmount = getBigDecimal(numerator, denominator); + setCurrency(Currency.getInstance(currencyCode)); } - + /** * Overloaded constructor. * Initializes the currency to that specified by {@link Money#DEFAULT_CURRENCY_CODE} @@ -165,8 +175,8 @@ public Money(String amount){ * @param money Money instance to be cloned */ public Money(Money money){ - setAmount(money.asBigDecimal()); - setCurrency(money.getCurrency()); + setCurrency(money.getCurrency()); + setAmount(money.asBigDecimal()); } /** @@ -213,10 +223,62 @@ public Money withCurrency(Currency currency){ * @param currency {@link Currency} to assign to the Money object */ private void setCurrency(Currency currency) { - //TODO: Consider doing a conversion of the value as well in the future this.mCurrency = currency; } + /** + * Returns the GnuCash format numerator for this amount. + *

Example: Given an amount 32.50$, the numerator will be 3250

+ * @return GnuCash numerator for this amount + */ + public long getNumerator() { + try { + return mAmount.scaleByPowerOfTen(getScale()).longValueExact(); + } catch (ArithmeticException e) { + Log.e(getClass().getName(), "Currency " + mCurrency.getCurrencyCode() + + " with scale " + getScale() + + " has amount " + mAmount.toString()); + throw e; + } + } + + /** + * Returns the GnuCash amount format denominator for this amount + *

The denominator is 10 raised to the power of number of fractional digits in the currency

+ * @return GnuCash format denominator + */ + public long getDenominator() { + switch (getScale()) { + case 0: + return 1; + case 1: + return 10; + case 2: + return 100; + case 3: + return 1000; + case 4: + return 10000; + } + throw new RuntimeException("Unsupported number of fraction digits " + getScale()); + } + + /** + * Returns the scale (precision) used for the decimal places of this amount. + *

The scale used depends on the currency

+ * @return Scale of amount as integer + */ + private int getScale() { + int scale = mCurrency.getDefaultFractionDigits(); + if (scale < 0) { + scale = mAmount.scale(); + } + if (scale < 0) { + scale = 0; + } + return scale; + } + /** * Returns the amount represented by this Money object * @return {@link BigDecimal} valure of amount in object @@ -232,7 +294,17 @@ public BigDecimal asBigDecimal() { public double asDouble(){ return mAmount.doubleValue(); } - + + /** + * Returns integer value of this Money amount. + * The fractional part is discarded + * @return Integer representation of this amount + * @see BigDecimal#intValue() + */ + public int intValue(){ + return mAmount.intValue(); + } + /** * An alias for {@link #toPlainString()} * @return Money formatted as a string (excludes the currency) @@ -244,14 +316,14 @@ public String asString(){ /** * Returns a string representation of the Money object formatted according to * the locale and includes the currency symbol. - * The output precision is limited to {@link #DECIMAL_PLACES}. + * The output precision is limited to the number of fractional digits supported by the currency * @param locale Locale to use when formatting the object * @return String containing formatted Money representation */ public String formattedString(Locale locale){ NumberFormat formatter = NumberFormat.getInstance(locale); - formatter.setMinimumFractionDigits(DECIMAL_PLACES); - formatter.setMaximumFractionDigits(DECIMAL_PLACES); + formatter.setMinimumFractionDigits(mCurrency.getDefaultFractionDigits()); + formatter.setMaximumFractionDigits(mCurrency.getDefaultFractionDigits()); return formatter.format(asDouble()) + " " + mCurrency.getSymbol(locale); } @@ -277,7 +349,7 @@ public Money negate(){ * @param amount {@link BigDecimal} amount to be set */ private void setAmount(BigDecimal amount) { - mAmount = amount.setScale(DECIMAL_PLACES, ROUNDING_MODE); + mAmount = amount.setScale(mCurrency.getDefaultFractionDigits(), ROUNDING_MODE); } /** @@ -365,8 +437,9 @@ public Money multiply(Money money){ } /** - * Returns a new Money object whose value is the product of the division of this objects - * value by the factor multiplier + * Returns a new Money object whose value is the product of this object + * and the factor multiplier + *

The currency of the returned object is the same as the current object

* @param multiplier Factor to multiply the amount by. * @return Money object whose value is the product of this objects values and multiplier */ @@ -374,7 +447,17 @@ public Money multiply(int multiplier){ Money moneyFactor = new Money(new BigDecimal(multiplier), mCurrency); return multiply(moneyFactor); } - + + /** + * Returns a new Money object whose value is the product of this object + * and the factor multiplier + * @param multiplier Factor to multiply the amount by. + * @return Money object whose value is the product of this objects values and multiplier + */ + public Money multiply(BigDecimal multiplier){ + return new Money(mAmount.multiply(multiplier), mCurrency); + } + /** * Returns true if the amount held by this Money object is negative * @return true if the amount is negative, false otherwise. @@ -384,13 +467,27 @@ public boolean isNegative(){ } /** - * Returns the string representation of the amount (without currency) of the Money object + * Returns the string representation of the amount (without currency) of the Money object. + *

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

* @return String representation of the amount (without currency) of the Money object */ public String toPlainString(){ - return mAmount.setScale(DECIMAL_PLACES, ROUNDING_MODE).toPlainString(); + return mAmount.setScale(mCurrency.getDefaultFractionDigits(), ROUNDING_MODE).toPlainString(); } - + + /** + * Returns the formatted amount in the default locale + *

This prints the money amount with locale formatting like the decimal separation character

+ * @return Locale-formatted amount + */ + public String formattedAmount(){ + NumberFormat formatter = NumberFormat.getInstance(); + formatter.setMinimumFractionDigits(mCurrency.getDefaultFractionDigits()); + formatter.setMaximumFractionDigits(mCurrency.getDefaultFractionDigits()); + formatter.setGroupingUsed(false); + return formatter.format(asDouble()); + } + /** * Returns the string representation of the Money object (value + currency) formatted according * to the default locale @@ -410,7 +507,7 @@ public int hashCode() { return result; } - /** + /** //FIXME: equality failing for money objects * Two Money objects are only equal if their amount (value) and currencies are equal * @param obj Object to compare with * @return true if the objects are equal, false otherwise diff --git a/app/src/main/java/org/gnucash/android/model/Price.java b/app/src/main/java/org/gnucash/android/model/Price.java new file mode 100644 index 000000000..084da5501 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/model/Price.java @@ -0,0 +1,124 @@ +package org.gnucash.android.model; + +import java.sql.Timestamp; + +/** + * Model for commodity prices + */ +public class Price extends BaseModel { + + private String mCommodityUID; + private String mCurrencyUID; + private Timestamp mDate; + private String mSource; + private String mType; + private long mValueNum; + private long mValueDenom; + + /** + * String indicating that the price was provided by the user + */ + public static final String SOURCE_USER = "user:xfer-dialog"; + + public Price(){ + mDate = new Timestamp(System.currentTimeMillis()); + } + + /** + * Create new instance with the GUIDs of the commodities + * @param commodityUID GUID of the origin commodity + * @param currencyUID GUID of the target commodity + */ + public Price(String commodityUID, String currencyUID){ + this.mCommodityUID = commodityUID; + this.mCurrencyUID = currencyUID; + mDate = new Timestamp(System.currentTimeMillis()); + } + + public String getCommodityUID() { + return mCommodityUID; + } + + public void setCommodityUID(String mCommodityUID) { + this.mCommodityUID = mCommodityUID; + } + + public String getCurrencyUID() { + return mCurrencyUID; + } + + public void setCurrencyUID(String currencyUID) { + this.mCurrencyUID = currencyUID; + } + + public Timestamp getDate() { + return mDate; + } + + public void setDate(Timestamp date) { + this.mDate = date; + } + + public String getSource() { + return mSource; + } + + public void setSource(String source) { + this.mSource = source; + } + + public String getType() { + return mType; + } + + public void setType(String type) { + this.mType = type; + } + + public long getValueNum() { + return mValueNum; + } + + public void setValueNum(long valueNum) { + this.mValueNum = valueNum; + } + + public long getValueDenom() { + return mValueDenom; + } + + public void setValueDenom(long valueDenom) { + this.mValueDenom = valueDenom; + } + + public void reduce() { + if (mValueDenom < 0) { + mValueDenom = -mValueDenom; + mValueNum = -mValueNum; + } + if (mValueDenom != 0 && mValueNum != 0) { + long num1 = mValueNum; + if (num1 < 0) { + num1 = -num1; + } + long num2 = mValueDenom; + long commonDivisor = 1; + for(;;) { + long r = num1 % num2; + if (r == 0) { + commonDivisor = num2; + break; + } + num1 = r; + r = num2 % num1; + if (r == 0) { + commonDivisor = num1; + break; + } + num2 = r; + } + mValueNum /= commonDivisor; + mValueDenom /= commonDivisor; + } + } +} diff --git a/app/src/main/java/org/gnucash/android/model/Split.java b/app/src/main/java/org/gnucash/android/model/Split.java index 297b83df7..3b14cdc33 100644 --- a/app/src/main/java/org/gnucash/android/model/Split.java +++ b/app/src/main/java/org/gnucash/android/model/Split.java @@ -2,6 +2,9 @@ import android.support.annotation.NonNull; +import android.util.Log; + +import org.gnucash.android.db.AccountsDbAdapter; /** * A split amount in a transaction. @@ -15,9 +18,14 @@ */ public class Split extends BaseModel{ /** - * Amount value of this split + * Amount value of this split which is in the currency of the transaction + */ + private Money mValue; + + /** + * Amount of the split in the currency of the account to which the split belongs */ - private Money mAmount; + private Money mQuantity; /** * Transaction UID which this split belongs to @@ -40,12 +48,28 @@ public class Split extends BaseModel{ private String mMemo; /** - * Initialize split with an amount and account - * @param amount Money amount of this split + * Initialize split with a value amount and account + * @param value Money value amount of this split + * @param accountUID String UID of transfer account + */ + public Split(@NonNull Money value, @NonNull Money quantity, String accountUID){ + setQuantity(quantity); + setValue(value); + setAccountUID(accountUID); + //NOTE: This is a rather simplististic approach to the split type. + //It typically also depends on the account type of the account. But we do not want to access + //the database everytime a split is created. So we keep it simple here. Set the type you want explicity. + mSplitType = value.isNegative() ? TransactionType.DEBIT : TransactionType.CREDIT; + } + + /** + * Initialize split with a value amount and account + * @param amount Money value amount of this split. Value is always in the currency the owning transaction * @param accountUID String UID of transfer account */ public Split(@NonNull Money amount, String accountUID){ - setAmount(amount); + setQuantity(amount); + setValue(amount); setAccountUID(accountUID); //NOTE: This is a rather simplististic approach to the split type. //It typically also depends on the account type of the account. But we do not want to access @@ -53,6 +77,7 @@ public Split(@NonNull Money amount, String accountUID){ mSplitType = amount.isNegative() ? TransactionType.DEBIT : TransactionType.CREDIT; } + /** * Clones the sourceSplit to create a new instance with same fields * @param sourceSplit Split to be cloned @@ -63,7 +88,8 @@ public Split(Split sourceSplit, boolean generateUID){ this.mAccountUID = sourceSplit.mAccountUID; this.mSplitType = sourceSplit.mSplitType; this.mTransactionUID = sourceSplit.mTransactionUID; - this.mAmount = sourceSplit.mAmount.absolute(); + this.mValue = new Money(sourceSplit.mValue); + this.mQuantity = new Money(sourceSplit.mQuantity); if (generateUID){ generateUID(); @@ -73,19 +99,41 @@ public Split(Split sourceSplit, boolean generateUID){ } /** - * Returns the amount of the split - * @return Money amount of the split + * Returns the value amount of the split + * @return Money amount of the split with the currency of the transaction + * @see #getQuantity() + */ + public Money getValue() { + return mValue; + } + + /** + * Sets the value amount of the split.
+ * The value is in the currency of the containing transaction + * @param value Money value of this split + * @see #setQuantity(Money) + */ + public void setValue(Money value) { + mValue = value; + } + + /** + * Returns the quantity amount of the split. + *

The quantity is in the currency of the account to which the split is associated

+ * @return Money quantity amount + * @see #getValue() */ - public Money getAmount() { - return mAmount; + public Money getQuantity() { + return mQuantity; } /** - * Sets the amount of the split - * @param amount Money amount of this split + * Sets the quantity value of the split + * @param quantity Money quantity amount + * @see #setValue(Money) */ - public void setAmount(Money amount) { - this.mAmount = amount; + public void setQuantity(Money quantity) { + this.mQuantity = quantity; } /** @@ -161,11 +209,11 @@ public void setMemo(String memo) { * @see TransactionType#invert() */ public Split createPair(String accountUID){ - Split pair = new Split(mAmount.absolute(), accountUID); + Split pair = new Split(mValue.absolute(), accountUID); pair.setType(mSplitType.invert()); pair.setMemo(mMemo); pair.setTransactionUID(mTransactionUID); - + pair.setQuantity(mQuantity); return pair; } @@ -175,11 +223,12 @@ public Split createPair(String accountUID){ */ protected Split clone() throws CloneNotSupportedException { super.clone(); - Split split = new Split(mAmount, mAccountUID); + Split split = new Split(mValue, mAccountUID); split.setUID(getUID()); split.setType(mSplitType); split.setMemo(mMemo); split.setTransactionUID(mTransactionUID); + split.setQuantity(mQuantity); return split; } @@ -190,45 +239,122 @@ protected Split clone() throws CloneNotSupportedException { * @return whether the two splits are a pair */ public boolean isPairOf(Split other) { - return mAmount.absolute().equals(other.mAmount.absolute()) + return mValue.absolute().equals(other.mValue.absolute()) && mSplitType.invert().equals(other.mSplitType); } + /** + * Returns the formatted amount (with or without negation sign) for the split value + * @return Money amount of value + * @see #getFormattedAmount(Money, String, TransactionType) + */ + public Money getFormattedValue(){ + return getFormattedAmount(mValue, mAccountUID, mSplitType); + } + + /** + * Returns the formatted amount (with or without negation sign) for the quantity + * @return Money amount of quantity + * @see #getFormattedAmount(Money, String, TransactionType) + */ + public Money getFormattedQuantity(){ + return getFormattedAmount(mQuantity, mAccountUID, mSplitType); + } + + /** + * Splits are saved as absolute values to the database, with no negative numbers. + * The type of movement the split causes to the balance of an account determines its sign, and + * that depends on the split type and the account type + * @param amount Money amount to format + * @param accountUID GUID of the account + * @param splitType Transaction type of the split + * @return -{@code amount} if the amount would reduce the balance of {@code account}, otherwise +{@code amount} + */ + public static Money getFormattedAmount(Money amount, String accountUID, TransactionType splitType){ + boolean isDebitAccount = AccountsDbAdapter.getInstance().getAccountType(accountUID).hasDebitNormalBalance(); + Money absAmount = amount.absolute(); + + boolean isDebitSplit = splitType == TransactionType.DEBIT; + if (isDebitAccount) { + if (isDebitSplit) { + return absAmount; + } else { + return absAmount.negate(); + } + } else { + if (isDebitSplit) { + return absAmount.negate(); + } else { + return absAmount; + } + } + } + @Override public String toString() { - return mSplitType.name() + " of " + mAmount.toString() + " in account: " + mAccountUID; + return mSplitType.name() + " of " + mValue.toString() + " in account: " + mAccountUID; } /** * Returns a string representation of the split which can be parsed again using {@link org.gnucash.android.model.Split#parseSplit(String)} + *

The string is formatted as:
+ * "<uid>;<valueNum>;<valueDenom>;<valueCurrencyCode>;<quantityNum>;<quantityDenom>;<quantityCurrencyCode>;<transaction_uid>;<account_uid>;<type>;<memo>" + *

+ *

Only the memo field is allowed to be null

* @return the converted CSV string of this split */ public String toCsv(){ String sep = ";"; - String splitString = mAmount.asString() + sep + mAmount.getCurrency().getCurrencyCode() - + sep + mAccountUID + sep + mTransactionUID + sep + mSplitType.name(); + + String splitString = getUID() + sep + mValue.getNumerator() + sep + mValue.getDenominator() + sep + mValue.getCurrency().getCurrencyCode() + sep + + mQuantity.getNumerator() + sep + mQuantity.getDenominator() + sep + mQuantity.getCurrency().getCurrencyCode() + + sep + mTransactionUID + sep + mAccountUID + sep + mSplitType.name(); if (mMemo != null){ - splitString = splitString + ";" + mMemo; + splitString = splitString + sep + mMemo; } return splitString; } /** - * Parses a split which is in the format ";;;;". + * Parses a split which is in the format:
+ * ";;;;;;;;;;". + *

Also supports parsing of the deprecated format ";;;;;". * The split input string is the same produced by the {@link Split#toCsv()} method - * - * @param splitString String containing formatted split + *

+ * @param splitCsvString String containing formatted split * @return Split instance parsed from the string */ - public static Split parseSplit(String splitString) { - String[] tokens = splitString.split(";"); - Money amount = new Money(tokens[0], tokens[1]); - Split split = new Split(amount, tokens[2]); - split.setTransactionUID(tokens[3]); - split.setType(TransactionType.valueOf(tokens[4])); - if (tokens.length == 6){ - split.setMemo(tokens[5]); + public static Split parseSplit(String splitCsvString) { + String[] tokens = splitCsvString.split(";"); + if (tokens.length < 8) { //old format splits + Money amount = new Money(tokens[0], tokens[1]); + Split split = new Split(amount, tokens[2]); + split.setTransactionUID(tokens[3]); + split.setType(TransactionType.valueOf(tokens[4])); + if (tokens.length == 6) { + split.setMemo(tokens[5]); + } + return split; + } else { + int valueNum = Integer.parseInt(tokens[1]); + int valueDenom = Integer.parseInt(tokens[2]); + String valueCurrencyCode = tokens[3]; + int quantityNum = Integer.parseInt(tokens[4]); + int quantityDenom = Integer.parseInt(tokens[5]); + String qtyCurrencyCode = tokens[6]; + + Money value = new Money(valueNum, valueDenom, valueCurrencyCode); + Money quantity = new Money(quantityNum, quantityDenom, qtyCurrencyCode); + + Split split = new Split(value, tokens[8]); + split.setUID(tokens[0]); + split.setQuantity(quantity); + split.setTransactionUID(tokens[7]); + split.setType(TransactionType.valueOf(tokens[9])); + if (tokens.length == 11) { + split.setMemo(tokens[10]); + } + return split; } - return split; } } diff --git a/app/src/main/java/org/gnucash/android/model/Transaction.java b/app/src/main/java/org/gnucash/android/model/Transaction.java index 00c9368b0..f6fc9beaa 100644 --- a/app/src/main/java/org/gnucash/android/model/Transaction.java +++ b/app/src/main/java/org/gnucash/android/model/Transaction.java @@ -19,7 +19,9 @@ import android.content.Intent; import org.gnucash.android.BuildConfig; +import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.CommoditiesDbAdapter; import org.gnucash.android.export.ofx.OfxHelper; import org.gnucash.android.model.Account.OfxAccountType; import org.w3c.dom.Document; @@ -85,10 +87,15 @@ public class Transaction extends BaseModel{ */ private String mCurrencyCode = Money.DEFAULT_CURRENCY_CODE; + /** + * GUID of commodity associated with this transaction + */ + private String mCommodityUID; + /** * The splits making up this transaction */ - private List mSplitList = new ArrayList(); + private List mSplitList = new ArrayList<>(); /** * Name describing the transaction @@ -161,6 +168,7 @@ public Transaction(Transaction transaction, boolean generateNewUID){ * Initializes the different fields to their default values. */ private void initDefaults(){ + mCurrencyCode = Money.DEFAULT_CURRENCY_CODE; this.mTimestamp = System.currentTimeMillis(); } @@ -173,22 +181,6 @@ private void initDefaults(){ * @return Split whose amount is the imbalance of this transaction */ public Split getAutoBalanceSplit(){ - //FIXME: when multiple currencies per transaction are supported - Currency lastCurrency = null; - for (Split split : mSplitList) { - Currency currentCurrency = split.getAmount().getCurrency(); - if (lastCurrency == null) - lastCurrency = currentCurrency; - else if (lastCurrency != currentCurrency){ - return null; //for now we will not autobalance multi-currency transactions - } - lastCurrency = currentCurrency; - } - - //if all the splits are the same currency but the transaction is another - if (!lastCurrency.getCurrencyCode().equals(mCurrencyCode)) - return null; - Money imbalance = getImbalance(); if (!imbalance.isAmountZero()){ Currency currency = Currency.getInstance(mCurrencyCode); @@ -214,7 +206,7 @@ public List getSplits(){ * @return List of {@link org.gnucash.android.model.Split}s */ public List getSplits(String accountUID){ - List splits = new ArrayList(); + List splits = new ArrayList<>(); for (Split split : mSplitList) { if (split.getAccountUID().equals(accountUID)){ splits.add(split); @@ -266,10 +258,11 @@ public Money getBalance(String accountUID){ public Money getImbalance(){ Money imbalance = Money.createZeroInstance(mCurrencyCode); for (Split split : mSplitList) { - //TODO: Handle this better when multi-currency support is introduced - if (!split.getAmount().getCurrency().getCurrencyCode().equals(mCurrencyCode)) - return Money.createZeroInstance(mCurrencyCode); //abort - Money amount = split.getAmount().absolute(); + if (!split.getValue().getCurrency().getCurrencyCode().equals(mCurrencyCode)) { + // values in transactions are always in the same currency + throw new RuntimeException("Splits values in transaction are not in the same currency"); + } + Money amount = split.getValue().absolute(); if (split.getType() == TransactionType.DEBIT) imbalance = imbalance.subtract(amount); else @@ -291,13 +284,19 @@ public static Money computeBalance(String accountUID, List splitList) { AccountsDbAdapter accountsDbAdapter = AccountsDbAdapter.getInstance(); AccountType accountType = accountsDbAdapter.getAccountType(accountUID); String currencyCode = accountsDbAdapter.getAccountCurrencyCode(accountUID); + Currency accountCurrency = Currency.getInstance(currencyCode); boolean isDebitAccount = accountType.hasDebitNormalBalance(); Money balance = Money.createZeroInstance(currencyCode); for (Split split : splitList) { if (!split.getAccountUID().equals(accountUID)) continue; - Money absAmount = split.getAmount().absolute().withCurrency(Currency.getInstance(currencyCode)); + Money absAmount; + if (split.getValue().getCurrency() == accountCurrency){ + absAmount = split.getValue().absolute(); + } else { //if this split belongs to the account, then either its value or quantity is in the account currency + absAmount = split.getQuantity().absolute(); + } boolean isDebitSplit = split.getType() == TransactionType.DEBIT; if (isDebitAccount) { if (isDebitSplit) { @@ -343,7 +342,23 @@ public Currency getCurrency(){ return Currency.getInstance(this.mCurrencyCode); } - /** + /** + * Returns the GUID of the commodity for this transaction + * @return GUID of commodity + */ + public String getCommodityUID() { + return mCommodityUID; + } + + /** + * Sets the commodity for this transaction + * @param commodityUID GUID of commodity + */ + public void setCommodityUID(String commodityUID) { + this.mCommodityUID = commodityUID; + } + + /** * Returns the description of the transaction * @return Transaction description */ diff --git a/app/src/main/java/org/gnucash/android/receivers/AccountCreator.java b/app/src/main/java/org/gnucash/android/receivers/AccountCreator.java index 8f0c172f4..3985e8504 100644 --- a/app/src/main/java/org/gnucash/android/receivers/AccountCreator.java +++ b/app/src/main/java/org/gnucash/android/receivers/AccountCreator.java @@ -57,7 +57,7 @@ public void onReceive(Context context, Intent intent) { if (uid != null) account.setUID(uid); - AccountsDbAdapter.getInstance().addAccount(account); + AccountsDbAdapter.getInstance().addRecord(account); } } diff --git a/app/src/main/java/org/gnucash/android/receivers/TransactionAppWidgetProvider.java b/app/src/main/java/org/gnucash/android/receivers/TransactionAppWidgetProvider.java index 34974ff8c..053c2a0f2 100644 --- a/app/src/main/java/org/gnucash/android/receivers/TransactionAppWidgetProvider.java +++ b/app/src/main/java/org/gnucash/android/receivers/TransactionAppWidgetProvider.java @@ -21,8 +21,8 @@ import android.content.SharedPreferences.Editor; import android.preference.PreferenceManager; -import org.gnucash.android.ui.UxArgument; -import org.gnucash.android.ui.widget.WidgetConfigurationActivity; +import org.gnucash.android.ui.common.UxArgument; +import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; /** * {@link AppWidgetProvider} which is responsible for managing widgets on the homescreen diff --git a/app/src/main/java/org/gnucash/android/receivers/TransactionRecorder.java b/app/src/main/java/org/gnucash/android/receivers/TransactionRecorder.java index 99c826d61..3e192af09 100644 --- a/app/src/main/java/org/gnucash/android/receivers/TransactionRecorder.java +++ b/app/src/main/java/org/gnucash/android/receivers/TransactionRecorder.java @@ -30,12 +30,13 @@ import org.gnucash.android.model.Split; import org.gnucash.android.model.Transaction; import org.gnucash.android.model.TransactionType; -import org.gnucash.android.ui.widget.WidgetConfigurationActivity; +import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; import java.math.BigDecimal; +import java.math.MathContext; import java.util.Currency; /** @@ -72,6 +73,7 @@ public void onReceive(Context context, Intent intent) { if (accountUID != null) { TransactionType type = TransactionType.valueOf(args.getString(Transaction.EXTRA_TRANSACTION_TYPE)); BigDecimal amountBigDecimal = (BigDecimal) args.getSerializable(Transaction.EXTRA_AMOUNT); + amountBigDecimal = amountBigDecimal.setScale(Currency.getInstance(currencyCode).getDefaultFractionDigits(), BigDecimal.ROUND_HALF_EVEN).round(MathContext.DECIMAL128); Money amount = new Money(amountBigDecimal, Currency.getInstance(currencyCode)); Split split = new Split(amount.absolute(), accountUID); split.setType(type); @@ -98,7 +100,7 @@ public void onReceive(Context context, Intent intent) { } } - TransactionsDbAdapter.getInstance().addTransaction(transaction); + TransactionsDbAdapter.getInstance().addRecord(transaction); WidgetConfigurationActivity.updateAllWidgets(context); } diff --git a/app/src/main/java/org/gnucash/android/service/SchedulerService.java b/app/src/main/java/org/gnucash/android/service/SchedulerService.java index aa85d9857..232f48c6f 100644 --- a/app/src/main/java/org/gnucash/android/service/SchedulerService.java +++ b/app/src/main/java/org/gnucash/android/service/SchedulerService.java @@ -99,7 +99,7 @@ private void executeScheduledEvent(ScheduledAction scheduledAction){ case TRANSACTION: String eventUID = scheduledAction.getActionUID(); TransactionsDbAdapter transactionsDbAdapter = TransactionsDbAdapter.getInstance(); - Transaction trxnTemplate = transactionsDbAdapter.getTransaction(eventUID); + Transaction trxnTemplate = transactionsDbAdapter.getRecord(eventUID); Transaction recurringTrxn = new Transaction(trxnTemplate, true); //we may be executing scheduled action significantly after scheduled time (depending on when Android fires the alarm) @@ -112,7 +112,7 @@ private void executeScheduledEvent(ScheduledAction scheduledAction){ } recurringTrxn.setTime(transactionTime); recurringTrxn.setCreatedTimestamp(new Timestamp(transactionTime)); - transactionsDbAdapter.addTransaction(recurringTrxn); + transactionsDbAdapter.addRecord(recurringTrxn); break; case BACKUP: diff --git a/app/src/main/java/org/gnucash/android/ui/BaseDrawerActivity.java b/app/src/main/java/org/gnucash/android/ui/BaseDrawerActivity.java deleted file mode 100644 index c6e1aa9f1..000000000 --- a/app/src/main/java/org/gnucash/android/ui/BaseDrawerActivity.java +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Copyright (c) 2015 Ngewi Fet - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.gnucash.android.ui; - -import android.app.Activity; -import android.content.Intent; -import android.content.res.Configuration; -import android.os.Bundle; -import android.support.v4.app.ActionBarDrawerToggle; -import android.support.v4.widget.DrawerLayout; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.Toast; - -import com.actionbarsherlock.app.SherlockFragmentActivity; -import com.actionbarsherlock.view.MenuItem; -import com.commonsware.cwac.merge.MergeAdapter; -import com.crashlytics.android.Crashlytics; - -import org.gnucash.android.R; -import org.gnucash.android.export.xml.GncXmlExporter; -import org.gnucash.android.importer.ImportAsyncTask; -import org.gnucash.android.ui.account.AccountsActivity; -import org.gnucash.android.ui.chart.ChartReportActivity; -import org.gnucash.android.ui.settings.SettingsActivity; -import org.gnucash.android.ui.transaction.ScheduledActionsActivity; - -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.util.ArrayList; - - -/** - * Base activity implementing the navigation drawer, to be extended by all activities requiring one - * - * @author Ngewi Fet - */ -public class BaseDrawerActivity extends SherlockFragmentActivity { - protected DrawerLayout mDrawerLayout; - protected ListView mDrawerList; - - protected CharSequence mTitle; - private ActionBarDrawerToggle mDrawerToggle; - - private class DrawerItemClickListener implements ListView.OnItemClickListener { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - selectItem(position); - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); - mDrawerList = (ListView) findViewById(R.id.left_drawer); - - MergeAdapter mergeAdapter = createNavDrawerMergeAdapter(); - - mDrawerList.setAdapter(mergeAdapter); - mDrawerList.setOnItemClickListener(new DrawerItemClickListener()); - - //FIXME: Migrate to the non-deprecated version when we remove ActionBarSherlock and support only API level 15 and above - mDrawerToggle = new ActionBarDrawerToggle( - this, /* host Activity */ - mDrawerLayout, /* DrawerLayout object */ - R.drawable.ic_drawer, /* nav drawer icon to replace 'Up' caret */ - R.string.drawer_open, /* "open drawer" description */ - R.string.drawer_close /* "close drawer" description */ - ) { - - /** Called when a drawer has settled in a completely closed state. */ - public void onDrawerClosed(View view) { - super.onDrawerClosed(view); - } - - /** Called when a drawer has settled in a completely open state. */ - public void onDrawerOpened(View drawerView) { - super.onDrawerOpened(drawerView); - getSupportActionBar().setTitle("GnuCash"); - } - }; - - mDrawerLayout.setDrawerListener(mDrawerToggle); - getSupportActionBar().setHomeButtonEnabled(true); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - - private MergeAdapter createNavDrawerMergeAdapter() { - //TODO: Localize nav drawer entries when features are finalized - ArrayList accountNavOptions = new ArrayList<>(); - accountNavOptions.add(getString(R.string.nav_menu_open)); - accountNavOptions.add(getString(R.string.nav_menu_favorites)); - accountNavOptions.add(getString(R.string.nav_menu_reports)); - - ArrayAdapter accountsNavAdapter = new ArrayAdapter<>(this, - R.layout.drawer_list_item, accountNavOptions); - - int titleColorGreen = getResources().getColor(R.color.title_green); - - ArrayList transactionsNavOptions = new ArrayList<>(); - transactionsNavOptions.add(getString(R.string.nav_menu_scheduled_transactions)); - transactionsNavOptions.add(getString(R.string.nav_menu_export)); - - ArrayAdapter transactionsNavAdapter = new ArrayAdapter<>(this, - R.layout.drawer_list_item, transactionsNavOptions); - - LayoutInflater inflater = getLayoutInflater(); - TextView accountHeader = (TextView) inflater.inflate(R.layout.drawer_section_header, null); - accountHeader.setText(R.string.title_accounts); - accountHeader.setTextColor(titleColorGreen); - - TextView transactionHeader = (TextView) inflater.inflate(R.layout.drawer_section_header, null); - transactionHeader.setText(R.string.title_transactions); - transactionHeader.setTextColor(titleColorGreen); - MergeAdapter mergeAdapter = new MergeAdapter(); - mergeAdapter.addView(accountHeader); - mergeAdapter.addAdapter(accountsNavAdapter); - mergeAdapter.addView(transactionHeader); - mergeAdapter.addAdapter(transactionsNavAdapter); - - mergeAdapter.addView(inflater.inflate(R.layout.horizontal_line, null)); - TextView settingsHeader = (TextView) inflater.inflate(R.layout.drawer_section_header, null); - settingsHeader.setText(R.string.title_settings); - settingsHeader.setTextColor(titleColorGreen); - - ArrayList aboutNavOptions = new ArrayList<>(); - aboutNavOptions.add(getString(R.string.nav_menu_scheduled_backups)); - aboutNavOptions.add(getString(R.string.nav_menu_settings)); - //TODO: add help view - ArrayAdapter aboutNavAdapter = new ArrayAdapter<>(this, - R.layout.drawer_list_item, aboutNavOptions); - - mergeAdapter.addView(settingsHeader); - mergeAdapter.addAdapter(aboutNavAdapter); - return mergeAdapter; - } - - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - mDrawerToggle.syncState(); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - mDrawerToggle.onConfigurationChanged(newConfig); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (!mDrawerLayout.isDrawerOpen(mDrawerList)) - mDrawerLayout.openDrawer(mDrawerList); - else - mDrawerLayout.closeDrawer(mDrawerList); - - return super.onOptionsItemSelected(item); - } - - /** - * Handler for the navigation drawer items - * */ - protected void selectItem(int position) { - switch (position){ - case 1: { //Open... files - Intent pickIntent = new Intent(Intent.ACTION_GET_CONTENT); - pickIntent.setType("application/*"); - Intent chooser = Intent.createChooser(pickIntent, getString(R.string.title_select_gnucash_xml_file)); - - startActivityForResult(chooser, AccountsActivity.REQUEST_PICK_ACCOUNTS_FILE); - } - break; - - case 2: { //favorite accounts - Intent intent = new Intent(this, AccountsActivity.class); - intent.putExtra(AccountsActivity.EXTRA_TAB_INDEX, - AccountsActivity.INDEX_FAVORITE_ACCOUNTS_FRAGMENT); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_SINGLE_TOP); - startActivity(intent); - } - break; - - case 3: - startActivity(new Intent(this, ChartReportActivity.class)); - break; - - case 5: { //show scheduled transactions - Intent intent = new Intent(this, ScheduledActionsActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_SINGLE_TOP); - intent.putExtra(ScheduledActionsActivity.EXTRA_DISPLAY_MODE, - ScheduledActionsActivity.DisplayMode.TRANSACTION_ACTIONS); - startActivity(intent); - } - break; - - case 6:{ - AccountsActivity.showExportDialog(this); - } - break; - - case 9: //scheduled backup - Intent intent = new Intent(this, ScheduledActionsActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_SINGLE_TOP); - intent.putExtra(ScheduledActionsActivity.EXTRA_DISPLAY_MODE, - ScheduledActionsActivity.DisplayMode.EXPORT_ACTIONS); - startActivity(intent); - break; - - case 10: //Settings activity - startActivity(new Intent(this, SettingsActivity.class)); - break; - - //TODO: add help option - } - - // Highlight the selected item, update the title, and close the drawer - mDrawerList.setItemChecked(position, true); -// setTitle(mNavDrawerEntries[position]); - mDrawerLayout.closeDrawer(mDrawerList); - } - - @Override - public void setTitle(CharSequence title) { - mTitle = title; - getSupportActionBar().setTitle(mTitle); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (resultCode == Activity.RESULT_CANCELED){ - return; - } - - switch (requestCode) { - case AccountsActivity.REQUEST_PICK_ACCOUNTS_FILE: - try { - GncXmlExporter.createBackup(); - InputStream accountInputStream = getContentResolver().openInputStream(data.getData()); - new ImportAsyncTask(this).execute(accountInputStream); - } catch (FileNotFoundException e) { - Crashlytics.logException(e); - Toast.makeText(this, R.string.toast_error_importing_accounts, Toast.LENGTH_SHORT).show(); - } - break; - } - } -} diff --git a/app/src/main/java/org/gnucash/android/ui/account/AccountFormFragment.java b/app/src/main/java/org/gnucash/android/ui/account/AccountFormFragment.java index 5d583bdd9..c5cca688a 100644 --- a/app/src/main/java/org/gnucash/android/ui/account/AccountFormFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/account/AccountFormFragment.java @@ -27,13 +27,23 @@ import android.graphics.Color; import android.os.Bundle; import android.preference.PreferenceManager; +import android.support.design.widget.TextInputLayout; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.widget.SimpleCursorAdapter; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.text.Editable; import android.text.TextUtils; +import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.ArrayAdapter; @@ -42,23 +52,20 @@ import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.EditText; import android.widget.Spinner; -import android.widget.Toast; - -import com.actionbarsherlock.app.SherlockFragment; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuInflater; -import com.actionbarsherlock.view.MenuItem; import org.gnucash.android.R; import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.CommoditiesDbAdapter; import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.model.Account; import org.gnucash.android.model.AccountType; +import org.gnucash.android.model.Commodity; import org.gnucash.android.model.Money; -import org.gnucash.android.ui.UxArgument; +import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.colorpicker.ColorPickerDialog; import org.gnucash.android.ui.colorpicker.ColorPickerSwatch; import org.gnucash.android.ui.colorpicker.ColorSquare; +import org.gnucash.android.util.CommoditiesCursorAdapter; import org.gnucash.android.util.QualifiedAccountNameCursorAdapter; import java.util.ArrayList; @@ -67,12 +74,15 @@ import java.util.HashMap; import java.util.List; +import butterknife.Bind; +import butterknife.ButterKnife; + /** * Fragment used for creating and editing accounts * @author Ngewi Fet * @author Yongxin Wang */ -public class AccountFormFragment extends SherlockFragment { +public class AccountFormFragment extends Fragment { /** * Tag for the color picker dialog fragment @@ -82,13 +92,15 @@ public class AccountFormFragment extends SherlockFragment { /** * EditText for the name of the account to be created/edited */ - private EditText mNameEditText; - + @Bind(R.id.input_account_name) EditText mNameEditText; + + @Bind(R.id.name_text_input_layout) TextInputLayout mTextInputLayout; + /** * Spinner for selecting the currency of the account * Currencies listed are those specified by ISO 4217 */ - private Spinner mCurrencySpinner; + @Bind(R.id.input_currency_spinner) Spinner mCurrencySpinner; /** * Accounts database adapter @@ -148,34 +160,39 @@ public class AccountFormFragment extends SherlockFragment { /** * Spinner for parent account list */ - private Spinner mParentAccountSpinner; + @Bind(R.id.input_parent_account) Spinner mParentAccountSpinner; /** * Checkbox which activates the parent account spinner when selected * Leaving this unchecked means it is a top-level root account */ - private CheckBox mParentCheckBox; + @Bind(R.id.checkbox_parent_account) CheckBox mParentCheckBox; /** * Spinner for the account type * @see org.gnucash.android.model.AccountType */ - private Spinner mAccountTypeSpinner; + @Bind(R.id.input_account_type_spinner) Spinner mAccountTypeSpinner; /** * Checkbox for activating the default transfer account spinner */ - private CheckBox mDefaultTransferAccountCheckBox; + @Bind(R.id.checkbox_default_transfer_account) CheckBox mDefaultTransferAccountCheckBox; /** * Spinner for selecting the default transfer account */ - private Spinner mDefaulTransferAccountSpinner; + @Bind(R.id.input_default_transfer_account) Spinner mDefaulTransferAccountSpinner; + + /** + * Account description input text view + */ + @Bind(R.id.input_account_description) EditText mDescriptionEditText; /** * Checkbox indicating if account is a placeholder account */ - private CheckBox mPlaceholderCheckBox; + @Bind(R.id.checkbox_placeholder_account) CheckBox mPlaceholderCheckBox; /** * Cursor adapter which binds to the spinner for default transfer account @@ -195,7 +212,7 @@ public class AccountFormFragment extends SherlockFragment { /** * Trigger for color picker dialog */ - private ColorSquare mColorSquare; + @Bind(R.id.input_color_picker) ColorSquare mColorSquare; private ColorPickerSwatch.OnColorSelectedListener mColorSelectedListener = new ColorPickerSwatch.OnColorSelectedListener() { @Override @@ -239,13 +256,28 @@ public void onCreate(Bundle savedInstanceState) { */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_new_account, container, false); - getSherlockActivity().getSupportActionBar().setTitle(R.string.label_create_account); - mCurrencySpinner = (Spinner) view.findViewById(R.id.input_currency_spinner); - mNameEditText = (EditText) view.findViewById(R.id.input_account_name); - //mNameEditText.requestFocus(); + View view = inflater.inflate(R.layout.fragment_account_form, container, false); + ButterKnife.bind(this, view); + + mNameEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + //nothing to see here, move along + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + //nothing to see here, move along + } + + @Override + public void afterTextChanged(Editable s) { + if (s.toString().length() > 0) { + mTextInputLayout.setErrorEnabled(false); + } + } + }); - mAccountTypeSpinner = (Spinner) view.findViewById(R.id.input_account_type_spinner); mAccountTypeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parentView, View selectedItemView, int position, long id) { @@ -260,24 +292,18 @@ public void onNothingSelected(AdapterView adapterView) { } }); - mPlaceholderCheckBox = (CheckBox) view.findViewById(R.id.checkbox_placeholder_account); - mParentAccountSpinner = (Spinner) view.findViewById(R.id.input_parent_account); mParentAccountSpinner.setEnabled(false); - mParentCheckBox = (CheckBox) view.findViewById(R.id.checkbox_parent_account); mParentCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() { - - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - mParentAccountSpinner.setEnabled(isChecked); - } - }); - mDefaulTransferAccountSpinner = (Spinner) view.findViewById(R.id.input_default_transfer_account); - mDefaulTransferAccountSpinner.setEnabled(false); + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + mParentAccountSpinner.setEnabled(isChecked); + } + }); - mDefaultTransferAccountCheckBox = (CheckBox) view.findViewById(R.id.checkbox_default_transfer_account); + mDefaulTransferAccountSpinner.setEnabled(false); mDefaultTransferAccountCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { @@ -285,7 +311,6 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { } }); - mColorSquare = (ColorSquare) view.findViewById(R.id.input_color_picker); mColorSquare.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { @@ -303,19 +328,23 @@ public void onClick(View view) { @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - - ArrayAdapter currencyArrayAdapter = new ArrayAdapter( - getActivity(), - android.R.layout.simple_spinner_item, - getResources().getStringArray(R.array.currency_names)); - currencyArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - mCurrencySpinner.setAdapter(currencyArrayAdapter); + + Cursor cursor = CommoditiesDbAdapter.getInstance().fetchAllRecords(); + CommoditiesCursorAdapter commoditiesAdapter = new CommoditiesCursorAdapter( + getActivity(), android.R.layout.simple_spinner_item); + commoditiesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + + mCurrencySpinner.setAdapter(commoditiesAdapter); + mAccountUID = getArguments().getString(UxArgument.SELECTED_ACCOUNT_UID); + ActionBar supportActionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); if (mAccountUID != null) { - mAccount = mAccountsDbAdapter.getAccount(mAccountUID); - getSherlockActivity().getSupportActionBar().setTitle(R.string.title_edit_account); + mAccount = mAccountsDbAdapter.getRecord(mAccountUID); + supportActionBar.setTitle(R.string.title_edit_account); + } else { + supportActionBar.setTitle(R.string.title_create_account); } mRootAccountUID = mAccountsDbAdapter.getOrCreateGnuCashRootAccountUID(); @@ -329,11 +358,12 @@ public void onActivityCreated(Bundle savedInstanceState) { if (mAccount != null){ initializeViewsWithAccount(mAccount); + //do not immediately open the keyboard when editing an account + getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); } else { initializeViews(); } - } /** @@ -361,14 +391,33 @@ private void initializeViewsWithAccount(Account account){ if (mAccountsDbAdapter.getTransactionMaxSplitNum(mAccount.getUID()) > 1) { + //TODO: Allow changing the currency and effecting the change for all transactions without any currency exchange (purely cosmetic change) mCurrencySpinner.setEnabled(false); } mNameEditText.setText(account.getName()); - - if (mUseDoubleEntry && account.getDefaultTransferAccountUID() != null) { - long doubleDefaultAccountId = mAccountsDbAdapter.getID(account.getDefaultTransferAccountUID()); - setDefaultTransferAccountSelection(doubleDefaultAccountId); + mNameEditText.setSelection(mNameEditText.getText().length()); + + if (account.getDescription() != null) + mDescriptionEditText.setText(account.getDescription()); + + if (mUseDoubleEntry) { + if (account.getDefaultTransferAccountUID() != null) { + long doubleDefaultAccountId = mAccountsDbAdapter.getID(account.getDefaultTransferAccountUID()); + setDefaultTransferAccountSelection(doubleDefaultAccountId, true); + } else { + String currentAccountUID = account.getParentUID(); + long defaultTransferAccountID = 0; + String rootAccountUID = mAccountsDbAdapter.getOrCreateGnuCashRootAccountUID(); + while (!currentAccountUID.equals(rootAccountUID)) { + defaultTransferAccountID = mAccountsDbAdapter.getDefaultTransferAccountID(mAccountsDbAdapter.getID(currentAccountUID)); + if (defaultTransferAccountID > 0) { + setDefaultTransferAccountSelection(defaultTransferAccountID, false); + break; //we found a parent with default transfer setting + } + currentAccountUID = mAccountsDbAdapter.getParentAccountUID(currentAccountUID); + } + } } mPlaceholderCheckBox.setChecked(account.isPlaceholderAccount()); @@ -433,10 +482,15 @@ private void setDefaultTransferAccountInputsVisible(boolean visible) { * @param currencyCode ISO 4217 currency code to be selected */ private void setSelectedCurrency(String currencyCode){ - mCurrencyCodes = Arrays.asList(getResources().getStringArray(R.array.key_currency_codes)); - if (mCurrencyCodes.contains(currencyCode)){ - mCurrencySpinner.setSelection(mCurrencyCodes.indexOf(currencyCode)); + CommoditiesDbAdapter commodityDbAdapter = CommoditiesDbAdapter.getInstance(); + long commodityId = commodityDbAdapter.getID(commodityDbAdapter.getCommodityUID(currencyCode)); + int position = 0; + for (int i = 0; i < mCurrencySpinner.getCount(); i++) { + if (commodityId == mCurrencySpinner.getItemIdAtPosition(i)) { + position = i; + } } + mCurrencySpinner.setSelection(position); } /** @@ -462,15 +516,15 @@ private void setParentAccountSelection(long parentAccountId){ * Selects the account with ID parentAccountId in the default transfer account spinner * @param defaultTransferAccountId Record ID of parent account to be selected */ - private void setDefaultTransferAccountSelection(long defaultTransferAccountId){ - if (defaultTransferAccountId > 0){ - mDefaultTransferAccountCheckBox.setChecked(true); - mDefaulTransferAccountSpinner.setEnabled(true); + private void setDefaultTransferAccountSelection(long defaultTransferAccountId, boolean enableTransferAccount) { + if (defaultTransferAccountId > 0) { + mDefaultTransferAccountCheckBox.setChecked(enableTransferAccount); + mDefaulTransferAccountSpinner.setEnabled(enableTransferAccount); } else return; for (int pos = 0; pos < mDefaultTransferAccountCursorAdapter.getCount(); pos++) { - if (mDefaultTransferAccountCursorAdapter.getItemId(pos) == defaultTransferAccountId){ + if (mDefaultTransferAccountCursorAdapter.getItemId(pos) == defaultTransferAccountId) { mDefaulTransferAccountSpinner.setSelection(pos); break; } @@ -514,7 +568,7 @@ private void showColorPickerDialog(){ } @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.default_save_actions, menu); } @@ -526,7 +580,7 @@ public boolean onOptionsItemSelected(MenuItem item) { saveAccount(); return true; - case R.id.menu_cancel: + case android.R.id.home: finishFragment(); return true; } @@ -551,9 +605,7 @@ private void loadDefaultTransferAccountList(){ } mDefaultTransferAccountCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), - android.R.layout.simple_spinner_item, defaultTransferAccountCursor); - mDefaultTransferAccountCursorAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); mDefaulTransferAccountSpinner.setAdapter(mDefaultTransferAccountCursorAdapter); } @@ -569,7 +621,7 @@ private void loadParentAccountList(AccountType accountType){ if (mAccount != null){ //if editing an account mDescendantAccountUIDs = mAccountsDbAdapter.getDescendantAccountUIDs(mAccount.getUID(), null, null); String rootAccountUID = mAccountsDbAdapter.getOrCreateGnuCashRootAccountUID(); - List descendantAccountUIDs = new ArrayList(mDescendantAccountUIDs); + List descendantAccountUIDs = new ArrayList<>(mDescendantAccountUIDs); if (rootAccountUID != null) descendantAccountUIDs.add(rootAccountUID); // limit cyclic account hierarchies. @@ -594,10 +646,7 @@ private void loadParentAccountList(AccountType accountType){ } mParentAccountCursorAdapter = new QualifiedAccountNameCursorAdapter( - getActivity(), - android.R.layout.simple_spinner_item, - mParentAccountCursor); - mParentAccountCursorAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + getActivity(), mParentAccountCursor); mParentAccountSpinner.setAdapter(mParentAccountCursorAdapter); } @@ -675,7 +724,7 @@ private void loadAccountTypesList(){ * Depends on how the fragment was loaded, it might have a backstack or not */ private void finishFragment() { - InputMethodManager imm = (InputMethodManager) getSherlockActivity().getSystemService( + InputMethodManager imm = (InputMethodManager) getActivity().getSystemService( Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(mNameEditText.getWindowToken(), 0); @@ -684,7 +733,7 @@ private void finishFragment() { getActivity().setResult(Activity.RESULT_OK); getActivity().finish(); } else { - getSherlockActivity().getSupportFragmentManager().popBackStack(); + getActivity().getSupportFragmentManager().popBackStack(); } } @@ -709,9 +758,8 @@ private void saveAccount() { if (mAccount == null){ String name = getEnteredName(); if (name == null || name.length() == 0){ - Toast.makeText(getSherlockActivity(), - R.string.toast_no_account_name_entered, - Toast.LENGTH_LONG).show(); + mTextInputLayout.setErrorEnabled(true); + mTextInputLayout.setError(getString(R.string.toast_no_account_name_entered)); return; } mAccount = new Account(getEnteredName()); @@ -720,14 +768,16 @@ private void saveAccount() { nameChanged = !mAccount.getName().equals(getEnteredName()); mAccount.setName(getEnteredName()); } - - String curCode = mCurrencyCodes.get(mCurrencySpinner - .getSelectedItemPosition()); - mAccount.setCurrency(Currency.getInstance(curCode)); + + long commodityId = mCurrencySpinner.getSelectedItemId(); + Commodity commodity = CommoditiesDbAdapter.getInstance().getRecord(commodityId); + mAccount.setCommodityUID(commodity.getUID()); + mAccount.setCurrency(Currency.getInstance(commodity.getMnemonic())); AccountType selectedAccountType = getSelectedAccountType(); mAccount.setAccountType(selectedAccountType); + mAccount.setDescription(mDescriptionEditText.getText().toString()); mAccount.setPlaceHolderFlag(mPlaceholderCheckBox.isChecked()); mAccount.setColorCode(mSelectedColor); @@ -799,7 +849,7 @@ private void saveAccount() { if (mAccountsDbAdapter == null) mAccountsDbAdapter = AccountsDbAdapter.getInstance(); // bulk update, will not update transactions - mAccountsDbAdapter.bulkAddAccounts(accountsToUpdate); + mAccountsDbAdapter.bulkAddRecords(accountsToUpdate); finishFragment(); } 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 f0660c74c..a9a260c97 100644 --- a/app/src/main/java/org/gnucash/android/ui/account/AccountsActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/account/AccountsActivity.java @@ -17,63 +17,70 @@ package org.gnucash.android.ui.account; +import android.Manifest; +import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; -import android.support.v4.app.DialogFragment; +import android.support.design.widget.CoordinatorLayout; +import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.Snackbar; +import android.support.design.widget.TabLayout; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentStatePagerAdapter; -import android.support.v4.app.FragmentTransaction; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; +import android.support.v7.widget.Toolbar; import android.util.Log; import android.util.SparseArray; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.ArrayAdapter; +import android.widget.Toast; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuInflater; -import com.actionbarsherlock.view.MenuItem; import com.crashlytics.android.Crashlytics; -import com.viewpagerindicator.TitlePageIndicator; +import com.kobakei.ratethisapp.RateThisApp; +import org.gnucash.android.BuildConfig; import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.db.AccountsDbAdapter; import org.gnucash.android.db.DatabaseSchema; -import org.gnucash.android.ui.export.ExportDialogFragment; import org.gnucash.android.export.xml.GncXmlExporter; import org.gnucash.android.importer.ImportAsyncTask; import org.gnucash.android.model.Money; -import org.gnucash.android.ui.UxArgument; -import org.gnucash.android.ui.chart.ChartReportActivity; -import org.gnucash.android.ui.passcode.PassLockActivity; -import org.gnucash.android.ui.settings.SettingsActivity; -import org.gnucash.android.ui.transaction.ScheduledActionsActivity; +import org.gnucash.android.ui.common.BaseDrawerActivity; +import org.gnucash.android.ui.common.FormActivity; +import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.transaction.TransactionsActivity; import org.gnucash.android.ui.util.OnAccountClickedListener; import org.gnucash.android.ui.util.Refreshable; import org.gnucash.android.ui.util.TaskDelegate; +import org.gnucash.android.ui.wizard.FirstRunWizardActivity; import java.io.FileNotFoundException; import java.io.InputStream; -import java.util.Arrays; -import java.util.Currency; -import java.util.List; -import java.util.Locale; +import java.util.ArrayList; + +import butterknife.Bind; +import butterknife.ButterKnife; /** * Manages actions related to accounts, displaying, exporting and creating new accounts @@ -82,7 +89,7 @@ * @author Ngewi Fet * @author Oleksandr Tyshkovets */ -public class AccountsActivity extends PassLockActivity implements OnAccountClickedListener { +public class AccountsActivity extends BaseDrawerActivity implements OnAccountClickedListener { /** * Request code for GnuCash account structure file to import @@ -95,16 +102,6 @@ public class AccountsActivity extends PassLockActivity implements OnAccountClick public static final int REQUEST_EDIT_ACCOUNT = 0x10; /** - * Tag used for identifying the account export fragment - */ - public static final String FRAGMENT_EXPORT_DIALOG = "export_fragment"; - - /** - * Tag for identifying the "New account" fragment - */ - protected static final String FRAGMENT_NEW_ACCOUNT = "new_account_dialog"; - - /** * Logging tag */ protected static final String LOG_TAG = "AccountsActivity"; @@ -138,6 +135,7 @@ public class AccountsActivity extends PassLockActivity implements OnAccountClick * 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 @@ -147,14 +145,14 @@ public class AccountsActivity extends PassLockActivity implements OnAccountClick /** * ViewPager which manages the different tabs */ - private ViewPager mPager; - - /** - * Dialog which is shown to the user on first start prompting the user to create some accounts - */ - private AlertDialog mDefaultAccountsDialog; - private TitlePageIndicator mTitlePageIndicator; + @Bind(R.id.pager) ViewPager mViewPager; + @Bind(R.id.fab_create_account) FloatingActionButton mFloatingActionButton; + @Bind(R.id.coordinatorLayout) CoordinatorLayout mCoordinatorLayout; + /** + * Configuration for rating the app + */ + public static RateThisApp.Config rateAppConfig = new RateThisApp.Config(14, 30);; /** * Adapter for managing the sub-account and transaction fragment pages in the accounts view @@ -215,52 +213,81 @@ public int getCount() { } public AccountsListFragment getCurrentAccountListFragment(){ - int index = mPager.getCurrentItem(); + int index = mViewPager.getCurrentItem(); return (AccountsListFragment)(mFragmentPageReferenceMap.get(index)); } @Override public void onCreate(Bundle savedInstanceState) { - //it is necessary to set the view first before calling super because of the nav drawer in BaseDrawerActivity - setContentView(R.layout.activity_accounts); super.onCreate(savedInstanceState); + setContentView(R.layout.activity_accounts); + setUpDrawer(); + ButterKnife.bind(this); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); final Intent intent = getIntent(); handleOpenFileIntent(intent); init(); - mPager = (ViewPager) findViewById(R.id.pager); - mTitlePageIndicator = (TitlePageIndicator) findViewById(R.id.titles); - - String action = intent.getAction(); - if (action != null && action.equals(Intent.ACTION_INSERT_OR_EDIT)) { - //enter account creation/edit mode if that was specified - mPager.setVisibility(View.GONE); - mTitlePageIndicator.setVisibility(View.GONE); - - String accountUID = intent.getStringExtra(UxArgument.SELECTED_ACCOUNT_UID); - if (accountUID != null) - showEditAccountFragment(accountUID); - else { - String parentAccountUID = intent.getStringExtra(UxArgument.PARENT_ACCOUNT_UID); - showAddAccountFragment(parentAccountUID); + TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout); + tabLayout.addTab(tabLayout.newTab().setText(R.string.title_recent_accounts)); + tabLayout.addTab(tabLayout.newTab().setText(R.string.title_all_accounts)); + tabLayout.addTab(tabLayout.newTab().setText(R.string.title_favorite_accounts)); + tabLayout.setTabGravity(TabLayout.GRAVITY_FILL); + + //show the simple accounts list + PagerAdapter mPagerAdapter = new AccountViewPagerAdapter(getSupportFragmentManager()); + mViewPager.setAdapter(mPagerAdapter); + + mViewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout)); + tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + mViewPager.setCurrentItem(tab.getPosition()); } - } else { - //show the simple accounts list - PagerAdapter mPagerAdapter = new AccountViewPagerAdapter(getSupportFragmentManager()); - mPager.setAdapter(mPagerAdapter); - mTitlePageIndicator.setViewPager(mPager); - - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - int lastTabIndex = preferences.getInt(LAST_OPEN_TAB_INDEX, INDEX_TOP_LEVEL_ACCOUNTS_FRAGMENT); - int index = intent.getIntExtra(EXTRA_TAB_INDEX, lastTabIndex); - mPager.setCurrentItem(index); - } + @Override + public void onTabUnselected(TabLayout.Tab tab) { + + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + + } + }); + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + int lastTabIndex = preferences.getInt(LAST_OPEN_TAB_INDEX, INDEX_TOP_LEVEL_ACCOUNTS_FRAGMENT); + int index = intent.getIntExtra(EXTRA_TAB_INDEX, lastTabIndex); + mViewPager.setCurrentItem(index); + + mFloatingActionButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent addAccountIntent = new Intent(AccountsActivity.this, FormActivity.class); + addAccountIntent.setAction(Intent.ACTION_INSERT_OR_EDIT); + addAccountIntent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.ACCOUNT.name()); + startActivityForResult(addAccountIntent, AccountsActivity.REQUEST_EDIT_ACCOUNT); + } + }); } + @Override + protected void onStart() { + super.onStart(); + + if (BuildConfig.CAN_REQUEST_RATING) { + RateThisApp.init(rateAppConfig); + RateThisApp.onStart(this); + RateThisApp.showRateDialogIfNeeded(this); + } + } + /** * Handles the case where another application has selected to open a (.gnucash or .gnca) file with this app * @param intent Intent containing the data to be imported @@ -285,6 +312,50 @@ 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); @@ -299,7 +370,7 @@ protected void onNewIntent(Intent intent) { * @param index Index of fragment to be loaded */ public void setTab(int index){ - mPager.setCurrentItem(index); + mViewPager.setCurrentItem(index); } /** @@ -313,21 +384,18 @@ private void init() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); boolean firstRun = prefs.getBoolean(getString(R.string.key_first_run), true); + if (firstRun){ - showFirstRunDialog(); + startActivity(new Intent(this, FirstRunWizardActivity.class)); + //default to using double entry and save the preference explicitly prefs.edit().putBoolean(getString(R.string.key_use_double_entry), true).apply(); + } else { + getSDWritePermission(); } if (hasNewFeatures()){ - AlertDialog dialog = showWhatsNewDialog(this); - //TODO: remove this when we upgrade to 1.7.0. Users will already know the nav drawer then - dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { - @Override - public void onDismiss(DialogInterface dialog) { - mDrawerLayout.openDrawer(mDrawerList); - } - }); + showWhatsNewDialog(this); } GnuCashApplication.startScheduledActionExecutionService(this); } @@ -336,7 +404,7 @@ public void onDismiss(DialogInterface dialog) { protected void onDestroy() { super.onDestroy(); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - preferences.edit().putInt(LAST_OPEN_TAB_INDEX, mPager.getCurrentItem()).apply(); + preferences.edit().putInt(LAST_OPEN_TAB_INDEX, mViewPager.getCurrentItem()).apply(); } /** @@ -389,23 +457,15 @@ public void onClick(DialogInterface dialog, int which) { /** * Displays the dialog for exporting transactions */ - public static void showExportDialog(FragmentActivity activity) { - FragmentManager manager = activity.getSupportFragmentManager(); - FragmentTransaction ft = manager.beginTransaction(); - Fragment prev = manager.findFragmentByTag(FRAGMENT_EXPORT_DIALOG); - if (prev != null) { - ft.remove(prev); - } - ft.addToBackStack(null); - - // Create and show the dialog. - DialogFragment exportFragment = new ExportDialogFragment(); - exportFragment.show(ft, FRAGMENT_EXPORT_DIALOG); + public static void openExportFragment(FragmentActivity activity) { + Intent intent = new Intent(activity, FormActivity.class); + intent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.EXPORT.name()); + activity.startActivity(intent); } @Override public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getSupportMenuInflater(); + MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.global_actions, menu); return true; } @@ -416,179 +476,11 @@ public boolean onOptionsItemSelected(MenuItem item) { case android.R.id.home: return super.onOptionsItemSelected(item); - case R.id.menu_recurring_transactions: - Intent intent = new Intent(this, ScheduledActionsActivity.class); - intent.putExtra(ScheduledActionsActivity.EXTRA_DISPLAY_MODE, - ScheduledActionsActivity.DisplayMode.TRANSACTION_ACTIONS); - startActivity(intent); - return true; - - case R.id.menu_settings: - startActivity(new Intent(this, SettingsActivity.class)); - return true; - - case R.id.menu_reports: - startActivity(new Intent(this, ChartReportActivity.class)); - return true; - default: return false; } } - /** - * Creates an intent which can be used start activity for creating new account - * @return Intent which can be used to start activity for creating new account - */ - private Intent createNewAccountIntent(){ - Intent addAccountIntent = new Intent(this, AccountsActivity.class); - addAccountIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - addAccountIntent.setAction(Intent.ACTION_INSERT_OR_EDIT); - return addAccountIntent; - } - - /** - * Shows form fragment for creating a new account - * @param parentAccountUID GUID of the parent account present. Can be 0 for top-level account - */ - private void showAddAccountFragment(String parentAccountUID){ - Bundle args = new Bundle(); - args.putString(UxArgument.PARENT_ACCOUNT_UID, parentAccountUID); - showAccountFormFragment(args); - } - - /** - * Shows the form fragment for editing the account with record ID accountId - * @param accountUID GUID of the account to be edited - */ - private void showEditAccountFragment(String accountUID) { - Bundle args = new Bundle(); - args.putString(UxArgument.SELECTED_ACCOUNT_UID, accountUID); - showAccountFormFragment(args); - } - - /** - * Shows the form for creating/editing accounts - * @param args Arguments to use for initializing the form. - * This could be an account to edit or a preset for the parent account - */ - private void showAccountFormFragment(Bundle args){ - FragmentManager fragmentManager = getSupportFragmentManager(); - FragmentTransaction fragmentTransaction = fragmentManager - .beginTransaction(); - - AccountFormFragment accountFormFragment = AccountFormFragment.newInstance(); - accountFormFragment.setArguments(args); - - fragmentTransaction.replace(R.id.fragment_container, - accountFormFragment, AccountsActivity.FRAGMENT_NEW_ACCOUNT); - - fragmentTransaction.commit(); - } - - /** - * Opens a dialog fragment to create a new account - * @param v View which triggered this callback - */ - public void onNewAccountClick(View v) { - startActivity(createNewAccountIntent()); - } - - /** - * Shows the user dialog to create default account structure or import existing account structure - */ - private void showFirstRunDialog() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.title_default_accounts); - builder.setMessage(R.string.msg_confirm_create_default_accounts_first_run); - - builder.setPositiveButton(R.string.btn_create_accounts, new DialogInterface.OnClickListener() { - AlertDialog currencyDialog; - @Override - public void onClick(DialogInterface dialog, int which) { - final AlertDialog.Builder adb = new AlertDialog.Builder(AccountsActivity.this); - adb.setTitle(R.string.title_choose_currency); - ArrayAdapter arrayAdapter = new ArrayAdapter<>( - AccountsActivity.this, - android.R.layout.select_dialog_singlechoice, - getResources().getStringArray(R.array.currency_names)); - - final List currencyCodes = Arrays.asList( - getResources().getStringArray(R.array.key_currency_codes)); - String userCurrencyCode = Currency.getInstance(Locale.getDefault()).getCurrencyCode(); - int currencyIndex = currencyCodes.indexOf(userCurrencyCode.toUpperCase()); - - adb.setSingleChoiceItems(arrayAdapter, currencyIndex, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - String currency = currencyCodes.get(which); - PreferenceManager.getDefaultSharedPreferences(AccountsActivity.this) - .edit() - .putString(getString(R.string.key_default_currency), currency) - .commit(); - - createDefaultAccounts(currency, AccountsActivity.this); - currencyDialog.dismiss(); - removeFirstRunFlag(); - } - }); - currencyDialog = adb.create(); - currencyDialog.show(); - } - }); - - builder.setNegativeButton(R.string.btn_cancel, new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - mDefaultAccountsDialog.dismiss(); - } - }); - - builder.setNeutralButton(R.string.btn_import_accounts, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - importAccounts(); - } - }); - - mDefaultAccountsDialog = builder.create(); - mDefaultAccountsDialog.setOnDismissListener(new DialogInterface.OnDismissListener() { - @Override - public void onDismiss(DialogInterface dialog) { - removeFirstRunFlag(); - mDrawerLayout.openDrawer(mDrawerList); - } - }); - mDefaultAccountsDialog.show(); - -/* - //TODO: For now logging is disabled only for production. In the future, consider enabling for production - //show dialog to get user consent for logging - new AlertDialog.Builder(this) - .setTitle(getString(R.string.title_enable_crashlytics)) - .setMessage(getString(R.string.msg_enable_crashlytics)) - .setPositiveButton(R.string.label_enable, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(AccountsActivity.this); - Editor editor = sharedPreferences.edit(); - editor.putBoolean(getString(R.string.key_enable_crashlytics), true); - editor.apply(); - } - }) - .setNegativeButton(R.string.btn_cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(AccountsActivity.this); - Editor editor = sharedPreferences.edit(); - editor.putBoolean(getString(R.string.key_enable_crashlytics), false); - editor.apply(); - } - }).create().show(); -*/ - } - /** * Creates default accounts with the specified currency code. * If the currency parameter is null, then locale currency will be used if available @@ -613,15 +505,48 @@ public void onTaskComplete() { /** * Starts Intent chooser for selecting a GnuCash accounts file to import. - * The accounts are actually imported in onActivityResult + *

The {@code activity} is responsible for the actual import of the file and can do so by calling {@link #importXmlFileFromIntent(Activity, Intent)}
+ * The calling class should respond to the request code {@link AccountsActivity#REQUEST_PICK_ACCOUNTS_FILE} in its {@link #onActivityResult(int, int, Intent)} method

+ * @param activity Activity starting the request and will also handle the response + * @see #importXmlFileFromIntent(Activity, Intent) */ - public void importAccounts() { + public static void startXmlFileChooser(Activity activity) { Intent pickIntent = new Intent(Intent.ACTION_GET_CONTENT); - pickIntent.setType("application/octet-stream"); +// ArrayList mimeTypes = new ArrayList<>(); +// mimeTypes.add("application/*"); +// mimeTypes.add("file/*"); +// mimeTypes.add("text/*"); +// mimeTypes.add("application/vnd.google-apps.file"); +// pickIntent.putStringArrayListExtra(Intent.EXTRA_MIME_TYPES, mimeTypes); + pickIntent.addCategory(Intent.CATEGORY_OPENABLE); + pickIntent.setType("*/*"); Intent chooser = Intent.createChooser(pickIntent, "Select GnuCash account file"); - startActivityForResult(chooser, REQUEST_PICK_ACCOUNTS_FILE); + try { + activity.startActivityForResult(chooser, REQUEST_PICK_ACCOUNTS_FILE); + } catch (ActivityNotFoundException ex){ + Crashlytics.log("No file manager for selecting files available"); + Crashlytics.logException(ex); + Toast.makeText(activity, R.string.toast_install_file_manager, Toast.LENGTH_LONG).show(); + } + } + + /** + * Reads and XML file from an intent and imports it into the database + *

This method is usually called in response to {@link AccountsActivity#startXmlFileChooser(Activity)}

+ * @param context Activity context + * @param data Intent data containing the XML uri + */ + public static void importXmlFileFromIntent(Activity context, Intent data) { + try { + GncXmlExporter.createBackup(); + InputStream accountInputStream = context.getContentResolver().openInputStream(data.getData()); + new ImportAsyncTask(context).execute(accountInputStream); + } catch (FileNotFoundException e) { + Crashlytics.logException(e); + Toast.makeText(context, R.string.toast_error_importing_accounts, Toast.LENGTH_SHORT).show(); + } } /** @@ -648,9 +573,10 @@ public void accountSelected(String accountUID) { * Removes the flag indicating that the app is being run for the first time. * This is called every time the app is started because the next time won't be the first time */ - private void removeFirstRunFlag(){ - Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit(); - editor.putBoolean(getString(R.string.key_first_run), false); + public static void removeFirstRunFlag(){ + Context context = GnuCashApplication.getAppContext(); + Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); + editor.putBoolean(context.getString(R.string.key_first_run), false); editor.commit(); } 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 ff8da4aef..74deec211 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 @@ -18,58 +18,66 @@ import android.app.Activity; import android.app.SearchManager; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Color; -import android.graphics.Rect; +import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; -import android.support.v4.widget.SimpleCursorAdapter; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.PopupMenu; +import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; -import android.view.TouchDelegate; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemLongClickListener; -import android.widget.ImageButton; -import android.widget.ListAdapter; -import android.widget.ListView; +import android.widget.ImageView; import android.widget.TextView; -import com.actionbarsherlock.app.ActionBar; -import com.actionbarsherlock.app.SherlockListFragment; -import com.actionbarsherlock.view.ActionMode; -import com.actionbarsherlock.view.ActionMode.Callback; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuInflater; -import com.actionbarsherlock.view.MenuItem; - import org.gnucash.android.R; import org.gnucash.android.db.AccountsDbAdapter; import org.gnucash.android.db.DatabaseCursorLoader; import org.gnucash.android.db.DatabaseSchema; -import org.gnucash.android.db.TransactionsDbAdapter; import org.gnucash.android.model.Account; -import org.gnucash.android.ui.UxArgument; -import org.gnucash.android.ui.transaction.TransactionsActivity; +import org.gnucash.android.ui.common.FormActivity; +import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.util.AccountBalanceTask; +import org.gnucash.android.ui.util.CursorRecyclerAdapter; +import org.gnucash.android.ui.util.widget.EmptyRecyclerView; import org.gnucash.android.ui.util.OnAccountClickedListener; import org.gnucash.android.ui.util.Refreshable; +import butterknife.Bind; +import butterknife.ButterKnife; + /** * Fragment for displaying the list of accounts in the database * * @author Ngewi Fet */ -public class AccountsListFragment extends SherlockListFragment implements +public class AccountsListFragment extends Fragment implements Refreshable, - LoaderCallbacks, OnItemLongClickListener, - com.actionbarsherlock.widget.SearchView.OnQueryTextListener, - com.actionbarsherlock.widget.SearchView.OnCloseListener { + LoaderCallbacks, + android.support.v7.widget.SearchView.OnQueryTextListener, + android.support.v7.widget.SearchView.OnCloseListener { + + AccountRecyclerAdapter mAccountRecyclerAdapter; + @Bind(R.id.account_recycler_view) EmptyRecyclerView mRecyclerView; + @Bind(R.id.empty_view) TextView mEmptyTextView; /** * Describes the kinds of accounts that should be loaded in the accounts list. @@ -90,11 +98,6 @@ public enum DisplayMode { */ protected static final String TAG = "AccountsListFragment"; - - /** - * {@link ListAdapter} for the accounts which will be bound to the list - */ - AccountsCursorAdapter mAccountsCursorAdapter; /** * Database adapter for loading Account records from the database */ @@ -103,30 +106,6 @@ public enum DisplayMode { * Listener to be notified when an account is clicked */ private OnAccountClickedListener mAccountSelectedListener; - /** - * Flag to indicate if the fragment is in edit mode - * Edit mode means an account has been selected (through long press) and the - * context action bar (CAB) is activated - */ - private boolean mInEditMode = false; - /** - * Android action mode - * Is not null only when an accoun is selected and the Context ActionBar (CAB) is activated - */ - private ActionMode mActionMode = null; - - /** - * Stores the database ID of the currently selected account when in action mode. - * This is necessary because getSelectedItemId() does not work properly (by design) - * in touch mode (which is the majority of devices today) - */ - private long mSelectedItemId = -1; - - /** - * Database record ID of the account whose children will be loaded by the list fragment. - * If no parent account is specified, then all top-level accounts are loaded. - */ -// private long mParentAccountId = -1; /** * GUID of the account whose children will be loaded in the list fragment. @@ -142,69 +121,7 @@ public enum DisplayMode { /** * Search view for searching accounts */ - private com.actionbarsherlock.widget.SearchView mSearchView; - - /** - * Callbacks for the CAB menu - */ - private ActionMode.Callback mActionModeCallbacks = new Callback() { - - String mSelectedAccountUID; - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - MenuInflater inflater = mode.getMenuInflater(); - inflater.inflate(R.menu.account_context_menu, menu); - mode.setTitle(getString(R.string.title_selected, 1)); - mSelectedAccountUID = mAccountsDbAdapter.getUID(mSelectedItemId); - return true; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - // nothing to see here, move along - MenuItem favoriteAccountMenuItem = menu.findItem(R.id.menu_favorite_account); - boolean isFavoriteAccount = AccountsDbAdapter.getInstance().isFavoriteAccount(mSelectedAccountUID); - - int favoriteIcon = isFavoriteAccount ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off; - favoriteAccountMenuItem.setIcon(favoriteIcon); - - return true; - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_favorite_account: - boolean isFavorite = mAccountsDbAdapter.isFavoriteAccount(mSelectedAccountUID); - //toggle favorite preference - mAccountsDbAdapter.updateAccount(mSelectedItemId, - DatabaseSchema.AccountEntry.COLUMN_FAVORITE, isFavorite ? "0" : "1"); - mode.invalidate(); - return true; - - case R.id.context_menu_edit_accounts: - openCreateOrEditActivity(mSelectedItemId); - mode.finish(); - mActionMode = null; - return true; - - case R.id.context_menu_delete: - tryDeleteAccount(mSelectedItemId); - mode.finish(); - mActionMode = null; - return true; - - default: - return false; - } - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - finishEditMode(); - } - }; + private android.support.v7.widget.SearchView mSearchView; public static AccountsListFragment newInstance(DisplayMode displayMode){ AccountsListFragment fragment = new AccountsListFragment(); @@ -217,8 +134,31 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_accounts_list, container, false); - TextView sumlabelTextView = (TextView) v.findViewById(R.id.label_sum); - sumlabelTextView.setText(R.string.account_balance); + + ButterKnife.bind(this, v); + mRecyclerView.setHasFixedSize(true); + mRecyclerView.setEmptyView(mEmptyTextView); + + switch (mDisplayMode){ + + case TOP_LEVEL: + mEmptyTextView.setText(R.string.label_no_accounts); + break; + case RECENT: + mEmptyTextView.setText(R.string.label_no_recent_accounts); + break; + case FAVORITES: + mEmptyTextView.setText(R.string.label_no_favorite_accounts); + break; + } + + if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { + GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), 2); + mRecyclerView.setLayoutManager(gridLayoutManager); + } else { + LinearLayoutManager mLayoutManager = new LinearLayoutManager(getActivity()); + mRecyclerView.setLayoutManager(mLayoutManager); + } return v; } @@ -231,34 +171,30 @@ public void onCreate(Bundle savedInstanceState) { mParentAccountUID = args.getString(UxArgument.PARENT_ACCOUNT_UID); mAccountsDbAdapter = AccountsDbAdapter.getInstance(); - mAccountsCursorAdapter = new AccountsCursorAdapter( - getActivity().getApplicationContext(), - R.layout.list_item_account, null, - new String[]{DatabaseSchema.AccountEntry.COLUMN_NAME}, - new int[]{R.id.primary_text}); - - setListAdapter(mAccountsCursorAdapter); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - ActionBar actionbar = getSherlockActivity().getSupportActionBar(); + ActionBar actionbar = ((AppCompatActivity) getActivity()).getSupportActionBar(); actionbar.setTitle(R.string.title_accounts); actionbar.setDisplayHomeAsUpEnabled(true); - setHasOptionsMenu(true); - ListView lv = getListView(); - lv.setOnItemLongClickListener(this); - lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + + // specify an adapter (see also next example) + mAccountRecyclerAdapter = new AccountRecyclerAdapter(null); + mRecyclerView.setAdapter(mAccountRecyclerAdapter); + getLoaderManager().initLoader(0, null, this); } @Override public void onResume() { super.onResume(); + ActionBar actionbar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + actionbar.setTitle(R.string.title_accounts); refresh(); } @@ -272,31 +208,8 @@ public void onAttach(Activity activity) { } } - @Override - public void onListItemClick(ListView listView, View view, int position, long id) { - super.onListItemClick(listView, view, position, id); - if (mInEditMode) { - mSelectedItemId = id; - listView.setItemChecked(position, true); - return; - } - mAccountSelectedListener.accountSelected(mAccountsDbAdapter.getUID(id)); - } - - @Override - public boolean onItemLongClick(AdapterView parent, View view, int position, - long id) { - if (mActionMode != null) { - return false; - } - mInEditMode = true; - mSelectedItemId = id; - // Start the CAB using the ActionMode.Callback defined above - mActionMode = getSherlockActivity().startActionMode( - mActionModeCallbacks); - - getListView().setItemChecked(position, true); - return true; + public void onListItemClick(String accountUID) { + mAccountSelectedListener.accountSelected(accountUID); } @Override @@ -315,12 +228,11 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { * @param rowId The record ID of the account */ public void tryDeleteAccount(long rowId) { - Account acc = mAccountsDbAdapter.getAccount(rowId); + Account acc = mAccountsDbAdapter.getRecord(rowId); if (acc.getTransactionCount() > 0 || mAccountsDbAdapter.getSubAccountCount(acc.getUID()) > 0) { showConfirmationDialog(rowId); } else { mAccountsDbAdapter.deleteRecord(rowId); - mAccountsCursorAdapter.swapCursor(null); refresh(); } } @@ -334,18 +246,7 @@ public void showConfirmationDialog(long id) { DeleteAccountDialogFragment alertFragment = DeleteAccountDialogFragment.newInstance(mAccountsDbAdapter.getUID(id)); alertFragment.setTargetFragment(this, 0); - alertFragment.show(getSherlockActivity().getSupportFragmentManager(), "delete_confirmation_dialog"); - } - - /** - * Finish the edit mode and dismisses the Contextual ActionBar - * Any selected (highlighted) accounts are deselected - */ - public void finishEditMode() { - mInEditMode = false; - getListView().setItemChecked(getListView().getCheckedItemPosition(), false); - mActionMode = null; - mSelectedItemId = -1; + alertFragment.show(getActivity().getSupportFragmentManager(), "delete_confirmation_dialog"); } @Override @@ -357,8 +258,8 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { // Associate searchable configuration with the SearchView SearchManager searchManager = (SearchManager) getActivity().getSystemService(Context.SEARCH_SERVICE); - mSearchView = - (com.actionbarsherlock.widget.SearchView) menu.findItem(R.id.menu_search).getActionView(); + mSearchView = (android.support.v7.widget.SearchView) + MenuItemCompat.getActionView(menu.findItem(R.id.menu_search)); if (mSearchView == null) return; @@ -369,25 +270,6 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { } } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - - case R.id.menu_add_account: - Intent addAccountIntent = new Intent(getActivity(), AccountsActivity.class); - addAccountIntent.setAction(Intent.ACTION_INSERT_OR_EDIT); - addAccountIntent.putExtra(UxArgument.PARENT_ACCOUNT_UID, mParentAccountUID); - startActivityForResult(addAccountIntent, AccountsActivity.REQUEST_EDIT_ACCOUNT); - return true; - - case R.id.menu_export: - AccountsActivity.showExportDialog(getActivity()); - return true; - - default: - return super.onOptionsItemSelected(item); - } - } @Override public void refresh(String parentAccountUID) { @@ -418,9 +300,10 @@ public void onDestroy() { * @param accountId Long record ID of account to be edited. Pass 0 to create a new account. */ public void openCreateOrEditActivity(long accountId){ - Intent editAccountIntent = new Intent(AccountsListFragment.this.getActivity(), AccountsActivity.class); + Intent editAccountIntent = new Intent(AccountsListFragment.this.getActivity(), FormActivity.class); editAccountIntent.setAction(Intent.ACTION_INSERT_OR_EDIT); editAccountIntent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, mAccountsDbAdapter.getUID(accountId)); + editAccountIntent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.ACCOUNT.name()); startActivityForResult(editAccountIntent, AccountsActivity.REQUEST_EDIT_ACCOUNT); } @@ -440,14 +323,14 @@ public Loader onCreateLoader(int id, Bundle args) { @Override public void onLoadFinished(Loader loaderCursor, Cursor cursor) { Log.d(TAG, "Accounts loader finished. Swapping in cursor"); - mAccountsCursorAdapter.swapCursor(cursor); - mAccountsCursorAdapter.notifyDataSetChanged(); + mAccountRecyclerAdapter.swapCursor(cursor); + mAccountRecyclerAdapter.notifyDataSetChanged(); } @Override public void onLoaderReset(Loader arg0) { Log.d(TAG, "Resetting the accounts loader"); - mAccountsCursorAdapter.swapCursor(null); + mAccountRecyclerAdapter.swapCursor(null); } @Override @@ -552,108 +435,138 @@ public Cursor loadInBackground() { } } - /** - * Overrides the {@link SimpleCursorAdapter} to provide custom binding of the - * information from the database to the views - * - * @author Ngewi Fet - */ - private class AccountsCursorAdapter extends SimpleCursorAdapter { - TransactionsDbAdapter transactionsDBAdapter; - public AccountsCursorAdapter(Context context, int layout, Cursor c, - String[] from, int[] to) { - super(context, layout, c, from, to, 0); - transactionsDBAdapter = TransactionsDbAdapter.getInstance(); + class AccountRecyclerAdapter extends CursorRecyclerAdapter { + + public AccountRecyclerAdapter(Cursor cursor){ + super(cursor); } @Override - public void bindView(View v, Context context, Cursor cursor) { - // perform the default binding - super.bindView(v, context, cursor); + public AccountViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.cardview_account, parent, false); + return new AccountViewHolder(v); + } + + @Override + public void onBindViewHolderCursor(final AccountViewHolder holder, final Cursor cursor) { final String accountUID = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.AccountEntry.COLUMN_UID)); + holder.accoundId = mAccountsDbAdapter.getID(accountUID); - TextView subAccountTextView = (TextView) v.findViewById(R.id.secondary_text); + holder.accountName.setText(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.AccountEntry.COLUMN_NAME))); int subAccountCount = mAccountsDbAdapter.getSubAccountCount(accountUID); if (subAccountCount > 0) { - subAccountTextView.setVisibility(View.VISIBLE); + holder.description.setVisibility(View.VISIBLE); String text = getResources().getQuantityString(R.plurals.label_sub_accounts, subAccountCount, subAccountCount); - subAccountTextView.setText(text); + holder.description.setText(text); } else - subAccountTextView.setVisibility(View.GONE); + holder.description.setVisibility(View.GONE); // add a summary of transactions to the account view - TextView accountBalanceTextView = (TextView) v - .findViewById(R.id.transactions_summary); - new AccountBalanceTask(accountBalanceTextView).execute(accountUID); - - View colorStripView = v.findViewById(R.id.account_color_strip); - String accountColor = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.AccountEntry.COLUMN_COLOR_CODE)); - if (accountColor != null){ - int color = Color.parseColor(accountColor); - colorStripView.setBackgroundColor(color); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + // Make sure the balance task is truely multithread + new AccountBalanceTask(holder.accountBalance).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, accountUID); } else { - colorStripView.setBackgroundColor(Color.TRANSPARENT); + new AccountBalanceTask(holder.accountBalance).execute(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); boolean isPlaceholderAccount = mAccountsDbAdapter.isPlaceholderAccount(accountUID); - ImageButton newTransactionButton = (ImageButton) v.findViewById(R.id.btn_new_transaction); - if (isPlaceholderAccount){ - newTransactionButton.setVisibility(View.GONE); - v.findViewById(R.id.vertical_line).setVisibility(View.GONE); + if (isPlaceholderAccount) { + holder.createTransaction.setVisibility(View.GONE); } else { - newTransactionButton.setOnClickListener(new View.OnClickListener() { + holder.createTransaction.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - Intent intent = new Intent(getActivity(), TransactionsActivity.class); + Intent intent = new Intent(getActivity(), FormActivity.class); intent.setAction(Intent.ACTION_INSERT_OR_EDIT); intent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, accountUID); + intent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.TRANSACTION.name()); getActivity().startActivity(intent); } }); } - newTransactionButton.setFocusable(false); - } - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View itemView = super.getView(position, convertView, parent); - TextView secondaryText = (TextView) itemView.findViewById(R.id.secondary_text); - - ListView listView = (ListView) parent; - if (mInEditMode && listView.isItemChecked(position)){ - itemView.setBackgroundColor(getResources().getColor(R.color.abs__holo_blue_light)); - secondaryText.setTextColor(getResources().getColor(android.R.color.white)); + if (mAccountsDbAdapter.isFavoriteAccount(accountUID)){ + holder.favoriteStatus.setImageResource(R.drawable.ic_star_black_24dp); } else { - itemView.setBackgroundColor(getResources().getColor(android.R.color.transparent)); - secondaryText.setTextColor(getResources().getColor(android.R.color.secondary_text_light_nodisable)); + holder.favoriteStatus.setImageResource(R.drawable.ic_star_border_black_24dp); } + holder.favoriteStatus.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + boolean isFavoriteAccount = mAccountsDbAdapter.isFavoriteAccount(accountUID); + + ContentValues contentValues = new ContentValues(); + contentValues.put(DatabaseSchema.AccountEntry.COLUMN_FAVORITE, !isFavoriteAccount); + mAccountsDbAdapter.updateRecord(accountUID, contentValues); + + int drawableResource = !isFavoriteAccount ? + R.drawable.ic_star_black_24dp : R.drawable.ic_star_border_black_24dp; + holder.favoriteStatus.setImageResource(drawableResource); + if (mDisplayMode == DisplayMode.FAVORITES) + refresh(); + } + }); - //increase the touch target area for the add new transaction button - - final View addTransactionButton = itemView.findViewById(R.id.btn_new_transaction); - final View parentView = itemView; - parentView.post(new Runnable() { + holder.itemView.setOnClickListener(new View.OnClickListener() { @Override - public void run() { - if (isAdded()){ //may be run when fragment has been unbound from activity - final android.graphics.Rect hitRect = new Rect(); - float extraPadding = getResources().getDimension(R.dimen.edge_padding); - addTransactionButton.getHitRect(hitRect); - hitRect.right += extraPadding; - hitRect.bottom += extraPadding; - hitRect.top -= extraPadding; - hitRect.left -= extraPadding; - parentView.setTouchDelegate(new TouchDelegate(hitRect, addTransactionButton)); - } + public void onClick(View v) { + onListItemClick(accountUID); } }); + } + + + class AccountViewHolder extends RecyclerView.ViewHolder implements PopupMenu.OnMenuItemClickListener{ + @Bind(R.id.primary_text) TextView accountName; + @Bind(R.id.secondary_text) TextView description; + @Bind(R.id.account_balance) TextView accountBalance; + @Bind(R.id.create_transaction) ImageView createTransaction; + @Bind(R.id.favorite_status) ImageView favoriteStatus; + @Bind(R.id.options_menu) ImageView optionsMenu; + @Bind(R.id.account_color_strip) View colorStripView; + long accoundId; + + public AccountViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + + optionsMenu.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + PopupMenu popup = new PopupMenu(getActivity(), v); + popup.setOnMenuItemClickListener(AccountViewHolder.this); + MenuInflater inflater = popup.getMenuInflater(); + inflater.inflate(R.menu.account_context_menu, popup.getMenu()); + popup.show(); + } + }); + + } - return itemView; + + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()){ + case R.id.context_menu_edit_accounts: + openCreateOrEditActivity(accoundId); + return true; + + case R.id.context_menu_delete: + tryDeleteAccount(accoundId); + return true; + + default: + return false; + } + } } } - } diff --git a/app/src/main/java/org/gnucash/android/ui/account/DeleteAccountDialogFragment.java b/app/src/main/java/org/gnucash/android/ui/account/DeleteAccountDialogFragment.java index ef70aab87..91663c3c2 100644 --- a/app/src/main/java/org/gnucash/android/ui/account/DeleteAccountDialogFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/account/DeleteAccountDialogFragment.java @@ -18,6 +18,7 @@ import android.database.Cursor; import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; import android.support.v4.widget.SimpleCursorAdapter; import android.text.TextUtils; import android.view.LayoutInflater; @@ -29,8 +30,6 @@ import android.widget.Spinner; import android.widget.TextView; -import com.actionbarsherlock.app.SherlockDialogFragment; - import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.db.AccountsDbAdapter; @@ -39,7 +38,7 @@ import org.gnucash.android.db.TransactionsDbAdapter; import org.gnucash.android.model.AccountType; import org.gnucash.android.ui.util.Refreshable; -import org.gnucash.android.ui.widget.WidgetConfigurationActivity; +import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; import org.gnucash.android.util.QualifiedAccountNameCursorAdapter; import java.util.Currency; @@ -53,7 +52,7 @@ * * @author Ngewi Fet */ -public class DeleteAccountDialogFragment extends SherlockDialogFragment { +public class DeleteAccountDialogFragment extends DialogFragment { /** * Spinner for selecting the account to move the transactions to @@ -125,7 +124,6 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, mCancelButton = (Button) view.findViewById(R.id.btn_cancel); mOkButton = (Button) view.findViewById(R.id.btn_save); mOkButton.setText(R.string.alert_dialog_ok_delete); - mOkButton.setCompoundDrawablesWithIntrinsicBounds(R.drawable.content_discard_holo_light,0,0,0); return view; } @@ -149,9 +147,7 @@ public void onActivityCreated(Bundle savedInstanceState) { Cursor cursor = accountsDbAdapter.fetchAccountsOrderedByFullName(transactionDeleteConditions, new String[]{mOriginAccountUID, currencyCode, accountType.name()}); - SimpleCursorAdapter mCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), - android.R.layout.simple_spinner_item, cursor); - mCursorAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + SimpleCursorAdapter mCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), cursor); mTransactionsDestinationAccountSpinner.setAdapter(mCursorAdapter); //target accounts for transactions and accounts have different conditions @@ -162,9 +158,7 @@ public void onActivityCreated(Bundle savedInstanceState) { + ")"; cursor = accountsDbAdapter.fetchAccountsOrderedByFullName(accountMoveConditions, new String[]{mOriginAccountUID, currencyCode, accountType.name()}); - mCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), - android.R.layout.simple_spinner_item, cursor); - mCursorAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), cursor); mAccountsDestinationAccountSpinner.setAdapter(mCursorAdapter); setListeners(); diff --git a/app/src/main/java/org/gnucash/android/ui/chart/ChartDatePickerFragment.java b/app/src/main/java/org/gnucash/android/ui/chart/ChartDatePickerFragment.java deleted file mode 100644 index 352a7f954..000000000 --- a/app/src/main/java/org/gnucash/android/ui/chart/ChartDatePickerFragment.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2015 Oleksandr Tyshkovets - * Copyright (c) 2015 Ngewi Fet - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.gnucash.android.ui.chart; - -import android.app.DatePickerDialog; -import android.app.DatePickerDialog.OnDateSetListener; -import android.app.Dialog; -import android.os.Build; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.util.Log; -import android.view.View; -import android.widget.DatePicker; - -import java.lang.reflect.Field; -import java.util.Calendar; - -/** - * Fragment for displaying a date picker dialog. - * @author Oleksandr Tyshkovets - * @author Ngewi Fet - */ -public class ChartDatePickerFragment extends DialogFragment { - - private static final String TAG = ChartDatePickerFragment.class.getSimpleName(); - - private OnDateSetListener callback; - private Calendar mCalendar = Calendar.getInstance(); - private long minDate; - private long maxDate; - - /** - * Required for when the device is rotated while the dialog is open. - * If this constructor is not present, the app will crash - */ - public ChartDatePickerFragment() {} - - /** - * Creates the date picker fragment without day field. - * @param callback the listener to notify when the date is set and the dialog is closed - * @param time the dialog init time in milliseconds - * @param minDate the earliest allowed date - * @param maxDate the latest allowed date - */ - public static ChartDatePickerFragment newInstance(OnDateSetListener callback, long time, long minDate, long maxDate) { - ChartDatePickerFragment chartDatePickerFragment = new ChartDatePickerFragment(); - chartDatePickerFragment.callback = callback; - chartDatePickerFragment.mCalendar.setTimeInMillis(time); - chartDatePickerFragment.minDate = minDate; - chartDatePickerFragment.maxDate = maxDate; - return chartDatePickerFragment; - } - - /** - * {@inheritDoc} - */ - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - DatePickerDialog dialog = new DatePickerDialog(getActivity(), callback, - mCalendar.get(Calendar.YEAR), mCalendar.get(Calendar.MONTH), mCalendar.get(Calendar.DAY_OF_MONTH)); - - try { - Field datePickerField = dialog.getClass().getDeclaredField("mDatePicker"); - datePickerField.setAccessible(true); - DatePicker datePicker = (DatePicker) datePickerField.get(dialog); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB ) { - datePicker.setMinDate(minDate); - datePicker.setMaxDate(maxDate); - } - - for (Field field : datePicker.getClass().getDeclaredFields()) { - if (field.getName().equals("mDaySpinner") || field.getName().equals("mDayPicker")) { - field.setAccessible(true); - ((View) field.get(datePicker)).setVisibility(View.GONE); - } - } - } catch (Exception e) { - Log.w(TAG, e.getMessage()); - } - - return dialog; - } - -} diff --git a/app/src/main/java/org/gnucash/android/ui/chart/ChartReportActivity.java b/app/src/main/java/org/gnucash/android/ui/chart/ChartReportActivity.java deleted file mode 100644 index bbfef97d9..000000000 --- a/app/src/main/java/org/gnucash/android/ui/chart/ChartReportActivity.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (c) 2015 Oleksandr Tyshkovets - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.gnucash.android.ui.chart; - -import android.content.Intent; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.view.View; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Spinner; - -import org.gnucash.android.R; -import org.gnucash.android.db.AccountsDbAdapter; -import org.gnucash.android.model.Money; -import org.gnucash.android.ui.passcode.PassLockActivity; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Currency; -import java.util.List; - -/** - * Allows to select chart by type - * - * @author Oleksandr Tyshkovets - */ -public class ChartReportActivity extends PassLockActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - //it is necessary to set the view first before calling super because of the nav drawer in BaseDrawerActivity - setContentView(R.layout.activity_chart_report); - super.onCreate(savedInstanceState); - getSupportActionBar().setTitle(R.string.title_reports); - - final List allCurrencyCodes = Arrays.asList(getResources().getStringArray(R.array.key_currency_codes)); - final List allCurrencyNames = Arrays.asList(getResources().getStringArray(R.array.currency_names)); - - Currency preferredCurrency = Currency.getInstance(PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()) - .getString(getString(R.string.key_report_currency), Money.DEFAULT_CURRENCY_CODE)); - List currencies = AccountsDbAdapter.getInstance().getCurrencies(); - if (currencies.remove(preferredCurrency)) { - currencies.add(0, preferredCurrency); - } - List currencyNames = new ArrayList<>(); - for (Currency currency : currencies) { - currencyNames.add(allCurrencyNames.get(allCurrencyCodes.indexOf(currency.getCurrencyCode()))); - } - - Spinner spinner = (Spinner) findViewById(R.id.report_currency_spinner); - ArrayAdapter dataAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, currencyNames); - dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - spinner.setAdapter(dataAdapter); - spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView adapterView, View view, int i, long l) { - String currencyName = (String) ((Spinner) findViewById(R.id.report_currency_spinner)).getSelectedItem(); - PreferenceManager.getDefaultSharedPreferences(getApplicationContext()) - .edit() - .putString(getString(R.string.key_report_currency), allCurrencyCodes.get(allCurrencyNames.indexOf(currencyName))) - .commit(); - } - - @Override - public void onNothingSelected(AdapterView adapterView) { - } - }); - - findViewById(R.id.pie_chart_button).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(view.getContext(), PieChartActivity.class)); - } - }); - findViewById(R.id.line_chart_button).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(view.getContext(), LineChartActivity.class)); - } - }); - findViewById(R.id.bar_chart_button).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(view.getContext(), BarChartActivity.class)); - } - }); - - } - -} diff --git a/app/src/main/java/org/gnucash/android/ui/chart/PieChartActivity.java b/app/src/main/java/org/gnucash/android/ui/chart/PieChartActivity.java deleted file mode 100644 index 20f54acdd..000000000 --- a/app/src/main/java/org/gnucash/android/ui/chart/PieChartActivity.java +++ /dev/null @@ -1,445 +0,0 @@ -/* - * Copyright (c) 2014-2015 Oleksandr Tyshkovets - * Copyright (c) 2015 Ngewi Fet - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.gnucash.android.ui.chart; - -import android.app.DatePickerDialog; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.support.v4.app.DialogFragment; -import android.view.View; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemSelectedListener; -import android.widget.ArrayAdapter; -import android.widget.DatePicker; -import android.widget.ImageButton; -import android.widget.Spinner; -import android.widget.TextView; - -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuItem; -import com.github.mikephil.charting.charts.PieChart; -import com.github.mikephil.charting.components.Legend.LegendForm; -import com.github.mikephil.charting.components.Legend.LegendPosition; -import com.github.mikephil.charting.data.Entry; -import com.github.mikephil.charting.data.PieData; -import com.github.mikephil.charting.data.PieDataSet; -import com.github.mikephil.charting.listener.OnChartValueSelectedListener; -import com.github.mikephil.charting.utils.Highlight; - -import org.gnucash.android.R; -import org.gnucash.android.db.AccountsDbAdapter; -import org.gnucash.android.db.TransactionsDbAdapter; -import org.gnucash.android.model.Account; -import org.gnucash.android.model.AccountType; -import org.gnucash.android.model.Money; -import org.gnucash.android.ui.passcode.PassLockActivity; -import org.joda.time.LocalDateTime; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Currency; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import static org.gnucash.android.db.DatabaseSchema.AccountEntry; - -/** - * Activity used for drawing a pie chart - * - * @author Oleksandr Tyshkovets - * @author Ngewi Fet - */ -public class PieChartActivity extends PassLockActivity implements OnChartValueSelectedListener, DatePickerDialog.OnDateSetListener { - - private static final int[] COLORS = { - Color.parseColor("#17ee4e"), Color.parseColor("#cc1f09"), Color.parseColor("#3940f7"), - Color.parseColor("#f9cd04"), Color.parseColor("#5f33a8"), Color.parseColor("#e005b6"), - Color.parseColor("#17d6ed"), Color.parseColor("#e4a9a2"), Color.parseColor("#8fe6cd"), - Color.parseColor("#8b48fb"), Color.parseColor("#343a36"), Color.parseColor("#6decb1"), - Color.parseColor("#a6dcfd"), Color.parseColor("#5c3378"), Color.parseColor("#a6dcfd"), - Color.parseColor("#ba037c"), Color.parseColor("#708809"), Color.parseColor("#32072c"), - Color.parseColor("#fddef8"), Color.parseColor("#fa0e6e"), Color.parseColor("#d9e7b5") - }; - - private static final String DATE_PATTERN = "MMMM\nYYYY"; - private static final String TOTAL_VALUE_LABEL_PATTERN = "%s\n%.2f %s"; - private static final int ANIMATION_DURATION = 1800; - - private PieChart mChart; - - private LocalDateTime mChartDate = new LocalDateTime(); - private TextView mChartDateTextView; - - private ImageButton mPreviousMonthButton; - private ImageButton mNextMonthButton; - - private AccountsDbAdapter mAccountsDbAdapter; - private TransactionsDbAdapter mTransactionsDbAdapter; - - private LocalDateTime mEarliestTransactionDate; - private LocalDateTime mLatestTransactionDate; - - private AccountType mAccountType = AccountType.EXPENSE; - - private boolean mChartDataPresent = true; - - private boolean mUseAccountColor = true; - - private double mSlicePercentThreshold = 6; - - private String mCurrencyCode; - - @Override - protected void onCreate(Bundle savedInstanceState) { - //it is necessary to set the view first before calling super because of the nav drawer in BaseDrawerActivity - setContentView(R.layout.activity_pie_chart); - super.onCreate(savedInstanceState); - getSupportActionBar().setTitle(R.string.title_pie_chart); - - mUseAccountColor = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()) - .getBoolean(getString(R.string.key_use_account_color), false); - - mPreviousMonthButton = (ImageButton) findViewById(R.id.previous_month_chart_button); - mNextMonthButton = (ImageButton) findViewById(R.id.next_month_chart_button); - mChartDateTextView = (TextView) findViewById(R.id.chart_date); - - mAccountsDbAdapter = AccountsDbAdapter.getInstance(); - mTransactionsDbAdapter = TransactionsDbAdapter.getInstance(); - - mCurrencyCode = PreferenceManager.getDefaultSharedPreferences(this) - .getString(getString(R.string.key_report_currency), Money.DEFAULT_CURRENCY_CODE); - - mChart = (PieChart) findViewById(R.id.pie_chart); - mChart.setCenterTextSize(18); - mChart.setDescription(""); - mChart.getLegend().setEnabled(false); - mChart.setOnChartValueSelectedListener(this); - - setUpSpinner(); - - mPreviousMonthButton.setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View view) { - mChartDate = mChartDate.minusMonths(1); - setData(true); - } - }); - mNextMonthButton.setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View view) { - mChartDate = mChartDate.plusMonths(1); - setData(true); - } - }); - - mChartDateTextView.setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View view) { - DialogFragment newFragment = ChartDatePickerFragment.newInstance(PieChartActivity.this, - mChartDate.toDate().getTime(), - mEarliestTransactionDate.toDate().getTime(), - mLatestTransactionDate.toDate().getTime()); - newFragment.show(getSupportFragmentManager(), "date_dialog"); - } - }); - } - - /** - * Sets the chart data - * @param forCurrentMonth sets data only for current month if {@code true}, otherwise for all time - */ - private void setData(boolean forCurrentMonth) { - mChartDateTextView.setText(forCurrentMonth ? mChartDate.toString(DATE_PATTERN) : getResources().getString(R.string.label_chart_overall)); - ((TextView) findViewById(R.id.selected_chart_slice)).setText(""); - mChart.highlightValues(null); - mChart.clear(); - - mChart.setData(getData(forCurrentMonth)); - if (mChartDataPresent) { - mChart.animateXY(ANIMATION_DURATION, ANIMATION_DURATION); - } - mChart.invalidate(); - - mChartDateTextView.setEnabled(mChartDataPresent); - setImageButtonEnabled(mNextMonthButton, - mChartDate.plusMonths(1).dayOfMonth().withMinimumValue().withMillisOfDay(0).isBefore(mLatestTransactionDate)); - setImageButtonEnabled(mPreviousMonthButton, (mEarliestTransactionDate.getYear() != 1970 - && mChartDate.minusMonths(1).dayOfMonth().withMaximumValue().withMillisOfDay(86399999).isAfter(mEarliestTransactionDate))); - } - - /** - * Returns {@code PieData} instance with data entries and labels - * @param forCurrentMonth sets data only for current month if {@code true}, otherwise for all time - * @return {@code PieData} instance - */ - private PieData getData(boolean forCurrentMonth) { - List accountList = mAccountsDbAdapter.getSimpleAccountList( - AccountEntry.COLUMN_TYPE + " = ? AND " + AccountEntry.COLUMN_PLACEHOLDER + " = ?", - new String[]{ mAccountType.name(), "0" }, null); - List uidList = new ArrayList<>(); - for (Account account : accountList) { - uidList.add(account.getUID()); - } - double sum; - if (forCurrentMonth) { - long start = mChartDate.dayOfMonth().withMinimumValue().millisOfDay().withMinimumValue().toDate().getTime(); - long end = mChartDate.dayOfMonth().withMaximumValue().millisOfDay().withMaximumValue().toDate().getTime(); - sum = mAccountsDbAdapter.getAccountsBalance(uidList, start, end).absolute().asDouble(); - } else { - sum = mAccountsDbAdapter.getAccountsBalance(uidList, -1, -1).absolute().asDouble(); - } - - double otherSlice = 0; - PieDataSet dataSet = new PieDataSet(null, ""); - List names = new ArrayList<>(); - List skipUUID = new ArrayList<>(); - for (Account account : getCurrencyCodeToAccountMap(accountList).get(mCurrencyCode)) { - if (mAccountsDbAdapter.getSubAccountCount(account.getUID()) > 0) { - skipUUID.addAll(mAccountsDbAdapter.getDescendantAccountUIDs(account.getUID(), null, null)); - } - if (!skipUUID.contains(account.getUID())) { - double balance; - if (forCurrentMonth) { - long start = mChartDate.dayOfMonth().withMinimumValue().millisOfDay().withMinimumValue().toDate().getTime(); - long end = mChartDate.dayOfMonth().withMaximumValue().millisOfDay().withMaximumValue().toDate().getTime(); - balance = mAccountsDbAdapter.getAccountBalance(account.getUID(), start, end).absolute().asDouble(); - } else { - balance = mAccountsDbAdapter.getAccountBalance(account.getUID()).absolute().asDouble(); - } - - if (balance / sum * 100 > mSlicePercentThreshold) { - dataSet.addEntry(new Entry((float) balance, dataSet.getEntryCount())); - if (mUseAccountColor) { - dataSet.getColors().set(dataSet.getColors().size() - 1, (account.getColorHexCode() != null) - ? Color.parseColor(account.getColorHexCode()) - : COLORS[(dataSet.getEntryCount() - 1) % COLORS.length]); - } - dataSet.addColor(COLORS[(dataSet.getEntryCount() - 1) % COLORS.length]); - names.add(account.getName()); - } else { - otherSlice += balance; - } - } - } - if (otherSlice > 0) { - dataSet.addEntry(new Entry((float) otherSlice, dataSet.getEntryCount())); - dataSet.getColors().set(dataSet.getColors().size() - 1, Color.LTGRAY); - names.add(getResources().getString(R.string.label_other_slice)); - } - - if (dataSet.getEntryCount() == 0) { - mChartDataPresent = false; - dataSet.addEntry(new Entry(1, 0)); - dataSet.setColor(Color.LTGRAY); - dataSet.setDrawValues(false); - names.add(""); - mChart.setCenterText(getResources().getString(R.string.label_chart_no_data)); - mChart.setTouchEnabled(false); - } else { - mChartDataPresent = true; - dataSet.setSliceSpace(2); - mChart.setCenterText(String.format(TOTAL_VALUE_LABEL_PATTERN, - getResources().getString(R.string.label_chart_total), - dataSet.getYValueSum(), - Currency.getInstance(mCurrencyCode).getSymbol(Locale.getDefault())) - ); - mChart.setTouchEnabled(true); - } - - return new PieData(names, dataSet); - } - - /** - * Returns a map with a currency code as key and corresponding accounts list - * as value from a specified list of accounts - * @param accountList a list of accounts - * @return a map with a currency code as key and corresponding accounts list as value - */ - private Map> getCurrencyCodeToAccountMap(List accountList) { - Map> currencyAccountMap = new HashMap<>(); - for (Currency currency : mAccountsDbAdapter.getCurrencies()) { - currencyAccountMap.put(currency.getCurrencyCode(), new ArrayList()); - } - - for (Account account : accountList) { - currencyAccountMap.get(account.getCurrency().getCurrencyCode()).add(account); - } - return currencyAccountMap; - } - - - /** - * Sets the image button to the given state and grays-out the icon - * - * @param enabled the button's state - * @param button the button item to modify - */ - private void setImageButtonEnabled(ImageButton button, boolean enabled) { - button.setEnabled(enabled); - Drawable originalIcon = button.getDrawable(); - if (enabled) { - originalIcon.clearColorFilter(); - } else { - originalIcon.setColorFilter(Color.GRAY, PorterDuff.Mode.SRC_IN); - } - button.setImageDrawable(originalIcon); - } - - /** - * Sorts the pie's slices in ascending order - */ - private void bubbleSort() { - List labels = mChart.getData().getXVals(); - List values = mChart.getData().getDataSet().getYVals(); - List colors = mChart.getData().getDataSet().getColors(); - float tmp1; - String tmp2; - Integer tmp3; - for(int i = 0; i < values.size() - 1; i++) { - for(int j = 1; j < values.size() - i; j++) { - if (values.get(j-1).getVal() > values.get(j).getVal()) { - tmp1 = values.get(j - 1).getVal(); - values.get(j - 1).setVal(values.get(j).getVal()); - values.get(j).setVal(tmp1); - - tmp2 = labels.get(j - 1); - labels.set(j - 1, labels.get(j)); - labels.set(j, tmp2); - - tmp3 = colors.get(j - 1); - colors.set(j - 1, colors.get(j)); - colors.set(j, tmp3); - } - } - } - - mChart.notifyDataSetChanged(); - mChart.highlightValues(null); - mChart.invalidate(); - } - - /** - * Sets up settings and data for the account type spinner. Currently used only {@code EXPENSE} and {@code INCOME} - * account types. - */ - private void setUpSpinner() { - Spinner spinner = (Spinner) findViewById(R.id.chart_data_spinner); - ArrayAdapter dataAdapter = new ArrayAdapter<>(this, - android.R.layout.simple_spinner_item, - Arrays.asList(AccountType.EXPENSE, AccountType.INCOME)); - dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - spinner.setAdapter(dataAdapter); - spinner.setOnItemSelectedListener(new OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView adapterView, View view, int i, long l) { - mAccountType = (AccountType) ((Spinner) findViewById(R.id.chart_data_spinner)).getSelectedItem(); - mEarliestTransactionDate = new LocalDateTime(mTransactionsDbAdapter.getTimestampOfEarliestTransaction(mAccountType, mCurrencyCode)); - mLatestTransactionDate = new LocalDateTime(mTransactionsDbAdapter.getTimestampOfLatestTransaction(mAccountType, mCurrencyCode)); - mChartDate = mLatestTransactionDate; - setData(false); - } - - @Override - public void onNothingSelected(AdapterView adapterView) {} - }); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getSupportMenuInflater().inflate(R.menu.chart_actions, menu); - return true; - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - menu.findItem(R.id.menu_order_by_size).setVisible(mChartDataPresent); - menu.findItem(R.id.menu_toggle_labels).setVisible(mChartDataPresent); - menu.findItem(R.id.menu_group_other_slice).setVisible(mChartDataPresent); - // hide line/bar chart specific menu items - menu.findItem(R.id.menu_percentage_mode).setVisible(false); - menu.findItem(R.id.menu_toggle_average_lines).setVisible(false); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_order_by_size: { - bubbleSort(); - break; - } - case R.id.menu_toggle_legend: { - mChart.getLegend().setEnabled(!mChart.getLegend().isEnabled()); - mChart.getLegend().setForm(LegendForm.CIRCLE); - mChart.getLegend().setPosition(LegendPosition.RIGHT_OF_CHART_CENTER); - mChart.notifyDataSetChanged(); - mChart.invalidate(); - break; - } - case R.id.menu_toggle_labels: { - mChart.getData().setDrawValues(!mChart.isDrawSliceTextEnabled()); - mChart.setDrawSliceText(!mChart.isDrawSliceTextEnabled()); - mChart.invalidate(); - break; - } - case R.id.menu_group_other_slice: { - mSlicePercentThreshold = Math.abs(mSlicePercentThreshold - 6); - setData(false); - break; - } - case android.R.id.home: { - finish(); - break; - } - } - return true; - } - - /** - * Since JellyBean, the onDateSet() method of the DatePicker class is called twice i.e. once when - * OK button is pressed and then when the DatePickerDialog is dismissed. It is a known bug. - */ - @Override - public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) { - if (view.isShown()) { - mChartDate = new LocalDateTime(year, monthOfYear + 1, dayOfMonth, 0, 0); - setData(true); - } - } - - @Override - public void onValueSelected(Entry e, int dataSetIndex, Highlight h) { - if (e == null) return; - ((TextView) findViewById(R.id.selected_chart_slice)) - .setText(mChart.getData().getXVals().get(e.getXIndex()) + " - " + e.getVal() - + " (" + String.format("%.2f", (e.getVal() / mChart.getYValueSum()) * 100) + " %)"); - } - - @Override - public void onNothingSelected() { - ((TextView) findViewById(R.id.selected_chart_slice)).setText(""); - } -} diff --git a/app/src/main/java/org/gnucash/android/ui/colorpicker/ColorPickerDialog.java b/app/src/main/java/org/gnucash/android/ui/colorpicker/ColorPickerDialog.java index 4e3f4495c..97e826eda 100644 --- a/app/src/main/java/org/gnucash/android/ui/colorpicker/ColorPickerDialog.java +++ b/app/src/main/java/org/gnucash/android/ui/colorpicker/ColorPickerDialog.java @@ -20,12 +20,11 @@ import android.app.AlertDialog; import android.app.Dialog; import android.os.Bundle; +import android.support.v4.app.DialogFragment; import android.view.LayoutInflater; import android.view.View; import android.widget.ProgressBar; -import com.actionbarsherlock.app.SherlockDialogFragment; - import org.gnucash.android.R; import org.gnucash.android.ui.colorpicker.ColorPickerSwatch.OnColorSelectedListener; @@ -33,7 +32,7 @@ * A dialog which takes in as input an array of colors and creates a palette allowing the user to * select a specific color swatch, which invokes a listener. */ -public class ColorPickerDialog extends SherlockDialogFragment implements OnColorSelectedListener { +public class ColorPickerDialog extends DialogFragment implements OnColorSelectedListener { public static final int SIZE_LARGE = 1; public static final int SIZE_SMALL = 2; 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 new file mode 100644 index 000000000..49ffe5adf --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/common/BaseDrawerActivity.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.gnucash.android.ui.common; + +import android.app.Activity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.design.widget.NavigationView; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarDrawerToggle; +import android.view.MenuItem; +import android.view.View; + +import com.uservoice.uservoicesdk.UserVoice; + +import org.gnucash.android.R; +import org.gnucash.android.ui.account.AccountsActivity; +import org.gnucash.android.ui.passcode.PasscodeLockActivity; +import org.gnucash.android.ui.report.ReportsActivity; +import org.gnucash.android.ui.settings.SettingsActivity; +import org.gnucash.android.ui.transaction.ScheduledActionsActivity; + + +/** + * Base activity implementing the navigation drawer, to be extended by all activities requiring one + *

All subclasses should call the {@link #setUpDrawer()} method in {@link #onCreate(Bundle)}, after the + * activity layout has been set.
+ * The activity layout of the subclass is expected to contain {@code DrawerLayout} and a {@code NavigationView}

+ * + * @author Ngewi Fet + */ +public class BaseDrawerActivity extends PasscodeLockActivity { + protected DrawerLayout mDrawerLayout; + protected NavigationView mNavigationView; + + protected ActionBarDrawerToggle mDrawerToggle; + + private class DrawerItemClickListener implements NavigationView.OnNavigationItemSelectedListener { + + @Override + public boolean onNavigationItemSelected(MenuItem menuItem) { + onDrawerMenuItemClicked(menuItem.getItemId()); + return true; + } + + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + /** + * Sets up the navigation drawer for this activity. + * + * This should be called from the activity's + * {@link Activity#onCreate(Bundle)} method after calling + * {@link Activity#setContentView(int)}. + * + */ + protected void setUpDrawer() { + final ActionBar actionBar = getSupportActionBar(); + if (actionBar != null){ + actionBar.setHomeButtonEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(true); + + } + mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); + mNavigationView = (NavigationView) findViewById(R.id.nav_view); + + mNavigationView.setNavigationItemSelectedListener(new DrawerItemClickListener()); + + mDrawerToggle = new ActionBarDrawerToggle( + this, /* host Activity */ + mDrawerLayout, /* DrawerLayout object */ + R.string.drawer_open, /* "open drawer" description */ + R.string.drawer_close /* "close drawer" description */ + ) { + + /** Called when a drawer has settled in a completely closed state. */ + public void onDrawerClosed(View view) { + super.onDrawerClosed(view); + } + + /** Called when a drawer has settled in a completely open state. */ + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + } + }; + + mDrawerLayout.setDrawerListener(mDrawerToggle); + } + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + mDrawerToggle.syncState(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mDrawerToggle.onConfigurationChanged(newConfig); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (!mDrawerLayout.isDrawerOpen(mNavigationView)) + mDrawerLayout.openDrawer(mNavigationView); + else + mDrawerLayout.closeDrawer(mNavigationView); + + return super.onOptionsItemSelected(item); + } + + /** + * Handler for the navigation drawer items + * */ + protected void onDrawerMenuItemClicked(int itemId) { + switch (itemId){ + case R.id.nav_item_open: { //Open... files + AccountsActivity.startXmlFileChooser(this); + } + break; + + case R.id.nav_item_favorites: { //favorite accounts + Intent intent = new Intent(this, AccountsActivity.class); + intent.putExtra(AccountsActivity.EXTRA_TAB_INDEX, + AccountsActivity.INDEX_FAVORITE_ACCOUNTS_FRAGMENT); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_SINGLE_TOP); + startActivity(intent); + } + break; + + case R.id.nav_item_reports: + startActivity(new Intent(this, ReportsActivity.class)); + break; + + case R.id.nav_item_scheduled_actions: { //show scheduled transactions + Intent intent = new Intent(this, ScheduledActionsActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + startActivity(intent); + } + break; + + case R.id.nav_item_export:{ + AccountsActivity.openExportFragment(this); + } + break; + + case R.id.nav_item_settings: //Settings activity + startActivity(new Intent(this, SettingsActivity.class)); + break; + + case R.id.nav_item_help: + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.edit().putBoolean(UxArgument.SKIP_PASSCODE_SCREEN, true).apply(); + UserVoice.launchUserVoice(this); + break; + } + mDrawerLayout.closeDrawer(mNavigationView); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_CANCELED){ + return; + } + + switch (requestCode) { + case AccountsActivity.REQUEST_PICK_ACCOUNTS_FILE: + AccountsActivity.importXmlFileFromIntent(this, data); + break; + } + } + +} 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 new file mode 100644 index 000000000..4af305350 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/common/FormActivity.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.ui.common; + +import android.content.Intent; +import android.graphics.drawable.ColorDrawable; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.support.v7.widget.Toolbar; +import android.view.MenuItem; + +import org.gnucash.android.R; +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.ui.account.AccountFormFragment; +import org.gnucash.android.ui.export.ExportFormFragment; +import org.gnucash.android.ui.passcode.PasscodeLockActivity; +import org.gnucash.android.ui.transaction.TransactionFormFragment; +import org.gnucash.android.ui.transaction.SplitEditorFragment; +import org.gnucash.android.ui.util.widget.CalculatorKeyboard; + +/** + * Activity for displaying forms in the application. + * The activity provides the standard close button, but it is up to the form fragments to display + * menu options (e.g. for saving etc) + * @author Ngewi Fet + */ +public class FormActivity extends PasscodeLockActivity { + + private String mAccountUID; + + private CalculatorKeyboard mOnBackListener; + + public enum FormType {ACCOUNT, TRANSACTION, EXPORT, SPLIT_EDITOR} + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_form); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + android.support.v7.app.ActionBar actionBar = getSupportActionBar(); + assert(actionBar != null); + actionBar.setHomeButtonEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp); + + final Intent intent = getIntent(); + String formtypeString = intent.getStringExtra(UxArgument.FORM_TYPE); + FormType formType = FormType.valueOf(formtypeString); + + mAccountUID = intent.getStringExtra(UxArgument.SELECTED_ACCOUNT_UID); + if (mAccountUID == null){ + mAccountUID = intent.getStringExtra(UxArgument.PARENT_ACCOUNT_UID); + } + if (mAccountUID != null) { + int colorCode = AccountsDbAdapter.getActiveAccountColorResource(mAccountUID); + actionBar.setBackgroundDrawable(new ColorDrawable(colorCode)); + if (Build.VERSION.SDK_INT > 20) + getWindow().setStatusBarColor(GnuCashApplication.darken(colorCode)); + } + switch (formType){ + case ACCOUNT: + showAccountFormFragment(intent.getExtras()); + break; + + case TRANSACTION: + showTransactionFormFragment(intent.getExtras()); + break; + + case EXPORT: + showExportFormFragment(null); + break; + + case SPLIT_EDITOR: + showSplitEditorFragment(intent.getExtras()); + break; + + default: + throw new IllegalArgumentException("No form display type specified"); + } + + + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()){ + case android.R.id.home: + setResult(RESULT_CANCELED); + finish(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + /** + * Return the GUID of the account for which the form is displayed. + * If the form is a transaction form, the transaction is created within that account. If it is + * an account form, then the GUID is the parent account + * @return GUID of account + */ + public String getCurrentAccountUID() { + return mAccountUID; + } + + /** + * Shows the form for creating/editing accounts + * @param args Arguments to use for initializing the form. + * This could be an account to edit or a preset for the parent account + */ + private void showAccountFormFragment(Bundle args){ + AccountFormFragment accountFormFragment = AccountFormFragment.newInstance(); + accountFormFragment.setArguments(args); + showFormFragment(accountFormFragment); + } + + /** + * Loads the transaction insert/edit fragment and passes the arguments + * @param args Bundle arguments to be passed to the fragment + */ + private void showTransactionFormFragment(Bundle args){ + TransactionFormFragment transactionFormFragment = new TransactionFormFragment(); + transactionFormFragment.setArguments(args); + showFormFragment(transactionFormFragment); + } + + /** + * Loads the export form fragment and passes the arguments + * @param args Bundle arguments + */ + private void showExportFormFragment(Bundle args){ + ExportFormFragment exportFragment = new ExportFormFragment(); + exportFragment.setArguments(args); + showFormFragment(exportFragment); + } + + /** + * Load the split editor fragment + * @param args View arguments + */ + private void showSplitEditorFragment(Bundle args){ + SplitEditorFragment splitEditor = SplitEditorFragment.newInstance(args); + showFormFragment(splitEditor); + } + + /** + * Loads the fragment into the fragment container, replacing whatever was there before + * @param fragment Fragment to be displayed + */ + private void showFormFragment(Fragment fragment){ + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = fragmentManager + .beginTransaction(); + + fragmentTransaction.add(R.id.fragment_container, fragment); + fragmentTransaction.commit(); + } + + + public void setOnBackListener(CalculatorKeyboard keyboard) { + mOnBackListener = keyboard; + } + + @Override + public void onBackPressed() { + boolean eventProcessed = false; + + if (mOnBackListener != null) + eventProcessed = mOnBackListener.onBackPressed(); + + if (!eventProcessed) + super.onBackPressed(); + } + +} diff --git a/app/src/main/java/org/gnucash/android/ui/UxArgument.java b/app/src/main/java/org/gnucash/android/ui/common/UxArgument.java similarity index 80% rename from app/src/main/java/org/gnucash/android/ui/UxArgument.java rename to app/src/main/java/org/gnucash/android/ui/common/UxArgument.java index d275bc33f..9226a5715 100644 --- a/app/src/main/java/org/gnucash/android/ui/UxArgument.java +++ b/app/src/main/java/org/gnucash/android/ui/common/UxArgument.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.gnucash.android.ui; +package org.gnucash.android.ui.common; /** * Collection of constants which are passed across multiple pieces of the UI (fragments, activities, dialogs) @@ -52,6 +52,11 @@ public final class UxArgument { */ public static final String PASSCODE = "passcode"; + /** + * Key for skipping the passcode screen. Use this only when there is no other choice. + */ + public static final String SKIP_PASSCODE_SCREEN = "skip_passcode_screen"; + /** * Amount passed as a string */ @@ -77,6 +82,22 @@ public final class UxArgument { */ public static final String SCHEDULED_ACTION_UID = "scheduled_action_uid"; + /** + * Type of form displayed in the {@link FormActivity} + */ + public static final String FORM_TYPE = "form_type"; + + /** + * List of splits which have been created using the split editor + */ + public static final String SPLIT_LIST = "split_list"; + + /** + * GUID of splits which have been removed from the split editor + */ + public static String REMOVED_SPLITS = "removed_split_guids"; + + //prevent initialization of instances of this class private UxArgument(){ //prevent even the native class from calling the ctor diff --git a/app/src/main/java/org/gnucash/android/ui/export/ExportDialogFragment.java b/app/src/main/java/org/gnucash/android/ui/export/ExportFormFragment.java similarity index 65% rename from app/src/main/java/org/gnucash/android/ui/export/ExportDialogFragment.java rename to app/src/main/java/org/gnucash/android/ui/export/ExportFormFragment.java index 6df927cbe..0303fde47 100644 --- a/app/src/main/java/org/gnucash/android/ui/export/ExportDialogFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/export/ExportFormFragment.java @@ -16,29 +16,36 @@ 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.os.Bundle; import android.preference.PreferenceManager; -import android.support.v4.app.DialogFragment; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; import android.text.format.Time; import android.util.Log; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; -import android.widget.Button; import android.widget.CheckBox; import android.widget.RadioButton; import android.widget.Spinner; import android.widget.TextView; -import com.doomonafireball.betterpickers.recurrencepicker.EventRecurrence; -import com.doomonafireball.betterpickers.recurrencepicker.EventRecurrenceFormatter; -import com.doomonafireball.betterpickers.recurrencepicker.RecurrencePickerDialog; +import com.codetroopers.betterpickers.recurrencepicker.EventRecurrence; +import com.codetroopers.betterpickers.recurrencepicker.EventRecurrenceFormatter; +import com.codetroopers.betterpickers.recurrencepicker.RecurrencePickerDialog; import com.dropbox.sync.android.DbxAccountManager; import org.gnucash.android.R; @@ -47,56 +54,54 @@ import org.gnucash.android.export.ExportAsyncTask; import org.gnucash.android.export.ExportFormat; import org.gnucash.android.export.ExportParams; +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.SettingsActivity; import org.gnucash.android.ui.util.RecurrenceParser; import java.util.List; -import java.util.UUID; + +import butterknife.Bind; +import butterknife.ButterKnife; /** - * Dialog fragment for exporting account information as OFX files. + * Dialog fragment for exporting accounts and transactions in various formats + *

The dialog is used for collecting information on the export options and then passing them + * to the {@link org.gnucash.android.export.Exporter} responsible for exporting

* @author Ngewi Fet */ -public class ExportDialogFragment extends DialogFragment implements RecurrencePickerDialog.OnRecurrenceSetListener { +public class + ExportFormFragment extends Fragment implements RecurrencePickerDialog.OnRecurrenceSetListener { /** * Spinner for selecting destination for the exported file. * The destination could either be SD card, or another application which * accepts files, like Google Drive. */ - Spinner mDestinationSpinner; + @Bind(R.id.spinner_export_destination) Spinner mDestinationSpinner; /** * Checkbox indicating that all transactions should be exported, * regardless of whether they have been exported previously or not */ - CheckBox mExportAllCheckBox; + @Bind(R.id.checkbox_export_all) CheckBox mExportAllCheckBox; /** * Checkbox for deleting all transactions after exporting them */ - CheckBox mDeleteAllCheckBox; - - /** - * Save button for saving the exported files - */ - Button mSaveButton; - - /** - * Cancels the export dialog - */ - Button mCancelButton; + @Bind(R.id.checkbox_post_export_delete) CheckBox mDeleteAllCheckBox; /** * Text view for showing warnings based on chosen export format */ - TextView mExportWarningTextView; + @Bind(R.id.export_warning) TextView mExportWarningTextView; /** * Recurrence text view */ - TextView mRecurrenceTextView; + @Bind(R.id.input_recurrence) TextView mRecurrenceTextView; /** * Event recurrence options @@ -111,7 +116,7 @@ public class ExportDialogFragment extends DialogFragment implements RecurrencePi /** * Tag for logging */ - private static final String TAG = "ExportDialogFragment"; + private static final String TAG = "ExportFormFragment"; /** * Export format @@ -121,35 +126,6 @@ public class ExportDialogFragment extends DialogFragment implements RecurrencePi private ExportParams.ExportTarget mExportTarget = ExportParams.ExportTarget.SD_CARD; - /** - * Click listener for positive button in the dialog. - * @author Ngewi Fet - */ - protected class ExportClickListener implements View.OnClickListener { - - @Override - public void onClick(View v) { - ExportParams exportParameters = new ExportParams(mExportFormat); - exportParameters.setExportAllTransactions(mExportAllCheckBox.isChecked()); - exportParameters.setExportTarget(mExportTarget); - exportParameters.setDeleteTransactionsAfterExport(mDeleteAllCheckBox.isChecked()); - - List scheduledActions = RecurrenceParser.parse(mEventRecurrence, - ScheduledAction.ActionType.BACKUP); - for (ScheduledAction scheduledAction : scheduledActions) { - scheduledAction.setTag(exportParameters.toCsv()); - scheduledAction.setActionUID(UUID.randomUUID().toString().replaceAll("-", "")); - ScheduledActionDbAdapter.getInstance().addScheduledAction(scheduledAction); - } - - Log.i(TAG, "Commencing async export of transactions"); - new ExportAsyncTask(getActivity()).execute(exportParameters); - - dismiss(); - } - - } - public void onRadioButtonClicked(View view){ switch (view.getId()){ case R.id.radio_ofx_format: @@ -176,6 +152,7 @@ public void onRadioButtonClicked(View view){ case R.id.radio_xml_format: mExportFormat = ExportFormat.XML; mExportWarningTextView.setText(R.string.export_warning_xml); + break; } } @@ -183,7 +160,9 @@ public void onRadioButtonClicked(View view){ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.dialog_export, container, false); + View view = inflater.inflate(R.layout.fragment_export_form, container, false); + ButterKnife.bind(this, view); + return view; } @Override @@ -191,17 +170,90 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.default_save_actions, menu); + MenuItem menuItem = menu.findItem(R.id.menu_save); + menuItem.setTitle(R.string.btn_export); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()){ + case R.id.menu_save: + startExport(); + return true; + + case android.R.id.home: + getActivity().finish(); + return true; + + default: + return super.onOptionsItemSelected(item); + } + } + @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); bindViews(); - getDialog().setTitle(R.string.title_export_dialog); + ActionBar supportActionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + assert supportActionBar != null; + supportActionBar.setTitle(R.string.title_export_dialog); + setHasOptionsMenu(true); + + getSDWritePermission(); + } + + @Override + public void onPause() { + super.onPause(); + // When the user try to export sharing to 3rd party service like DropBox + // then pausing all activities. That cause passcode screen appearing happened. + // We use a disposable flag to skip this unnecessary passcode screen. + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); + 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(){ + ExportParams exportParameters = new ExportParams(mExportFormat); + exportParameters.setExportAllTransactions(mExportAllCheckBox.isChecked()); + exportParameters.setExportTarget(mExportTarget); + exportParameters.setDeleteTransactionsAfterExport(mDeleteAllCheckBox.isChecked()); + + List scheduledActions = RecurrenceParser.parse(mEventRecurrence, + ScheduledAction.ActionType.BACKUP); + for (ScheduledAction scheduledAction : scheduledActions) { + scheduledAction.setTag(exportParameters.toCsv()); + scheduledAction.setActionUID(BaseModel.generateUID()); + ScheduledActionDbAdapter.getInstance().addRecord(scheduledAction); + } + + Log.i(TAG, "Commencing async export of transactions"); + new ExportAsyncTask(getActivity()).execute(exportParameters); + + // finish the activity will cause the progress dialog to be leaked + // which would throw an exception + //getActivity().finish(); } private void bindViews(){ - View v = getView(); - assert v != null; - mDestinationSpinner = (Spinner) v.findViewById(R.id.spinner_export_destination); ArrayAdapter adapter = ArrayAdapter.createFromResource(getActivity(), R.array.export_destinations, android.R.layout.simple_spinner_item); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); @@ -210,7 +262,7 @@ private void bindViews(){ @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { View recurrenceOptionsView = getView().findViewById(R.id.recurrence_options); - switch (position){ + switch (position) { case 0: mExportTarget = ExportParams.ExportTarget.SD_CARD; recurrenceOptionsView.setVisibility(View.VISIBLE); @@ -218,11 +270,11 @@ public void onItemSelected(AdapterView parent, View view, int position, long case 1: recurrenceOptionsView.setVisibility(View.VISIBLE); mExportTarget = ExportParams.ExportTarget.DROPBOX; - String dropboxAppKey = getString(R.string.dropbox_app_key, SettingsActivity.DROPBOX_APP_KEY); - String dropboxAppSecret = getString(R.string.dropbox_app_secret, SettingsActivity.DROPBOX_APP_SECRET); + String dropboxAppKey = getString(R.string.dropbox_app_key, SettingsActivity.DROPBOX_APP_KEY); + String dropboxAppSecret = getString(R.string.dropbox_app_secret, SettingsActivity.DROPBOX_APP_SECRET); DbxAccountManager mDbxAccountManager = DbxAccountManager.getInstance(getActivity().getApplicationContext(), dropboxAppKey, dropboxAppSecret); - if (!mDbxAccountManager.hasLinkedAccount()){ + if (!mDbxAccountManager.hasLinkedAccount()) { mDbxAccountManager.startLink(getActivity(), 0); } break; @@ -249,27 +301,10 @@ public void onNothingSelected(AdapterView parent) { } }); SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); - mExportAllCheckBox = (CheckBox) v.findViewById(R.id.checkbox_export_all); mExportAllCheckBox.setChecked(sharedPrefs.getBoolean(getString(R.string.key_export_all_transactions), false)); - mDeleteAllCheckBox = (CheckBox) v.findViewById(R.id.checkbox_post_export_delete); mDeleteAllCheckBox.setChecked(sharedPrefs.getBoolean(getString(R.string.key_delete_transactions_after_export), false)); - - mSaveButton = (Button) v.findViewById(R.id.btn_save); - mSaveButton.setText(R.string.btn_export); - mCancelButton = (Button) v.findViewById(R.id.btn_cancel); - - mCancelButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - dismiss(); - } - }); - - mSaveButton.setOnClickListener(new ExportClickListener()); - - mRecurrenceTextView = (TextView) v.findViewById(R.id.input_recurrence); mRecurrenceTextView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { @@ -290,16 +325,15 @@ public void onClick(View view) { } rpd = new RecurrencePickerDialog(); rpd.setArguments(b); - rpd.setOnRecurrenceSetListener(ExportDialogFragment.this); + rpd.setOnRecurrenceSetListener(ExportFormFragment.this); rpd.show(fm, "recurrence_picker"); } }); - mExportWarningTextView = (TextView) v.findViewById(R.id.export_warning); - //this part (setting the export format) must come after the recurrence view bindings above String defaultExportFormat = sharedPrefs.getString(getString(R.string.key_default_export_format), ExportFormat.QIF.name()); mExportFormat = ExportFormat.valueOf(defaultExportFormat); + View.OnClickListener clickListener = new View.OnClickListener() { @Override public void onClick(View view) { @@ -307,6 +341,9 @@ public void onClick(View view) { } }; + View v = getView(); + assert v != null; + RadioButton ofxRadioButton = (RadioButton) v.findViewById(R.id.radio_ofx_format); ofxRadioButton.setOnClickListener(clickListener); if (defaultExportFormat.equalsIgnoreCase(ExportFormat.OFX.name())) { @@ -324,6 +361,13 @@ public void onClick(View view) { if (defaultExportFormat.equalsIgnoreCase(ExportFormat.XML.name())){ xmlRadioButton.performClick(); } + + if (GnuCashApplication.isDoubleEntryEnabled()){ + ofxRadioButton.setVisibility(View.GONE); + } else { + xmlRadioButton.setVisibility(View.GONE); + } + } @Override diff --git a/app/src/main/java/org/gnucash/android/ui/export/ScheduledExportListFragment.java b/app/src/main/java/org/gnucash/android/ui/export/ScheduledExportListFragment.java deleted file mode 100644 index 0f3a4ee09..000000000 --- a/app/src/main/java/org/gnucash/android/ui/export/ScheduledExportListFragment.java +++ /dev/null @@ -1,382 +0,0 @@ -/* - * Copyright (c) 2015 Ngewi Fet - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.gnucash.android.ui.export; - -import android.content.Context; -import android.content.res.Resources; -import android.database.Cursor; -import android.graphics.Rect; -import android.os.Bundle; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.Loader; -import android.support.v4.widget.SimpleCursorAdapter; -import android.util.Log; -import android.util.SparseBooleanArray; -import android.view.LayoutInflater; -import android.view.TouchDelegate; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.ListView; -import android.widget.TextView; - -import com.actionbarsherlock.app.ActionBar; -import com.actionbarsherlock.app.SherlockListFragment; -import com.actionbarsherlock.view.ActionMode; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuInflater; -import com.actionbarsherlock.view.MenuItem; - -import org.gnucash.android.R; -import org.gnucash.android.db.DatabaseCursorLoader; -import org.gnucash.android.db.DatabaseSchema; -import org.gnucash.android.db.ScheduledActionDbAdapter; -import org.gnucash.android.export.ExportParams; -import org.gnucash.android.model.ScheduledAction; -import org.gnucash.android.ui.account.AccountsActivity; - -/** - * Fragment for displayed scheduled backup entries in the database - */ -public class ScheduledExportListFragment extends SherlockListFragment implements - LoaderManager.LoaderCallbacks { - - /** - * Logging tag - */ - protected static final String TAG = "ScheduledTrxnFragment"; - - private ScheduledActionDbAdapter mScheduledActionDbAdapter; - private SimpleCursorAdapter mCursorAdapter; - private ActionMode mActionMode = null; - - /** - * Flag which is set when a transaction is selected - */ - private boolean mInEditMode = false; - - - /** - * Callbacks for the menu items in the Context ActionBar (CAB) in action mode - */ - private ActionMode.Callback mActionModeCallbacks = new ActionMode.Callback() { - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - MenuInflater inflater = mode.getMenuInflater(); - inflater.inflate(R.menu.transactions_context_menu, menu); - menu.removeItem(R.id.context_menu_move_transactions); - return true; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - //nothing to see here, move along - return false; - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - finishEditMode(); - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - switch (item.getItemId()) { - case R.id.context_menu_delete: - for (long id : getListView().getCheckedItemIds()) { - Log.i(TAG, "Deleting scheduled export(s)"); - mScheduledActionDbAdapter.deleteRecord(id); - } - mode.finish(); - getLoaderManager().destroyLoader(0); - refreshList(); - return true; - - default: - return false; - } - } - }; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mScheduledActionDbAdapter = ScheduledActionDbAdapter.getInstance(); - mCursorAdapter = new ScheduledExportCursorAdapter( - getActivity().getApplicationContext(), - R.layout.list_item_scheduled_trxn, null, - new String[]{}, new int[]{}); - setListAdapter(mCursorAdapter); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_scheduled_events_list, container, false); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - ActionBar actionBar = getSherlockActivity().getSupportActionBar(); - actionBar.setDisplayShowTitleEnabled(true); - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setHomeButtonEnabled(true); - actionBar.setTitle(R.string.title_scheduled_exports); - - setHasOptionsMenu(true); - getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); - ((TextView)getListView().getEmptyView()).setText(R.string.label_no_scheduled_exports_to_display); - } - - /** - * Reload the list of transactions and recompute account balances - */ - public void refreshList(){ - getLoaderManager().restartLoader(0, null, this); - } - - @Override - public void onResume() { - super.onResume(); - refreshList(); - } - - @Override - public void onListItemClick(ListView l, View v, int position, long id) { - super.onListItemClick(l, v, position, id); - if (mActionMode != null){ - CheckBox checkbox = (CheckBox) v.findViewById(R.id.checkbox); - checkbox.setChecked(!checkbox.isChecked()); - return; - } else { - startActionMode(); - } - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - menu.removeItem(R.id.menu_search); - menu.removeItem(R.id.menu_settings); - inflater.inflate(R.menu.scheduled_export_actions, menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()){ - case R.id.menu_add_scheduled_export: - AccountsActivity.showExportDialog(getActivity()); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - @Override - public Loader onCreateLoader(int arg0, Bundle arg1) { - Log.d(TAG, "Creating transactions loader"); - return new ScheduledExportCursorLoader(getActivity()); - } - - @Override - public void onLoadFinished(Loader loader, Cursor cursor) { - Log.d(TAG, "Scheduled backup loader finished. Swapping in cursor"); - mCursorAdapter.swapCursor(cursor); - mCursorAdapter.notifyDataSetChanged(); - } - - @Override - public void onLoaderReset(Loader loader) { - Log.d(TAG, "Resetting scheduled backup loader"); - mCursorAdapter.swapCursor(null); - } - - /** - * Finishes the edit mode in the list. - * Edit mode is started when at least one list item is selected - */ - public void finishEditMode(){ - mInEditMode = false; - uncheckAllItems(); - mActionMode = null; - } - - /** - * Sets the title of the Context ActionBar when in action mode. - * It sets the number highlighted items - */ - public void setActionModeTitle(){ - int count = getListView().getCheckedItemIds().length; //mSelectedIds.size(); - if (count > 0){ - mActionMode.setTitle(getResources().getString(R.string.title_selected, count)); - } - } - - /** - * Unchecks all the checked items in the list - */ - private void uncheckAllItems() { - SparseBooleanArray checkedPositions = getListView().getCheckedItemPositions(); - ListView listView = getListView(); - for (int i = 0; i < checkedPositions.size(); i++) { - int position = checkedPositions.keyAt(i); - listView.setItemChecked(position, false); - } - } - - - /** - * Starts action mode and activates the Context ActionBar (CAB) - * Action mode is initiated as soon as at least one transaction is selected (highlighted) - */ - private void startActionMode(){ - if (mActionMode != null) { - return; - } - mInEditMode = true; - // Start the CAB using the ActionMode.Callback defined above - mActionMode = getSherlockActivity().startActionMode(mActionModeCallbacks); - } - - /** - * Stops action mode and deselects all selected transactions. - * This method only has effect if the number of checked items is greater than 0 and {@link #mActionMode} is not null - */ - private void stopActionMode(){ - int checkedCount = getListView().getCheckedItemIds().length; - if (checkedCount <= 0 && mActionMode != null) { - mActionMode.finish(); - } - } - - - /** - * Extends a simple cursor adapter to bind transaction attributes to views - * @author Ngewi Fet - */ - protected class ScheduledExportCursorAdapter extends SimpleCursorAdapter { - - public ScheduledExportCursorAdapter(Context context, int layout, Cursor c, - String[] from, int[] to) { - super(context, layout, c, from, to, 0); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - final View view = super.getView(position, convertView, parent); - final int itemPosition = position; - CheckBox checkbox = (CheckBox) view.findViewById(R.id.checkbox); - //TODO: Revisit this if we ever change the application theme - int id = Resources.getSystem().getIdentifier("btn_check_holo_light", "drawable", "android"); - checkbox.setButtonDrawable(id); - - final TextView secondaryText = (TextView) view.findViewById(R.id.secondary_text); - - checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - getListView().setItemChecked(itemPosition, isChecked); - if (isChecked) { - startActionMode(); - } else { - stopActionMode(); - } - setActionModeTitle(); - } - }); - - - ListView listView = (ListView) parent; - if (mInEditMode && listView.isItemChecked(position)){ - view.setBackgroundColor(getResources().getColor(R.color.abs__holo_blue_light)); - secondaryText.setTextColor(getResources().getColor(android.R.color.white)); - } else { - view.setBackgroundColor(getResources().getColor(android.R.color.transparent)); - secondaryText.setTextColor(getResources().getColor(android.R.color.secondary_text_light_nodisable)); - checkbox.setChecked(false); - } - - final View checkBoxView = checkbox; - final View parentView = view; - parentView.post(new Runnable() { - @Override - public void run() { - if (isAdded()){ //may be run when fragment has been unbound from activity - float extraPadding = getResources().getDimension(R.dimen.edge_padding); - final android.graphics.Rect hitRect = new Rect(); - checkBoxView.getHitRect(hitRect); - hitRect.right += extraPadding; - hitRect.bottom += 3*extraPadding; - hitRect.top -= extraPadding; - hitRect.left -= 2*extraPadding; - parentView.setTouchDelegate(new TouchDelegate(hitRect, checkBoxView)); - } - } - }); - - return view; - } - - @Override - public void bindView(View view, Context context, Cursor cursor) { - super.bindView(view, context, cursor); - - ScheduledAction scheduledAction = mScheduledActionDbAdapter.buildScheduledActionInstance(cursor); - - TextView primaryTextView = (TextView) view.findViewById(R.id.primary_text); - ExportParams params = ExportParams.parseCsv(scheduledAction.getTag()); - primaryTextView.setText(params.getExportFormat().name() + " " - + scheduledAction.getActionType().name().toLowerCase() + " to " - + params.getExportTarget().name().toLowerCase()); - - view.findViewById(R.id.right_text).setVisibility(View.GONE); - - TextView descriptionTextView = (TextView) view.findViewById(R.id.secondary_text); - descriptionTextView.setText(scheduledAction.getRepeatString()); - - } - } - - /** - * {@link DatabaseCursorLoader} for loading recurring transactions asynchronously from the database - * @author Ngewi Fet - */ - protected static class ScheduledExportCursorLoader extends DatabaseCursorLoader { - - public ScheduledExportCursorLoader(Context context) { - super(context); - } - - @Override - public Cursor loadInBackground() { - mDatabaseAdapter = ScheduledActionDbAdapter.getInstance(); - - Cursor c = mDatabaseAdapter.fetchAllRecords( - DatabaseSchema.ScheduledActionEntry.COLUMN_TYPE + "=?", - new String[]{ScheduledAction.ActionType.BACKUP.name()}); - - registerContentObserver(c); - return c; - } - } - -} diff --git a/app/src/main/java/org/gnucash/android/ui/widget/WidgetConfigurationActivity.java b/app/src/main/java/org/gnucash/android/ui/homescreen/WidgetConfigurationActivity.java similarity index 94% rename from app/src/main/java/org/gnucash/android/ui/widget/WidgetConfigurationActivity.java rename to app/src/main/java/org/gnucash/android/ui/homescreen/WidgetConfigurationActivity.java index df30e071a..99f55eab7 100644 --- a/app/src/main/java/org/gnucash/android/ui/widget/WidgetConfigurationActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/homescreen/WidgetConfigurationActivity.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.gnucash.android.ui.widget; +package org.gnucash.android.ui.homescreen; import android.app.Activity; import android.app.PendingIntent; @@ -40,7 +40,8 @@ import org.gnucash.android.model.Account; import org.gnucash.android.model.Money; import org.gnucash.android.receivers.TransactionAppWidgetProvider; -import org.gnucash.android.ui.UxArgument; +import org.gnucash.android.ui.common.FormActivity; +import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.account.AccountsActivity; import org.gnucash.android.ui.transaction.TransactionsActivity; import org.gnucash.android.util.QualifiedAccountNameCursorAdapter; @@ -78,10 +79,7 @@ public void onCreate(Bundle savedInstanceState) { finish(); } - SimpleCursorAdapter cursorAdapter = new QualifiedAccountNameCursorAdapter(this, - android.R.layout.simple_spinner_item, - cursor); - cursorAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + SimpleCursorAdapter cursorAdapter = new QualifiedAccountNameCursorAdapter(this, cursor); mAccountsSpinner.setAdapter(cursorAdapter); bindListeners(); @@ -147,7 +145,7 @@ public static void updateWidget(Context context, int appWidgetId, String account AccountsDbAdapter accountsDbAdapter = AccountsDbAdapter.getInstance(); Account account; try { - account = accountsDbAdapter.getAccount(accountUID); + account = accountsDbAdapter.getRecord(accountUID); } catch (IllegalArgumentException e) { Log.i("WidgetConfiguration", "Account not found, resetting widget " + appWidgetId); //if account has been deleted, let the user know @@ -181,15 +179,16 @@ public static void updateWidget(Context context, int appWidgetId, String account Intent accountViewIntent = new Intent(context, TransactionsActivity.class); accountViewIntent.setAction(Intent.ACTION_VIEW); - accountViewIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK); + accountViewIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); accountViewIntent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, accountUID); PendingIntent accountPendingIntent = PendingIntent .getActivity(context, appWidgetId, accountViewIntent, 0); views.setOnClickPendingIntent(R.id.widget_layout, accountPendingIntent); - Intent newTransactionIntent = new Intent(context, TransactionsActivity.class); + Intent newTransactionIntent = new Intent(context, FormActivity.class); newTransactionIntent.setAction(Intent.ACTION_INSERT_OR_EDIT); newTransactionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + newTransactionIntent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.TRANSACTION.name()); newTransactionIntent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, accountUID); PendingIntent pendingIntent = PendingIntent .getActivity(context, appWidgetId, newTransactionIntent, 0); diff --git a/app/src/main/java/org/gnucash/android/ui/passcode/KeyboardFragment.java b/app/src/main/java/org/gnucash/android/ui/passcode/KeyboardFragment.java index 0f456d7a4..2ed59b6ef 100644 --- a/app/src/main/java/org/gnucash/android/ui/passcode/KeyboardFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/passcode/KeyboardFragment.java @@ -19,20 +19,19 @@ import android.app.Activity; import android.os.Bundle; import android.os.Handler; +import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -import com.actionbarsherlock.app.SherlockFragment; - import org.gnucash.android.R; /** * Soft numeric keyboard for lock screen and passcode preference. * @author Oleksandr Tyshkovets */ -public class KeyboardFragment extends SherlockFragment { +public class KeyboardFragment extends Fragment { private static final int DELAY = 500; diff --git a/app/src/main/java/org/gnucash/android/ui/passcode/PassLockActivity.java b/app/src/main/java/org/gnucash/android/ui/passcode/PasscodeLockActivity.java similarity index 64% rename from app/src/main/java/org/gnucash/android/ui/passcode/PassLockActivity.java rename to app/src/main/java/org/gnucash/android/ui/passcode/PasscodeLockActivity.java index 368fd1aff..0934053dc 100644 --- a/app/src/main/java/org/gnucash/android/ui/passcode/PassLockActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/passcode/PasscodeLockActivity.java @@ -19,10 +19,12 @@ import android.content.Intent; import android.content.SharedPreferences; import android.preference.PreferenceManager; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.view.WindowManager.LayoutParams; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.ui.BaseDrawerActivity; -import org.gnucash.android.ui.UxArgument; +import org.gnucash.android.ui.common.UxArgument; /** * This activity used as the parent class for enabling passcode lock @@ -31,28 +33,43 @@ * @see org.gnucash.android.ui.account.AccountsActivity * @see org.gnucash.android.ui.transaction.TransactionsActivity */ -public class PassLockActivity extends BaseDrawerActivity { +public class PasscodeLockActivity extends AppCompatActivity { - private static final String TAG = "PassLockActivity"; + private static final String TAG = "PasscodeLockActivity"; @Override protected void onResume() { super.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); + } + } else { + getWindow().clearFlags(LayoutParams.FLAG_SECURE); + } + // Only for Android Lollipop that brings a few changes to the recent apps feature if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) { GnuCashApplication.PASSCODE_SESSION_INIT_TIME = 0; } - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + + // see ExportFormFragment.onPause() + boolean skipPasscode = prefs.getBoolean(UxArgument.SKIP_PASSCODE_SCREEN, false); + prefs.edit().remove(UxArgument.SKIP_PASSCODE_SCREEN).apply(); String passCode = prefs.getString(UxArgument.PASSCODE, ""); - if (prefs.getBoolean(UxArgument.ENABLED_PASSCODE, false) && !isSessionActive() && !passCode.trim().isEmpty()) { - startActivity(new Intent(this, PasscodeLockScreenActivity.class) + + if (isPassEnabled && !isSessionActive() && !passCode.trim().isEmpty() && !skipPasscode) { + Log.v(TAG, "Show passcode screen"); + Intent intent = new Intent(this, PasscodeLockScreenActivity.class) .setAction(getIntent().getAction()) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK) - .putExtra(UxArgument.PASSCODE_CLASS_CALLER, this.getClass().getName()) - .putExtra(UxArgument.SELECTED_ACCOUNT_UID, - getIntent().getStringExtra(UxArgument.SELECTED_ACCOUNT_UID)) - ); + .putExtra(UxArgument.PASSCODE_CLASS_CALLER, this.getClass().getName()); + if (getIntent().getExtras() != null) + intent.putExtras(getIntent().getExtras()); + startActivity(intent); } } diff --git a/app/src/main/java/org/gnucash/android/ui/passcode/PasscodeLockScreenActivity.java b/app/src/main/java/org/gnucash/android/ui/passcode/PasscodeLockScreenActivity.java index 88c8a574c..607ac8b9b 100644 --- a/app/src/main/java/org/gnucash/android/ui/passcode/PasscodeLockScreenActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/passcode/PasscodeLockScreenActivity.java @@ -19,20 +19,19 @@ import android.content.Intent; import android.os.Bundle; import android.preference.PreferenceManager; +import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.widget.Toast; -import com.actionbarsherlock.app.SherlockFragmentActivity; - import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.ui.UxArgument; +import org.gnucash.android.ui.common.UxArgument; /** * Activity for displaying and managing the passcode lock screen. * @author Oleksandr Tyshkovets */ -public class PasscodeLockScreenActivity extends SherlockFragmentActivity +public class PasscodeLockScreenActivity extends AppCompatActivity implements KeyboardFragment.OnPasscodeEnteredListener { private static final String TAG = "PassLockScreenActivity"; @@ -60,7 +59,7 @@ public void onPasscodeEntered(String pass) { .setClassName(this, getIntent().getStringExtra(UxArgument.PASSCODE_CLASS_CALLER)) .setAction(getIntent().getAction()) .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK) - .putExtra(UxArgument.SELECTED_ACCOUNT_UID, getIntent().getStringExtra(UxArgument.SELECTED_ACCOUNT_UID)) + .putExtras(getIntent().getExtras()) ); } else { Toast.makeText(this, R.string.toast_wrong_passcode, Toast.LENGTH_SHORT).show(); diff --git a/app/src/main/java/org/gnucash/android/ui/passcode/PasscodePreferenceActivity.java b/app/src/main/java/org/gnucash/android/ui/passcode/PasscodePreferenceActivity.java index cd664f0e8..1f134efe5 100644 --- a/app/src/main/java/org/gnucash/android/ui/passcode/PasscodePreferenceActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/passcode/PasscodePreferenceActivity.java @@ -19,19 +19,18 @@ import android.content.Intent; import android.os.Bundle; import android.preference.PreferenceManager; +import android.support.v7.app.AppCompatActivity; import android.widget.TextView; import android.widget.Toast; -import com.actionbarsherlock.app.SherlockFragmentActivity; - import org.gnucash.android.R; -import org.gnucash.android.ui.UxArgument; +import org.gnucash.android.ui.common.UxArgument; /** * Activity for entering and confirming passcode * @author Oleksandr Tyshkovets */ -public class PasscodePreferenceActivity extends SherlockFragmentActivity +public class PasscodePreferenceActivity extends AppCompatActivity implements KeyboardFragment.OnPasscodeEnteredListener { private boolean mIsPassEnabled; diff --git a/app/src/main/java/org/gnucash/android/ui/report/BalanceSheetFragment.java b/app/src/main/java/org/gnucash/android/ui/report/BalanceSheetFragment.java new file mode 100644 index 000000000..79f77a5ee --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/report/BalanceSheetFragment.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.gnucash.android.ui.report; + +import android.database.Cursor; +import android.graphics.Typeface; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v7.app.AppCompatActivity; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TableLayout; +import android.widget.TextView; + +import org.gnucash.android.R; +import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.DatabaseSchema; +import org.gnucash.android.model.AccountType; +import org.gnucash.android.model.Money; +import org.gnucash.android.ui.transaction.TransactionsActivity; + +import java.util.ArrayList; +import java.util.List; + +import butterknife.Bind; +import butterknife.ButterKnife; + +/** + * Fragment report as text + * @author Ngewi Fet + */ +public class BalanceSheetFragment extends Fragment { + + @Bind(R.id.table_assets) TableLayout mAssetsTableLayout; + @Bind(R.id.table_liabilities) TableLayout mLiabilitiesTableLayout; + @Bind(R.id.table_equity) TableLayout mEquityTableLayout; + + @Bind(R.id.total_liability_and_equity) TextView mNetWorth; + + + AccountsDbAdapter mAccountsDbAdapter = AccountsDbAdapter.getInstance(); + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_text_report, container, false); + ButterKnife.bind(this, view); + return view; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + ((AppCompatActivity)getActivity()).getSupportActionBar().setTitle(R.string.title_balance_sheet_report); + setHasOptionsMenu(true); + + List accountTypes = new ArrayList<>(); + accountTypes.add(AccountType.ASSET); + accountTypes.add(AccountType.CASH); + accountTypes.add(AccountType.BANK); + loadAccountViews(accountTypes, mAssetsTableLayout); + Money assetsBalance = mAccountsDbAdapter.getAccountBalance(accountTypes, -1, System.currentTimeMillis()); + + accountTypes.clear(); + accountTypes.add(AccountType.LIABILITY); + accountTypes.add(AccountType.CREDIT); + loadAccountViews(accountTypes, mLiabilitiesTableLayout); + Money liabilitiesBalance = mAccountsDbAdapter.getAccountBalance(accountTypes, -1, System.currentTimeMillis()); + + accountTypes.clear(); + accountTypes.add(AccountType.EQUITY); + loadAccountViews(accountTypes, mEquityTableLayout); + + TransactionsActivity.displayBalance(mNetWorth, assetsBalance.subtract(liabilitiesBalance)); + } + + @Override + public void onResume() { + super.onResume(); + ((ReportsActivity)getActivity()).setAppBarColor(R.color.account_purple); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + menu.findItem(R.id.menu_group_reports_by).setVisible(false); + } + + /** + * Loads rows for the individual accounts and adds them to the report + * @param accountTypes Account types for which to load balances + * @param tableLayout Table layout into which to load the rows + */ + private void loadAccountViews(List accountTypes, TableLayout tableLayout){ + LayoutInflater inflater = LayoutInflater.from(getActivity()); + + Cursor cursor = mAccountsDbAdapter.fetchAccounts(DatabaseSchema.AccountEntry.COLUMN_TYPE + + " IN ( '" + TextUtils.join("' , '", accountTypes) + "' ) AND " + + DatabaseSchema.AccountEntry.COLUMN_PLACEHOLDER + " = 0", + null, DatabaseSchema.AccountEntry.COLUMN_FULL_NAME + " ASC"); + + while (cursor.moveToNext()){ + String accountUID = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.AccountEntry.COLUMN_UID)); + String name = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.AccountEntry.COLUMN_NAME)); + Money balance = mAccountsDbAdapter.getAccountBalance(accountUID); + View view = inflater.inflate(R.layout.row_balance_sheet, tableLayout, false); + ((TextView)view.findViewById(R.id.account_name)).setText(name); + TextView balanceTextView = ((TextView) view.findViewById(R.id.account_balance)); + TransactionsActivity.displayBalance(balanceTextView, balance); + tableLayout.addView(view); + } + + View totalView = inflater.inflate(R.layout.row_balance_sheet, tableLayout, false); + TableLayout.LayoutParams layoutParams = (TableLayout.LayoutParams) totalView.getLayoutParams(); + layoutParams.setMargins(layoutParams.leftMargin, 20, layoutParams.rightMargin, layoutParams.bottomMargin); + totalView.setLayoutParams(layoutParams); + + TextView accountName = (TextView) totalView.findViewById(R.id.account_name); + accountName.setTextSize(16); + accountName.setText(R.string.label_balance_sheet_total); + TextView accountBalance = (TextView) totalView.findViewById(R.id.account_balance); + accountBalance.setTextSize(16); + accountBalance.setTypeface(null, Typeface.BOLD); + TransactionsActivity.displayBalance(accountBalance, mAccountsDbAdapter.getAccountBalance(accountTypes, -1, System.currentTimeMillis())); + + tableLayout.addView(totalView); + } + +} diff --git a/app/src/main/java/org/gnucash/android/ui/chart/BarChartActivity.java b/app/src/main/java/org/gnucash/android/ui/report/BarChartFragment.java similarity index 51% rename from app/src/main/java/org/gnucash/android/ui/chart/BarChartActivity.java rename to app/src/main/java/org/gnucash/android/ui/report/BarChartFragment.java index b66478969..2942a2b52 100644 --- a/app/src/main/java/org/gnucash/android/ui/chart/BarChartActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/report/BarChartFragment.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2015 Oleksandr Tyshkovets + * Copyright (c) 2015 Ngewi Fet * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,42 +15,44 @@ * limitations under the License. */ -package org.gnucash.android.ui.chart; +package org.gnucash.android.ui.report; import android.graphics.Color; import android.os.Bundle; import android.preference.PreferenceManager; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v7.app.AppCompatActivity; import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.LinearLayout; -import android.widget.Spinner; +import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuItem; import com.github.mikephil.charting.charts.BarChart; import com.github.mikephil.charting.components.Legend; import com.github.mikephil.charting.data.BarData; import com.github.mikephil.charting.data.BarDataSet; import com.github.mikephil.charting.data.BarEntry; import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.highlight.Highlight; import com.github.mikephil.charting.listener.OnChartValueSelectedListener; -import com.github.mikephil.charting.utils.Highlight; import com.github.mikephil.charting.utils.LargeValueFormatter; import org.gnucash.android.R; +import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.db.AccountsDbAdapter; import org.gnucash.android.db.TransactionsDbAdapter; import org.gnucash.android.model.Account; import org.gnucash.android.model.AccountType; -import org.gnucash.android.model.Money; -import org.gnucash.android.ui.passcode.PassLockActivity; import org.joda.time.LocalDate; import org.joda.time.LocalDateTime; import org.joda.time.Months; +import org.joda.time.Years; import java.util.ArrayList; import java.util.Arrays; @@ -61,89 +64,145 @@ import java.util.Locale; import java.util.Map; +import butterknife.Bind; +import butterknife.ButterKnife; + +import static org.gnucash.android.ui.report.ReportsActivity.COLORS; +import static org.gnucash.android.ui.report.ReportsActivity.GroupInterval; + /** * Activity used for drawing a bar chart * * @author Oleksandr Tyshkovets + * @author Ngewi Fet */ -public class BarChartActivity extends PassLockActivity implements OnChartValueSelectedListener { +public class BarChartFragment extends Fragment implements OnChartValueSelectedListener, + ReportOptionsListener { - private static final String TAG = "BarChartActivity"; - private static final String X_AXIS_PATTERN = "MMM YY"; + private static final String TAG = "BarChartFragment"; + private static final String X_AXIS_MONTH_PATTERN = "MMM YY"; + private static final String X_AXIS_QUARTER_PATTERN = "Q%d %s"; + private static final String X_AXIS_YEAR_PATTERN = "YYYY"; private static final String SELECTED_VALUE_PATTERN = "%s - %.2f (%.2f %%)"; - private static final int ANIMATION_DURATION = 3000; + private static final int ANIMATION_DURATION = 2000; private static final int NO_DATA_COLOR = Color.LTGRAY; private static final int NO_DATA_BAR_COUNTS = 3; - private static final int[] COLORS = { - Color.parseColor("#17ee4e"), Color.parseColor("#cc1f09"), Color.parseColor("#3940f7"), - Color.parseColor("#f9cd04"), Color.parseColor("#5f33a8"), Color.parseColor("#e005b6"), - Color.parseColor("#17d6ed"), Color.parseColor("#e4a9a2"), Color.parseColor("#8fe6cd"), - Color.parseColor("#8b48fb"), Color.parseColor("#343a36"), Color.parseColor("#6decb1"), - Color.parseColor("#a6dcfd"), Color.parseColor("#5c3378"), Color.parseColor("#a6dcfd"), - Color.parseColor("#ba037c"), Color.parseColor("#708809"), Color.parseColor("#32072c"), - Color.parseColor("#fddef8"), Color.parseColor("#fa0e6e"), Color.parseColor("#d9e7b5") - }; private AccountsDbAdapter mAccountsDbAdapter = AccountsDbAdapter.getInstance(); - private TextView selectedValueTextView; - - private BarChart mChart; + @Bind(R.id.selected_chart_slice) TextView selectedValueTextView; + @Bind(R.id.bar_chart) BarChart mChart; private Currency mCurrency; + private AccountType mAccountType; + private boolean mUseAccountColor = true; private boolean mTotalPercentageMode = true; private boolean mChartDataPresent = true; + /** + * Reporting period start time + */ + private long mReportStartTime = -1; + /** + * Reporting period end time + */ + private long mReportEndTime = -1; + + private GroupInterval mGroupInterval = GroupInterval.MONTH; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_bar_chart, container, false); + ButterKnife.bind(this, view); + return view; + } + + @Override + public void onResume() { + super.onResume(); + ((ReportsActivity)getActivity()).setAppBarColor(R.color.account_red); + } + @Override - protected void onCreate(Bundle savedInstanceState) { - //it is necessary to set the view first before calling super because of the nav drawer in BaseDrawerActivity - setContentView(R.layout.activity_bar_chart); - super.onCreate(savedInstanceState); - getSupportActionBar().setTitle(R.string.title_bar_chart); + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); - selectedValueTextView = (TextView) findViewById(R.id.selected_chart_slice); + ((AppCompatActivity)getActivity()).getSupportActionBar().setTitle(R.string.title_bar_chart); + setHasOptionsMenu(true); - mUseAccountColor = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()) + mUseAccountColor = PreferenceManager.getDefaultSharedPreferences(getActivity()) .getBoolean(getString(R.string.key_use_account_color), false); - mCurrency = Currency.getInstance(PreferenceManager.getDefaultSharedPreferences(this) - .getString(getString(R.string.key_report_currency), Money.DEFAULT_CURRENCY_CODE)); + mCurrency = Currency.getInstance(GnuCashApplication.getDefaultCurrencyCode()); + + ReportsActivity reportsActivity = (ReportsActivity) getActivity(); + mReportStartTime = reportsActivity.getReportStartTime(); + mReportEndTime = reportsActivity.getReportEndTime(); + mAccountType = reportsActivity.getAccountType(); - mChart = new BarChart(this); - ((LinearLayout) findViewById(R.id.bar_chart)).addView(mChart); mChart.setOnChartValueSelectedListener(this); mChart.setDescription(""); - mChart.setDrawValuesForWholeStack(false); +// mChart.setDrawValuesForWholeStack(false); mChart.getXAxis().setDrawGridLines(false); mChart.getAxisRight().setEnabled(false); mChart.getAxisLeft().enableGridDashedLine(4.0f, 4.0f, 0); mChart.getAxisLeft().setValueFormatter(new LargeValueFormatter(mCurrency.getSymbol(Locale.getDefault()))); - mChart.getLegend().setForm(Legend.LegendForm.CIRCLE); - mChart.getLegend().setPosition(Legend.LegendPosition.RIGHT_OF_CHART_INSIDE); + Legend chartLegend = mChart.getLegend(); + chartLegend.setForm(Legend.LegendForm.CIRCLE); + chartLegend.setPosition(Legend.LegendPosition.BELOW_CHART_CENTER); + chartLegend.setWordWrapEnabled(true); - setUpSpinner(); + mChart.setData(getData()); + displayChart(); } + /** * Returns a data object that represents a user data of the specified account types - * @param accountType account's type which will be displayed * @return a {@code BarData} instance that represents a user data */ - private BarData getData(AccountType accountType) { + private BarData getData() { List values = new ArrayList<>(); List labels = new ArrayList<>(); List colors = new ArrayList<>(); Map accountToColorMap = new LinkedHashMap<>(); List xValues = new ArrayList<>(); - LocalDateTime tmpDate = new LocalDateTime(getStartDate(accountType).toDate().getTime()); - for (int i = 0; i <= Months.monthsBetween(getStartDate(accountType), getEndDate(accountType)).getMonths(); i++) { - long start = tmpDate.dayOfMonth().withMinimumValue().millisOfDay().withMinimumValue().toDate().getTime(); - long end = tmpDate.dayOfMonth().withMaximumValue().millisOfDay().withMaximumValue().toDate().getTime(); + LocalDateTime tmpDate = new LocalDateTime(getStartDate(mAccountType).toDate().getTime()); + int count = getDateDiff(new LocalDateTime(getStartDate(mAccountType).toDate().getTime()), + new LocalDateTime(getEndDate(mAccountType).toDate().getTime())); + for (int i = 0; i <= count; i++) { + long start = 0; + long end = 0; + switch (mGroupInterval) { + case MONTH: + start = tmpDate.dayOfMonth().withMinimumValue().millisOfDay().withMinimumValue().toDate().getTime(); + end = tmpDate.dayOfMonth().withMaximumValue().millisOfDay().withMaximumValue().toDate().getTime(); + + xValues.add(tmpDate.toString(X_AXIS_MONTH_PATTERN)); + tmpDate = tmpDate.plusMonths(1); + break; + case QUARTER: + int quarter = getQuarter(tmpDate); + start = tmpDate.withMonthOfYear(quarter * 3 - 2).dayOfMonth().withMinimumValue().millisOfDay().withMinimumValue().toDate().getTime(); + end = tmpDate.withMonthOfYear(quarter * 3).dayOfMonth().withMaximumValue().millisOfDay().withMaximumValue().toDate().getTime(); + + xValues.add(String.format(X_AXIS_QUARTER_PATTERN, quarter, tmpDate.toString(" YY"))); + tmpDate = tmpDate.plusMonths(3); + break; + case YEAR: + start = tmpDate.dayOfYear().withMinimumValue().millisOfDay().withMinimumValue().toDate().getTime(); + end = tmpDate.dayOfYear().withMaximumValue().millisOfDay().withMaximumValue().toDate().getTime(); + + xValues.add(tmpDate.toString(X_AXIS_YEAR_PATTERN)); + tmpDate = tmpDate.plusYears(1); + break; + } List stack = new ArrayList<>(); for (Account account : mAccountsDbAdapter.getSimpleAccountList()) { - if (account.getAccountType() == accountType + if (account.getAccountType() == mAccountType && !account.isPlaceholderAccount() && account.getCurrency() == mCurrency) { @@ -165,20 +224,17 @@ private BarData getData(AccountType accountType) { stack.add((float) balance); labels.add(account.getName()); colors.add(accountToColorMap.get(account.getUID())); - Log.d(TAG, accountType + tmpDate.toString(" MMMM yyyy ") + account.getName() + " = " + stack.get(stack.size() - 1)); + Log.d(TAG, mAccountType + tmpDate.toString(" MMMM yyyy ") + account.getName() + " = " + stack.get(stack.size() - 1)); } } } String stackLabels = labels.subList(labels.size() - stack.size(), labels.size()).toString(); values.add(new BarEntry(floatListToArray(stack), i, stackLabels)); - - xValues.add(tmpDate.toString(X_AXIS_PATTERN)); - - tmpDate = tmpDate.plusMonths(1); } BarDataSet set = new BarDataSet(values, ""); + set.setDrawValues(false); set.setStackLabels(labels.toArray(new String[labels.size()])); set.setColors(colors); @@ -216,7 +272,13 @@ private BarData getEmptyData() { private LocalDate getStartDate(AccountType accountType) { TransactionsDbAdapter adapter = TransactionsDbAdapter.getInstance(); String code = mCurrency.getCurrencyCode(); - LocalDate startDate = new LocalDate(adapter.getTimestampOfEarliestTransaction(accountType, code)).withDayOfMonth(1); + LocalDate startDate; + if (mReportStartTime == -1) { + startDate = new LocalDate(adapter.getTimestampOfEarliestTransaction(accountType, code)); + } else { + startDate = new LocalDate(mReportStartTime); + } + startDate = startDate.withDayOfMonth(1); Log.d(TAG, accountType + " X-axis star date: " + startDate.toString("dd MM yyyy")); return startDate; } @@ -229,11 +291,46 @@ private LocalDate getStartDate(AccountType accountType) { private LocalDate getEndDate(AccountType accountType) { TransactionsDbAdapter adapter = TransactionsDbAdapter.getInstance(); String code = mCurrency.getCurrencyCode(); - LocalDate endDate = new LocalDate(adapter.getTimestampOfLatestTransaction(accountType, code)).withDayOfMonth(1); + LocalDate endDate; + if (mReportEndTime == -1) { + endDate = new LocalDate(adapter.getTimestampOfLatestTransaction(accountType, code)); + } else { + endDate = new LocalDate(mReportEndTime); + } + endDate = endDate.withDayOfMonth(1); Log.d(TAG, accountType + " X-axis end date: " + endDate.toString("dd MM yyyy")); return endDate; } + /** + * Calculates difference between two date values accordingly to {@code mGroupInterval} + * @param start start date + * @param end end date + * @return difference between two dates or {@code -1} + */ + private int getDateDiff(LocalDateTime start, LocalDateTime end) { + switch (mGroupInterval) { + case QUARTER: + int y = Years.yearsBetween(start.withDayOfYear(1).withMillisOfDay(0), end.withDayOfYear(1).withMillisOfDay(0)).getYears(); + return (getQuarter(end) - getQuarter(start) + y * 4); + case MONTH: + return Months.monthsBetween(start.withDayOfMonth(1).withMillisOfDay(0), end.withDayOfMonth(1).withMillisOfDay(0)).getMonths(); + case YEAR: + return Years.yearsBetween(start.withDayOfYear(1).withMillisOfDay(0), end.withDayOfYear(1).withMillisOfDay(0)).getYears(); + default: + return -1; + } + } + + /** + * Returns a quarter of the specified date + * @param date date + * @return a quarter + */ + private int getQuarter(LocalDateTime date) { + return ((date.getMonthOfYear() - 1) / 3 + 1); + } + /** * Converts the specified list of floats to an array * @param list a list of floats @@ -247,36 +344,13 @@ private float[] floatListToArray(List list) { return array; } - /** - * Sets up settings and data for the account type spinner. Currently used only {@code EXPENSE} and {@code INCOME} - * account types. - */ - private void setUpSpinner() { - final Spinner spinner = (Spinner) findViewById(R.id.chart_data_spinner); - ArrayAdapter dataAdapter = new ArrayAdapter<>(this, - android.R.layout.simple_spinner_item, - Arrays.asList(AccountType.EXPENSE, AccountType.INCOME)); - dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - spinner.setAdapter(dataAdapter); - spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView adapterView, View view, int i, long l) { - mChart.setData(getData((AccountType) spinner.getSelectedItem())); - displayChart(); - } - - @Override - public void onNothingSelected(AdapterView adapterView) { - } - }); - } - /** * Displays the stacked bar chart */ private void displayChart() { mChart.highlightValues(null); - mChart.getLegend().setEnabled(false); + setCustomLegend(); + mChart.notifyDataSetChanged(); mChart.getAxisLeft().setDrawLabels(mChartDataPresent); mChart.getXAxis().setDrawLabels(mChartDataPresent); @@ -294,50 +368,92 @@ private void displayChart() { mChart.invalidate(); } + /** + * Sets custom legend. Disable legend if its items count greater than {@code COLORS} array size. + */ + private void setCustomLegend() { + Legend legend = mChart.getLegend(); + BarDataSet dataSet = mChart.getData().getDataSetByIndex(0); + + LinkedHashSet labels = new LinkedHashSet<>(Arrays.asList(dataSet.getStackLabels())); + LinkedHashSet colors = new LinkedHashSet<>(dataSet.getColors()); + + if (COLORS.length >= labels.size()) { + legend.setCustom(new ArrayList<>(colors), new ArrayList<>(labels)); + return; + } + legend.setEnabled(false); + } + + @Override + public void onTimeRangeUpdated(long start, long end) { + if (mReportStartTime != start || mReportEndTime != end) { + mReportStartTime = start; + mReportEndTime = end; + + mChart.setData(getData()); + displayChart(); + } + } + + @Override + public void onGroupingUpdated(GroupInterval groupInterval) { + if (mGroupInterval != groupInterval) { + mGroupInterval = groupInterval; + mChart.setData(getData()); + displayChart(); + } + } + + @Override + public void onAccountTypeUpdated(AccountType accountType) { + if (mAccountType != accountType) { + mAccountType = accountType; + mChart.setData(getData()); + displayChart(); + } + } + @Override - public boolean onCreateOptionsMenu(Menu menu) { - getSupportMenuInflater().inflate(R.menu.chart_actions, menu); - return true; + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.chart_actions, menu); } @Override - public boolean onPrepareOptionsMenu(Menu menu) { + public void onPrepareOptionsMenu(Menu menu) { menu.findItem(R.id.menu_percentage_mode).setVisible(mChartDataPresent); // hide pie/line chart specific menu items menu.findItem(R.id.menu_order_by_size).setVisible(false); menu.findItem(R.id.menu_toggle_labels).setVisible(false); menu.findItem(R.id.menu_toggle_average_lines).setVisible(false); menu.findItem(R.id.menu_group_other_slice).setVisible(false); - return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { + if (item.isCheckable()) + item.setChecked(!item.isChecked()); switch (item.getItemId()) { case R.id.menu_toggle_legend: - // workaround for buggy legend Legend legend = mChart.getLegend(); - legend.setEnabled(!mChart.getLegend().isEnabled()); - BarDataSet dataSet = mChart.getData().getDataSetByIndex(0); - LinkedHashSet labels = new LinkedHashSet<>(Arrays.asList(dataSet.getStackLabels())); - legend.setLabels(labels.toArray(new String[labels.size()])); - LinkedHashSet colors = new LinkedHashSet<>(dataSet.getColors()); - legend.setColors(Arrays.asList(colors.toArray(new Integer[colors.size()]))); - mChart.invalidate(); - break; + if (!legend.isLegendCustom()) { + Toast.makeText(getActivity(), R.string.toast_legend_too_long, Toast.LENGTH_LONG).show(); + } else { + legend.setEnabled(!mChart.getLegend().isEnabled()); + mChart.invalidate(); + } + return true; case R.id.menu_percentage_mode: mTotalPercentageMode = !mTotalPercentageMode; int msgId = mTotalPercentageMode ? R.string.toast_chart_percentage_mode_total : R.string.toast_chart_percentage_mode_current_bar; - Toast.makeText(this, msgId, Toast.LENGTH_LONG).show(); - break; + Toast.makeText(getActivity(), msgId, Toast.LENGTH_LONG).show(); + return true; - case android.R.id.home: - finish(); - break; + default: + return super.onOptionsItemSelected(item); } - return true; } @Override @@ -355,6 +471,6 @@ public void onValueSelected(Entry e, int dataSetIndex, Highlight h) { @Override public void onNothingSelected() { - selectedValueTextView.setText(""); + selectedValueTextView.setText(R.string.label_select_bar_to_view_details); } } diff --git a/app/src/main/java/org/gnucash/android/ui/chart/LineChartActivity.java b/app/src/main/java/org/gnucash/android/ui/report/LineChartFragment.java similarity index 53% rename from app/src/main/java/org/gnucash/android/ui/chart/LineChartActivity.java rename to app/src/main/java/org/gnucash/android/ui/report/LineChartFragment.java index de11b6766..5c9e12aa5 100644 --- a/app/src/main/java/org/gnucash/android/ui/chart/LineChartActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/report/LineChartFragment.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2015 Oleksandr Tyshkovets + * Copyright (c) 2015 Ngewi Fet * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,37 +15,43 @@ * limitations under the License. */ -package org.gnucash.android.ui.chart; +package org.gnucash.android.ui.report; import android.graphics.Color; import android.os.Bundle; -import android.preference.PreferenceManager; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v7.app.AppCompatActivity; import android.util.Log; -import android.widget.LinearLayout; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; import android.widget.TextView; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuItem; import com.github.mikephil.charting.charts.LineChart; import com.github.mikephil.charting.components.Legend; import com.github.mikephil.charting.components.LimitLine; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.data.LineData; import com.github.mikephil.charting.data.LineDataSet; +import com.github.mikephil.charting.highlight.Highlight; import com.github.mikephil.charting.listener.OnChartValueSelectedListener; -import com.github.mikephil.charting.utils.Highlight; import com.github.mikephil.charting.utils.LargeValueFormatter; import org.gnucash.android.R; +import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.db.AccountsDbAdapter; import org.gnucash.android.db.TransactionsDbAdapter; import org.gnucash.android.model.Account; import org.gnucash.android.model.AccountType; -import org.gnucash.android.model.Money; -import org.gnucash.android.ui.passcode.PassLockActivity; +import org.gnucash.android.ui.report.ReportsActivity.GroupInterval; import org.joda.time.LocalDate; import org.joda.time.LocalDateTime; import org.joda.time.Months; +import org.joda.time.Years; import java.util.ArrayList; import java.util.Arrays; @@ -56,14 +63,19 @@ import java.util.Locale; import java.util.Map; +import butterknife.Bind; +import butterknife.ButterKnife; + /** - * Activity used for drawing a line chart + * Fragment for line chart reports * * @author Oleksandr Tyshkovets + * @author Ngewi Fet */ -public class LineChartActivity extends PassLockActivity implements OnChartValueSelectedListener { +public class LineChartFragment extends Fragment implements OnChartValueSelectedListener, + ReportOptionsListener{ - private static final String TAG = "LineChartActivity"; + private static final String TAG = "LineChartFragment"; private static final String X_AXIS_PATTERN = "MMM YY"; private static final String SELECTED_VALUE_PATTERN = "%s - %.2f (%.2f %%)"; private static final int ANIMATION_DURATION = 3000; @@ -78,7 +90,6 @@ public class LineChartActivity extends PassLockActivity implements OnChartValueS Color.parseColor("#0065FF"), Color.parseColor("#8F038A"), }; - private LineChart mChart; private AccountsDbAdapter mAccountsDbAdapter = AccountsDbAdapter.getInstance(); private Map mEarliestTimestampsMap = new HashMap<>(); private Map mLatestTimestampsMap = new HashMap<>(); @@ -87,18 +98,42 @@ public class LineChartActivity extends PassLockActivity implements OnChartValueS private boolean mChartDataPresent = true; private Currency mCurrency; + private GroupInterval mGroupInterval = GroupInterval.MONTH; + + /** + * Reporting period start time + */ + private long mReportStartTime = -1; + + /** + * Reporting period end time + */ + private long mReportEndTime = -1; + + @Bind(R.id.line_chart) LineChart mChart; + @Bind(R.id.selected_chart_slice) TextView mChartSliceInfo; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_line_chart, container, false); + ButterKnife.bind(this, view); + return view; + } + @Override - protected void onCreate(Bundle savedInstanceState) { - //it is necessary to set the view first before calling super because of the nav drawer in BaseDrawerActivity - setContentView(R.layout.activity_line_chart); - super.onCreate(savedInstanceState); - getSupportActionBar().setTitle(R.string.title_line_chart); + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); - mCurrency = Currency.getInstance(PreferenceManager.getDefaultSharedPreferences(this) - .getString(getString(R.string.key_report_currency), Money.DEFAULT_CURRENCY_CODE)); + ((AppCompatActivity)getActivity()).getSupportActionBar().setTitle(R.string.title_line_chart); + setHasOptionsMenu(true); + + mCurrency = Currency.getInstance(GnuCashApplication.getDefaultCurrencyCode()); + + ReportsActivity reportsActivity = (ReportsActivity) getActivity(); + mReportStartTime = reportsActivity.getReportStartTime(); + mReportEndTime = reportsActivity.getReportEndTime(); - mChart = new LineChart(this); - ((LinearLayout) findViewById(R.id.chart)).addView(mChart); mChart.setOnChartValueSelectedListener(this); mChart.setDescription(""); mChart.getXAxis().setDrawGridLines(false); @@ -110,7 +145,8 @@ protected void onCreate(Bundle savedInstanceState) { mChart.setData(getData(new ArrayList<>(Arrays.asList(AccountType.INCOME, AccountType.EXPENSE)))); Legend legend = mChart.getLegend(); - legend.setPosition(Legend.LegendPosition.RIGHT_OF_CHART_INSIDE); + legend.setPosition(Legend.LegendPosition.BELOW_CHART_CENTER); + legend.setTextSize(16); legend.setForm(Legend.LegendForm.CIRCLE); if (!mChartDataPresent) { @@ -118,28 +154,61 @@ protected void onCreate(Bundle savedInstanceState) { mChart.getAxisLeft().setDrawLabels(false); mChart.getXAxis().setDrawLabels(false); mChart.setTouchEnabled(false); - ((TextView) findViewById(R.id.selected_chart_slice)).setText(getResources().getString(R.string.label_chart_no_data)); + mChartSliceInfo.setText(getResources().getString(R.string.label_chart_no_data)); } else { mChart.animateX(ANIMATION_DURATION); } mChart.invalidate(); } + @Override + public void onResume() { + super.onResume(); + ((ReportsActivity)getActivity()).setAppBarColor(R.color.account_blue); + } + /** * Returns a data object that represents a user data of the specified account types * @param accountTypeList account's types which will be displayed * @return a {@code LineData} instance that represents a user data */ private LineData getData(List accountTypeList) { + Log.w(TAG, "getData"); calculateEarliestAndLatestTimestamps(accountTypeList); + // LocalDateTime? + LocalDate startDate; + LocalDate endDate; + if (mReportStartTime == -1 && mReportEndTime == -1) { + startDate = new LocalDate(mEarliestTransactionTimestamp).withDayOfMonth(1); + endDate = new LocalDate(mLatestTransactionTimestamp).withDayOfMonth(1); + } else { + startDate = new LocalDate(mReportStartTime).withDayOfMonth(1); + endDate = new LocalDate(mReportEndTime).withDayOfMonth(1); + } - LocalDate startDate = new LocalDate(mEarliestTransactionTimestamp).withDayOfMonth(1); - LocalDate endDate = new LocalDate(mLatestTransactionTimestamp).withDayOfMonth(1); + int count = getDateDiff(new LocalDateTime(startDate.toDate().getTime()), new LocalDateTime(endDate.toDate().getTime())); + Log.d(TAG, "X-axis count" + count); List xValues = new ArrayList<>(); - while (!startDate.isAfter(endDate)) { - xValues.add(startDate.toString(X_AXIS_PATTERN)); - Log.d(TAG, "X axis " + startDate.toString("MM yy")); - startDate = startDate.plusMonths(1); + for (int i = 0; i <= count; i++) { + switch (mGroupInterval) { + case MONTH: + xValues.add(startDate.toString(X_AXIS_PATTERN)); + Log.d(TAG, "X-axis " + startDate.toString("MM yy")); + startDate = startDate.plusMonths(1); + break; + case QUARTER: + int quarter = getQuarter(new LocalDateTime(startDate.toDate().getTime())); + xValues.add("Q" + quarter + startDate.toString(" yy")); + Log.d(TAG, "X-axis " + "Q" + quarter + startDate.toString(" MM yy")); + startDate = startDate.plusMonths(3); + break; + case YEAR: + xValues.add(startDate.toString("yyyy")); + Log.d(TAG, "X-axis " + startDate.toString("yyyy")); + startDate = startDate.plusYears(1); + break; +// default: + } } List dataSets = new ArrayList<>(); @@ -161,6 +230,35 @@ private LineData getData(List accountTypeList) { return lineData; } + /** + * Calculates difference between two date values accordingly to {@code mGroupInterval} + * @param start start date + * @param end end date + * @return difference between two dates or {@code -1} + */ + private int getDateDiff(LocalDateTime start, LocalDateTime end) { + switch (mGroupInterval) { + case QUARTER: + int y = Years.yearsBetween(start.withDayOfYear(1).withMillisOfDay(0), end.withDayOfYear(1).withMillisOfDay(0)).getYears(); + return (getQuarter(end) - getQuarter(start) + y * 4); + case MONTH: + return Months.monthsBetween(start.withDayOfMonth(1).withMillisOfDay(0), end.withDayOfMonth(1).withMillisOfDay(0)).getMonths(); + case YEAR: + return Years.yearsBetween(start.withDayOfYear(1).withMillisOfDay(0), end.withDayOfYear(1).withMillisOfDay(0)).getYears(); + default: + return -1; + } + } + + /** + * Returns a quarter of the specified date + * @param date date + * @return a quarter + */ + private int getQuarter(LocalDateTime date) { + return ((date.getMonthOfYear() - 1) / 3 + 1); + } + /** * Returns a data object that represents situation when no user data available * @return a {@code LineData} instance for situation when no user data available @@ -196,22 +294,49 @@ private List getEntryList(AccountType accountType) { } } - LocalDateTime earliest = new LocalDateTime(mEarliestTimestampsMap.get(accountType)); - LocalDateTime latest = new LocalDateTime(mLatestTimestampsMap.get(accountType)); + LocalDateTime earliest; + LocalDateTime latest; + if (mReportStartTime == -1 && mReportEndTime == -1) { + earliest = new LocalDateTime(mEarliestTimestampsMap.get(accountType)); + latest = new LocalDateTime(mLatestTimestampsMap.get(accountType)); + } else { + earliest = new LocalDateTime(mReportStartTime); + latest = new LocalDateTime(mReportEndTime); + } Log.d(TAG, "Earliest " + accountType + " date " + earliest.toString("dd MM yyyy")); Log.d(TAG, "Latest " + accountType + " date " + latest.toString("dd MM yyyy")); - int months = Months.monthsBetween(earliest.withDayOfMonth(1).withMillisOfDay(0), - latest.withDayOfMonth(1).withMillisOfDay(0)).getMonths(); - - int offset = getXAxisOffset(accountType); - List values = new ArrayList<>(months + 1); - for (int i = 0; i < months + 1; i++) { - long start = earliest.dayOfMonth().withMinimumValue().millisOfDay().withMinimumValue().toDate().getTime(); - long end = earliest.dayOfMonth().withMaximumValue().millisOfDay().withMaximumValue().toDate().getTime(); + + int xAxisOffset = getDateDiff(new LocalDateTime(mEarliestTransactionTimestamp), earliest); + int count = getDateDiff(earliest, latest); + List values = new ArrayList<>(count + 1); + for (int i = 0; i <= count; i++) { + long start = 0; + long end = 0; + switch (mGroupInterval) { + case QUARTER: + int quarter = getQuarter(earliest); + start = earliest.withMonthOfYear(quarter * 3 - 2).dayOfMonth().withMinimumValue().millisOfDay().withMinimumValue().toDate().getTime(); + end = earliest.withMonthOfYear(quarter * 3).dayOfMonth().withMaximumValue().millisOfDay().withMaximumValue().toDate().getTime(); + + earliest = earliest.plusMonths(3); + break; + case MONTH: + start = earliest.dayOfMonth().withMinimumValue().millisOfDay().withMinimumValue().toDate().getTime(); + end = earliest.dayOfMonth().withMaximumValue().millisOfDay().withMaximumValue().toDate().getTime(); + + earliest = earliest.plusMonths(1); + break; + case YEAR: + start = earliest.dayOfYear().withMinimumValue().millisOfDay().withMinimumValue().toDate().getTime(); + end = earliest.dayOfYear().withMaximumValue().millisOfDay().withMaximumValue().toDate().getTime(); + + earliest = earliest.plusYears(1); + break; + } float balance = (float) mAccountsDbAdapter.getAccountsBalance(accountUIDList, start, end).asDouble(); - values.add(new Entry(balance, i + offset)); + values.add(new Entry(balance, i + xAxisOffset)); Log.d(TAG, accountType + earliest.toString(" MMM yyyy") + ", balance = " + balance); - earliest = earliest.plusMonths(1); + } return values; @@ -222,6 +347,12 @@ private List getEntryList(AccountType accountType) { * @param accountTypeList account's types which will be processed */ private void calculateEarliestAndLatestTimestamps(List accountTypeList) { + if (mReportStartTime != -1 && mReportEndTime != -1) { + mEarliestTransactionTimestamp = mReportStartTime; + mLatestTransactionTimestamp = mReportEndTime; + return; + } + TransactionsDbAdapter dbAdapter = TransactionsDbAdapter.getInstance(); for (Iterator iter = accountTypeList.iterator(); iter.hasNext();) { AccountType type = iter.next(); @@ -246,43 +377,54 @@ private void calculateEarliestAndLatestTimestamps(List accountTypeL mLatestTransactionTimestamp = timestamps.get(timestamps.size() - 1); } - /** - * Returns a difference in months between the global earliest timestamp and the earliest - * transaction's timestamp of the specified account type - * @param accountType the account type - * @return the difference in months - */ - private int getXAxisOffset(AccountType accountType) { - return Months.monthsBetween( - new LocalDate(mEarliestTransactionTimestamp).withDayOfMonth(1), - new LocalDate(mEarliestTimestampsMap.get(accountType)).withDayOfMonth(1) - ).getMonths(); + @Override + public void onTimeRangeUpdated(long start, long end) { + if (mReportStartTime != start || mReportEndTime != end) { + mReportStartTime = start; + mReportEndTime = end; + mChart.setData(getData(new ArrayList<>(Arrays.asList(AccountType.INCOME, AccountType.EXPENSE)))); + mChart.invalidate(); + } + } + + @Override + public void onGroupingUpdated(GroupInterval groupInterval) { + if (mGroupInterval != groupInterval) { + mGroupInterval = groupInterval; + mChart.setData(getData(new ArrayList<>(Arrays.asList(AccountType.INCOME, AccountType.EXPENSE)))); + mChart.invalidate(); + } + } + + @Override + public void onAccountTypeUpdated(AccountType accountType) { + //nothing to see here, line chart shows both income and expense } @Override - public boolean onCreateOptionsMenu(Menu menu) { - getSupportMenuInflater().inflate(R.menu.chart_actions, menu); - return true; + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.chart_actions, menu); } @Override - public boolean onPrepareOptionsMenu(Menu menu) { + public void onPrepareOptionsMenu(Menu menu) { menu.findItem(R.id.menu_toggle_average_lines).setVisible(mChartDataPresent); // hide pie/bar chart specific menu items menu.findItem(R.id.menu_order_by_size).setVisible(false); menu.findItem(R.id.menu_toggle_labels).setVisible(false); menu.findItem(R.id.menu_percentage_mode).setVisible(false); menu.findItem(R.id.menu_group_other_slice).setVisible(false); - return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { + if (item.isCheckable()) + item.setChecked(!item.isChecked()); switch (item.getItemId()) { case R.id.menu_toggle_legend: mChart.getLegend().setEnabled(!mChart.getLegend().isEnabled()); mChart.invalidate(); - break; + return true; case R.id.menu_toggle_average_lines: if (mChart.getAxisLeft().getLimitLines().isEmpty()) { @@ -296,13 +438,11 @@ public boolean onOptionsItemSelected(MenuItem item) { mChart.getAxisLeft().removeAllLimitLines(); } mChart.invalidate(); - break; + return true; - case android.R.id.home: - finish(); - break; + default: + return super.onOptionsItemSelected(item); } - return true; } @Override @@ -311,12 +451,11 @@ public void onValueSelected(Entry e, int dataSetIndex, Highlight h) { String label = mChart.getData().getXVals().get(e.getXIndex()); double value = e.getVal(); double sum = mChart.getData().getDataSetByIndex(dataSetIndex).getYValueSum(); - ((TextView) findViewById(R.id.selected_chart_slice)) - .setText(String.format(SELECTED_VALUE_PATTERN, label, value, value / sum * 100)); + mChartSliceInfo.setText(String.format(SELECTED_VALUE_PATTERN, label, value, value / sum * 100)); } @Override public void onNothingSelected() { - ((TextView) findViewById(R.id.selected_chart_slice)).setText(""); + mChartSliceInfo.setText(""); } } diff --git a/app/src/main/java/org/gnucash/android/ui/report/PieChartFragment.java b/app/src/main/java/org/gnucash/android/ui/report/PieChartFragment.java new file mode 100644 index 000000000..64334bedd --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/report/PieChartFragment.java @@ -0,0 +1,377 @@ +/* + * Copyright (c) 2014-2015 Oleksandr Tyshkovets + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.ui.report; + +import android.content.Context; +import android.graphics.Color; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v7.app.AppCompatActivity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.github.mikephil.charting.charts.PieChart; +import com.github.mikephil.charting.components.Legend.LegendForm; +import com.github.mikephil.charting.components.Legend.LegendPosition; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.PieData; +import com.github.mikephil.charting.data.PieDataSet; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.listener.OnChartValueSelectedListener; + +import org.gnucash.android.R; +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.model.Account; +import org.gnucash.android.model.AccountType; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Currency; +import java.util.List; +import java.util.Locale; + +import butterknife.Bind; +import butterknife.ButterKnife; + +/** + * Activity used for drawing a pie chart + * + * @author Oleksandr Tyshkovets + * @author Ngewi Fet + */ +public class PieChartFragment extends Fragment implements OnChartValueSelectedListener, + ReportOptionsListener { + + public static final String SELECTED_VALUE_PATTERN = "%s - %.2f (%.2f %%)"; + public static final String TOTAL_VALUE_LABEL_PATTERN = "%s\n%.2f %s"; + private static final int ANIMATION_DURATION = 1800; + public static final int NO_DATA_COLOR = Color.LTGRAY; + public static final int CENTER_TEXT_SIZE = 18; + /** + * The space in degrees between the chart slices + */ + public static final float SPACE_BETWEEN_SLICES = 2f; + /** + * All pie slices less than this threshold will be group in "other" slice. Using percents not absolute values. + */ + private static final double GROUPING_SMALLER_SLICES_THRESHOLD = 5; + + @Bind(R.id.pie_chart) PieChart mChart; + @Bind(R.id.selected_chart_slice) TextView mSelectedValueTextView; + + private AccountsDbAdapter mAccountsDbAdapter; + private TransactionsDbAdapter mTransactionsDbAdapter; + + private AccountType mAccountType; + + private boolean mChartDataPresent = true; + + private boolean mUseAccountColor = true; + + private boolean mGroupSmallerSlices = true; + + private String mCurrencyCode; + + /** + * Start time for reporting period in millis + */ + private long mReportStartTime = -1; + + /** + * End time for reporting period in millis + */ + private long mReportEndTime = -1; + + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_pie_chart, container, false); + ButterKnife.bind(this, view); + return view; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + ((AppCompatActivity)getActivity()).getSupportActionBar().setTitle(R.string.title_pie_chart); + setHasOptionsMenu(true); + + mUseAccountColor = PreferenceManager.getDefaultSharedPreferences(getActivity()) + .getBoolean(getString(R.string.key_use_account_color), false); + + mAccountsDbAdapter = AccountsDbAdapter.getInstance(); + mTransactionsDbAdapter = TransactionsDbAdapter.getInstance(); + + mCurrencyCode = GnuCashApplication.getDefaultCurrencyCode(); + + mChart.setCenterTextSize(CENTER_TEXT_SIZE); + mChart.setDescription(""); + mChart.getLegend().setWordWrapEnabled(true); + mChart.setOnChartValueSelectedListener(this); + + ReportsActivity reportsActivity = (ReportsActivity) getActivity(); + mReportStartTime = reportsActivity.getReportStartTime(); + mReportEndTime = reportsActivity.getReportEndTime(); + mAccountType = reportsActivity.getAccountType(); + + displayChart(); + } + + /** + * Sets the app bar color + */ + @Override + public void onResume() { + super.onResume(); + ((ReportsActivity)getActivity()).setAppBarColor(R.color.account_green); + } + + /** + * Manages all actions about displaying the pie chart + */ + private void displayChart() { + mSelectedValueTextView.setText(R.string.label_select_pie_slice_to_see_details); + mChart.highlightValues(null); + mChart.clear(); + + PieData pieData = getData(); + if (pieData != null && pieData.getYValCount() != 0) { + mChartDataPresent = true; + mChart.setData(mGroupSmallerSlices ? groupSmallerSlices(pieData, getActivity()) : pieData); + float sum = mChart.getData().getYValueSum(); + String total = getResources().getString(R.string.label_chart_total); + String currencySymbol = Currency.getInstance(mCurrencyCode).getSymbol(Locale.getDefault()); + mChart.setCenterText(String.format(TOTAL_VALUE_LABEL_PATTERN, total, sum, currencySymbol)); + mChart.animateXY(ANIMATION_DURATION, ANIMATION_DURATION); + } else { + mChartDataPresent = false; + mChart.setCenterText(getResources().getString(R.string.label_chart_no_data)); + mChart.setData(getEmptyData()); + } + + mChart.setTouchEnabled(mChartDataPresent); + mChart.invalidate(); + } + + /** + * Returns {@code PieData} instance with data entries, colors and labels + * @return {@code PieData} instance + */ + private PieData getData() { + PieDataSet dataSet = new PieDataSet(null, ""); + List labels = new ArrayList<>(); + List colors = new ArrayList<>(); + for (Account account : mAccountsDbAdapter.getSimpleAccountList()) { + if (account.getAccountType() == mAccountType + && !account.isPlaceholderAccount() + && account.getCurrency() == Currency.getInstance(mCurrencyCode)) { + + double balance = mAccountsDbAdapter.getAccountsBalance(Collections.singletonList(account.getUID()), + mReportStartTime, mReportEndTime).absolute().asDouble(); + if (balance != 0) { + dataSet.addEntry(new Entry((float) balance, dataSet.getEntryCount())); + colors.add(mUseAccountColor && account.getColorHexCode() != null + ? Color.parseColor(account.getColorHexCode()) + : ReportsActivity.COLORS[(dataSet.getEntryCount() - 1) % ReportsActivity.COLORS.length]); + labels.add(account.getName()); + } + } + } + dataSet.setColors(colors); + dataSet.setSliceSpace(SPACE_BETWEEN_SLICES); + return new PieData(labels, dataSet); + } + + @Override + public void onTimeRangeUpdated(long start, long end) { + if (mReportStartTime != start || mReportEndTime != end) { + mReportStartTime = start; + mReportEndTime = end; + displayChart(); + } + } + + @Override + public void onGroupingUpdated(ReportsActivity.GroupInterval groupInterval) { + //nothing to see here, this doesn't make sense for a pie chart + } + + @Override + public void onAccountTypeUpdated(AccountType accountType) { + if (mAccountType != accountType) { + mAccountType = accountType; + displayChart(); + } + } + + /** + * Returns a data object that represents situation when no user data available + * @return a {@code PieData} instance for situation when no user data available + */ + private PieData getEmptyData() { + PieDataSet dataSet = new PieDataSet(null, getResources().getString(R.string.label_chart_no_data)); + dataSet.addEntry(new Entry(1, 0)); + dataSet.setColor(NO_DATA_COLOR); + dataSet.setDrawValues(false); + return new PieData(Collections.singletonList(""), dataSet); + } + + /** + * Sorts the pie's slices in ascending order + */ + private void bubbleSort() { + List labels = mChart.getData().getXVals(); + List values = mChart.getData().getDataSet().getYVals(); + List colors = mChart.getData().getDataSet().getColors(); + float tmp1; + String tmp2; + Integer tmp3; + for(int i = 0; i < values.size() - 1; i++) { + for(int j = 1; j < values.size() - i; j++) { + if (values.get(j-1).getVal() > values.get(j).getVal()) { + tmp1 = values.get(j - 1).getVal(); + values.get(j - 1).setVal(values.get(j).getVal()); + values.get(j).setVal(tmp1); + + tmp2 = labels.get(j - 1); + labels.set(j - 1, labels.get(j)); + labels.set(j, tmp2); + + tmp3 = colors.get(j - 1); + colors.set(j - 1, colors.get(j)); + colors.set(j, tmp3); + } + } + } + + mChart.notifyDataSetChanged(); + mChart.highlightValues(null); + mChart.invalidate(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.chart_actions, menu); + menu.findItem(R.id.menu_toggle_legend).setChecked(false); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + menu.findItem(R.id.menu_order_by_size).setVisible(mChartDataPresent); + menu.findItem(R.id.menu_toggle_labels).setVisible(mChartDataPresent); + menu.findItem(R.id.menu_group_other_slice).setVisible(mChartDataPresent); + // hide line/bar chart specific menu items + menu.findItem(R.id.menu_percentage_mode).setVisible(false); + menu.findItem(R.id.menu_toggle_average_lines).setVisible(false); + menu.findItem(R.id.menu_group_reports_by).setVisible(false); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.isCheckable()) + item.setChecked(!item.isChecked()); + switch (item.getItemId()) { + case R.id.menu_order_by_size: { + bubbleSort(); + return true; + } + case R.id.menu_toggle_legend: { + mChart.getLegend().setEnabled(!mChart.getLegend().isEnabled()); + mChart.getLegend().setForm(LegendForm.CIRCLE); + mChart.getLegend().setPosition(LegendPosition.RIGHT_OF_CHART_CENTER); + mChart.notifyDataSetChanged(); + mChart.invalidate(); + return true; + } + case R.id.menu_toggle_labels: { + mChart.getData().setDrawValues(!mChart.isDrawSliceTextEnabled()); + mChart.setDrawSliceText(!mChart.isDrawSliceTextEnabled()); + mChart.invalidate(); + return true; + } + case R.id.menu_group_other_slice: { + mGroupSmallerSlices = !mGroupSmallerSlices; + displayChart(); + return true; + } + + default: + return super.onOptionsItemSelected(item); + } + } + + /** + * Groups smaller slices. All smaller slices will be combined and displayed as a single "Other". + * @param data the pie data which smaller slices will be grouped + * @param context Context for retrieving resources + * @return a {@code PieData} instance with combined smaller slices + */ + public static PieData groupSmallerSlices(PieData data, Context context) { + float otherSlice = 0f; + List newEntries = new ArrayList<>(); + List newLabels = new ArrayList<>(); + List newColors = new ArrayList<>(); + List entries = data.getDataSet().getYVals(); + for (int i = 0; i < entries.size(); i++) { + float val = entries.get(i).getVal(); + if (val / data.getYValueSum() * 100 > GROUPING_SMALLER_SLICES_THRESHOLD) { + newEntries.add(new Entry(val, newEntries.size())); + newLabels.add(data.getXVals().get(i)); + newColors.add(data.getDataSet().getColors().get(i)); + } else { + otherSlice += val; + } + } + + if (otherSlice > 0) { + newEntries.add(new Entry(otherSlice, newEntries.size())); + newLabels.add(context.getResources().getString(R.string.label_other_slice)); + newColors.add(Color.LTGRAY); + } + + PieDataSet dataSet = new PieDataSet(newEntries, ""); + dataSet.setSliceSpace(SPACE_BETWEEN_SLICES); + dataSet.setColors(newColors); + return new PieData(newLabels, dataSet); + } + + @Override + public void onValueSelected(Entry e, int dataSetIndex, Highlight h) { + if (e == null) return; + String label = mChart.getData().getXVals().get(e.getXIndex()); + float value = e.getVal(); + float percent = value / mChart.getYValueSum() * 100; + mSelectedValueTextView.setText(String.format(SELECTED_VALUE_PATTERN, label, value, percent)); + } + + @Override + public void onNothingSelected() { + mSelectedValueTextView.setText(""); + } +} diff --git a/app/src/main/java/org/gnucash/android/ui/report/ReportOptionsListener.java b/app/src/main/java/org/gnucash/android/ui/report/ReportOptionsListener.java new file mode 100644 index 000000000..7dfc1c717 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/report/ReportOptionsListener.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.gnucash.android.ui.report; + +import org.gnucash.android.model.AccountType; + +/** + * Listener interface for passing reporting options from activity to the report fragments + */ +public interface ReportOptionsListener { + + /** + * Notify the implementing class of the selected date range + * @param start Start date in milliseconds since epoch + * @param end End date in milliseconds since epoch + */ + void onTimeRangeUpdated(long start, long end); + + /** + * Updates the listener on a change of the grouping for the report + * @param groupInterval Group interval + */ + void onGroupingUpdated(ReportsActivity.GroupInterval groupInterval); + + /** + * Update to the account type for the report + * @param accountType Account type + */ + void onAccountTypeUpdated(AccountType accountType); +} diff --git a/app/src/main/java/org/gnucash/android/ui/report/ReportSummaryFragment.java b/app/src/main/java/org/gnucash/android/ui/report/ReportSummaryFragment.java new file mode 100644 index 000000000..f41a2ae56 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/report/ReportSummaryFragment.java @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.gnucash.android.ui.report; + +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.view.ViewCompat; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.AppCompatButton; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import com.github.mikephil.charting.charts.PieChart; +import com.github.mikephil.charting.components.Legend; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.PieData; +import com.github.mikephil.charting.data.PieDataSet; + +import org.gnucash.android.R; +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.model.Account; +import org.gnucash.android.model.AccountType; +import org.gnucash.android.model.Money; +import org.gnucash.android.ui.transaction.TransactionsActivity; +import org.joda.time.LocalDate; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Currency; +import java.util.List; +import java.util.Locale; + +import butterknife.Bind; +import butterknife.ButterKnife; + +/** + * Shows a summary of reports + * @author Ngewi Fet + */ +public class ReportSummaryFragment extends Fragment { + + public static final int LEGEND_TEXT_SIZE = 14; + + @Bind(R.id.btn_pie_chart) Button mPieChartButton; + @Bind(R.id.btn_bar_chart) Button mBarChartButton; + @Bind(R.id.btn_line_chart) Button mLineChartButton; + @Bind(R.id.btn_balance_sheet) Button mBalanceSheetButton; + + @Bind(R.id.pie_chart) PieChart mChart; + @Bind(R.id.total_assets) TextView mTotalAssets; + @Bind(R.id.total_liabilities) TextView mTotalLiabilities; + @Bind(R.id.net_worth) TextView mNetWorth; + + private AccountsDbAdapter mAccountsDbAdapter; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mAccountsDbAdapter = AccountsDbAdapter.getInstance(); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_report_summary, container, false); + ButterKnife.bind(this, view); + + mPieChartButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + loadFragment(new PieChartFragment()); + } + }); + + mLineChartButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + loadFragment(new LineChartFragment()); + } + }); + + mBarChartButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + loadFragment(new BarChartFragment()); + } + }); + + mBalanceSheetButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + loadFragment(new BalanceSheetFragment()); + } + }); + + return view; + } + + @Override + public void onResume() { + super.onResume(); + ((AppCompatActivity)getActivity()).getSupportActionBar().setTitle(R.string.title_reports); + ((ReportsActivity)getActivity()).setAppBarColor(R.color.theme_primary); + + getActivity().findViewById(R.id.time_range_layout).setVisibility(View.GONE); + getActivity().findViewById(R.id.date_range_divider).setVisibility(View.GONE); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + setHasOptionsMenu(true); + + mChart.setCenterTextSize(PieChartFragment.CENTER_TEXT_SIZE); + mChart.setDescription(""); + mChart.getLegend().setEnabled(true); + mChart.getLegend().setPosition(Legend.LegendPosition.RIGHT_OF_CHART_CENTER); + mChart.getLegend().setTextSize(LEGEND_TEXT_SIZE); + + ColorStateList csl = new ColorStateList(new int[][]{new int[0]}, new int[]{getResources().getColor(R.color.account_green)}); + setButtonTint(mPieChartButton, csl); + csl = new ColorStateList(new int[][]{new int[0]}, new int[]{getResources().getColor(R.color.account_red)}); + setButtonTint(mBarChartButton, csl); + csl = new ColorStateList(new int[][]{new int[0]}, new int[]{getResources().getColor(R.color.account_blue)}); + setButtonTint(mLineChartButton, csl); + csl = new ColorStateList(new int[][]{new int[0]}, new int[]{getResources().getColor(R.color.account_purple)}); + setButtonTint(mBalanceSheetButton, csl); + + + List accountTypes = new ArrayList<>(); + accountTypes.add(AccountType.ASSET); + accountTypes.add(AccountType.CASH); + accountTypes.add(AccountType.BANK); + Money assetsBalance = mAccountsDbAdapter.getAccountBalance(accountTypes, -1, System.currentTimeMillis()); + + accountTypes.clear(); + accountTypes.add(AccountType.LIABILITY); + accountTypes.add(AccountType.CREDIT); + Money liabilitiesBalance = mAccountsDbAdapter.getAccountBalance(accountTypes, -1, System.currentTimeMillis()); + + TransactionsActivity.displayBalance(mTotalAssets, assetsBalance); + TransactionsActivity.displayBalance(mTotalLiabilities, liabilitiesBalance); + TransactionsActivity.displayBalance(mNetWorth, assetsBalance.subtract(liabilitiesBalance)); + + displayChart(); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + menu.findItem(R.id.menu_group_reports_by).setVisible(false); + } + + /** + * Returns {@code PieData} instance with data entries, colors and labels + * @return {@code PieData} instance + */ + private PieData getData() { + String mCurrencyCode = GnuCashApplication.getDefaultCurrencyCode(); + PieDataSet dataSet = new PieDataSet(null, ""); + List labels = new ArrayList<>(); + List colors = new ArrayList<>(); + for (Account account : mAccountsDbAdapter.getSimpleAccountList()) { + if (account.getAccountType() == AccountType.EXPENSE + && !account.isPlaceholderAccount() + && account.getCurrency() == Currency.getInstance(mCurrencyCode)) { + + long start = new LocalDate().minusMonths(2).dayOfMonth().withMinimumValue().toDate().getTime(); + long end = new LocalDate().plusDays(1).toDate().getTime(); + double balance = mAccountsDbAdapter.getAccountsBalance(Collections.singletonList(account.getUID()), start, end).absolute().asDouble(); + if (balance != 0) { + dataSet.addEntry(new Entry((float) balance, dataSet.getEntryCount())); + colors.add(account.getColorHexCode() != null + ? Color.parseColor(account.getColorHexCode()) + : ReportsActivity.COLORS[(dataSet.getEntryCount() - 1) % ReportsActivity.COLORS.length]); + labels.add(account.getName()); + } + } + } + dataSet.setColors(colors); + dataSet.setSliceSpace(PieChartFragment.SPACE_BETWEEN_SLICES); + return new PieData(labels, dataSet); + } + + /** + * Manages all actions about displaying the pie chart + */ + private void displayChart() { + mChart.highlightValues(null); + mChart.clear(); + + PieData pieData = PieChartFragment.groupSmallerSlices(getData(), getActivity()); + if (pieData != null && pieData.getYValCount() != 0) { + mChart.setData(pieData); + float sum = mChart.getData().getYValueSum(); + String total = getResources().getString(R.string.label_chart_total); + String currencySymbol = Currency.getInstance(GnuCashApplication.getDefaultCurrencyCode()).getSymbol(Locale.getDefault()); + mChart.setCenterText(String.format(PieChartFragment.TOTAL_VALUE_LABEL_PATTERN, total, sum, currencySymbol)); + mChart.animateXY(1800, 1800); + mChart.setTouchEnabled(true); + } else { + mChart.setData(getEmptyData()); + } + + mChart.invalidate(); + } + + /** + * Returns a data object that represents situation when no user data available + * @return a {@code PieData} instance for situation when no user data available + */ + private PieData getEmptyData() { + PieDataSet dataSet = new PieDataSet(null, getResources().getString(R.string.label_chart_no_data)); + dataSet.addEntry(new Entry(1, 0)); + dataSet.setColor(PieChartFragment.NO_DATA_COLOR); + dataSet.setDrawValues(false); + return new PieData(Collections.singletonList(""), dataSet); + } + + + public void setButtonTint(Button button, ColorStateList tint) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP && button instanceof AppCompatButton) { + ((AppCompatButton) button).setSupportBackgroundTintList(tint); + } else { + ViewCompat.setBackgroundTintList(button, tint); + } + button.setTextColor(getResources().getColor(android.R.color.white)); + } + + private void loadFragment(Fragment fragment){ + FragmentManager fragmentManager = getActivity().getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + + fragmentTransaction.replace(R.id.fragment_container, fragment); + fragmentTransaction.addToBackStack(null); + fragmentTransaction.commit(); + } +} diff --git a/app/src/main/java/org/gnucash/android/ui/report/ReportsActivity.java b/app/src/main/java/org/gnucash/android/ui/report/ReportsActivity.java new file mode 100644 index 000000000..a01c9be5a --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/report/ReportsActivity.java @@ -0,0 +1,320 @@ +/* + * Copyright (c) 2015 Oleksandr Tyshkovets + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.ui.report; + +import android.app.DatePickerDialog; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.DatePicker; +import android.widget.Spinner; + +import org.gnucash.android.R; +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.model.AccountType; +import org.gnucash.android.ui.common.BaseDrawerActivity; +import org.gnucash.android.ui.report.dialog.DateRangePickerDialogFragment; +import org.joda.time.LocalDate; + +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +import butterknife.Bind; +import butterknife.ButterKnife; + +/** + * base activity for reporting + * + * @author Oleksandr Tyshkovets + * @author Ngewi Fet + */ +public class ReportsActivity extends BaseDrawerActivity implements AdapterView.OnItemSelectedListener, + DatePickerDialog.OnDateSetListener, DateRangePickerDialogFragment.OnDateRangeSetListener{ + + static final int[] COLORS = { + Color.parseColor("#17ee4e"), Color.parseColor("#cc1f09"), Color.parseColor("#3940f7"), + Color.parseColor("#f9cd04"), Color.parseColor("#5f33a8"), Color.parseColor("#e005b6"), + Color.parseColor("#17d6ed"), Color.parseColor("#e4a9a2"), Color.parseColor("#8fe6cd"), + Color.parseColor("#8b48fb"), Color.parseColor("#343a36"), Color.parseColor("#6decb1"), + Color.parseColor("#f0f8ff"), Color.parseColor("#5c3378"), Color.parseColor("#a6dcfd"), + Color.parseColor("#ba037c"), Color.parseColor("#708809"), Color.parseColor("#32072c"), + Color.parseColor("#fddef8"), Color.parseColor("#fa0e6e"), Color.parseColor("#d9e7b5") + }; + + @Bind(R.id.time_range_spinner) Spinner mTimeRangeSpinner; + @Bind(R.id.report_account_type_spinner) Spinner mAccountTypeSpinner; + + private TransactionsDbAdapter mTransactionsDbAdapter; + private AccountType mAccountType = AccountType.EXPENSE; + + public enum GroupInterval {WEEK, MONTH, QUARTER, YEAR, ALL} + + // default time range is the last 3 months + private long mReportStartTime = new LocalDate().minusMonths(2).dayOfMonth().withMinimumValue().toDate().getTime(); + private long mReportEndTime = new LocalDate().plusDays(1).toDate().getTime(); + + private GroupInterval mReportGroupInterval = GroupInterval.MONTH; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_reports); + setUpDrawer(); + ButterKnife.bind(this); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + ActionBar actionBar = getSupportActionBar(); + assert actionBar != null; + actionBar.setTitle(R.string.title_reports); + actionBar.setDisplayHomeAsUpEnabled(true); + + mTransactionsDbAdapter = TransactionsDbAdapter.getInstance(); + + + ArrayAdapter adapter = ArrayAdapter.createFromResource(this, R.array.report_time_range, + android.R.layout.simple_spinner_item); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mTimeRangeSpinner.setAdapter(adapter); + mTimeRangeSpinner.setOnItemSelectedListener(this); + mTimeRangeSpinner.setSelection(1); + + ArrayAdapter dataAdapter = new ArrayAdapter<>(this, + android.R.layout.simple_spinner_item, + Arrays.asList(AccountType.EXPENSE, AccountType.INCOME)); + dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mAccountTypeSpinner.setAdapter(dataAdapter); + mAccountTypeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView adapterView, View view, int i, long l) { + mAccountType = (AccountType) mAccountTypeSpinner.getSelectedItem(); + updateAccountTypeOnFragments(); + } + + @Override + public void onNothingSelected(AdapterView adapterView) { + //nothing to see here, move along + } + }); + + if (savedInstanceState == null) { + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = fragmentManager + .beginTransaction(); + + fragmentTransaction.replace(R.id.fragment_container, new ReportSummaryFragment()); + fragmentTransaction.commit(); + } + } + + @Override + public void onAttachFragment(Fragment fragment) { + super.onAttachFragment(fragment); + View timeRangeLayout = findViewById(R.id.time_range_layout); + View dateRangeDivider = findViewById(R.id.date_range_divider); + if (timeRangeLayout != null && dateRangeDivider != null) { + if (fragment instanceof ReportSummaryFragment || fragment instanceof BalanceSheetFragment) { + timeRangeLayout.setVisibility(View.GONE); + dateRangeDivider.setVisibility(View.GONE); + } else { + timeRangeLayout.setVisibility(View.VISIBLE); + dateRangeDivider.setVisibility(View.VISIBLE); + } + } + View accountTypeSpinner = findViewById(R.id.report_account_type_spinner); + if (accountTypeSpinner != null) { + if (fragment instanceof LineChartFragment) { + accountTypeSpinner.setVisibility(View.GONE); + } else { + accountTypeSpinner.setVisibility(View.VISIBLE); + } + } + } + + /** + * Sets the color Action Bar and Status bar (where applicable) + */ + public void setAppBarColor(int color) { + int resolvedColor = getResources().getColor(color); + if (getSupportActionBar() != null) + getSupportActionBar().setBackgroundDrawable(new ColorDrawable(resolvedColor)); + + if (Build.VERSION.SDK_INT > 20) + getWindow().setStatusBarColor(GnuCashApplication.darken(resolvedColor)); + } + + /** + * Updates the reporting time range for all listening fragments + */ + private void updateDateRangeOnFragment(){ + List fragments = getSupportFragmentManager().getFragments(); + for (Fragment fragment : fragments) { + if (fragment instanceof ReportOptionsListener){ + ((ReportOptionsListener) fragment).onTimeRangeUpdated(mReportStartTime, mReportEndTime); + } + } + } + + /** + * Updates the account type for all attached fragments which are listening + */ + private void updateAccountTypeOnFragments(){ + List fragments = getSupportFragmentManager().getFragments(); + for (Fragment fragment : fragments) { + if (fragment instanceof ReportOptionsListener){ + ((ReportOptionsListener) fragment).onAccountTypeUpdated(mAccountType); + } + } + } + + /** + * Updates the report grouping interval on all attached fragments which are listening + */ + private void updateGroupingOnFragments(){ + List fragments = getSupportFragmentManager().getFragments(); + for (Fragment fragment : fragments) { + if (fragment instanceof ReportOptionsListener){ + ((ReportOptionsListener) fragment).onGroupingUpdated(mReportGroupInterval); + } + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.report_actions, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()){ + case R.id.menu_group_reports_by: + return true; + + case R.id.group_by_month: + item.setChecked(true); + mReportGroupInterval = GroupInterval.MONTH; + updateGroupingOnFragments(); + return true; + + case R.id.group_by_quarter: + item.setChecked(true); + mReportGroupInterval = GroupInterval.QUARTER; + updateGroupingOnFragments(); + return true; + + case R.id.group_by_year: + item.setChecked(true); + mReportGroupInterval = GroupInterval.YEAR; + updateGroupingOnFragments(); + return true; + + case android.R.id.home: + super.onOptionsItemSelected(item); + + default: + return false; + } + + } + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + mReportEndTime = new LocalDate().plusDays(1).toDate().getTime(); + switch (position){ + case 0: //current month + mReportStartTime = new LocalDate().dayOfMonth().withMinimumValue().toDate().getTime(); + break; + case 1: // last 3 months. x-2, x-1, x + mReportStartTime = new LocalDate().minusMonths(2).dayOfMonth().withMinimumValue().toDate().getTime(); + break; + case 2: + mReportStartTime = new LocalDate().minusMonths(5).dayOfMonth().withMinimumValue().toDate().getTime(); + break; + case 3: + mReportStartTime = new LocalDate().minusMonths(11).dayOfMonth().withMinimumValue().toDate().getTime(); + break; + case 4: //ALL TIME + mReportStartTime = -1; + mReportEndTime = -1; + break; + case 5: + String mCurrencyCode = GnuCashApplication.getDefaultCurrencyCode(); + long earliestTransactionTime = mTransactionsDbAdapter.getTimestampOfEarliestTransaction(mAccountType, mCurrencyCode); + DialogFragment rangeFragment = DateRangePickerDialogFragment.newInstance( + earliestTransactionTime, + new LocalDate().plusDays(1).toDate().getTime(), + this); + rangeFragment.show(getSupportFragmentManager(), "range_dialog"); + break; + } + if (position != 5){ //the date picker will trigger the update itself + updateDateRangeOnFragment(); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + //nothing to see here, move along + } + + @Override + public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) { + Calendar calendar = Calendar.getInstance(); + calendar.set(year, monthOfYear, dayOfMonth); + mReportStartTime = calendar.getTimeInMillis(); + updateDateRangeOnFragment(); + } + + @Override + public void onDateRangeSet(Date startDate, Date endDate) { + mReportStartTime = startDate.getTime(); + mReportEndTime = endDate.getTime(); + updateDateRangeOnFragment(); + + } + + public AccountType getAccountType(){ + return mAccountType; + } + + public long getReportEndTime() { + return mReportEndTime; + } + + public long getReportStartTime() { + return mReportStartTime; + } + +} diff --git a/app/src/main/java/org/gnucash/android/ui/report/dialog/DateRangePickerDialogFragment.java b/app/src/main/java/org/gnucash/android/ui/report/dialog/DateRangePickerDialogFragment.java new file mode 100644 index 000000000..7681e7a08 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/report/dialog/DateRangePickerDialogFragment.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.ui.report.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +import com.squareup.timessquare.CalendarPickerView; + +import org.gnucash.android.R; +import org.joda.time.LocalDate; + +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +import butterknife.Bind; +import butterknife.ButterKnife; + +/** + * Dialog for picking date ranges in terms of months. + * It is currently used for selecting ranges for reports + * @author Ngewi Fet + */ +public class DateRangePickerDialogFragment extends DialogFragment{ + + @Bind(R.id.calendar_view) CalendarPickerView mCalendarPickerView; + @Bind(R.id.btn_save) Button mDoneButton; + @Bind(R.id.btn_cancel) Button mCancelButton; + + private Date mStartRange = LocalDate.now().minusMonths(1).toDate(); + private Date mEndRange = LocalDate.now().toDate(); + private OnDateRangeSetListener mDateRangeSetListener; + + public static DateRangePickerDialogFragment newInstance(OnDateRangeSetListener dateRangeSetListener){ + DateRangePickerDialogFragment fragment = new DateRangePickerDialogFragment(); + fragment.mDateRangeSetListener = dateRangeSetListener; + return fragment; + } + + public static DateRangePickerDialogFragment newInstance(long startDate, long endDate, + OnDateRangeSetListener dateRangeSetListener){ + DateRangePickerDialogFragment fragment = new DateRangePickerDialogFragment(); + fragment.mStartRange = new Date(startDate); + fragment.mEndRange = new Date(endDate); + fragment.mDateRangeSetListener = dateRangeSetListener; + return fragment; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.dialog_date_range_picker, container, false); + ButterKnife.bind(this, view); + + + Calendar nextYear = Calendar.getInstance(); + nextYear.add(Calendar.YEAR, 1); + + Date today = new Date(); + mCalendarPickerView.init(mStartRange, mEndRange) + .inMode(CalendarPickerView.SelectionMode.RANGE) + .withSelectedDate(today); + + mDoneButton.setText("Done"); + mDoneButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + List selectedDates = mCalendarPickerView.getSelectedDates(); + Date startDate = selectedDates.get(0); + Date endDate = selectedDates.size() == 2 ? selectedDates.get(1) : new Date(); + mDateRangeSetListener.onDateRangeSet(startDate, endDate); + dismiss(); + } + }); + + mCancelButton.setOnClickListener(new View.OnClickListener(){ + @Override + public void onClick(View v) { + dismiss(); + } + }); + return view; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.setTitle("Pick time range"); + return dialog; + } + + public interface OnDateRangeSetListener { + void onDateRangeSet(Date startDate, Date endDate); + } +} diff --git a/app/src/main/java/org/gnucash/android/ui/settings/AboutPreferenceFragment.java b/app/src/main/java/org/gnucash/android/ui/settings/AboutPreferenceFragment.java index 18f39dae0..6194f0c38 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/AboutPreferenceFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/AboutPreferenceFragment.java @@ -17,15 +17,12 @@ package org.gnucash.android.ui.settings; import android.annotation.TargetApi; -import android.content.SharedPreferences; import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceFragment; -import android.preference.PreferenceManager; - -import com.actionbarsherlock.app.ActionBar; -import com.actionbarsherlock.app.SherlockPreferenceActivity; +import android.support.v7.app.ActionBar; +import org.gnucash.android.BuildConfig; import org.gnucash.android.R; import org.gnucash.android.ui.account.AccountsActivity; @@ -42,7 +39,7 @@ public class AboutPreferenceFragment extends PreferenceFragment{ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.fragment_about_preferences); - ActionBar actionBar = ((SherlockPreferenceActivity) getActivity()).getSupportActionBar(); + ActionBar actionBar = ((AppCompatPreferenceActivity) getActivity()).getSupportActionBar(); actionBar.setHomeButtonEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.title_about_gnucash); @@ -52,7 +49,10 @@ public void onCreate(Bundle savedInstanceState) { @Override public void onResume() { super.onResume(); - Preference pref = findPreference(getString(R.string.key_build_version)); + Preference pref = findPreference(getString(R.string.key_about_gnucash)); + if (BuildConfig.FLAVOR.equals("development")){ + pref.setSummary(pref.getSummary() + " - Built: " + BuildConfig.BUILD_TIME); + } pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { diff --git a/app/src/main/java/org/gnucash/android/ui/settings/AccountPreferencesFragment.java b/app/src/main/java/org/gnucash/android/ui/settings/AccountPreferencesFragment.java index 29d7baf7c..81804ccce 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/AccountPreferencesFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/AccountPreferencesFragment.java @@ -25,9 +25,8 @@ import android.preference.Preference; import android.preference.PreferenceFragment; import android.preference.PreferenceManager; - -import com.actionbarsherlock.app.ActionBar; -import com.actionbarsherlock.app.SherlockPreferenceActivity; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; import org.gnucash.android.R; import org.gnucash.android.model.Money; @@ -49,7 +48,7 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.fragment_account_preferences); - ActionBar actionBar = ((SherlockPreferenceActivity) getActivity()).getSupportActionBar(); + ActionBar actionBar = ((AppCompatPreferenceActivity) getActivity()).getSupportActionBar(); actionBar.setHomeButtonEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.title_account_preferences); @@ -85,7 +84,7 @@ public boolean onPreferenceClick(Preference preference) { new AlertDialog.Builder(getActivity()) .setTitle(R.string.title_create_default_accounts) .setMessage(R.string.msg_confirm_create_default_accounts_setting) - .setIcon(android.R.drawable.ic_dialog_alert) + .setIcon(R.drawable.ic_warning_black_24dp) .setPositiveButton(R.string.btn_create_accounts, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { diff --git a/app/src/main/java/org/gnucash/android/ui/settings/AppCompatPreferenceActivity.java b/app/src/main/java/org/gnucash/android/ui/settings/AppCompatPreferenceActivity.java new file mode 100644 index 000000000..c0cea92c3 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/settings/AppCompatPreferenceActivity.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.ui.settings; + +import android.content.res.Configuration; +import android.os.Bundle; +import android.preference.PreferenceActivity; +import android.support.annotation.LayoutRes; +import android.support.annotation.Nullable; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatDelegate; +import android.support.v7.widget.Toolbar; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; + +/** + * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls + * to be used with AppCompat. + * + * This technique can be used with an {@link android.app.Activity} class, not just + * {@link android.preference.PreferenceActivity}. + */ +public abstract class AppCompatPreferenceActivity extends PreferenceActivity { + + private AppCompatDelegate mDelegate; + + @Override + protected void onCreate(Bundle savedInstanceState) { + getDelegate().installViewFactory(); + getDelegate().onCreate(savedInstanceState); + super.onCreate(savedInstanceState); + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + getDelegate().onPostCreate(savedInstanceState); + } + + public ActionBar getSupportActionBar() { + return getDelegate().getSupportActionBar(); + } + + public void setSupportActionBar(@Nullable Toolbar toolbar) { + getDelegate().setSupportActionBar(toolbar); + } + + @Override + public MenuInflater getMenuInflater() { + return getDelegate().getMenuInflater(); + } + + @Override + public void setContentView(@LayoutRes int layoutResID) { + getDelegate().setContentView(layoutResID); + } + + @Override + public void setContentView(View view) { + getDelegate().setContentView(view); + } + + @Override + public void setContentView(View view, ViewGroup.LayoutParams params) { + getDelegate().setContentView(view, params); + } + + @Override + public void addContentView(View view, ViewGroup.LayoutParams params) { + getDelegate().addContentView(view, params); + } + + @Override + protected void onPostResume() { + super.onPostResume(); + getDelegate().onPostResume(); + } + + @Override + protected void onTitleChanged(CharSequence title, int color) { + super.onTitleChanged(title, color); + getDelegate().setTitle(title); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + getDelegate().onConfigurationChanged(newConfig); + } + + @Override + protected void onStop() { + super.onStop(); + getDelegate().onStop(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + getDelegate().onDestroy(); + } + + public void invalidateOptionsMenu() { + getDelegate().invalidateOptionsMenu(); + } + + private AppCompatDelegate getDelegate() { + if (mDelegate == null) { + mDelegate = AppCompatDelegate.create(this, null); + } + return mDelegate; + } +} \ No newline at end of file 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 4c7dc24f3..0f222e714 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 @@ -23,9 +23,8 @@ import android.preference.Preference.OnPreferenceChangeListener; import android.preference.PreferenceFragment; import android.preference.PreferenceManager; - -import com.actionbarsherlock.app.ActionBar; -import com.actionbarsherlock.app.SherlockPreferenceActivity; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; import org.gnucash.android.R; import org.gnucash.android.model.Money; @@ -44,7 +43,7 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.fragment_backup_preferences); - ActionBar actionBar = ((SherlockPreferenceActivity) getActivity()).getSupportActionBar(); + ActionBar actionBar = ((AppCompatPreferenceActivity) getActivity()).getSupportActionBar(); actionBar.setHomeButtonEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.title_backup_prefs); diff --git a/app/src/main/java/org/gnucash/android/ui/settings/DeleteAllAccountsConfirmationDialog.java b/app/src/main/java/org/gnucash/android/ui/settings/DeleteAllAccountsConfirmationDialog.java index f1d73bafc..11001b531 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/DeleteAllAccountsConfirmationDialog.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/DeleteAllAccountsConfirmationDialog.java @@ -28,7 +28,7 @@ import org.gnucash.android.R; import org.gnucash.android.db.AccountsDbAdapter; import org.gnucash.android.export.xml.GncXmlExporter; -import org.gnucash.android.ui.widget.WidgetConfigurationActivity; +import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; /** * Confirmation dialog for deleting all accounts from the system. diff --git a/app/src/main/java/org/gnucash/android/ui/settings/DeleteAllTransactionsConfirmationDialog.java b/app/src/main/java/org/gnucash/android/ui/settings/DeleteAllTransactionsConfirmationDialog.java index 3f37e2ee3..22ae73078 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/DeleteAllTransactionsConfirmationDialog.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/DeleteAllTransactionsConfirmationDialog.java @@ -32,7 +32,7 @@ import org.gnucash.android.db.TransactionsDbAdapter; import org.gnucash.android.export.xml.GncXmlExporter; import org.gnucash.android.model.Transaction; -import org.gnucash.android.ui.widget.WidgetConfigurationActivity; +import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; import java.util.ArrayList; import java.util.List; @@ -73,7 +73,7 @@ public void onClick(DialogInterface dialog, int whichButton) { Log.i("DeleteDialog", String.format("Deleted %d transactions successfully", count)); if (preserveOpeningBalances) { - transactionsDbAdapter.bulkAddTransactions(openingBalances); + transactionsDbAdapter.bulkAddRecords(openingBalances); } Toast.makeText(context, R.string.toast_all_transactions_deleted, Toast.LENGTH_SHORT).show(); WidgetConfigurationActivity.updateAllWidgets(getActivity()); diff --git a/app/src/main/java/org/gnucash/android/ui/settings/PasscodePreferenceFragment.java b/app/src/main/java/org/gnucash/android/ui/settings/GeneralPreferenceFragment.java similarity index 85% rename from app/src/main/java/org/gnucash/android/ui/settings/PasscodePreferenceFragment.java rename to app/src/main/java/org/gnucash/android/ui/settings/GeneralPreferenceFragment.java index d1662d998..18f8a9011 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/PasscodePreferenceFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/GeneralPreferenceFragment.java @@ -26,23 +26,21 @@ import android.preference.Preference.OnPreferenceChangeListener; import android.preference.PreferenceFragment; import android.preference.PreferenceManager; +import android.support.v7.app.ActionBar; import android.widget.Toast; -import com.actionbarsherlock.app.ActionBar; -import com.actionbarsherlock.app.SherlockPreferenceActivity; - import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; -import org.gnucash.android.ui.UxArgument; +import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.passcode.PasscodeLockScreenActivity; import org.gnucash.android.ui.passcode.PasscodePreferenceActivity; /** - * Fragment for configuring passcode to the application + * Fragment for general preferences. Currently caters to the passcode and reporting preferences * @author Oleksandr Tyshkovets */ @TargetApi(11) -public class PasscodePreferenceFragment extends PreferenceFragment { +public class GeneralPreferenceFragment extends PreferenceFragment implements OnPreferenceChangeListener{ /** * Request code for retrieving passcode to store @@ -63,12 +61,12 @@ public class PasscodePreferenceFragment extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.fragment_passcode_preferences); + addPreferencesFromResource(R.xml.fragment_general_preferences); - ActionBar actionBar = ((SherlockPreferenceActivity) getActivity()).getSupportActionBar(); + ActionBar actionBar = ((AppCompatPreferenceActivity) getActivity()).getSupportActionBar(); actionBar.setHomeButtonEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setTitle(R.string.title_passcode_preferences); + actionBar.setTitle(R.string.title_general_prefs); } @Override @@ -105,6 +103,17 @@ public boolean onPreferenceClick(Preference preference) { }); } + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference.getKey().equals(getString(R.string.key_use_account_color))) { + PreferenceManager.getDefaultSharedPreferences(getActivity()) + .edit() + .putBoolean(getString(R.string.key_use_account_color), Boolean.valueOf(newValue.toString())) + .commit(); + } + return true; + } + @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); diff --git a/app/src/main/java/org/gnucash/android/ui/settings/ReportPreferenceFragment.java b/app/src/main/java/org/gnucash/android/ui/settings/ReportPreferenceFragment.java deleted file mode 100644 index abfeaa056..000000000 --- a/app/src/main/java/org/gnucash/android/ui/settings/ReportPreferenceFragment.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2015 Oleksandr Tyshkovets - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.gnucash.android.ui.settings; - -import android.annotation.TargetApi; -import android.os.Bundle; -import android.preference.Preference; -import android.preference.Preference.OnPreferenceChangeListener; -import android.preference.PreferenceFragment; -import android.preference.PreferenceManager; - -import com.actionbarsherlock.app.ActionBar; -import com.actionbarsherlock.app.SherlockPreferenceActivity; - -import org.gnucash.android.R; - -/** - * Report settings fragment inside the Settings activity - * @author Oleksandr Tyshkovets - */ -@TargetApi(11) -public class ReportPreferenceFragment extends PreferenceFragment implements OnPreferenceChangeListener { - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - addPreferencesFromResource(R.xml.fragment_report_preferences); - ActionBar actionBar = ((SherlockPreferenceActivity) getActivity()).getSupportActionBar(); - actionBar.setHomeButtonEnabled(true); - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setTitle(R.string.title_report_prefs); - - findPreference(getString(R.string.key_use_account_color)).setOnPreferenceChangeListener(this); - } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - if (preference.getKey().equals(getString(R.string.key_use_account_color))) { - PreferenceManager.getDefaultSharedPreferences(getActivity()) - .edit() - .putBoolean(getString(R.string.key_use_account_color), Boolean.valueOf(newValue.toString())) - .commit(); - } - return true; - } - -} diff --git a/app/src/main/java/org/gnucash/android/ui/settings/SettingsActivity.java b/app/src/main/java/org/gnucash/android/ui/settings/SettingsActivity.java index 3aca7b379..2e1e6247a 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/SettingsActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/SettingsActivity.java @@ -21,7 +21,6 @@ import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; -import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -33,13 +32,12 @@ import android.preference.Preference; import android.preference.Preference.OnPreferenceChangeListener; import android.preference.PreferenceManager; +import android.support.v7.app.ActionBar; import android.util.Log; +import android.view.MenuItem; import android.widget.ArrayAdapter; import android.widget.Toast; -import com.actionbarsherlock.app.ActionBar; -import com.actionbarsherlock.app.SherlockPreferenceActivity; -import com.actionbarsherlock.view.MenuItem; import com.crashlytics.android.Crashlytics; import com.dropbox.sync.android.DbxAccountManager; import com.google.android.gms.common.ConnectionResult; @@ -60,8 +58,8 @@ import org.gnucash.android.importer.ImportAsyncTask; import org.gnucash.android.model.Money; import org.gnucash.android.model.Transaction; -import org.gnucash.android.ui.UxArgument; import org.gnucash.android.ui.account.AccountsActivity; +import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.passcode.PasscodeLockScreenActivity; import org.gnucash.android.ui.passcode.PasscodePreferenceActivity; @@ -86,7 +84,8 @@ * @author Oleksandr Tyshkovets * @author Yongxin Wang */ -public class SettingsActivity extends SherlockPreferenceActivity implements OnPreferenceChangeListener, Preference.OnPreferenceClickListener{ +public class SettingsActivity extends AppCompatPreferenceActivity + implements OnPreferenceChangeListener, Preference.OnPreferenceClickListener{ public static final String LOG_TAG = "SettingsActivity"; @@ -131,6 +130,7 @@ public class SettingsActivity extends SherlockPreferenceActivity implements OnPr */ public static GoogleApiClient mGoogleApiClient; + /** * Constructs the headers to display in the header list when the Settings activity is first opened * Only available on Honeycomb and above @@ -143,7 +143,7 @@ public void onBuildHeaders(List
target) { @SuppressWarnings("deprecation") @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String dropboxAppKey = getString(R.string.dropbox_app_key, DROPBOX_APP_KEY); @@ -160,11 +160,10 @@ protected void onCreate(Bundle savedInstanceState) { actionBar.setDisplayHomeAsUpEnabled(true); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB){ + addPreferencesFromResource(R.xml.fragment_general_preferences); addPreferencesFromResource(R.xml.fragment_account_preferences); addPreferencesFromResource(R.xml.fragment_transaction_preferences); addPreferencesFromResource(R.xml.fragment_backup_preferences); - addPreferencesFromResource(R.xml.fragment_passcode_preferences); - addPreferencesFromResource(R.xml.fragment_report_preferences); addPreferencesFromResource(R.xml.fragment_about_preferences); setDefaultCurrencyListener(); @@ -183,7 +182,7 @@ protected void onCreate(Bundle savedInstanceState) { pref = findPreference(getString(R.string.key_delete_all_accounts)); pref.setOnPreferenceClickListener(this); - pref = findPreference(getString(R.string.key_build_version)); + pref = findPreference(getString(R.string.key_about_gnucash)); pref.setOnPreferenceClickListener(this); pref = findPreference(getString(R.string.key_change_passcode)); @@ -255,11 +254,11 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { } else if (preference.getKey().equals(getString(R.string.key_enable_passcode))) { if ((Boolean) newValue) { startActivityForResult(new Intent(this, PasscodePreferenceActivity.class), - PasscodePreferenceFragment.PASSCODE_REQUEST_CODE); + GeneralPreferenceFragment.PASSCODE_REQUEST_CODE); } else { Intent passIntent = new Intent(this, PasscodeLockScreenActivity.class); passIntent.putExtra(UxArgument.DISABLE_PASSCODE, UxArgument.DISABLE_PASSCODE); - startActivityForResult(passIntent, PasscodePreferenceFragment.REQUEST_DISABLE_PASSCODE); + startActivityForResult(passIntent, GeneralPreferenceFragment.REQUEST_DISABLE_PASSCODE); } } else if (preference.getKey().equals(getString(R.string.key_use_double_entry))){ setImbalanceAccountsHidden((Boolean) newValue); @@ -272,16 +271,15 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { protected boolean isValidFragment(String fragmentName) { return BackupPreferenceFragment.class.getName().equals(fragmentName) || AccountPreferencesFragment.class.getName().equals(fragmentName) - || PasscodePreferenceFragment.class.getName().equals(fragmentName) + || GeneralPreferenceFragment.class.getName().equals(fragmentName) || TransactionsPreferenceFragment.class.getName().equals(fragmentName) - || AboutPreferenceFragment.class.getName().equals(fragmentName) - || ReportPreferenceFragment.class.getName().equals(fragmentName); + || AboutPreferenceFragment.class.getName().equals(fragmentName); } public void setImbalanceAccountsHidden(boolean useDoubleEntry) { String isHidden = useDoubleEntry ? "0" : "1"; AccountsDbAdapter accountsDbAdapter = AccountsDbAdapter.getInstance(); - List currencies = accountsDbAdapter.getCurrencies(); + List currencies = accountsDbAdapter.getCurrenciesInUse(); for (Currency currency : currencies) { String uid = accountsDbAdapter.getImbalanceAccountUID(currency); if (uid != null){ @@ -304,7 +302,7 @@ public boolean onPreferenceClick(Preference preference) { String key = preference.getKey(); if (key.equals(getString(R.string.key_import_accounts))){ - importAccounts(); + AccountsActivity.startXmlFileChooser(this); return true; } @@ -312,7 +310,7 @@ public boolean onPreferenceClick(Preference preference) { restoreBackup(); } - if (key.equals(getString(R.string.key_build_version))){ + if (key.equals(getString(R.string.key_about_gnucash))){ AccountsActivity.showWhatsNewDialog(this); return true; } @@ -366,7 +364,7 @@ public boolean onPreferenceClick(Preference preference) { transactionsDbAdapter.deleteAllRecords(); if (preserveOpeningBalances) { - transactionsDbAdapter.bulkAddTransactions(openingBalances); + transactionsDbAdapter.bulkAddRecords(openingBalances); } Toast.makeText(this, R.string.toast_all_transactions_deleted, Toast.LENGTH_LONG).show(); } @@ -377,7 +375,7 @@ public boolean onPreferenceClick(Preference preference) { if (key.equals(getString(R.string.key_change_passcode))){ startActivityForResult(new Intent(this, PasscodePreferenceActivity.class), - PasscodePreferenceFragment.REQUEST_CHANGE_PASSCODE); + GeneralPreferenceFragment.REQUEST_CHANGE_PASSCODE); return true; } @@ -500,23 +498,6 @@ public void run() { } } - /** - * Starts a request to pick a file to import into GnuCash - */ - public void importAccounts() { - Intent pickIntent = new Intent(Intent.ACTION_GET_CONTENT); - pickIntent.setType("application/*"); - Intent chooser = Intent.createChooser(pickIntent, getString(R.string.title_select_gnucash_xml_file)); - - try { - startActivityForResult(chooser, AccountsActivity.REQUEST_PICK_ACCOUNTS_FILE); - } catch (ActivityNotFoundException ex){ - Crashlytics.log("No file manager for selecting files available"); - Crashlytics.logException(ex); - Toast.makeText(this, R.string.toast_install_file_manager, Toast.LENGTH_LONG).show(); - } - } - /** * Opens a dialog for a user to select a backup to restore and then restores the backup */ @@ -577,16 +558,10 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case AccountsActivity.REQUEST_PICK_ACCOUNTS_FILE: if (resultCode == Activity.RESULT_OK && data != null) { - try { - InputStream accountInputStream = getContentResolver().openInputStream(data.getData()); - new ImportAsyncTask(this).execute(accountInputStream); - } catch (FileNotFoundException e) { - Crashlytics.logException(e); - Toast.makeText(this, R.string.toast_error_importing_accounts, Toast.LENGTH_SHORT).show(); - } + AccountsActivity.importXmlFileFromIntent(this, data); } break; - case PasscodePreferenceFragment.PASSCODE_REQUEST_CODE: + case GeneralPreferenceFragment.PASSCODE_REQUEST_CODE: if (resultCode == Activity.RESULT_OK && data != null) { PreferenceManager.getDefaultSharedPreferences(getApplicationContext()) .edit() @@ -609,7 +584,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { } break; - case PasscodePreferenceFragment.REQUEST_DISABLE_PASSCODE: + case GeneralPreferenceFragment.REQUEST_DISABLE_PASSCODE: boolean flag = resultCode != Activity.RESULT_OK; PreferenceManager.getDefaultSharedPreferences(getApplicationContext()) .edit() @@ -618,7 +593,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { ((CheckBoxPreference) findPreference(getString(R.string.key_enable_passcode))).setChecked(flag); break; - case PasscodePreferenceFragment.REQUEST_CHANGE_PASSCODE: + case GeneralPreferenceFragment.REQUEST_CHANGE_PASSCODE: if (resultCode == Activity.RESULT_OK && data != null) { PreferenceManager.getDefaultSharedPreferences(getApplicationContext()) .edit() 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 8348d7d59..b379eeacf 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 @@ -23,9 +23,8 @@ import android.preference.Preference.OnPreferenceChangeListener; import android.preference.PreferenceFragment; import android.preference.PreferenceManager; - -import com.actionbarsherlock.app.ActionBar; -import com.actionbarsherlock.app.SherlockPreferenceActivity; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; import org.gnucash.android.R; @@ -42,7 +41,7 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.fragment_transaction_preferences); - ActionBar actionBar = ((SherlockPreferenceActivity) getActivity()).getSupportActionBar(); + ActionBar actionBar = ((AppCompatPreferenceActivity) getActivity()).getSupportActionBar(); actionBar.setHomeButtonEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.title_transaction_preferences); diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledActionsActivity.java b/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledActionsActivity.java index 327b1ffe2..594228dc2 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledActionsActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledActionsActivity.java @@ -16,79 +16,105 @@ package org.gnucash.android.ui.transaction; import android.os.Bundle; +import android.support.design.widget.TabLayout; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.support.v7.widget.Toolbar; import org.gnucash.android.R; -import org.gnucash.android.ui.export.ScheduledExportListFragment; -import org.gnucash.android.ui.passcode.PassLockActivity; - -import java.util.MissingFormatArgumentException; +import org.gnucash.android.model.ScheduledAction; +import org.gnucash.android.ui.common.BaseDrawerActivity; /** * Activity for displaying scheduled actions * @author Ngewi Fet */ -public class ScheduledActionsActivity extends PassLockActivity { - - public enum DisplayMode {ALL_ACTIONS, TRANSACTION_ACTIONS, EXPORT_ACTIONS} +public class ScheduledActionsActivity extends BaseDrawerActivity { - public static final String EXTRA_DISPLAY_MODE = "org.gnucash.android.extra.DISPLAY_MODE"; + public static final int INDEX_SCHEDULED_TRANSACTIONS = 0; + public static final int INDEX_SCHEDULED_EXPORTS = 1; - private DisplayMode mDisplayMode = DisplayMode.ALL_ACTIONS; + ViewPager mViewPager; @Override protected void onCreate(Bundle savedInstanceState) { - setContentView(R.layout.activity_scheduled_events); super.onCreate(savedInstanceState); - - mDisplayMode = (DisplayMode) getIntent().getSerializableExtra(EXTRA_DISPLAY_MODE); - if (mDisplayMode == null) - throw new MissingFormatArgumentException("Missing argument for which kind of scheduled events to display"); - - switch (mDisplayMode){ - case ALL_ACTIONS: - //TODO: do we even want this option. For now fall through to SX - - case TRANSACTION_ACTIONS: - showScheduledTransactionsFragment(); - break; - - case EXPORT_ACTIONS: - showScheduledExportsFragment(); - break; - } + setContentView(R.layout.activity_scheduled_events); + setUpDrawer(); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setTitle(R.string.nav_menu_scheduled_actions); + + TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout); + tabLayout.addTab(tabLayout.newTab().setText(R.string.title_scheduled_transactions)); + tabLayout.addTab(tabLayout.newTab().setText(R.string.title_scheduled_exports)); + tabLayout.setTabGravity(TabLayout.GRAVITY_FILL); + + mViewPager = (ViewPager) findViewById(R.id.pager); + + //show the simple accounts list + PagerAdapter mPagerAdapter = new ScheduledActionsViewPager(getSupportFragmentManager()); + mViewPager.setAdapter(mPagerAdapter); + + mViewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout)); + tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + mViewPager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + //nothing to see here, move along + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + //nothing to see here, move along + } + }); } - /** - * Shows the fragment with scheduled exports - */ - private void showScheduledExportsFragment(){ - FragmentManager fragmentManager = getSupportFragmentManager(); - FragmentTransaction fragmentTransaction = fragmentManager - .beginTransaction(); - - ScheduledExportListFragment exportListFragment = new ScheduledExportListFragment(); - - fragmentTransaction.replace(R.id.fragment_container, - exportListFragment, "fragment_recurring_transactions"); - - fragmentTransaction.commit(); - } /** - * Launches the fragment which lists the recurring transactions in the database + * View pager adapter for managing the scheduled action views */ - private void showScheduledTransactionsFragment(){ - FragmentManager fragmentManager = getSupportFragmentManager(); - FragmentTransaction fragmentTransaction = fragmentManager - .beginTransaction(); + private class ScheduledActionsViewPager extends FragmentStatePagerAdapter { - ScheduledTransactionsListFragment scheduledTransactionsListFragment = new ScheduledTransactionsListFragment(); + public ScheduledActionsViewPager(FragmentManager fm) { + super(fm); + } - fragmentTransaction.replace(R.id.fragment_container, - scheduledTransactionsListFragment, "fragment_recurring_transactions"); + @Override + public CharSequence getPageTitle(int position) { + switch (position){ + case INDEX_SCHEDULED_TRANSACTIONS: + return getString(R.string.title_scheduled_transactions); + case INDEX_SCHEDULED_EXPORTS: + return getString(R.string.title_scheduled_exports); + default: + return super.getPageTitle(position); + } + } - fragmentTransaction.commit(); + @Override + public Fragment getItem(int position) { + switch (position){ + case INDEX_SCHEDULED_TRANSACTIONS: + return ScheduledActionsListFragment.getInstance(ScheduledAction.ActionType.TRANSACTION); + case INDEX_SCHEDULED_EXPORTS: + return ScheduledActionsListFragment.getInstance(ScheduledAction.ActionType.BACKUP); + } + return null; + } + + @Override + public int getCount() { + return 2; + } } } diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledTransactionsListFragment.java b/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledActionsListFragment.java similarity index 55% rename from app/src/main/java/org/gnucash/android/ui/transaction/ScheduledTransactionsListFragment.java rename to app/src/main/java/org/gnucash/android/ui/transaction/ScheduledActionsListFragment.java index b633dddd7..2358ddbf1 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledTransactionsListFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledActionsListFragment.java @@ -16,18 +16,28 @@ package org.gnucash.android.ui.transaction; +import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Rect; +import android.os.Build; import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.ListFragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; import android.support.v4.widget.SimpleCursorAdapter; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.view.ActionMode; import android.util.Log; import android.util.SparseBooleanArray; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.TouchDelegate; import android.view.View; import android.view.ViewGroup; @@ -37,36 +47,34 @@ import android.widget.TextView; import android.widget.Toast; -import com.actionbarsherlock.app.ActionBar; -import com.actionbarsherlock.app.SherlockListFragment; -import com.actionbarsherlock.view.ActionMode; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuInflater; -import com.actionbarsherlock.view.MenuItem; - import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.db.DatabaseCursorLoader; import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.db.ScheduledActionDbAdapter; import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.export.ExportParams; import org.gnucash.android.model.ScheduledAction; import org.gnucash.android.model.Transaction; -import org.gnucash.android.ui.UxArgument; +import org.gnucash.android.ui.common.FormActivity; +import org.gnucash.android.ui.common.UxArgument; +import java.text.DateFormat; +import java.util.Date; import java.util.List; /** - * Fragment which displays the recurring transactions in the system. + * Fragment which displays the scheduled actions in the system + *

Currently, it handles the display of scheduled transactions and scheduled exports

* @author Ngewi Fet */ -public class ScheduledTransactionsListFragment extends SherlockListFragment implements +public class ScheduledActionsListFragment extends ListFragment implements LoaderManager.LoaderCallbacks { /** * Logging tag */ - protected static final String TAG = "ScheduledTrxnFragment"; + protected static final String TAG = "ScheduledActionFragment"; private TransactionsDbAdapter mTransactionsDbAdapter; private SimpleCursorAdapter mCursorAdapter; @@ -77,6 +85,7 @@ public class ScheduledTransactionsListFragment extends SherlockListFragment impl */ private boolean mInEditMode = false; + private ScheduledAction.ActionType mActionType = ScheduledAction.ActionType.TRANSACTION; /** * Callbacks for the menu items in the Context ActionBar (CAB) in action mode @@ -87,7 +96,6 @@ public class ScheduledTransactionsListFragment extends SherlockListFragment impl public boolean onCreateActionMode(ActionMode mode, Menu menu) { MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.transactions_context_menu, menu); - menu.removeItem(R.id.context_menu_move_transactions); return true; } @@ -120,26 +128,60 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { } } mode.finish(); + setDefaultStatusBarColor(); getLoaderManager().destroyLoader(0); refreshList(); return true; default: + setDefaultStatusBarColor(); return false; } } }; + private void setDefaultStatusBarColor() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getActivity().getWindow().setStatusBarColor(getResources().getColor(R.color.theme_primary_dark)); + } + } + + /** + * Returns a new instance of the fragment for displayed the scheduled action + * @param actionType Type of scheduled action to be displayed + * @return New instance of fragment + */ + public static Fragment getInstance(ScheduledAction.ActionType actionType){ + ScheduledActionsListFragment fragment = new ScheduledActionsListFragment(); + fragment.mActionType = actionType; + return fragment; + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mTransactionsDbAdapter = TransactionsDbAdapter.getInstance(); - mCursorAdapter = new TransactionsCursorAdapter( - getActivity().getApplicationContext(), - R.layout.list_item_scheduled_trxn, null, - new String[] {DatabaseSchema.TransactionEntry.COLUMN_DESCRIPTION}, - new int[] {R.id.primary_text}); + switch (mActionType){ + case TRANSACTION: + mCursorAdapter = new ScheduledTransactionsCursorAdapter( + getActivity().getApplicationContext(), + R.layout.list_item_scheduled_trxn, null, + new String[] {DatabaseSchema.TransactionEntry.COLUMN_DESCRIPTION}, + new int[] {R.id.primary_text}); + break; + case BACKUP: + mCursorAdapter = new ScheduledExportCursorAdapter( + getActivity().getApplicationContext(), + R.layout.list_item_scheduled_trxn, null, + new String[]{}, new int[]{}); + break; + + default: + throw new IllegalArgumentException("Unable to display scheduled actions for the specified action type"); + + } + setListAdapter(mCursorAdapter); } @@ -153,14 +195,19 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - ActionBar actionBar = getSherlockActivity().getSupportActionBar(); + ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); actionBar.setDisplayShowTitleEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setHomeButtonEnabled(true); - actionBar.setTitle(R.string.title_scheduled_transactions); setHasOptionsMenu(true); getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + ((TextView)getListView().getEmptyView()).setTextColor(getResources().getColor(R.color.theme_accent)); + if (mActionType == ScheduledAction.ActionType.TRANSACTION){ + ((TextView)getListView().getEmptyView()).setText(R.string.label_no_recurring_transactions); + } else if (mActionType == ScheduledAction.ActionType.BACKUP){ + ((TextView)getListView().getEmptyView()).setText(R.string.label_no_scheduled_exports_to_display); + } } /** @@ -176,6 +223,26 @@ public void onResume() { refreshList(); } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (mActionType == ScheduledAction.ActionType.BACKUP) + inflater.inflate(R.menu.scheduled_export_actions, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()){ + case R.id.menu_add_scheduled_export: + Intent intent = new Intent(getActivity(), FormActivity.class); + intent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.EXPORT.name()); + startActivityForResult(intent, 0x1); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + @Override public void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); @@ -184,7 +251,11 @@ public void onListItemClick(ListView l, View v, int position, long id) { checkbox.setChecked(!checkbox.isChecked()); return; } - Transaction transaction = mTransactionsDbAdapter.getTransaction(id); + + if (mActionType == ScheduledAction.ActionType.BACKUP) //nothing to do for export actions + return; + + Transaction transaction = mTransactionsDbAdapter.getRecord(id); //this should actually never happen, but has happened once. So perform check for the future if (transaction.getSplits().size() == 0){ @@ -203,26 +274,24 @@ public void onListItemClick(ListView l, View v, int position, long id) { * @param transactionUID GUID of transaction to be edited */ public void openTransactionForEdit(String accountUID, String transactionUID, String scheduledActionUid){ - Intent createTransactionIntent = new Intent(getActivity(), TransactionsActivity.class); + Intent createTransactionIntent = new Intent(getActivity(), FormActivity.class); createTransactionIntent.setAction(Intent.ACTION_INSERT_OR_EDIT); + createTransactionIntent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.TRANSACTION.name()); createTransactionIntent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, accountUID); createTransactionIntent.putExtra(UxArgument.SELECTED_TRANSACTION_UID, transactionUID); createTransactionIntent.putExtra(UxArgument.SCHEDULED_ACTION_UID, scheduledActionUid); startActivity(createTransactionIntent); } - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { -// inflater.inflate(R.menu.transactions_list_actions, menu); - //remove menu items from the AccountsActivity - menu.removeItem(R.id.menu_search); -// menu.removeItem(R.id.menu_settings); - } - @Override public Loader onCreateLoader(int arg0, Bundle arg1) { Log.d(TAG, "Creating transactions loader"); - return new ScheduledTransactionsCursorLoader(getActivity()); + if (mActionType == ScheduledAction.ActionType.TRANSACTION) + return new ScheduledTransactionsCursorLoader(getActivity()); + else if (mActionType == ScheduledAction.ActionType.BACKUP){ + return new ScheduledExportCursorLoader(getActivity()); + } + return null; } @Override @@ -282,7 +351,11 @@ private void startActionMode(){ } mInEditMode = true; // Start the CAB using the ActionMode.Callback defined above - mActionMode = getSherlockActivity().startActionMode(mActionModeCallbacks); + mActionMode = ((AppCompatActivity) getActivity()) + .startSupportActionMode(mActionModeCallbacks); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){ + getActivity().getWindow().setStatusBarColor(getResources().getColor(android.R.color.darker_gray)); + } } /** @@ -293,18 +366,26 @@ private void stopActionMode(){ int checkedCount = getListView().getCheckedItemIds().length; if (checkedCount <= 0 && mActionMode != null) { mActionMode.finish(); + setDefaultStatusBarColor(); } } + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK) { + refreshList(); + super.onActivityResult(requestCode, resultCode, data); + } + } /** * Extends a simple cursor adapter to bind transaction attributes to views * @author Ngewi Fet */ - protected class TransactionsCursorAdapter extends SimpleCursorAdapter { + protected class ScheduledTransactionsCursorAdapter extends SimpleCursorAdapter { - public TransactionsCursorAdapter(Context context, int layout, Cursor c, - String[] from, int[] to) { + public ScheduledTransactionsCursorAdapter(Context context, int layout, Cursor c, + String[] from, int[] to) { super(context, layout, c, from, to, 0); } @@ -369,12 +450,12 @@ public void run() { public void bindView(View view, Context context, Cursor cursor) { super.bindView(view, context, cursor); - Transaction transaction = mTransactionsDbAdapter.buildTransactionInstance(cursor); + Transaction transaction = mTransactionsDbAdapter.buildModelInstance(cursor); TextView amountTextView = (TextView) view.findViewById(R.id.right_text); if (transaction.getSplits().size() == 2){ if (transaction.getSplits().get(0).isPairOf(transaction.getSplits().get(1))){ - amountTextView.setText(transaction.getSplits().get(0).getAmount().formattedString()); + amountTextView.setText(transaction.getSplits().get(0).getValue().formattedString()); } } else { amountTextView.setText(transaction.getSplits().size() + " splits"); @@ -384,12 +465,114 @@ public void bindView(View view, Context context, Cursor cursor) { ScheduledActionDbAdapter scheduledActionDbAdapter = ScheduledActionDbAdapter.getInstance(); String scheduledActionUID = cursor.getString(cursor.getColumnIndexOrThrow("origin_scheduled_action_uid")); //column created from join when fetching scheduled transactions view.setTag(scheduledActionUID); - ScheduledAction scheduledAction = scheduledActionDbAdapter.getScheduledAction(scheduledActionUID); - descriptionTextView.setText(scheduledAction.getRepeatString()); + ScheduledAction scheduledAction = scheduledActionDbAdapter.getRecord(scheduledActionUID); + long endTime = scheduledAction.getEndTime(); + if (endTime > 0 && endTime < System.currentTimeMillis()){ + ((TextView)view.findViewById(R.id.primary_text)).setTextColor(getResources().getColor(android.R.color.darker_gray)); + descriptionTextView.setText(getString(R.string.label_scheduled_action_ended, + DateFormat.getInstance().format(new Date(scheduledAction.getLastRun())))); + } else { + descriptionTextView.setText(scheduledAction.getRepeatString()); + } + } + } + /** + * Extends a simple cursor adapter to bind transaction attributes to views + * @author Ngewi Fet + */ + protected class ScheduledExportCursorAdapter extends SimpleCursorAdapter { + + public ScheduledExportCursorAdapter(Context context, int layout, Cursor c, + String[] from, int[] to) { + super(context, layout, c, from, to, 0); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final View view = super.getView(position, convertView, parent); + final int itemPosition = position; + CheckBox checkbox = (CheckBox) view.findViewById(R.id.checkbox); + //TODO: Revisit this if we ever change the application theme + int id = Resources.getSystem().getIdentifier("btn_check_holo_light", "drawable", "android"); + checkbox.setButtonDrawable(id); + + final TextView secondaryText = (TextView) view.findViewById(R.id.secondary_text); + + checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + getListView().setItemChecked(itemPosition, isChecked); + if (isChecked) { + startActionMode(); + } else { + stopActionMode(); + } + setActionModeTitle(); + } + }); + + + ListView listView = (ListView) parent; + if (mInEditMode && listView.isItemChecked(position)){ + view.setBackgroundColor(getResources().getColor(R.color.abs__holo_blue_light)); + secondaryText.setTextColor(getResources().getColor(android.R.color.white)); + } else { + view.setBackgroundColor(getResources().getColor(android.R.color.transparent)); + secondaryText.setTextColor(getResources().getColor(android.R.color.secondary_text_light_nodisable)); + checkbox.setChecked(false); + } + + final View checkBoxView = checkbox; + final View parentView = view; + parentView.post(new Runnable() { + @Override + public void run() { + if (isAdded()){ //may be run when fragment has been unbound from activity + float extraPadding = getResources().getDimension(R.dimen.edge_padding); + final android.graphics.Rect hitRect = new Rect(); + checkBoxView.getHitRect(hitRect); + hitRect.right += extraPadding; + hitRect.bottom += 3*extraPadding; + hitRect.top -= extraPadding; + hitRect.left -= 2*extraPadding; + parentView.setTouchDelegate(new TouchDelegate(hitRect, checkBoxView)); + } + } + }); + + return view; + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + super.bindView(view, context, cursor); + ScheduledActionDbAdapter mScheduledActionDbAdapter = ScheduledActionDbAdapter.getInstance(); + ScheduledAction scheduledAction = mScheduledActionDbAdapter.buildModelInstance(cursor); + + TextView primaryTextView = (TextView) view.findViewById(R.id.primary_text); + ExportParams params = ExportParams.parseCsv(scheduledAction.getTag()); + primaryTextView.setText(params.getExportFormat().name() + " " + + scheduledAction.getActionType().name().toLowerCase() + " to " + + params.getExportTarget().name().toLowerCase()); + + view.findViewById(R.id.right_text).setVisibility(View.GONE); + + TextView descriptionTextView = (TextView) view.findViewById(R.id.secondary_text); + descriptionTextView.setText(scheduledAction.getRepeatString()); + long endTime = scheduledAction.getEndTime(); + if (endTime > 0 && endTime < System.currentTimeMillis()){ + ((TextView)view.findViewById(R.id.primary_text)).setTextColor(getResources().getColor(android.R.color.darker_gray)); + descriptionTextView.setText(getString(R.string.label_scheduled_action_ended, + DateFormat.getInstance().format(new Date(scheduledAction.getLastRun())))); + } else { + descriptionTextView.setText(scheduledAction.getRepeatString()); + } } } + /** * {@link DatabaseCursorLoader} for loading recurring transactions asynchronously from the database * @author Ngewi Fet @@ -411,5 +594,28 @@ public Cursor loadInBackground() { } } + /** + * {@link DatabaseCursorLoader} for loading recurring transactions asynchronously from the database + * @author Ngewi Fet + */ + protected static class ScheduledExportCursorLoader extends DatabaseCursorLoader { + + public ScheduledExportCursorLoader(Context context) { + super(context); + } + + @Override + public Cursor loadInBackground() { + mDatabaseAdapter = ScheduledActionDbAdapter.getInstance(); + + Cursor c = mDatabaseAdapter.fetchAllRecords( + DatabaseSchema.ScheduledActionEntry.COLUMN_TYPE + "=?", + new String[]{ScheduledAction.ActionType.BACKUP.name()}); + + registerContentObserver(c); + return c; + } + } + } diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/SplitEditorFragment.java b/app/src/main/java/org/gnucash/android/ui/transaction/SplitEditorFragment.java new file mode 100644 index 000000000..0d2afb910 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/transaction/SplitEditorFragment.java @@ -0,0 +1,419 @@ +/* + * Copyright (c) 2014 - 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.gnucash.android.ui.transaction; + +import android.app.Activity; +import android.content.Intent; +import android.content.res.Configuration; +import android.database.Cursor; +import android.inputmethodservice.KeyboardView; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.widget.SimpleCursorAdapter; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +import org.gnucash.android.R; +import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.DatabaseSchema; +import org.gnucash.android.model.AccountType; +import org.gnucash.android.model.BaseModel; +import org.gnucash.android.model.Money; +import org.gnucash.android.model.Split; +import org.gnucash.android.model.Transaction; +import org.gnucash.android.model.TransactionType; +import org.gnucash.android.ui.common.FormActivity; +import org.gnucash.android.ui.common.UxArgument; +import org.gnucash.android.ui.transaction.dialog.TransferFundsDialogFragment; +import org.gnucash.android.ui.util.OnTransferFundsListener; +import org.gnucash.android.ui.util.widget.CalculatorEditText; +import org.gnucash.android.ui.util.widget.CalculatorKeyboard; +import org.gnucash.android.ui.util.widget.TransactionTypeSwitch; +import org.gnucash.android.util.QualifiedAccountNameCursorAdapter; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Currency; +import java.util.List; + +import butterknife.Bind; +import butterknife.ButterKnife; + +/** + * Dialog for editing the splits in a transaction + * + * @author Ngewi Fet + */ +public class SplitEditorFragment extends Fragment { + + @Bind(R.id.split_list_layout) LinearLayout mSplitsLinearLayout; + @Bind(R.id.calculator_keyboard) KeyboardView mKeyboardView; + + private AccountsDbAdapter mAccountsDbAdapter; + private Cursor mCursor; + private SimpleCursorAdapter mCursorAdapter; + private List mSplitItemViewList; + private String mAccountUID; + + private BigDecimal mBaseAmount = BigDecimal.ZERO; + + private ArrayList mRemovedSplitUIDs = new ArrayList<>(); + + CalculatorKeyboard mCalculatorKeyboard; + + /** + * Create and return a new instance of the fragment with the appropriate paramenters + * @param args Arguments to be set to the fragment.
+ * See {@link UxArgument#AMOUNT_STRING} and {@link UxArgument#SPLIT_LIST} + * @return New instance of SplitEditorFragment + */ + public static SplitEditorFragment newInstance(Bundle args){ + SplitEditorFragment fragment = new SplitEditorFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_split_editor, container, false); + ButterKnife.bind(this, view); + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + ActionBar actionBar = ((AppCompatActivity)getActivity()).getSupportActionBar(); + assert actionBar != null; + actionBar.setTitle(R.string.title_split_editor); + setHasOptionsMenu(true); + + mCalculatorKeyboard = new CalculatorKeyboard(getActivity(), mKeyboardView, R.xml.calculator_keyboard); + mSplitItemViewList = new ArrayList<>(); + + //we are editing splits for a new transaction. + // But the user may have already created some splits before. Let's check + List splitStrings = getArguments().getStringArrayList(UxArgument.SPLIT_LIST); + List splitList = new ArrayList<>(); + if (splitStrings != null) { + for (String splitString : splitStrings) { + splitList.add(Split.parseSplit(splitString)); + } + } + + initArgs(); + if (!splitList.isEmpty()) { + //aha! there are some splits. Let's load those instead + loadSplitViews(splitList); + } else { + final Currency currency = Currency.getInstance(mAccountsDbAdapter.getAccountCurrencyCode(mAccountUID)); + Split split = new Split(new Money(mBaseAmount.abs(), currency), mAccountUID); + AccountType accountType = mAccountsDbAdapter.getAccountType(mAccountUID); + TransactionType transactionType = Transaction.getTypeForBalance(accountType, mBaseAmount.signum() < 0); + split.setType(transactionType); + View view = addSplitView(split); + view.findViewById(R.id.input_accounts_spinner).setEnabled(false); + view.findViewById(R.id.btn_remove_split).setVisibility(View.GONE); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mCalculatorKeyboard = new CalculatorKeyboard(getActivity(), mKeyboardView, R.xml.calculator_keyboard); + } + + private void loadSplitViews(List splitList) { + for (Split split : splitList) { + addSplitView(split); + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.split_editor_actions, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + + switch (item.getItemId()) { + case android.R.id.home: + getActivity().setResult(Activity.RESULT_CANCELED); + getActivity().finish(); + return true; + + case R.id.menu_save: + saveSplits(); + return true; + + case R.id.menu_add_split: + addSplitView(null); + return true; + + default: + return super.onOptionsItemSelected(item); + } + } + + /** + * Add a split view and initialize it with split + * @param split Split to initialize the contents to + * @return Returns the split view which was added + */ + private View addSplitView(Split split){ + LayoutInflater layoutInflater = getActivity().getLayoutInflater(); + View splitView = layoutInflater.inflate(R.layout.item_split_entry, mSplitsLinearLayout, false); + mSplitsLinearLayout.addView(splitView,0); + SplitViewHolder viewHolder = new SplitViewHolder(splitView, split); + splitView.setTag(viewHolder); + mSplitItemViewList.add(splitView); + return splitView; + } + + /** + * Extracts arguments passed to the view and initializes necessary adapters and cursors + */ + private void initArgs() { + mAccountsDbAdapter = AccountsDbAdapter.getInstance(); + + Bundle args = getArguments(); + mAccountUID = ((FormActivity) getActivity()).getCurrentAccountUID(); + mBaseAmount = new BigDecimal(args.getString(UxArgument.AMOUNT_STRING)); + + String conditions = "(" + + DatabaseSchema.AccountEntry.COLUMN_HIDDEN + " = 0 AND " + + DatabaseSchema.AccountEntry.COLUMN_PLACEHOLDER + " = 0" + + ")"; + mCursor = mAccountsDbAdapter.fetchAccountsOrderedByFullName(conditions, null); + } + + /** + * Holds a split item view and binds the items in it + */ + class SplitViewHolder implements OnTransferFundsListener{ + @Bind(R.id.input_split_memo) EditText splitMemoEditText; + @Bind(R.id.input_split_amount) CalculatorEditText splitAmountEditText; + @Bind(R.id.btn_remove_split) ImageView removeSplitButton; + @Bind(R.id.input_accounts_spinner) Spinner accountsSpinner; + @Bind(R.id.split_currency_symbol) TextView splitCurrencyTextView; + @Bind(R.id.split_uid) TextView splitUidTextView; + @Bind(R.id.btn_split_type) TransactionTypeSwitch splitTypeButton; + + View splitView; + Money quantity; + + public SplitViewHolder(View splitView, Split split){ + ButterKnife.bind(this, splitView); + this.splitView = splitView; + if (split != null && !split.getQuantity().equals(split.getValue())) + this.quantity = split.getQuantity(); + setListeners(split); + } + + @Override + public void transferComplete(Money amount) { + quantity = amount; + } + + private void setListeners(Split split){ + splitAmountEditText.bindListeners(mCalculatorKeyboard); + + removeSplitButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + mRemovedSplitUIDs.add(splitUidTextView.getText().toString()); + mSplitsLinearLayout.removeView(splitView); + mSplitItemViewList.remove(splitView); + } + }); + + updateTransferAccountsList(accountsSpinner); + + Currency accountCurrency = Currency.getInstance(mAccountsDbAdapter.getCurrencyCode(mAccountUID)); + splitCurrencyTextView.setText(accountCurrency.getSymbol()); + splitTypeButton.setAmountFormattingListener(splitAmountEditText, splitCurrencyTextView); + splitTypeButton.setChecked(mBaseAmount.signum() > 0); + splitUidTextView.setText(BaseModel.generateUID()); + + if (split != null) { + splitAmountEditText.setCurrency(split.getValue().getCurrency()); + splitAmountEditText.setValue(split.getFormattedValue().asBigDecimal()); + splitCurrencyTextView.setText(split.getValue().getCurrency().getSymbol()); + splitMemoEditText.setText(split.getMemo()); + splitUidTextView.setText(split.getUID()); + String splitAccountUID = split.getAccountUID(); + setSelectedTransferAccount(mAccountsDbAdapter.getID(splitAccountUID), accountsSpinner); + splitTypeButton.setAccountType(mAccountsDbAdapter.getAccountType(splitAccountUID)); + splitTypeButton.setChecked(split.getType()); + } + + accountsSpinner.setOnItemSelectedListener(new SplitAccountListener(splitTypeButton, this)); + + } + } + + /** + * Updates the spinner to the selected transfer account + * @param accountId Database ID of the transfer account + */ + private void setSelectedTransferAccount(long accountId, final Spinner accountsSpinner){ + for (int pos = 0; pos < mCursorAdapter.getCount(); pos++) { + if (mCursorAdapter.getItemId(pos) == accountId){ + accountsSpinner.setSelection(pos); + break; + } + } + } + /** + * Updates the list of possible transfer accounts. + * Only accounts with the same currency can be transferred to + */ + private void updateTransferAccountsList(Spinner transferAccountSpinner){ + mCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), mCursor); + transferAccountSpinner.setAdapter(mCursorAdapter); + } + + /** + * Check if all the split amounts have valid values that can be saved + * @return {@code true} if splits can be saved, {@code false} otherwise + */ + private boolean canSave(){ + for (View splitView : mSplitItemViewList) { + SplitViewHolder viewHolder = (SplitViewHolder) splitView.getTag(); + viewHolder.splitAmountEditText.evaluate(); + if (viewHolder.splitAmountEditText.getError() != null){ + return false; + } + //TODO: also check that multicurrency splits have a conversion amount present + } + return true; + } + + /** + * Save all the splits from the split editor + */ + private void saveSplits() { + if (!canSave()){ + Toast.makeText(getActivity(), R.string.toast_error_check_split_amounts, + Toast.LENGTH_SHORT).show(); + return; + } + + List splitList = extractSplitsFromView(); + ArrayList splitStrings = new ArrayList<>(); + for (Split split : splitList) { + splitStrings.add(split.toCsv()); + } + Intent data = new Intent(); + data.putStringArrayListExtra(UxArgument.SPLIT_LIST, splitStrings); + data.putStringArrayListExtra(UxArgument.REMOVED_SPLITS, mRemovedSplitUIDs); + getActivity().setResult(Activity.RESULT_OK, data); + + getActivity().finish(); + } + + /** + * Extracts the input from the views and builds {@link org.gnucash.android.model.Split}s to correspond to the input. + * @return List of {@link org.gnucash.android.model.Split}s represented in the view + */ + private List extractSplitsFromView(){ + List splitList = new ArrayList<>(); + for (View splitView : mSplitItemViewList) { + SplitViewHolder viewHolder = (SplitViewHolder) splitView.getTag(); + if (viewHolder.splitAmountEditText.getValue() == null) + continue; + + BigDecimal amountBigDecimal = viewHolder.splitAmountEditText.getValue(); + + String currencyCode = mAccountsDbAdapter.getCurrencyCode(mAccountUID); + Money valueAmount = new Money(amountBigDecimal.abs(), Currency.getInstance(currencyCode)); + + String accountUID = mAccountsDbAdapter.getUID(viewHolder.accountsSpinner.getSelectedItemId()); + Split split = new Split(valueAmount, accountUID); + split.setMemo(viewHolder.splitMemoEditText.getText().toString()); + split.setType(viewHolder.splitTypeButton.getTransactionType()); + split.setUID(viewHolder.splitUidTextView.getText().toString().trim()); + if (viewHolder.quantity != null) + split.setQuantity(viewHolder.quantity.absolute()); + splitList.add(split); + } + return splitList; + } + + /** + * Listens to changes in the transfer account and updates the currency symbol, the label of the + * transaction type and if neccessary + */ + private class SplitAccountListener implements AdapterView.OnItemSelectedListener { + TransactionTypeSwitch mTypeToggleButton; + SplitViewHolder mSplitViewHolder; + + /** + * Flag to know when account spinner callback is due to user interaction or layout of components + */ + boolean userInteraction = false; + + public SplitAccountListener(TransactionTypeSwitch typeToggleButton, SplitViewHolder viewHolder){ + this.mTypeToggleButton = typeToggleButton; + this.mSplitViewHolder = viewHolder; + } + + @Override + public void onItemSelected(AdapterView parentView, View selectedItemView, int position, long id) { + AccountType accountType = mAccountsDbAdapter.getAccountType(id); + mTypeToggleButton.setAccountType(accountType); + + String fromCurrencyCode = mAccountsDbAdapter.getCurrencyCode(mAccountUID); + String targetCurrencyCode = mAccountsDbAdapter.getCurrencyCode(mAccountsDbAdapter.getUID(id)); + + if (!userInteraction || fromCurrencyCode.equals(targetCurrencyCode)){ + //first call is on layout, subsequent calls will be true and transfer will work as usual + userInteraction = true; + return; + } + + BigDecimal amountBigD = mSplitViewHolder.splitAmountEditText.getValue(); + if (amountBigD == null) + return; + + Money amount = new Money(amountBigD, Currency.getInstance(fromCurrencyCode)); + TransferFundsDialogFragment fragment + = TransferFundsDialogFragment.getInstance(amount, targetCurrencyCode, mSplitViewHolder); + fragment.show(getFragmentManager(), "tranfer_funds_editor"); + } + + @Override + public void onNothingSelected(AdapterView adapterView) { + //nothing to see here, move along + } + } + +} diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/TransactionDetailActivity.java b/app/src/main/java/org/gnucash/android/ui/transaction/TransactionDetailActivity.java new file mode 100644 index 000000000..364966a04 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/transaction/TransactionDetailActivity.java @@ -0,0 +1,211 @@ +package org.gnucash.android.ui.transaction; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.drawable.ColorDrawable; +import android.os.Build; +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.TableLayout; +import android.widget.TextView; + +import org.gnucash.android.R; +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.db.ScheduledActionDbAdapter; +import org.gnucash.android.db.TransactionsDbAdapter; +import org.gnucash.android.model.Money; +import org.gnucash.android.model.ScheduledAction; +import org.gnucash.android.model.Split; +import org.gnucash.android.model.Transaction; +import org.gnucash.android.ui.common.FormActivity; +import org.gnucash.android.ui.common.UxArgument; +import org.gnucash.android.ui.passcode.PasscodeLockActivity; + +import java.text.DateFormat; +import java.util.Date; +import java.util.MissingFormatArgumentException; + +import butterknife.Bind; +import butterknife.ButterKnife; +import butterknife.OnClick; + +/** + * Activity for displaying transaction information + * @author Ngewi Fet + */ +public class TransactionDetailActivity extends PasscodeLockActivity { + + @Bind(R.id.trn_description) TextView mTransactionDescription; + @Bind(R.id.trn_time_and_date) TextView mTimeAndDate; + @Bind(R.id.trn_recurrence) TextView mRecurrence; + @Bind(R.id.trn_notes) TextView mNotes; + @Bind(R.id.toolbar) Toolbar mToolBar; + @Bind(R.id.transaction_account) TextView mTransactionAccount; + @Bind(R.id.balance_debit) TextView mDebitBalance; + @Bind(R.id.balance_credit) TextView mCreditBalance; + + @Bind(R.id.fragment_transaction_details) + TableLayout mDetailTableLayout; + + private String mTransactionUID; + private String mAccountUID; + + public static final int REQUEST_EDIT_TRANSACTION = 0x10; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_transaction_detail); + + mTransactionUID = getIntent().getStringExtra(UxArgument.SELECTED_TRANSACTION_UID); + mAccountUID = getIntent().getStringExtra(UxArgument.SELECTED_ACCOUNT_UID); + + if (mTransactionUID == null || mAccountUID == null){ + throw new MissingFormatArgumentException("You must specify both the transaction and account GUID"); + } + + ButterKnife.bind(this); + setSupportActionBar(mToolBar); + + ActionBar actionBar = getSupportActionBar(); + assert actionBar != null; + actionBar.setElevation(0); + actionBar.setHomeButtonEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp); + actionBar.setDisplayShowTitleEnabled(false); + + bindViews(); + + int themeColor = AccountsDbAdapter.getActiveAccountColorResource(mAccountUID); + actionBar.setBackgroundDrawable(new ColorDrawable(themeColor)); + mToolBar.setBackgroundColor(themeColor); + if (Build.VERSION.SDK_INT > 20) + getWindow().setStatusBarColor(GnuCashApplication.darken(themeColor)); + + } + + class SplitAmountViewHolder { + @Bind(R.id.split_account_name) TextView accountName; + @Bind(R.id.split_debit) TextView splitDebit; + @Bind(R.id.split_credit) TextView splitCredit; + + View itemView; + + public SplitAmountViewHolder(View view, Split split){ + itemView = view; + ButterKnife.bind(this, view); + + AccountsDbAdapter accountsDbAdapter = AccountsDbAdapter.getInstance(); + accountName.setText(accountsDbAdapter.getAccountFullName(split.getAccountUID())); + Money quantity = split.getFormattedQuantity(); + TextView balanceView = quantity.isNegative() ? splitDebit : splitCredit; + TransactionsActivity.displayBalance(balanceView, quantity); + } + } + + /** + * Reads the transaction information from the database and binds it to the views + */ + private void bindViews(){ + TransactionsDbAdapter transactionsDbAdapter = TransactionsDbAdapter.getInstance(); + Transaction transaction = transactionsDbAdapter.getRecord(mTransactionUID); + + mTransactionDescription.setText(transaction.getDescription()); + mTransactionAccount.setText("in " + AccountsDbAdapter.getInstance().getAccountFullName(mAccountUID)); + + AccountsDbAdapter accountsDbAdapter = AccountsDbAdapter.getInstance(); + + Money accountBalance = accountsDbAdapter.getAccountBalance(mAccountUID, -1, transaction.getTimeMillis()); + TextView balanceTextView = accountBalance.isNegative() ? mDebitBalance : mCreditBalance; + TransactionsActivity.displayBalance(balanceTextView, accountBalance); + + boolean useDoubleEntry = GnuCashApplication.isDoubleEntryEnabled(); + LayoutInflater inflater = LayoutInflater.from(this); + int index = 0; + for (Split split : transaction.getSplits()) { + if (!useDoubleEntry && split.getAccountUID().equals(accountsDbAdapter.getImbalanceAccountUID(split.getValue().getCurrency()))){ + //do now show imbalance accounts for single entry use case + continue; + } + View view = inflater.inflate(R.layout.item_split_amount_info, mDetailTableLayout, false); + SplitAmountViewHolder viewHolder = new SplitAmountViewHolder(view, split); + mDetailTableLayout.addView(viewHolder.itemView, index++); + } + + + Date trnDate = new Date(transaction.getTimeMillis()); + String timeAndDate = DateFormat.getDateInstance(DateFormat.FULL).format(trnDate); + mTimeAndDate.setText(timeAndDate); + + if (transaction.getScheduledActionUID() != null){ + ScheduledAction scheduledAction = ScheduledActionDbAdapter.getInstance().getRecord(transaction.getScheduledActionUID()); + mRecurrence.setText(scheduledAction.getRepeatString()); + findViewById(R.id.row_trn_recurrence).setVisibility(View.VISIBLE); + + } else { + findViewById(R.id.row_trn_recurrence).setVisibility(View.GONE); + } + + if (transaction.getNote() != null && !transaction.getNote().isEmpty()){ + mNotes.setText(transaction.getNote()); + findViewById(R.id.row_trn_notes).setVisibility(View.VISIBLE); + } else { + findViewById(R.id.row_trn_notes).setVisibility(View.GONE); + } + + } + + /** + * Refreshes the transaction information + */ + private void refresh(){ + removeSplitItemViews(); + bindViews(); + } + + /** + * Remove the split item views from the transaction detail prior to refreshing them + */ + private void removeSplitItemViews(){ + long splitCount = TransactionsDbAdapter.getInstance().getSplitCount(mTransactionUID); + mDetailTableLayout.removeViews(0, (int)splitCount); + mDebitBalance.setText(""); + mCreditBalance.setText(""); + } + + + @OnClick(R.id.fab_edit_transaction) + public void editTransaction(){ + Intent createTransactionIntent = new Intent(this.getApplicationContext(), FormActivity.class); + createTransactionIntent.setAction(Intent.ACTION_INSERT_OR_EDIT); + createTransactionIntent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, mAccountUID); + createTransactionIntent.putExtra(UxArgument.SELECTED_TRANSACTION_UID, mTransactionUID); + createTransactionIntent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.TRANSACTION.name()); + startActivityForResult(createTransactionIntent, REQUEST_EDIT_TRANSACTION); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK){ + refresh(); + } + } +} 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 bee5a152a..025d305bd 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012 - 2014 Ngewi Fet + * Copyright (c) 2012 - 2015 Ngewi Fet * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,42 +16,45 @@ package org.gnucash.android.ui.transaction; +import android.app.Activity; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; +import android.content.res.Configuration; import android.database.Cursor; +import android.inputmethodservice.KeyboardView; import android.os.Bundle; import android.preference.PreferenceManager; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; import android.support.v4.widget.SimpleCursorAdapter; -import android.text.Editable; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; import android.text.format.DateUtils; import android.text.format.Time; import android.util.Log; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.AutoCompleteTextView; -import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.FilterQueryProvider; +import android.widget.ImageView; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; -import com.actionbarsherlock.app.ActionBar; -import com.actionbarsherlock.app.SherlockFragment; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuInflater; -import com.actionbarsherlock.view.MenuItem; -import com.doomonafireball.betterpickers.calendardatepicker.CalendarDatePickerDialog; -import com.doomonafireball.betterpickers.radialtimepicker.RadialTimePickerDialog; -import com.doomonafireball.betterpickers.recurrencepicker.EventRecurrence; -import com.doomonafireball.betterpickers.recurrencepicker.EventRecurrenceFormatter; -import com.doomonafireball.betterpickers.recurrencepicker.RecurrencePickerDialog; +import com.codetroopers.betterpickers.calendardatepicker.CalendarDatePickerDialog; +import com.codetroopers.betterpickers.radialtimepicker.RadialTimePickerDialog; +import com.codetroopers.betterpickers.recurrencepicker.EventRecurrence; +import com.codetroopers.betterpickers.recurrencepicker.EventRecurrenceFormatter; +import com.codetroopers.betterpickers.recurrencepicker.RecurrencePickerDialog; import org.gnucash.android.R; import org.gnucash.android.db.AccountsDbAdapter; @@ -64,12 +67,14 @@ import org.gnucash.android.model.Split; import org.gnucash.android.model.Transaction; import org.gnucash.android.model.TransactionType; -import org.gnucash.android.ui.UxArgument; -import org.gnucash.android.ui.transaction.dialog.SplitEditorDialogFragment; -import org.gnucash.android.ui.util.AmountInputFormatter; +import org.gnucash.android.ui.common.FormActivity; +import org.gnucash.android.ui.common.UxArgument; +import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; +import org.gnucash.android.ui.transaction.dialog.TransferFundsDialogFragment; +import org.gnucash.android.ui.util.OnTransferFundsListener; import org.gnucash.android.ui.util.RecurrenceParser; -import org.gnucash.android.ui.util.TransactionTypeToggleButton; -import org.gnucash.android.ui.widget.WidgetConfigurationActivity; +import org.gnucash.android.ui.util.widget.CalculatorEditText; +import org.gnucash.android.ui.util.widget.TransactionTypeSwitch; import org.gnucash.android.util.QualifiedAccountNameCursorAdapter; import java.math.BigDecimal; @@ -84,16 +89,19 @@ import java.util.List; import java.util.Locale; +import butterknife.Bind; +import butterknife.ButterKnife; + /** * Fragment for creating or editing transactions * @author Ngewi Fet */ -public class TransactionFormFragment extends SherlockFragment implements +public class TransactionFormFragment extends Fragment implements CalendarDatePickerDialog.OnDateSetListener, RadialTimePickerDialog.OnTimeSetListener, - RecurrencePickerDialog.OnRecurrenceSetListener { + RecurrencePickerDialog.OnRecurrenceSetListener, OnTransferFundsListener { - public static final String FRAGMENT_TAG_SPLITS_EDITOR = "splits_editor"; private static final String FRAGMENT_TAG_RECURRENCE_PICKER = "recurrence_picker"; + private static final int REQUEST_SPLIT_EDITOR = 0x11; /** * Transactions database adapter @@ -133,58 +141,65 @@ public class TransactionFormFragment extends SherlockFragment implements /** * Button for setting the transaction type, either credit or debit */ - private TransactionTypeToggleButton mTransactionTypeButton; + @Bind(R.id.input_transaction_type) TransactionTypeSwitch mTransactionTypeSwitch; /** * Input field for the transaction name (description) */ - private AutoCompleteTextView mDescriptionEditText; + @Bind(R.id.input_transaction_name) AutoCompleteTextView mDescriptionEditText; /** * Input field for the transaction amount */ - private EditText mAmountEditText; + @Bind(R.id.input_transaction_amount) CalculatorEditText mAmountEditText; /** * Field for the transaction currency. * The transaction uses the currency of the account */ - private TextView mCurrencyTextView; + @Bind(R.id.currency_symbol) TextView mCurrencyTextView; /** * Input field for the transaction description (note) */ - private EditText mNotesEditText; + @Bind(R.id.input_description) EditText mNotesEditText; /** * Input field for the transaction date */ - private TextView mDateTextView; + @Bind(R.id.input_date) TextView mDateTextView; /** * Input field for the transaction time */ - private TextView mTimeTextView; - - /** - * {@link Calendar} for holding the set date - */ - private Calendar mDate; - - /** - * {@link Calendar} object holding the set time - */ - private Calendar mTime; + @Bind(R.id.input_time) TextView mTimeTextView; /** * Spinner for selecting the transfer account */ - private Spinner mDoubleAccountSpinner; + @Bind(R.id.input_transfer_account_spinner) Spinner mTransferAccountSpinner; /** * Checkbox indicating if this transaction should be saved as a template or not */ - private CheckBox mSaveTemplateCheckbox; + @Bind(R.id.checkbox_save_template) CheckBox mSaveTemplateCheckbox; + + @Bind(R.id.input_recurrence) TextView mRecurrenceTextView; + + /** + * View which displays the calculator keyboard + */ + @Bind(R.id.calculator_keyboard) KeyboardView mKeyboardView; + + /** + * Open the split editor + */ + @Bind(R.id.btn_split_editor) ImageView mOpenSplitEditor; + + /** + * Layout for transfer account and associated views + */ + @Bind(R.id.layout_double_entry) View mDoubleEntryLayout; /** * Flag to note if double entry accounting is in use or not @@ -192,9 +207,14 @@ public class TransactionFormFragment extends SherlockFragment implements private boolean mUseDoubleEntry; /** - * Flag to not if the transaction involves multiple currency + * {@link Calendar} for holding the set date */ - private boolean mMultiCurrency; + private Calendar mDate; + + /** + * {@link Calendar} object holding the set time + */ + private Calendar mTime; /** * The AccountType of the account to which this transaction belongs. @@ -202,85 +222,115 @@ public class TransactionFormFragment extends SherlockFragment implements */ AccountType mAccountType; - TextView mRecurrenceTextView; private String mRecurrenceRule; private EventRecurrence mEventRecurrence = new EventRecurrence(); - private AmountInputFormatter mAmountInputFormatter; - - private Button mOpenSplitsButton; private String mAccountUID; - private List mSplitsList = new ArrayList(); + private List mSplitsList = new ArrayList<>(); private boolean mEditMode = false; + /** + * Split quantity which will be set from the funds transfer dialog + */ + private Money mSplitQuantity; + /** * Create the view and retrieve references to the UI elements */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.fragment_new_transaction, container, false); - - mDescriptionEditText = (AutoCompleteTextView) v.findViewById(R.id.input_transaction_name); - mNotesEditText = (EditText) v.findViewById(R.id.input_description); - mDateTextView = (TextView) v.findViewById(R.id.input_date); - mTimeTextView = (TextView) v.findViewById(R.id.input_time); - mAmountEditText = (EditText) v.findViewById(R.id.input_transaction_amount); - mCurrencyTextView = (TextView) v.findViewById(R.id.currency_symbol); - mTransactionTypeButton = (TransactionTypeToggleButton) v.findViewById(R.id.input_transaction_type); - mDoubleAccountSpinner = (Spinner) v.findViewById(R.id.input_double_entry_accounts_spinner); - mOpenSplitsButton = (Button) v.findViewById(R.id.btn_open_splits); - mRecurrenceTextView = (TextView) v.findViewById(R.id.input_recurrence); - mSaveTemplateCheckbox = (CheckBox) v.findViewById(R.id.checkbox_save_template); + View v = inflater.inflate(R.layout.fragment_transaction_form, container, false); + ButterKnife.bind(this, v); + mAmountEditText.bindListeners(mKeyboardView); + mOpenSplitEditor.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + openSplitEditor(); + } + }); return v; } - @Override + /** + * Starts the transfer of funds from one currency to another + */ + private void startTransferFunds() { + Currency fromCurrency = Currency.getInstance(mTransactionsDbAdapter.getAccountCurrencyCode(mAccountUID)); + long id = mTransferAccountSpinner.getSelectedItemId(); + String targetCurrency = mAccountsDbAdapter.getCurrencyCode((mAccountsDbAdapter.getUID(id))); + + if (fromCurrency.equals(Currency.getInstance(targetCurrency)) + || !mAmountEditText.isInputModified() + || mSplitQuantity != null) //if both accounts have same currency + return; + + BigDecimal amountBigd = mAmountEditText.getValue(); + if (mSplitQuantity != null || amountBigd.equals(BigDecimal.ZERO)) + return; + Money amount = new Money(amountBigd, fromCurrency).absolute(); + + TransferFundsDialogFragment fragment + = TransferFundsDialogFragment.getInstance(amount, targetCurrency, this); + fragment.show(getFragmentManager(), "transfer_funds_editor"); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mAmountEditText.bindListeners(mKeyboardView); + } + + @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setHasOptionsMenu(true); - ActionBar actionBar = getSherlockActivity().getSupportActionBar(); - actionBar.setHomeButtonEnabled(true); - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setDisplayShowTitleEnabled(false); SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); mUseDoubleEntry = sharedPrefs.getBoolean(getString(R.string.key_use_double_entry), false); if (!mUseDoubleEntry){ - getView().findViewById(R.id.layout_double_entry).setVisibility(View.GONE); - mOpenSplitsButton.setVisibility(View.GONE); + mDoubleEntryLayout.setVisibility(View.GONE); } mAccountUID = getArguments().getString(UxArgument.SELECTED_ACCOUNT_UID); + assert(mAccountUID != null); mAccountsDbAdapter = AccountsDbAdapter.getInstance(); mAccountType = mAccountsDbAdapter.getAccountType(mAccountUID); String transactionUID = getArguments().getString(UxArgument.SELECTED_TRANSACTION_UID); mTransactionsDbAdapter = TransactionsDbAdapter.getInstance(); - if (transactionUID != null) - mTransaction = mTransactionsDbAdapter.getTransaction(transactionUID); - if (mTransaction != null) { - mMultiCurrency = mTransactionsDbAdapter.getNumCurrencies(mTransaction.getUID()) > 1; + if (transactionUID != null) { + mTransaction = mTransactionsDbAdapter.getRecord(transactionUID); } + setListeners(); //updateTransferAccountsList must only be called after initializing mAccountsDbAdapter - // it needs mMultiCurrency to be properly initialized updateTransferAccountsList(); + mTransferAccountSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + /** + * Flag for ignoring first call to this listener. + * The first call is during layout, but we want it called only in response to user interaction + */ + boolean userInteraction = false; - mDoubleAccountSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView adapterView, View view, int position, long id) { - if (mSplitsList.size() == 2){ //when handling simple transfer to one account + if (mSplitsList.size() == 2) { //when handling simple transfer to one account for (Split split : mSplitsList) { - if (!split.getAccountUID().equals(mAccountUID)){ + if (!split.getAccountUID().equals(mAccountUID)) { split.setAccountUID(mAccountsDbAdapter.getUID(id)); } // else case is handled when saving the transactions } } + if (!userInteraction) { + userInteraction = true; + return; + } + startTransferFunds(); } @Override @@ -289,16 +339,19 @@ public void onNothingSelected(AdapterView adapterView) { } }); - setListeners(); - if (mTransaction == null) { + ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + assert actionBar != null; +// actionBar.setSubtitle(mAccountsDbAdapter.getFullyQualifiedAccountName(mAccountUID)); + + if (mTransaction == null) { + actionBar.setTitle(R.string.title_add_transaction); initalizeViews(); initTransactionNameAutocomplete(); } else { + actionBar.setTitle(R.string.title_edit_transaction); initializeViewsWithTransaction(); mEditMode = true; } - - } /** @@ -354,24 +407,24 @@ public Cursor runQuery(CharSequence name) { mDescriptionEditText.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView adapterView, View view, int position, long id) { - mTransaction = new Transaction(mTransactionsDbAdapter.getTransaction(id), true); + mTransaction = new Transaction(mTransactionsDbAdapter.getRecord(id), true); mTransaction.setTime(System.currentTimeMillis()); //we check here because next method will modify it and we want to catch user-modification - boolean amountEntered = mAmountInputFormatter.isInputModified(); + boolean amountEntered = mAmountEditText.isInputModified(); initializeViewsWithTransaction(); List splitList = mTransaction.getSplits(); boolean isSplitPair = splitList.size() == 2 && splitList.get(0).isPairOf(splitList.get(1)); if (isSplitPair){ mSplitsList.clear(); if (!amountEntered) //if user already entered an amount - mAmountEditText.setText(splitList.get(0).getAmount().toPlainString()); + mAmountEditText.setValue(splitList.get(0).getValue().asBigDecimal()); } else { if (amountEntered){ //if user entered own amount, clear loaded splits and use the user value mSplitsList.clear(); - setAmountEditViewVisible(View.VISIBLE); + setDoubleEntryViewsVisibility(View.VISIBLE); } else { if (mUseDoubleEntry) { //don't hide the view in single entry mode - setAmountEditViewVisible(View.GONE); + setDoubleEntryViewsVisibility(View.GONE); } } } @@ -388,13 +441,14 @@ public void onItemClick(AdapterView adapterView, View view, int position, lon */ private void initializeViewsWithTransaction(){ mDescriptionEditText.setText(mTransaction.getDescription()); + mDescriptionEditText.setSelection(mDescriptionEditText.getText().length()); - mTransactionTypeButton.setAccountType(mAccountType); - mTransactionTypeButton.setChecked(mTransaction.getBalance(mAccountUID).isNegative()); + mTransactionTypeSwitch.setAccountType(mAccountType); + mTransactionTypeSwitch.setChecked(mTransaction.getBalance(mAccountUID).isNegative()); - if (!mAmountInputFormatter.isInputModified()){ + if (!mAmountEditText.isInputModified()){ //when autocompleting, only change the amount if the user has not manually changed it already - mAmountEditText.setText(mTransaction.getBalance(mAccountUID).toPlainString()); + mAmountEditText.setValue(mTransaction.getBalance(mAccountUID).asBigDecimal()); } mCurrencyTextView.setText(mTransaction.getCurrency().getSymbol(Locale.getDefault())); mNotesEditText.setText(mTransaction.getNote()); @@ -406,8 +460,17 @@ private void initializeViewsWithTransaction(){ //TODO: deep copy the split list. We need a copy so we can modify with impunity mSplitsList = new ArrayList<>(mTransaction.getSplits()); - mAmountEditText.setEnabled(mSplitsList.size() <= 2); + toggleAmountInputEntryMode(mSplitsList.size() <= 2); + if (mSplitsList.size() == 2){ + for (Split split : mSplitsList) { + if (split.getAccountUID().equals(mAccountUID)) { + if (!split.getQuantity().getCurrency().equals(mTransaction.getCurrency())){ + mSplitQuantity = split.getQuantity(); + } + } + } + } //if there are more than two splits (which is the default for one entry), then //disable editing of the transfer account. User should open editor if (mSplitsList.size() == 2 && mSplitsList.get(0).isPairOf(mSplitsList.get(1))) { @@ -418,44 +481,41 @@ private void initializeViewsWithTransaction(){ } } } else { - if (mUseDoubleEntry) { - setAmountEditViewVisible(View.GONE); - } + setDoubleEntryViewsVisibility(View.GONE); } String currencyCode = mTransactionsDbAdapter.getAccountCurrencyCode(mAccountUID); Currency accountCurrency = Currency.getInstance(currencyCode); mCurrencyTextView.setText(accountCurrency.getSymbol()); - if (mMultiCurrency) { - enableControls(false); - } mSaveTemplateCheckbox.setChecked(mTransaction.isTemplate()); String scheduledActionUID = getArguments().getString(UxArgument.SCHEDULED_ACTION_UID); if (scheduledActionUID != null && !scheduledActionUID.isEmpty()) { - ScheduledAction scheduledAction = ScheduledActionDbAdapter.getInstance().getScheduledAction(scheduledActionUID); + ScheduledAction scheduledAction = ScheduledActionDbAdapter.getInstance().getRecord(scheduledActionUID); mRecurrenceRule = scheduledAction.getRuleString(); mEventRecurrence.parse(mRecurrenceRule); mRecurrenceTextView.setText(scheduledAction.getRepeatString()); } } - private void enableControls(boolean b) { - mDescriptionEditText.setEnabled(b); - mNotesEditText.setEnabled(b); - mDateTextView.setEnabled(b); - mTimeTextView.setEnabled(b); - mAmountEditText.setEnabled(b); - mCurrencyTextView.setEnabled(b); - mTransactionTypeButton.setEnabled(b); - mDoubleAccountSpinner.setEnabled(b); - // the next is always enabled, so the user can check the detailed info of splits - // mOpenSplitsButton; + private void setDoubleEntryViewsVisibility(int visibility) { + mDoubleEntryLayout.setVisibility(visibility); + mTransactionTypeSwitch.setVisibility(visibility); } - private void setAmountEditViewVisible(int visibility) { - getView().findViewById(R.id.layout_double_entry).setVisibility(visibility); - mTransactionTypeButton.setVisibility(visibility); + private void toggleAmountInputEntryMode(boolean enabled){ + if (enabled){ + mAmountEditText.setFocusable(true); + mAmountEditText.bindListeners(mKeyboardView); + } else { + mAmountEditText.setFocusable(false); + mAmountEditText.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + openSplitEditor(); + } + }); + } } /** @@ -467,9 +527,9 @@ private void initalizeViews() { mTimeTextView.setText(TIME_FORMATTER.format(time)); mTime = mDate = Calendar.getInstance(); - mTransactionTypeButton.setAccountType(mAccountType); + mTransactionTypeSwitch.setAccountType(mAccountType); String typePref = PreferenceManager.getDefaultSharedPreferences(getActivity()).getString(getString(R.string.key_default_transaction_type), "DEBIT"); - mTransactionTypeButton.setChecked(TransactionType.valueOf(typePref)); + mTransactionTypeSwitch.setChecked(TransactionType.valueOf(typePref)); String code = Money.DEFAULT_CURRENCY_CODE; if (mAccountUID != null){ @@ -499,8 +559,7 @@ private void initalizeViews() { */ private void updateTransferAccountsList(){ String conditions = "(" + DatabaseSchema.AccountEntry.COLUMN_UID + " != ?" - + " AND " + (mMultiCurrency ? "" : (DatabaseSchema.AccountEntry.COLUMN_CURRENCY + " = '" + mAccountsDbAdapter.getCurrencyCode(mAccountUID) + "'" - + " AND ")) + DatabaseSchema.AccountEntry.COLUMN_TYPE + " != ?" + + " AND " + DatabaseSchema.AccountEntry.COLUMN_TYPE + " != ?" + " AND " + DatabaseSchema.AccountEntry.COLUMN_PLACEHOLDER + " = 0" + ")"; @@ -509,84 +568,81 @@ private void updateTransferAccountsList(){ } mCursor = mAccountsDbAdapter.fetchAccountsOrderedByFullName(conditions, new String[]{mAccountUID, AccountType.ROOT.name()}); - mCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), - android.R.layout.simple_spinner_item, mCursor); - mCursorAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - mDoubleAccountSpinner.setAdapter(mCursorAdapter); + mCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), mCursor); + mTransferAccountSpinner.setAdapter(mCursorAdapter); } /** * Opens the split editor dialog */ private void openSplitEditor(){ - if (mAmountEditText.getText().toString().length() == 0){ + if (mAmountEditText.getValue() == null){ Toast.makeText(getActivity(), "Please enter an amount to split", Toast.LENGTH_SHORT).show(); return; } - FragmentManager fragmentManager = getActivity().getSupportFragmentManager(); + String baseAmountString; if (mTransaction == null){ //if we are creating a new transaction (not editing an existing one) - BigDecimal enteredAmount = parseInputToDecimal(mAmountEditText.getText().toString()); + BigDecimal enteredAmount = mAmountEditText.getValue(); baseAmountString = enteredAmount.toPlainString(); } else { Money biggestAmount = Money.createZeroInstance(mTransaction.getCurrencyCode()); for (Split split : mTransaction.getSplits()) { - if (split.getAmount().asBigDecimal().compareTo(biggestAmount.asBigDecimal()) > 0) - biggestAmount = split.getAmount(); + if (split.getValue().asBigDecimal().compareTo(biggestAmount.asBigDecimal()) > 0) + biggestAmount = split.getValue(); } baseAmountString = biggestAmount.toPlainString(); } - SplitEditorDialogFragment splitEditorDialogFragment = - SplitEditorDialogFragment.newInstance(baseAmountString); - splitEditorDialogFragment.setTargetFragment(TransactionFormFragment.this, 0); - splitEditorDialogFragment.show(fragmentManager, FRAGMENT_TAG_SPLITS_EDITOR); + Intent intent = new Intent(getActivity(), FormActivity.class); + intent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.SPLIT_EDITOR.name()); + intent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, mAccountUID); + intent.putExtra(UxArgument.AMOUNT_STRING, baseAmountString); + if (mSplitsList != null) { + ArrayList splitStrings = new ArrayList<>(); + for (Split split : mSplitsList) { + splitStrings.add(split.toCsv()); + } + intent.putStringArrayListExtra(UxArgument.SPLIT_LIST, splitStrings); + } + startActivityForResult(intent, REQUEST_SPLIT_EDITOR); } + /** * Sets click listeners for the dialog buttons */ private void setListeners() { - mAmountInputFormatter = new AmountTextWatcher(mAmountEditText); //new AmountInputFormatter(mAmountEditText); - mAmountEditText.addTextChangedListener(mAmountInputFormatter); - - mOpenSplitsButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - openSplitEditor(); - } - }); - - mTransactionTypeButton.setAmountFormattingListener(mAmountEditText, mCurrencyTextView); + mTransactionTypeSwitch.setAmountFormattingListener(mAmountEditText, mCurrencyTextView); mDateTextView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - long dateMillis = 0; - try { - Date date = DATE_FORMATTER.parse(mDateTextView.getText().toString()); - dateMillis = date.getTime(); - } catch (ParseException e) { - Log.e(getTag(), "Error converting input time to Date object"); - } + @Override + public void onClick(View v) { + long dateMillis = 0; + try { + Date date = DATE_FORMATTER.parse(mDateTextView.getText().toString()); + dateMillis = date.getTime(); + } catch (ParseException e) { + Log.e(getTag(), "Error converting input time to Date object"); + } Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(dateMillis); int year = calendar.get(Calendar.YEAR); int monthOfYear = calendar.get(Calendar.MONTH); int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); - CalendarDatePickerDialog datePickerDialog = CalendarDatePickerDialog.newInstance(TransactionFormFragment.this, + CalendarDatePickerDialog datePickerDialog = CalendarDatePickerDialog.newInstance( + TransactionFormFragment.this, year, monthOfYear, dayOfMonth); datePickerDialog.show(getFragmentManager(), "date_picker_fragment"); - } - }); + } + }); mTimeTextView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - FragmentTransaction ft = getFragmentManager().beginTransaction(); long timeMillis = 0; try { Date date = TIME_FORMATTER.parse(mTimeTextView.getText().toString()); @@ -608,7 +664,7 @@ public void onClick(View v) { mRecurrenceTextView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - FragmentManager fm = getSherlockActivity().getSupportFragmentManager(); + FragmentManager fm = getActivity().getSupportFragmentManager(); Bundle b = new Bundle(); Time t = new Time(); t.setToNow(); @@ -639,42 +695,23 @@ private void setSelectedTransferAccount(long accountId){ for (int pos = 0; pos < mCursorAdapter.getCount(); pos++) { if (mCursorAdapter.getItemId(pos) == accountId){ final int position = pos; - mDoubleAccountSpinner.postDelayed(new Runnable() { + mTransferAccountSpinner.postDelayed(new Runnable() { @Override public void run() { - mDoubleAccountSpinner.setSelection(position); + mTransferAccountSpinner.setSelection(position); } - }, 200); + }, 100); break; } } } - /** - * Callback when the account in the navigation bar is changed by the user - * @param newAccountUID GUID of the newly selected account - */ - public void onAccountChanged(String newAccountUID) { - if (mMultiCurrency) { - Toast.makeText(getActivity(), R.string.toast_error_edit_multi_currency_transaction, Toast.LENGTH_LONG).show(); - return; - } - AccountsDbAdapter accountsDbAdapter = AccountsDbAdapter.getInstance(); - String currencyCode = accountsDbAdapter.getCurrencyCode(newAccountUID); - Currency currency = Currency.getInstance(currencyCode); - mCurrencyTextView.setText(currency.getSymbol(Locale.getDefault())); - - mAccountType = accountsDbAdapter.getAccountType(newAccountUID); - mTransactionTypeButton.setAccountType(mAccountType); - mAccountUID = newAccountUID; - updateTransferAccountsList(); - } - /** * Collects information from the fragment views and uses it to create * and save a transaction */ private void saveNewTransaction() { + mAmountEditText.getCalculatorKeyboard().hideCustomKeyboard(); Calendar cal = new GregorianCalendar( mDate.get(Calendar.YEAR), mDate.get(Calendar.MONTH), @@ -684,7 +721,7 @@ private void saveNewTransaction() { mTime.get(Calendar.SECOND)); String description = mDescriptionEditText.getText().toString(); String notes = mNotesEditText.getText().toString(); - BigDecimal amountBigd = parseInputToDecimal(mAmountEditText.getText().toString()); + BigDecimal amountBigd = mAmountEditText.getValue(); Currency currency = Currency.getInstance(mTransactionsDbAdapter.getAccountCurrencyCode(mAccountUID)); Money amount = new Money(amountBigd, currency).absolute(); @@ -692,7 +729,7 @@ private void saveNewTransaction() { if (mSplitsList.size() == 1){ //means split editor was opened but no split was added String transferAcctUID; if (mUseDoubleEntry) { - long transferAcctId = mDoubleAccountSpinner.getSelectedItemId(); + long transferAcctId = mTransferAccountSpinner.getSelectedItemId(); transferAcctUID = mAccountsDbAdapter.getUID(transferAcctId); } else { transferAcctUID = mAccountsDbAdapter.getOrCreateImbalanceAccountUID(currency); @@ -705,66 +742,72 @@ private void saveNewTransaction() { //if it is a simple transfer where the editor was not used, then respect the button for (Split split : mSplitsList) { if (split.getAccountUID().equals(mAccountUID)){ - split.setType(mTransactionTypeButton.getTransactionType()); - split.setAmount(amount); + split.setType(mTransactionTypeSwitch.getTransactionType()); + split.setValue(amount); + split.setQuantity(amount); } else { - split.setType(mTransactionTypeButton.getTransactionType().invert()); - split.setAmount(amount); + split.setType(mTransactionTypeSwitch.getTransactionType().invert()); + if (mSplitQuantity != null) + split.setQuantity(mSplitQuantity); + else + split.setQuantity(amount); + split.setValue(amount); } } } - Money splitSum = Money.createZeroInstance(currency.getCurrencyCode()); - for (Split split : mSplitsList) { - Money amt = split.getAmount().absolute(); - if (split.getType() == TransactionType.DEBIT) - splitSum = splitSum.subtract(amt); - else - splitSum = splitSum.add(amt); - } mAccountsDbAdapter.beginTransaction(); try { - if (!splitSum.isAmountZero()) { - Split imbSplit = new Split(splitSum.negate(), mAccountsDbAdapter.getOrCreateImbalanceAccountUID(currency)); - mSplitsList.add(imbSplit); - } if (mTransaction != null) { //if editing an existing transaction mTransaction.setSplits(mSplitsList); mTransaction.setDescription(description); } else { mTransaction = new Transaction(description); - if (mSplitsList.isEmpty()) { //amount entered in the simple interface (not using splits Editor) + //****************** amount entered in the simple interface (not using splits Editor) ************************ + if (mSplitsList.isEmpty()) { Split split = new Split(amount, mAccountUID); - split.setType(mTransactionTypeButton.getTransactionType()); + split.setType(mTransactionTypeSwitch.getTransactionType()); mTransaction.addSplit(split); String transferAcctUID; if (mUseDoubleEntry) { - long transferAcctId = mDoubleAccountSpinner.getSelectedItemId(); + long transferAcctId = mTransferAccountSpinner.getSelectedItemId(); transferAcctUID = mAccountsDbAdapter.getUID(transferAcctId); } else { transferAcctUID = mAccountsDbAdapter.getOrCreateImbalanceAccountUID(currency); } - mTransaction.addSplit(split.createPair(transferAcctUID)); + Split pair = split.createPair(transferAcctUID); + if (mSplitQuantity != null) + pair.setQuantity(mSplitQuantity); + else { + if (!mAccountsDbAdapter.getCurrencyCode(transferAcctUID).equals(currency.getCurrencyCode())){ + startTransferFunds(); + mTransaction = null; + return; + } + } + mTransaction.addSplit(pair); } else { //split editor was used to enter splits mTransaction.setSplits(mSplitsList); } } - mTransaction.setCurrencyCode(mAccountsDbAdapter.getAccountCurrencyCode(mAccountUID)); + String currencyCode = mAccountsDbAdapter.getAccountCurrencyCode(mAccountUID); + mTransaction.setCurrencyCode(currencyCode); + mTransaction.setCommodityUID(mAccountsDbAdapter.getCommodityUID(currencyCode)); mTransaction.setTime(cal.getTimeInMillis()); mTransaction.setNote(notes); // set as not exported because we have just edited it mTransaction.setExported(false); - mTransactionsDbAdapter.addTransaction(mTransaction); + mTransactionsDbAdapter.addRecord(mTransaction); if (mSaveTemplateCheckbox.isChecked()) {//template is automatically checked when a transaction is scheduled if (!mEditMode) { //means it was new transaction, so a new template Transaction templateTransaction = new Transaction(mTransaction, true); templateTransaction.setTemplate(true); - mTransactionsDbAdapter.addTransaction(templateTransaction); + mTransactionsDbAdapter.addRecord(templateTransaction); scheduleRecurringTransaction(templateTransaction.getUID()); } else scheduleRecurringTransaction(mTransaction.getUID()); @@ -784,7 +827,7 @@ private void saveNewTransaction() { //update widgets, if any WidgetConfigurationActivity.updateAllWidgets(getActivity().getApplicationContext()); - finish(); + finish(Activity.RESULT_OK); } /** @@ -815,13 +858,11 @@ private void scheduleRecurringTransaction(String transactionUID) { for (ScheduledAction event : events) { event.setActionUID(transactionUID); - scheduledActionDbAdapter.addScheduledAction(event); + scheduledActionDbAdapter.addRecord(event); Log.i("TransactionFormFragment", event.toString()); } - Toast.makeText(getActivity(), "Scheduled recurring transaction", Toast.LENGTH_SHORT).show(); - - //TODO: localize this toast string for all supported locales + Toast.makeText(getActivity(), R.string.toast_scheduled_recurring_transaction, Toast.LENGTH_SHORT).show(); } @@ -845,23 +886,22 @@ public boolean onOptionsItemSelected(MenuItem item) { imm.hideSoftInputFromWindow(mDescriptionEditText.getApplicationWindowToken(), 0); switch (item.getItemId()) { - case R.id.menu_cancel: - finish(); - return true; + case android.R.id.home: + finish(Activity.RESULT_CANCELED); + return true; case R.id.menu_save: - if (mMultiCurrency) { - Toast.makeText(getActivity(), R.string.toast_error_edit_multi_currency_transaction, Toast.LENGTH_LONG).show(); - finish(); - } - else if (mAmountEditText.getText().length() == 0) { - Toast.makeText(getActivity(), R.string.toast_transanction_amount_required, Toast.LENGTH_SHORT).show(); - } else if (mUseDoubleEntry && mDoubleAccountSpinner.getCount() == 0){ - Toast.makeText(getActivity(), - R.string.toast_disable_double_entry_to_save_transaction, - Toast.LENGTH_LONG).show(); - } else { + if (canSave()){ saveNewTransaction(); + } else { + if (mAmountEditText.getValue() == null) { + Toast.makeText(getActivity(), R.string.toast_transanction_amount_required, Toast.LENGTH_SHORT).show(); + } + if (mUseDoubleEntry && mTransferAccountSpinner.getCount() == 0){ + Toast.makeText(getActivity(), + R.string.toast_disable_double_entry_to_save_transaction, + Toast.LENGTH_LONG).show(); + } } return true; @@ -870,6 +910,16 @@ else if (mAmountEditText.getText().length() == 0) { } } + /** + * Checks if the pre-requisites for saving the transaction are fulfilled + *

The conditions checked are that a valid amount is entered and that a transfer account is set (where applicable)

+ * @return {@code true} if the transaction can be saved, {@code false} otherwise + */ + private boolean canSave(){ + return (mAmountEditText.isInputValid()) + || (mUseDoubleEntry && mTransferAccountSpinner.getCount() == 0); + } + /** * Called by the split editor fragment to notify of finished editing * @param splitList List of splits produced in the fragment @@ -878,34 +928,29 @@ public void setSplitList(List splitList, List removedSplitUIDs){ mSplitsList = splitList; Money balance = Transaction.computeBalance(mAccountUID, mSplitsList); - mAmountEditText.setText(balance.toPlainString()); - mTransactionTypeButton.setChecked(balance.isNegative()); + mAmountEditText.setValue(balance.asBigDecimal()); + mTransactionTypeSwitch.setChecked(balance.isNegative()); //once we set the split list, do not allow direct editing of the total if (mSplitsList.size() > 1){ - mAmountEditText.setEnabled(false); - setAmountEditViewVisible(View.GONE); + toggleAmountInputEntryMode(false); + setDoubleEntryViewsVisibility(View.GONE); + mOpenSplitEditor.setVisibility(View.VISIBLE); } } - /** - * Returns the list of splits currently in editing - * @return List of splits - */ - public List getSplitList(){ - return mSplitsList; - } /** * Finishes the fragment appropriately. * Depends on how the fragment was loaded, it might have a backstack or not */ - private void finish() { + private void finish(int resultCode) { if (getActivity().getSupportFragmentManager().getBackStackEntryCount() == 0){ + getActivity().setResult(resultCode); //means we got here directly from the accounts list activity, need to finish getActivity().finish(); } else { //go back to transactions list - getSherlockActivity().getSupportFragmentManager().popBackStack(); + getActivity().getSupportFragmentManager().popBackStack(); } } @@ -963,6 +1008,10 @@ public static BigDecimal parseInputToDecimal(String amountString){ RoundingMode.HALF_EVEN); } + @Override + public void transferComplete(Money amount) { + mSplitQuantity = amount; + } @Override public void onRecurrenceSet(String rrule) { @@ -982,21 +1031,17 @@ public void onRecurrenceSet(String rrule) { mRecurrenceTextView.setText(repeatString); } - private class AmountTextWatcher extends AmountInputFormatter { - - public AmountTextWatcher(EditText amountInput) { - super(amountInput); - } - @Override - public void afterTextChanged(Editable s) { - String value = s.toString(); - if (value.length() > 0 && mTransactionTypeButton.isChecked()){ - if (s.charAt(0) != '-'){ - s = Editable.Factory.getInstance().newEditable("-" + value); - } + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK){ + List splits = data.getStringArrayListExtra(UxArgument.SPLIT_LIST); + List splitList = new ArrayList<>(); + for (String splitCsv : splits) { + splitList.add(Split.parseSplit(splitCsv)); } - super.afterTextChanged(s); + List removedSplits = data.getStringArrayListExtra(UxArgument.REMOVED_SPLITS); + setSplitList(splitList, removedSplits); } } } 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 03a8a7300..350f0a503 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 @@ -20,59 +20,63 @@ import android.content.Context; import android.content.Intent; import android.database.Cursor; -import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; +import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.TabLayout; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentStatePagerAdapter; -import android.support.v4.app.FragmentTransaction; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; -import android.support.v4.widget.ResourceCursorAdapter; +import android.support.v7.widget.Toolbar; import android.util.Log; import android.util.SparseArray; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Spinner; import android.widget.SpinnerAdapter; import android.widget.TextView; -import com.actionbarsherlock.app.ActionBar; -import com.actionbarsherlock.app.ActionBar.OnNavigationListener; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuItem; -import com.viewpagerindicator.TitlePageIndicator; - import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.db.AccountsDbAdapter; import org.gnucash.android.db.DatabaseSchema; +import org.gnucash.android.db.TransactionsDbAdapter; import org.gnucash.android.model.Account; import org.gnucash.android.model.Money; -import org.gnucash.android.ui.UxArgument; +import org.gnucash.android.ui.common.BaseDrawerActivity; +import org.gnucash.android.ui.common.FormActivity; +import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.account.AccountsActivity; import org.gnucash.android.ui.account.AccountsListFragment; -import org.gnucash.android.ui.passcode.PassLockActivity; +import org.gnucash.android.ui.util.AccountBalanceTask; import org.gnucash.android.ui.util.OnAccountClickedListener; import org.gnucash.android.ui.util.OnTransactionClickedListener; import org.gnucash.android.ui.util.Refreshable; import org.gnucash.android.util.QualifiedAccountNameCursorAdapter; +import java.math.BigDecimal; + +import butterknife.Bind; +import butterknife.ButterKnife; + /** * Activity for displaying, creating and editing transactions * @author Ngewi Fet */ -public class TransactionsActivity extends PassLockActivity implements +public class TransactionsActivity extends BaseDrawerActivity implements Refreshable, OnAccountClickedListener, OnTransactionClickedListener{ /** * Logging tag */ - protected static final String TAG = "AccountsActivity"; - - /** - * Tag for {@link TransactionFormFragment} - */ - public static final String FRAGMENT_NEW_TRANSACTION = "new_transaction"; + protected static final String TAG = "TransactionsActivity"; /** * ViewPager index for sub-accounts fragment @@ -94,15 +98,6 @@ public class TransactionsActivity extends PassLockActivity implements */ private String mAccountUID = null; - /** - * Flag which is used to determine if the activity is running or not. - * Basically if onCreate has already been called or not. It is used - * to determine if to call addToBackStack() for fragments. When adding - * the very first fragment, it should not be added to the backstack. - * @see #showTransactionFormFragment(Bundle) - */ - private boolean mActivityRunning = false; - /** * Account database adapter for manipulating the accounts list in navigation */ @@ -113,36 +108,52 @@ public class TransactionsActivity extends PassLockActivity implements */ private Cursor mAccountsCursor = null; - private TextView mSectionHeaderTransactions; - private TitlePageIndicator mTitlePageIndicator; - - private ViewPager mPager; + @Bind(R.id.pager) ViewPager mViewPager; + @Bind(R.id.spinner_toolbar) Spinner mToolbarSpinner; + @Bind(R.id.tab_layout) TabLayout mTabLayout; + @Bind(R.id.transactions_sum) TextView mSumTextView; + @Bind(R.id.fab_create_transaction) FloatingActionButton mCreateFloatingButton; private SparseArray mFragmentPageReferenceMap = new SparseArray<>(); - private OnNavigationListener mTransactionListNavigationListener = new OnNavigationListener() { - - @Override - public boolean onNavigationItemSelected(int position, long itemId) { - mAccountUID = mAccountsDbAdapter.getUID(itemId); - FragmentManager fragmentManager = getSupportFragmentManager(); - - //inform new accounts fragment that account was changed - TransactionFormFragment newTransactionsFragment = (TransactionFormFragment) fragmentManager - .findFragmentByTag(FRAGMENT_NEW_TRANSACTION); - if (newTransactionsFragment != null){ - newTransactionsFragment.onAccountChanged(mAccountUID); - //if we do not return, the transactions list fragment could also be found (although it's not visible) - return true; - } + /** + * Flag for determining is the currently displayed account is a placeholder account or not. + * This will determine if the transactions tab is displayed or not + */ + private boolean mIsPlaceholderAccount; + + private AdapterView.OnItemSelectedListener mTransactionListNavigationListener = new AdapterView.OnItemSelectedListener() { + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + mAccountUID = mAccountsDbAdapter.getUID(id); + getIntent().putExtra(UxArgument.SELECTED_ACCOUNT_UID, mAccountUID); //update the intent in case the account gets rotated + mIsPlaceholderAccount = mAccountsDbAdapter.isPlaceholderAccount(mAccountUID); + if (mIsPlaceholderAccount){ + if (mTabLayout.getTabCount() > 1) { + mPagerAdapter.notifyDataSetChanged(); + mTabLayout.removeTabAt(1); + } + } else { + if (mTabLayout.getTabCount() < 2) { + mPagerAdapter.notifyDataSetChanged(); + mTabLayout.addTab(mTabLayout.newTab().setText(R.string.section_header_transactions)); + } + } //refresh any fragments in the tab with the new account UID refresh(); - return true; - } + } + + @Override + public void onNothingSelected(AdapterView parent) { + //nothing to see here, move along + } }; + private PagerAdapter mPagerAdapter; + /** * Adapter for managing the sub-account and transaction fragment pages in the accounts view */ @@ -154,7 +165,7 @@ public AccountViewPagerAdapter(FragmentManager fm){ @Override public Fragment getItem(int i) { - if (isPlaceHolderAccount()){ + if (mIsPlaceholderAccount){ Fragment transactionsListFragment = prepareSubAccountsListFragment(); mFragmentPageReferenceMap.put(i, (Refreshable) transactionsListFragment); return transactionsListFragment; @@ -184,7 +195,7 @@ public void destroyItem(ViewGroup container, int position, Object object) { @Override public CharSequence getPageTitle(int position) { - if (isPlaceHolderAccount()) + if (mIsPlaceholderAccount) return getString(R.string.section_header_subaccounts); switch (position){ @@ -199,7 +210,7 @@ public CharSequence getPageTitle(int position) { @Override public int getCount() { - if (isPlaceHolderAccount()) + if (mIsPlaceholderAccount) return 1; else return DEFAULT_NUM_PAGES; @@ -231,14 +242,6 @@ private TransactionsListFragment prepareTransactionsListFragment(){ } } - /** - * Returns true is the current account is a placeholder account, false otherwise. - * @return true is the current account is a placeholder account, false otherwise. - */ - private boolean isPlaceHolderAccount(){ - return mAccountsDbAdapter.isPlaceholderAccount(mAccountUID); - } - /** * Refreshes the fragments currently in the transactions activity */ @@ -247,8 +250,16 @@ public void refresh(String accountUID) { for (int i = 0; i < mFragmentPageReferenceMap.size(); i++) { mFragmentPageReferenceMap.valueAt(i).refresh(accountUID); } - mTitlePageIndicator.notifyDataSetChanged(); - mPagerAdapter.notifyDataSetChanged(); + + 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); + } } @Override @@ -259,58 +270,78 @@ public void refresh(){ @Override protected void onCreate(Bundle savedInstanceState) { - //it is necessary to set the view first before calling super because of the nav drawer in BaseDrawerActivity - setContentView(R.layout.activity_transactions); super.onCreate(savedInstanceState); + setContentView(R.layout.activity_transactions); + setUpDrawer(); - mPager = (ViewPager) findViewById(R.id.pager); - mTitlePageIndicator = (TitlePageIndicator) findViewById(R.id.titles); - mSectionHeaderTransactions = (TextView) findViewById(R.id.section_header_transactions); + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayShowTitleEnabled(false); - mAccountUID = getIntent().getStringExtra(UxArgument.SELECTED_ACCOUNT_UID); + ButterKnife.bind(this); + mAccountUID = getIntent().getStringExtra(UxArgument.SELECTED_ACCOUNT_UID); mAccountsDbAdapter = AccountsDbAdapter.getInstance(); - setupActionBarNavigation(); + mIsPlaceholderAccount = mAccountsDbAdapter.isPlaceholderAccount(mAccountUID); - final String action = getIntent().getAction(); - if (action.equals(Intent.ACTION_INSERT_OR_EDIT) || action.equals(Intent.ACTION_INSERT)) { - mPager.setVisibility(View.GONE); - mTitlePageIndicator.setVisibility(View.GONE); + mTabLayout.addTab(mTabLayout.newTab().setText(R.string.section_header_subaccounts)); + if (!mIsPlaceholderAccount) { + mTabLayout.addTab(mTabLayout.newTab().setText(R.string.section_header_transactions)); + } - initializeCreateOrEditTransaction(); - } else { //load the transactions list - mSectionHeaderTransactions.setVisibility(View.GONE); + setupActionBarNavigation(); - mPagerAdapter = new AccountViewPagerAdapter(getSupportFragmentManager()); - mPager.setAdapter(mPagerAdapter); - mTitlePageIndicator.setViewPager(mPager); + mPagerAdapter = new AccountViewPagerAdapter(getSupportFragmentManager()); + mViewPager.setAdapter(mPagerAdapter); + mViewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(mTabLayout)); - mPager.setCurrentItem(INDEX_TRANSACTIONS_FRAGMENT); - } + mTabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + mViewPager.setCurrentItem(tab.getPosition()); + } - // done creating, activity now running - mActivityRunning = true; - } + @Override + public void onTabUnselected(TabLayout.Tab tab) { + //nothing to see here, move along + } - /** - * Loads the fragment for creating/editing transactions and initializes it to be displayed - */ - private void initializeCreateOrEditTransaction() { - String transactionUID = getIntent().getStringExtra(UxArgument.SELECTED_TRANSACTION_UID); - String scheduledActionUID = getIntent().getStringExtra(UxArgument.SCHEDULED_ACTION_UID); - Bundle args = new Bundle(); - if (transactionUID != null) { - mSectionHeaderTransactions.setText(R.string.title_edit_transaction); - args.putString(UxArgument.SELECTED_TRANSACTION_UID, transactionUID); - args.putString(UxArgument.SELECTED_ACCOUNT_UID, mAccountUID); - args.putString(UxArgument.SCHEDULED_ACTION_UID, scheduledActionUID); + @Override + public void onTabReselected(TabLayout.Tab tab) { + //nothing to see here, move along + } + }); + + //if there are no transactions, and there are sub-accounts, show the sub-accounts + if (TransactionsDbAdapter.getInstance().getTransactionsCount(mAccountUID) == 0 + && mAccountsDbAdapter.getSubAccountCount(mAccountUID) > 0){ + mViewPager.setCurrentItem(INDEX_SUB_ACCOUNTS_FRAGMENT); } else { - mSectionHeaderTransactions.setText(R.string.title_add_transaction); - args.putString(UxArgument.SELECTED_ACCOUNT_UID, mAccountUID); + mViewPager.setCurrentItem(INDEX_TRANSACTIONS_FRAGMENT); } - showTransactionFormFragment(args); - } + + mCreateFloatingButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + switch (mViewPager.getCurrentItem()) { + case INDEX_SUB_ACCOUNTS_FRAGMENT: + Intent addAccountIntent = new Intent(TransactionsActivity.this, FormActivity.class); + addAccountIntent.setAction(Intent.ACTION_INSERT_OR_EDIT); + addAccountIntent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.ACCOUNT.name()); + addAccountIntent.putExtra(UxArgument.PARENT_ACCOUNT_UID, mAccountUID); + startActivityForResult(addAccountIntent, AccountsActivity.REQUEST_EDIT_ACCOUNT); + ; + break; + + case INDEX_TRANSACTIONS_FRAGMENT: + createNewTransaction(mAccountUID); + break; + + } + } + }); + } @Override protected void onResume() { @@ -322,37 +353,15 @@ protected void onResume() { * Sets the color for the ViewPager title indicator to match the account color */ private void setTitleIndicatorColor() { - //Basically, if we are in a top level account, use the default title color. - //but propagate a parent account's title color to children who don't have own color - long accountId = -1; - try { - accountId = mAccountsDbAdapter.getID(mAccountUID); - } catch (IllegalArgumentException e){ - Log.e(TAG, e.getMessage()); - } - String colorCode = mAccountsDbAdapter.getAccountColorCode(accountId); - int iColor = -1; - if (colorCode != null){ - iColor = Color.parseColor(colorCode); - } else { - String accountUID = mAccountUID; - while ((accountUID = mAccountsDbAdapter.getParentAccountUID(accountUID)) != null) { - colorCode = mAccountsDbAdapter.getAccountColorCode(mAccountsDbAdapter.getID(accountUID)); - if (colorCode != null) { - iColor = Color.parseColor(colorCode); - break; - } - } - if (colorCode == null) - { - iColor = getResources().getColor(R.color.title_green); - } - } + int iColor = AccountsDbAdapter.getActiveAccountColorResource(mAccountUID); - mTitlePageIndicator.setSelectedColor(iColor); - mTitlePageIndicator.setTextColor(iColor); - mTitlePageIndicator.setFooterColor(iColor); - mSectionHeaderTransactions.setBackgroundColor(iColor); + mTabLayout.setBackgroundColor(iColor); + + if (getSupportActionBar() != null) + getSupportActionBar().setBackgroundDrawable(new ColorDrawable(iColor)); + + if (Build.VERSION.SDK_INT > 20) + getWindow().setStatusBarColor(GnuCashApplication.darken(iColor)); } /** @@ -366,15 +375,11 @@ private void setupActionBarNavigation() { mAccountsCursor = mAccountsDbAdapter.fetchAllRecordsOrderedByFullName(); SpinnerAdapter mSpinnerAdapter = new QualifiedAccountNameCursorAdapter( - getSupportActionBar().getThemedContext(), - R.layout.sherlock_spinner_item, mAccountsCursor); - ((ResourceCursorAdapter) mSpinnerAdapter) - .setDropDownViewResource(R.layout.sherlock_spinner_dropdown_item); - ActionBar actionBar = getSupportActionBar(); - actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST); - actionBar.setListNavigationCallbacks(mSpinnerAdapter, - mTransactionListNavigationListener); - actionBar.setDisplayHomeAsUpEnabled(true); + getSupportActionBar().getThemedContext(), mAccountsCursor); + + mToolbarSpinner.setAdapter(mSpinnerAdapter); + mToolbarSpinner.setOnItemSelectedListener(mTransactionListNavigationListener); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); updateNavigationSelection(); } @@ -390,7 +395,7 @@ public void updateNavigationSelection() { while (accountsCursor.moveToNext()) { String uid = accountsCursor.getString(accountsCursor.getColumnIndexOrThrow(DatabaseSchema.AccountEntry.COLUMN_UID)); if (mAccountUID.equals(uid)) { - getSupportActionBar().setSelectedNavigationItem(i); + mToolbarSpinner.setSelection(i); break; } ++i; @@ -407,7 +412,7 @@ public boolean onPrepareOptionsMenu(Menu menu) { boolean isFavoriteAccount = AccountsDbAdapter.getInstance().isFavoriteAccount(mAccountUID); - int favoriteIcon = isFavoriteAccount ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off; + int favoriteIcon = isFavoriteAccount ? R.drawable.ic_star_white_24dp : R.drawable.ic_star_border_white_24dp; favoriteAccountMenuItem.setIcon(favoriteIcon); return super.onPrepareOptionsMenu(menu); @@ -429,9 +434,10 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; case R.id.menu_edit_account: - Intent editAccountIntent = new Intent(this, AccountsActivity.class); + Intent editAccountIntent = new Intent(this, FormActivity.class); editAccountIntent.setAction(Intent.ACTION_INSERT_OR_EDIT); editAccountIntent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, mAccountUID); + editAccountIntent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.ACCOUNT.name()); startActivityForResult(editAccountIntent, AccountsActivity.REQUEST_EDIT_ACCOUNT); return true; @@ -447,6 +453,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { refresh(); setupActionBarNavigation(); + super.onActivityResult(requestCode, resultCode, data); } @Override @@ -455,15 +462,6 @@ protected void onDestroy() { mAccountsCursor.close(); } - /** - * Returns the current fragment (either sub-accounts, or transactions) displayed in the activity - * @return Current fragment displayed by the view pager - */ - public Fragment getCurrentPagerFragment(){ - int index = mPager.getCurrentItem(); - return (Fragment) mFragmentPageReferenceMap.get(index); - } - /** * Returns the global unique ID of the current account * @return GUID of the current account @@ -472,48 +470,6 @@ public String getCurrentAccountUID(){ return mAccountUID; } - /** - * Opens a fragment to create a new transaction. - * Is called from the XML views - * @param v View which triggered this method - */ - public void onNewTransactionClick(View v){ - createNewTransaction(mAccountUID); - } - - - /** - * Opens a dialog fragment to create a new account which is a sub account of the current account - * @param v View which triggered this callback - */ - public void onNewAccountClick(View v) { - Intent addAccountIntent = new Intent(this, AccountsActivity.class); - addAccountIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - addAccountIntent.setAction(Intent.ACTION_INSERT_OR_EDIT); - addAccountIntent.putExtra(UxArgument.PARENT_ACCOUNT_UID, mAccountUID); - startActivityForResult(addAccountIntent, AccountsActivity.REQUEST_EDIT_ACCOUNT); - } - - /** - * Loads the transaction insert/edit fragment and passes the arguments - * @param args Bundle arguments to be passed to the fragment - */ - private void showTransactionFormFragment(Bundle args){ - FragmentManager fragmentManager = getSupportFragmentManager(); - FragmentTransaction fragmentTransaction = fragmentManager - .beginTransaction(); - - TransactionFormFragment transactionFormFragment = new TransactionFormFragment(); - transactionFormFragment.setArguments(args); - - fragmentTransaction.add(R.id.fragment_container, - transactionFormFragment, TransactionsActivity.FRAGMENT_NEW_TRANSACTION); - - if (mActivityRunning) - fragmentTransaction.addToBackStack(null); - fragmentTransaction.commit(); - } - /** * Display the balance of a transaction in a text view and format the text color to match the sign of the amount * @param balanceTextView {@link android.widget.TextView} where balance is to be displayed @@ -525,23 +481,27 @@ public static void displayBalance(TextView balanceTextView, Money balance){ int fontColor = balance.isNegative() ? context.getResources().getColor(R.color.debit_red) : context.getResources().getColor(R.color.credit_green); + if (balance.asBigDecimal().compareTo(BigDecimal.ZERO) == 0) + fontColor = context.getResources().getColor(android.R.color.black); balanceTextView.setTextColor(fontColor); } @Override public void createNewTransaction(String accountUID) { - Intent createTransactionIntent = new Intent(this.getApplicationContext(), TransactionsActivity.class); + Intent createTransactionIntent = new Intent(this.getApplicationContext(), FormActivity.class); createTransactionIntent.setAction(Intent.ACTION_INSERT_OR_EDIT); createTransactionIntent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, accountUID); + createTransactionIntent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.TRANSACTION.name()); startActivity(createTransactionIntent); } @Override public void editTransaction(String transactionUID){ - Intent createTransactionIntent = new Intent(this.getApplicationContext(), TransactionsActivity.class); + Intent createTransactionIntent = new Intent(this.getApplicationContext(), FormActivity.class); createTransactionIntent.setAction(Intent.ACTION_INSERT_OR_EDIT); createTransactionIntent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, mAccountUID); createTransactionIntent.putExtra(UxArgument.SELECTED_TRANSACTION_UID, transactionUID); + createTransactionIntent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.TRANSACTION.name()); startActivity(createTransactionIntent); } diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsListFragment.java b/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsListFragment.java index 015cdaa5b..b6f80e65e 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsListFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/TransactionsListFragment.java @@ -16,62 +16,59 @@ package org.gnucash.android.ui.transaction; -import android.app.Activity; import android.content.Context; -import android.content.res.Resources; +import android.content.Intent; +import android.content.res.Configuration; import android.database.Cursor; -import android.graphics.Rect; import android.os.Bundle; -import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; -import android.support.v4.widget.SimpleCursorAdapter; -import android.text.format.DateUtils; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.PopupMenu; +import android.support.v7.widget.RecyclerView; import android.util.Log; -import android.util.SparseBooleanArray; import android.view.LayoutInflater; -import android.view.TouchDelegate; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.CompoundButton.OnCheckedChangeListener; -import android.widget.ListView; +import android.widget.ImageView; import android.widget.TextView; -import com.actionbarsherlock.app.ActionBar; -import com.actionbarsherlock.app.SherlockListFragment; -import com.actionbarsherlock.view.ActionMode; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuInflater; -import com.actionbarsherlock.view.MenuItem; - import org.gnucash.android.R; +import org.gnucash.android.db.AccountsDbAdapter; +import org.gnucash.android.ui.util.CursorRecyclerAdapter; import org.gnucash.android.db.DatabaseCursorLoader; import org.gnucash.android.db.DatabaseSchema; +import org.gnucash.android.db.SplitsDbAdapter; import org.gnucash.android.db.TransactionsDbAdapter; import org.gnucash.android.model.Money; -import org.gnucash.android.ui.UxArgument; -import org.gnucash.android.ui.transaction.dialog.BulkMoveDialogFragment; -import org.gnucash.android.ui.util.AccountBalanceTask; -import org.gnucash.android.ui.util.OnTransactionClickedListener; +import org.gnucash.android.model.Split; +import org.gnucash.android.ui.common.FormActivity; +import org.gnucash.android.ui.common.UxArgument; +import org.gnucash.android.ui.util.widget.EmptyRecyclerView; import org.gnucash.android.ui.util.Refreshable; -import org.gnucash.android.ui.widget.WidgetConfigurationActivity; +import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; +import org.ocpsoft.prettytime.PrettyTime; -import java.text.SimpleDateFormat; import java.util.Date; +import java.util.List; + +import butterknife.Bind; +import butterknife.ButterKnife; /** * List Fragment for displaying list of transactions for an account * @author Ngewi Fet * */ -public class TransactionsListFragment extends SherlockListFragment implements - Refreshable, LoaderCallbacks, AdapterView.OnItemLongClickListener{ +public class TransactionsListFragment extends Fragment implements + Refreshable, LoaderCallbacks{ /** * Logging tag @@ -79,64 +76,14 @@ public class TransactionsListFragment extends SherlockListFragment implements protected static final String LOG_TAG = "TransactionListFragment"; private TransactionsDbAdapter mTransactionsDbAdapter; - private SimpleCursorAdapter mCursorAdapter; - private ActionMode mActionMode = null; - private boolean mInEditMode = false; private String mAccountUID; - /** - * Callback listener for editing transactions - */ - private OnTransactionClickedListener mTransactionEditListener; - - /** - * Callbacks for the menu items in the Context ActionBar (CAB) in action mode - */ - private ActionMode.Callback mActionModeCallbacks = new ActionMode.Callback() { - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - MenuInflater inflater = mode.getMenuInflater(); - inflater.inflate(R.menu.transactions_context_menu, menu); - return true; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - //nothing to see here, move along - return false; - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - finishEditMode(); - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - switch (item.getItemId()) { - case R.id.context_menu_move_transactions: - showBulkMoveDialog(); - mode.finish(); - WidgetConfigurationActivity.updateAllWidgets(getActivity()); - return true; - - case R.id.context_menu_delete: - for (long id : getListView().getCheckedItemIds()) { - mTransactionsDbAdapter.deleteRecord(id); - } - refresh(); - mode.finish(); - WidgetConfigurationActivity.updateAllWidgets(getActivity()); - return true; - - default: - return false; - } - } - }; - @Override + private TransactionRecyclerAdapter mTransactionRecyclerAdapter; + @Bind(R.id.transaction_recycler_view) EmptyRecyclerView mRecyclerView; + + + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); @@ -144,30 +91,38 @@ public void onCreate(Bundle savedInstanceState) { mAccountUID = args.getString(UxArgument.SELECTED_ACCOUNT_UID); mTransactionsDbAdapter = TransactionsDbAdapter.getInstance(); - mCursorAdapter = new TransactionsCursorAdapter( - getActivity().getApplicationContext(), - R.layout.list_item_transaction, null, - new String[] {DatabaseSchema.TransactionEntry.COLUMN_DESCRIPTION}, - new int[] {R.id.primary_text}); - setListAdapter(mCursorAdapter); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_transactions_list, container, false); + View view = inflater.inflate(R.layout.fragment_transactions_list, container, false); + ButterKnife.bind(this, view); + + mRecyclerView.setHasFixedSize(true); + if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { + GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), 2); + mRecyclerView.setLayoutManager(gridLayoutManager); + } else { + LinearLayoutManager mLayoutManager = new LinearLayoutManager(getActivity()); + mRecyclerView.setLayoutManager(mLayoutManager); + } + mRecyclerView.setEmptyView(view.findViewById(R.id.empty_view)); + + return view; } @Override - public void onActivityCreated(Bundle savedInstanceState) { + public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - ActionBar aBar = getSherlockActivity().getSupportActionBar(); + ActionBar aBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); aBar.setDisplayShowTitleEnabled(false); aBar.setDisplayHomeAsUpEnabled(true); - getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); - getListView().setOnItemLongClickListener(this); + mTransactionRecyclerAdapter = new TransactionRecyclerAdapter(null); + mRecyclerView.setAdapter(mTransactionRecyclerAdapter); + setHasOptionsMenu(true); } @@ -188,63 +143,32 @@ public void refresh(String accountUID){ public void refresh(){ getLoaderManager().restartLoader(0, null, this); - /* - Text view displaying the sum of the accounts - */ - TextView mSumTextView = (TextView) getView().findViewById(R.id.transactions_sum); - new AccountBalanceTask(mSumTextView).execute(mAccountUID); - - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - try { - mTransactionEditListener = (OnTransactionClickedListener) activity; - } catch (ClassCastException e) { - throw new ClassCastException(activity.toString() + " must implement OnAccountSelectedListener"); - } } @Override public void onResume() { super.onResume(); - ((TransactionsActivity)getSherlockActivity()).updateNavigationSelection(); + ((TransactionsActivity)getActivity()).updateNavigationSelection(); refresh(); } - - @Override - public void onListItemClick(ListView l, View v, int position, long id) { - super.onListItemClick(l, v, position, id); - if (mInEditMode){ - CheckBox checkbox = (CheckBox) v.findViewById(R.id.checkbox_transaction); - checkbox.setChecked(!checkbox.isChecked()); - return; - } - mTransactionEditListener.editTransaction(mTransactionsDbAdapter.getUID(id)); - } - @Override - public boolean onItemLongClick(AdapterView adapterView, View view, int position, long id) { - getListView().setItemChecked(position, true); - CheckBox checkbox = (CheckBox) view.findViewById(R.id.checkbox_transaction); - checkbox.setChecked(true); - startActionMode(); - return true; + public void onListItemClick(long id) { + Intent intent = new Intent(getActivity(), TransactionDetailActivity.class); + intent.putExtra(UxArgument.SELECTED_TRANSACTION_UID, mTransactionsDbAdapter.getUID(id)); + intent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, mAccountUID); + startActivity(intent); +// mTransactionEditListener.editTransaction(mTransactionsDbAdapter.getUID(id)); } + @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.transactions_list_actions, menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { - case R.id.menu_add_transaction: - mTransactionEditListener.createNewTransaction(mAccountUID); - return true; - default: return super.onOptionsItemSelected(item); } @@ -259,234 +183,16 @@ public Loader onCreateLoader(int arg0, Bundle arg1) { @Override public void onLoadFinished(Loader loader, Cursor cursor) { Log.d(LOG_TAG, "Transactions loader finished. Swapping in cursor"); - mCursorAdapter.swapCursor(cursor); - mCursorAdapter.notifyDataSetChanged(); + mTransactionRecyclerAdapter.swapCursor(cursor); + mTransactionRecyclerAdapter.notifyDataSetChanged(); } @Override public void onLoaderReset(Loader loader) { Log.d(LOG_TAG, "Resetting transactions loader"); - mCursorAdapter.swapCursor(null); - } - - /** - * Finishes the edit mode in the transactions list. - * Edit mode is started when at least one transaction is selected - */ - public void finishEditMode(){ - mInEditMode = false; - uncheckAllItems(); - mActionMode = null; - } - - /** - * Sets the title of the Context ActionBar when in action mode. - * It sets the number highlighted items - */ - public void setActionModeTitle(){ - int count = getListView().getCheckedItemIds().length; //mSelectedIds.size(); - if (count > 0 && mActionMode != null){ - mActionMode.setTitle(getResources().getString(R.string.title_selected, count)); - } + mTransactionRecyclerAdapter.swapCursor(null); } - - /** - * Unchecks all the checked items in the list - */ - private void uncheckAllItems() { - SparseBooleanArray checkedPositions = getListView().getCheckedItemPositions(); - ListView listView = getListView(); - for (int i = 0; i < checkedPositions.size(); i++) { - int position = checkedPositions.keyAt(i); - listView.setItemChecked(position, false); - } - } - - - /** - * Starts action mode and activates the Context ActionBar (CAB) - * Action mode is initiated as soon as at least one transaction is selected (highlighted) - */ - private void startActionMode(){ - if (mActionMode != null) { - return; - } - mInEditMode = true; - // Start the CAB using the ActionMode.Callback defined above - mActionMode = getSherlockActivity().startActionMode(mActionModeCallbacks); - } - - /** - * Stops action mode and deselects all selected transactions. - * This method only has effect if the number of checked items is greater than 0 and {@link #mActionMode} is not null - */ - private void stopActionMode(){ - int checkedCount = getListView().getCheckedItemIds().length; - if (checkedCount <= 0 && mActionMode != null) { - mActionMode.finish(); - } - } - - /** - * Prepares and displays the dialog for bulk moving transactions to another account - */ - protected void showBulkMoveDialog(){ - FragmentManager manager = getActivity().getSupportFragmentManager(); - FragmentTransaction ft = manager.beginTransaction(); - Fragment prev = manager.findFragmentByTag("bulk_move_dialog"); - if (prev != null) { - ft.remove(prev); - } - ft.addToBackStack(null); - - // Create and show the dialog. - DialogFragment bulkMoveFragment = new BulkMoveDialogFragment(); - Bundle args = new Bundle(); - args.putString(UxArgument.ORIGIN_ACCOUNT_UID, mAccountUID); - args.putLongArray(UxArgument.SELECTED_TRANSACTION_IDS, getListView().getCheckedItemIds()); - bulkMoveFragment.setArguments(args); - bulkMoveFragment.setTargetFragment(this, 0); - bulkMoveFragment.show(ft, "bulk_move_dialog"); - } - - /** - * Extends a simple cursor adapter to bind transaction attributes to views - * @author Ngewi Fet - */ - protected class TransactionsCursorAdapter extends SimpleCursorAdapter { - - public TransactionsCursorAdapter(Context context, int layout, Cursor c, - String[] from, int[] to) { - super(context, layout, c, from, to, 0); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - final View view = super.getView(position, convertView, parent); - final int itemPosition = position; - CheckBox checkbox = (CheckBox) view.findViewById(R.id.checkbox_transaction); - final TextView secondaryText = (TextView) view.findViewById(R.id.secondary_text); - - //TODO: Revisit this if we ever change the application theme - int id = Resources.getSystem().getIdentifier("btn_check_holo_light", "drawable", "android"); - checkbox.setButtonDrawable(id); - checkbox.setOnCheckedChangeListener(new OnCheckedChangeListener() { - - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - getListView().setItemChecked(itemPosition, isChecked); - if (isChecked) { - startActionMode(); - } else { - stopActionMode(); - } - setActionModeTitle(); - } - }); - - - ListView listView = (ListView) parent; - if (mInEditMode && listView.isItemChecked(position)){ - view.setBackgroundColor(getResources().getColor(R.color.abs__holo_blue_light)); - secondaryText.setTextColor(getResources().getColor(android.R.color.white)); - } else { - view.setBackgroundColor(getResources().getColor(android.R.color.transparent)); - secondaryText.setTextColor(getResources().getColor(android.R.color.secondary_text_light_nodisable)); - checkbox.setChecked(false); - } - - //increase the touch target area for the add new transaction button - - final View checkBoxView = checkbox; - final View parentView = view; - parentView.post(new Runnable() { - @Override - public void run() { - if (isAdded()){ //may be run when fragment has been unbound from activity - float extraPadding = getResources().getDimension(R.dimen.edge_padding); - final android.graphics.Rect hitRect = new Rect(); - checkBoxView.getHitRect(hitRect); - hitRect.right += extraPadding; - hitRect.bottom += 3*extraPadding; - hitRect.top -= extraPadding; - hitRect.left -= 2*extraPadding; - parentView.setTouchDelegate(new TouchDelegate(hitRect, checkBoxView)); - } - } - }); - - return view; - } - - @Override - public void bindView(View view, Context context, Cursor cursor) { - super.bindView(view, context, cursor); - String transactionUID = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.TransactionEntry.COLUMN_UID)); - Money amount = mTransactionsDbAdapter.getBalance(transactionUID, mAccountUID); - TextView amountTextView = (TextView) view.findViewById(R.id.transaction_amount); - TransactionsActivity.displayBalance(amountTextView, amount); - - TextView trNote = (TextView) view.findViewById(R.id.secondary_text); - String notes = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.TransactionEntry.COLUMN_NOTES)); - if (notes == null || notes.length() == 0) - trNote.setVisibility(View.GONE); - else { - trNote.setVisibility(View.VISIBLE); - trNote.setText(notes); - } - - setSectionHeaderVisibility(view, cursor); - } - - /** - * Toggles the visibilty of the section header based on whether the previous transaction and current were - * booked on the same day or not. Transactions a generally grouped by day - * @param view Parent view within which to find the section header - * @param cursor Cursor containing transaction data set - * @see #isSameDay(long, long) - */ - private void setSectionHeaderVisibility(View view, Cursor cursor) { - long transactionTime = cursor.getLong(cursor.getColumnIndexOrThrow(DatabaseSchema.TransactionEntry.COLUMN_TIMESTAMP)); - int position = cursor.getPosition(); - - boolean hasSectionHeader; - if (position == 0){ - hasSectionHeader = true; - } else { - cursor.moveToPosition(position - 1); - long previousTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(DatabaseSchema.TransactionEntry.COLUMN_TIMESTAMP)); - cursor.moveToPosition(position); - //has header if two consecutive transactions were not on same day - hasSectionHeader = !isSameDay(previousTimestamp, transactionTime); - } - - TextView dateHeader = (TextView) view.findViewById(R.id.date_section_header); - - if (hasSectionHeader){ - String dateString = DateUtils.formatDateTime(getActivity(), transactionTime, - DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR); - dateHeader.setText(dateString); - dateHeader.setVisibility(View.VISIBLE); - } else { - dateHeader.setVisibility(View.GONE); - } - } - - /** - * Checks if two timestamps have the same calendar day - * @param timeMillis1 Timestamp in milliseconds - * @param timeMillis2 Timestamp in milliseconds - * @return true if both timestamps are on same day, false otherwise - */ - private boolean isSameDay(long timeMillis1, long timeMillis2){ - Date date1 = new Date(timeMillis1); - Date date2 = new Date(timeMillis2); - - SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMdd"); - return fmt.format(date1).equals(fmt.format(date2)); - } - } /** * {@link DatabaseCursorLoader} for loading transactions asynchronously from the database @@ -510,4 +216,114 @@ public Cursor loadInBackground() { } } + public class TransactionRecyclerAdapter extends CursorRecyclerAdapter{ + + private final PrettyTime prettyTime = new PrettyTime(); + public TransactionRecyclerAdapter(Cursor cursor) { + super(cursor); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.cardview_transaction, parent, false); + return new ViewHolder(v); + } + + + @Override + public void onBindViewHolderCursor(ViewHolder holder, Cursor cursor) { + holder.transactionId = cursor.getLong(cursor.getColumnIndexOrThrow(DatabaseSchema.TransactionEntry._ID)); + + String description = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.TransactionEntry.COLUMN_DESCRIPTION)); + holder.transactionDescription.setText(description); + + final String transactionUID = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.TransactionEntry.COLUMN_UID)); + Money amount = mTransactionsDbAdapter.getBalance(transactionUID, mAccountUID); + TransactionsActivity.displayBalance(holder.transactionAmount, amount); + + List splits = SplitsDbAdapter.getInstance().getSplitsForTransaction(transactionUID); + String text = ""; + + if (splits.size() == 2 && splits.get(0).isPairOf(splits.get(1))){ + for (Split split : splits) { + if (!split.getAccountUID().equals(mAccountUID)){ + text = AccountsDbAdapter.getInstance().getFullyQualifiedAccountName(split.getAccountUID()); + break; + } + } + } + + if (splits.size() > 2){ + text = splits.size() + " splits"; + } + holder.transactionNote.setText(text); + + long dateMillis = cursor.getLong(cursor.getColumnIndexOrThrow(DatabaseSchema.TransactionEntry.COLUMN_TIMESTAMP)); + holder.transactionDate.setText(prettyTime.format(new Date(dateMillis))); + + final long id = holder.transactionId; + holder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onListItemClick(id); + } + }); + + holder.editTransaction.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(getActivity(), FormActivity.class); + intent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.TRANSACTION.name()); + intent.putExtra(UxArgument.SELECTED_TRANSACTION_UID, transactionUID); + intent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, mAccountUID); + startActivity(intent); + } + }); + + } + + + public class ViewHolder extends RecyclerView.ViewHolder implements PopupMenu.OnMenuItemClickListener{ + @Bind(R.id.primary_text) public TextView transactionDescription; + @Bind(R.id.secondary_text) public TextView transactionNote; + @Bind(R.id.transaction_amount) public TextView transactionAmount; + @Bind(R.id.transaction_date) public TextView transactionDate; + @Bind(R.id.edit_transaction) public ImageView editTransaction; + @Bind(R.id.options_menu) public ImageView optionsMenu; + + long transactionId; + + public ViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); + + optionsMenu.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + PopupMenu popup = new PopupMenu(getActivity(), v); + popup.setOnMenuItemClickListener(ViewHolder.this); + MenuInflater inflater = popup.getMenuInflater(); + inflater.inflate(R.menu.transactions_context_menu, popup.getMenu()); + popup.show(); + } + }); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.context_menu_delete: + mTransactionsDbAdapter.deleteRecord(transactionId); + WidgetConfigurationActivity.updateAllWidgets(getActivity()); + refresh(); + return true; + + default: + return false; + + } + } + } + } } diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/dialog/BulkMoveDialogFragment.java b/app/src/main/java/org/gnucash/android/ui/transaction/dialog/BulkMoveDialogFragment.java index fa0b0ddbc..9a2eb2af6 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/dialog/BulkMoveDialogFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/dialog/BulkMoveDialogFragment.java @@ -32,10 +32,10 @@ import org.gnucash.android.db.AccountsDbAdapter; import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.db.TransactionsDbAdapter; -import org.gnucash.android.ui.UxArgument; +import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.transaction.TransactionsActivity; import org.gnucash.android.ui.util.Refreshable; -import org.gnucash.android.ui.widget.WidgetConfigurationActivity; +import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; import org.gnucash.android.util.QualifiedAccountNameCursorAdapter; /** @@ -101,6 +101,7 @@ public void onActivityCreated(Bundle savedInstanceState) { /* Accounts database adapter */ + //FIXME: move only to accounts which have the same currency as this one AccountsDbAdapter accountsDbAdapter = AccountsDbAdapter.getInstance(); String conditions = "(" + DatabaseSchema.AccountEntry.COLUMN_UID + " != ? AND " + DatabaseSchema.AccountEntry.COLUMN_CURRENCY + " = ? AND " @@ -113,9 +114,7 @@ public void onActivityCreated(Bundle savedInstanceState) { "" + accountsDbAdapter.getOrCreateGnuCashRootAccountUID() }); - SimpleCursorAdapter mCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), - android.R.layout.simple_spinner_item, cursor); - mCursorAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + SimpleCursorAdapter mCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), cursor); mDestinationAccountSpinner.setAdapter(mCursorAdapter); setListeners(); } diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/dialog/SplitEditorDialogFragment.java b/app/src/main/java/org/gnucash/android/ui/transaction/dialog/SplitEditorDialogFragment.java deleted file mode 100644 index 0b57d23a1..000000000 --- a/app/src/main/java/org/gnucash/android/ui/transaction/dialog/SplitEditorDialogFragment.java +++ /dev/null @@ -1,429 +0,0 @@ -/* - * Copyright (c) 2014 Ngewi Fet - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.gnucash.android.ui.transaction.dialog; - -import android.database.Cursor; -import android.os.Bundle; -import android.support.v4.app.DialogFragment; -import android.support.v4.widget.SimpleCursorAdapter; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.AdapterView; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ImageButton; -import android.widget.LinearLayout; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.Toast; - -import org.gnucash.android.R; -import org.gnucash.android.db.AccountsDbAdapter; -import org.gnucash.android.db.DatabaseSchema; -import org.gnucash.android.db.SplitsDbAdapter; -import org.gnucash.android.model.AccountType; -import org.gnucash.android.model.Money; -import org.gnucash.android.model.Split; -import org.gnucash.android.model.Transaction; -import org.gnucash.android.model.TransactionType; -import org.gnucash.android.ui.UxArgument; -import org.gnucash.android.ui.transaction.TransactionFormFragment; -import org.gnucash.android.ui.transaction.TransactionsActivity; -import org.gnucash.android.ui.util.AmountInputFormatter; -import org.gnucash.android.ui.util.TransactionTypeToggleButton; -import org.gnucash.android.util.QualifiedAccountNameCursorAdapter; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Currency; -import java.util.List; -import java.util.UUID; - -/** - * Dialog for editing the splits in a transaction - * - * @author Ngewi Fet - */ -public class SplitEditorDialogFragment extends DialogFragment { - - private LinearLayout mSplitsLinearLayout; - private TextView mImbalanceTextView; - private Button mAddSplit; - private Button mSaveButton; - private Button mCancelButton; - - private AccountsDbAdapter mAccountsDbAdapter; - private SplitsDbAdapter mSplitsDbAdapter; - private Cursor mCursor; - private SimpleCursorAdapter mCursorAdapter; - private List mSplitItemViewList; - private String mAccountUID; - - private BalanceTextWatcher mBalanceUpdater = new BalanceTextWatcher(); - private BigDecimal mBaseAmount = BigDecimal.ZERO; - - private List mRemovedSplitUIDs = new ArrayList(); - - private boolean mMultiCurrency = false; - /** - * Create and return a new instance of the fragment with the appropriate paramenters - * @param baseAmountString String with base amount which is being split - * @return New instance of SplitEditorDialogFragment - */ - public static SplitEditorDialogFragment newInstance(String baseAmountString){ - SplitEditorDialogFragment fragment = new SplitEditorDialogFragment(); - Bundle args = new Bundle(); - args.putString(UxArgument.AMOUNT_STRING, baseAmountString); - fragment.setArguments(args); - return fragment; - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.dialog_split_editor, container, false); - mSplitsLinearLayout = (LinearLayout) view.findViewById(R.id.split_list_layout); - - mImbalanceTextView = (TextView) view.findViewById(R.id.imbalance_textview); - - mAddSplit = (Button) view.findViewById(R.id.btn_add_split); - mSaveButton = (Button) view.findViewById(R.id.btn_save); - mCancelButton = (Button) view.findViewById(R.id.btn_cancel); - return view; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - getDialog().getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.MATCH_PARENT); - - getDialog().setTitle(R.string.title_transaction_splits); - - mSplitItemViewList = new ArrayList<>(); - mSplitsDbAdapter = SplitsDbAdapter.getInstance(); - - //we are editing splits for a new transaction. - // But the user may have already created some splits before. Let's check - List splitList = ((TransactionFormFragment) getTargetFragment()).getSplitList(); - { - Currency currency = null; - for (Split split : splitList) { - if (currency == null) { - currency = split.getAmount().getCurrency(); - } else if (currency != split.getAmount().getCurrency()) { - mMultiCurrency = true; - } - } - } - - initArgs(); - if (!splitList.isEmpty()) { - //aha! there are some splits. Let's load those instead - loadSplitViews(splitList); - } else { - final Currency currency = Currency.getInstance(mAccountsDbAdapter.getAccountCurrencyCode(mAccountUID)); - Split split = new Split(new Money(mBaseAmount, currency), mAccountUID); - AccountType accountType = mAccountsDbAdapter.getAccountType(mAccountUID); - TransactionType transactionType = Transaction.getTypeForBalance(accountType, mBaseAmount.signum() < 0); - split.setType(transactionType); - View view = addSplitView(split); - view.findViewById(R.id.input_accounts_spinner).setEnabled(false); - view.findViewById(R.id.btn_remove_split).setVisibility(View.GONE); - } - - setListeners(); - updateTotal(); - } - - private void loadSplitViews(List splitList) { - for (Split split : splitList) { - addSplitView(split); - } - if (mMultiCurrency) { - enableAllControls(false); - } - } - - private void enableAllControls(boolean b) { - for (View splitView : mSplitItemViewList) { - EditText splitMemoEditText = (EditText) splitView.findViewById(R.id.input_split_memo); - final EditText splitAmountEditText = (EditText) splitView.findViewById(R.id.input_split_amount); - ImageButton removeSplitButton = (ImageButton) splitView.findViewById(R.id.btn_remove_split); - Spinner accountsSpinner = (Spinner) splitView.findViewById(R.id.input_accounts_spinner); - final TextView splitCurrencyTextView = (TextView) splitView.findViewById(R.id.split_currency_symbol); - final TextView splitUidTextView = (TextView) splitView.findViewById(R.id.split_uid); - final TransactionTypeToggleButton splitTypeButton = (TransactionTypeToggleButton) splitView.findViewById(R.id.btn_split_type); - splitMemoEditText.setEnabled(b); - splitAmountEditText.setEnabled(b); - removeSplitButton.setEnabled(b); - accountsSpinner.setEnabled(b); - splitCurrencyTextView.setEnabled(b); - splitUidTextView.setEnabled(b); - splitTypeButton.setEnabled(b); - } - } - - /** - * Add a split view and initialize it with split - * @param split Split to initialize the contents to - * @return Returns the split view which was added - */ - private View addSplitView(Split split){ - LayoutInflater layoutInflater = getActivity().getLayoutInflater(); - View splitView = layoutInflater.inflate(R.layout.item_split_entry, mSplitsLinearLayout, false); - mSplitsLinearLayout.addView(splitView,0); - bindSplitView(splitView, split); - mSplitItemViewList.add(splitView); - return splitView; - } - - /** - * Extracts arguments passed to the view and initializes necessary adapters and cursors - */ - private void initArgs() { - mAccountsDbAdapter = AccountsDbAdapter.getInstance(); - - Bundle args = getArguments(); - mAccountUID = ((TransactionsActivity) getActivity()).getCurrentAccountUID(); - mBaseAmount = new BigDecimal(args.getString(UxArgument.AMOUNT_STRING)); - - String conditions = "(" //+ AccountEntry._ID + " != " + mAccountId + " AND " - + (mMultiCurrency ? "" : (DatabaseSchema.AccountEntry.COLUMN_CURRENCY + " = ? AND ")) - + DatabaseSchema.AccountEntry.COLUMN_UID + " != '" + mAccountsDbAdapter.getOrCreateGnuCashRootAccountUID() + "' AND " - + DatabaseSchema.AccountEntry.COLUMN_PLACEHOLDER + " = 0" - + ")"; - mCursor = mAccountsDbAdapter.fetchAccountsOrderedByFullName(conditions, - mMultiCurrency ? new String[]{"" + mAccountsDbAdapter.getOrCreateGnuCashRootAccountUID()} : - new String[]{mAccountsDbAdapter.getCurrencyCode(mAccountUID)} - ); - } - - /** - * Binds the different UI elements of an inflated list view to corresponding actions - * @param splitView Split item view - * @param split {@link org.gnucash.android.model.Split} to use to populate the view - */ - private void bindSplitView(final View splitView, Split split){ - EditText splitMemoEditText = (EditText) splitView.findViewById(R.id.input_split_memo); - final EditText splitAmountEditText = (EditText) splitView.findViewById(R.id.input_split_amount); - ImageButton removeSplitButton = (ImageButton) splitView.findViewById(R.id.btn_remove_split); - Spinner accountsSpinner = (Spinner) splitView.findViewById(R.id.input_accounts_spinner); - final TextView splitCurrencyTextView = (TextView) splitView.findViewById(R.id.split_currency_symbol); - final TextView splitUidTextView = (TextView) splitView.findViewById(R.id.split_uid); - final TransactionTypeToggleButton splitTypeButton = (TransactionTypeToggleButton) splitView.findViewById(R.id.btn_split_type); - - splitAmountEditText.addTextChangedListener(new AmountInputFormatter(splitAmountEditText)); - - removeSplitButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - mRemovedSplitUIDs.add(splitUidTextView.getText().toString()); - mSplitsLinearLayout.removeView(splitView); - mSplitItemViewList.remove(splitView); - updateTotal(); - } - }); - - updateTransferAccountsList(accountsSpinner); - accountsSpinner.setOnItemSelectedListener(new TypeButtonLabelUpdater(splitTypeButton)); - - Currency accountCurrency = Currency.getInstance(mAccountsDbAdapter.getCurrencyCode( - split == null ? mAccountUID : split.getAccountUID())); - splitCurrencyTextView.setText(accountCurrency.getSymbol()); - splitTypeButton.setAmountFormattingListener(splitAmountEditText, splitCurrencyTextView); - splitTypeButton.setChecked(mBaseAmount.signum() > 0); - splitUidTextView.setText(UUID.randomUUID().toString()); - - if (split != null) { - splitAmountEditText.setText(split.getAmount().toPlainString()); - splitMemoEditText.setText(split.getMemo()); - splitUidTextView.setText(split.getUID()); - String splitAccountUID = split.getAccountUID(); - setSelectedTransferAccount(mAccountsDbAdapter.getID(splitAccountUID), accountsSpinner); - splitTypeButton.setAccountType(mAccountsDbAdapter.getAccountType(splitAccountUID)); - splitTypeButton.setChecked(split.getType()); - } - - //put these balance update triggers last last so as to avoid computing while still loading - splitAmountEditText.addTextChangedListener(mBalanceUpdater); - splitTypeButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - updateTotal(); - } - }); - } - - /** - * Updates the spinner to the selected transfer account - * @param accountId Database ID of the transfer account - */ - private void setSelectedTransferAccount(long accountId, final Spinner accountsSpinner){ - for (int pos = 0; pos < mCursorAdapter.getCount(); pos++) { - if (mCursorAdapter.getItemId(pos) == accountId){ - accountsSpinner.setSelection(pos); - break; - } - } - } - /** - * Updates the list of possible transfer accounts. - * Only accounts with the same currency can be transferred to - */ - private void updateTransferAccountsList(Spinner transferAccountSpinner){ - - mCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), - android.R.layout.simple_spinner_item, mCursor); - mCursorAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - transferAccountSpinner.setAdapter(mCursorAdapter); - } - - /** - * Attaches listeners for the buttons of the dialog - */ - protected void setListeners(){ - mCancelButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - dismiss(); - } - }); - - mSaveButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if (mMultiCurrency) { - Toast.makeText(getActivity(), R.string.toast_error_edit_multi_currency_transaction, Toast.LENGTH_LONG).show(); - } - else { - List splitList = extractSplitsFromView(); - ((TransactionFormFragment) getTargetFragment()).setSplitList(splitList, mRemovedSplitUIDs); - } - dismiss(); - } - }); - - mAddSplit.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if (mMultiCurrency) { - Toast.makeText(getActivity(), R.string.toast_error_edit_multi_currency_transaction, Toast.LENGTH_LONG).show(); - } - else { - addSplitView(null); - } - } - }); - } - - /** - * Extracts the input from the views and builds {@link org.gnucash.android.model.Split}s to correspond to the input. - * @return List of {@link org.gnucash.android.model.Split}s represented in the view - */ - private List extractSplitsFromView(){ - List splitList = new ArrayList(); - for (View splitView : mSplitItemViewList) { - EditText splitMemoEditText = (EditText) splitView.findViewById(R.id.input_split_memo); - EditText splitAmountEditText = (EditText) splitView.findViewById(R.id.input_split_amount); - Spinner accountsSpinner = (Spinner) splitView.findViewById(R.id.input_accounts_spinner); - TextView splitUidTextView = (TextView) splitView.findViewById(R.id.split_uid); - TransactionTypeToggleButton splitTypeButton = (TransactionTypeToggleButton) splitView.findViewById(R.id.btn_split_type); - - BigDecimal amountBigDecimal = TransactionFormFragment.parseInputToDecimal(splitAmountEditText.getText().toString()); - String accountUID = mAccountsDbAdapter.getUID(accountsSpinner.getSelectedItemId()); - String currencyCode = mAccountsDbAdapter.getCurrencyCode(accountUID); - Money amount = new Money(amountBigDecimal, Currency.getInstance(currencyCode)); - Split split = new Split(amount, accountUID); - split.setMemo(splitMemoEditText.getText().toString()); - split.setType(splitTypeButton.getTransactionType()); - split.setUID(splitUidTextView.getText().toString().trim()); - splitList.add(split); - } - return splitList; - } - - /** - * Updates the displayed total for the transaction. - * Computes the total of the splits, the unassigned balance and the split sum - */ - private void updateTotal(){ - List splitList = extractSplitsFromView(); - String currencyCode = mAccountsDbAdapter.getCurrencyCode(mAccountUID); - Money splitSum = Money.createZeroInstance(currencyCode); - if (!mMultiCurrency) { - for (Split split : splitList) { - Money amount = split.getAmount().absolute(); - if (split.getType() == TransactionType.DEBIT) - splitSum = splitSum.subtract(amount); - else - splitSum = splitSum.add(amount); - } - } - TransactionsActivity.displayBalance(mImbalanceTextView, splitSum); - } - - @Override - public void onDestroy() { - super.onDestroy(); - } - - /** - * Updates the displayed balance of the accounts when the amount of a split is changed - */ - private class BalanceTextWatcher implements TextWatcher { - - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { - - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { - - } - - @Override - public void afterTextChanged(Editable editable) { - updateTotal(); - } - } - - /** - * Updates the account type for the TransactionTypeButton when the selected account is changed in the spinner - */ - private class TypeButtonLabelUpdater implements AdapterView.OnItemSelectedListener { - TransactionTypeToggleButton mTypeToggleButton; - - public TypeButtonLabelUpdater(TransactionTypeToggleButton typeToggleButton){ - this.mTypeToggleButton = typeToggleButton; - } - - @Override - public void onItemSelected(AdapterView parentView, View selectedItemView, int position, long id) { - AccountType accountType = mAccountsDbAdapter.getAccountType(id); - mTypeToggleButton.setAccountType(accountType); - } - - @Override - public void onNothingSelected(AdapterView adapterView) { - - } - } -} diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransactionsDeleteConfirmationDialogFragment.java b/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransactionsDeleteConfirmationDialogFragment.java index 47341061f..3b9bcdbbb 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransactionsDeleteConfirmationDialogFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransactionsDeleteConfirmationDialogFragment.java @@ -20,8 +20,7 @@ import android.app.Dialog; import android.content.DialogInterface; import android.os.Bundle; - -import com.actionbarsherlock.app.SherlockDialogFragment; +import android.support.v4.app.DialogFragment; import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; @@ -29,10 +28,9 @@ import org.gnucash.android.db.TransactionsDbAdapter; import org.gnucash.android.export.xml.GncXmlExporter; import org.gnucash.android.model.Transaction; -import org.gnucash.android.ui.UxArgument; -import org.gnucash.android.ui.account.AccountsListFragment; +import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.util.Refreshable; -import org.gnucash.android.ui.widget.WidgetConfigurationActivity; +import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; import java.util.ArrayList; import java.util.List; @@ -43,7 +41,7 @@ * @author Ngewi Fet * */ -public class TransactionsDeleteConfirmationDialogFragment extends SherlockDialogFragment { +public class TransactionsDeleteConfirmationDialogFragment extends DialogFragment { public static TransactionsDeleteConfirmationDialogFragment newInstance(int title, long id) { TransactionsDeleteConfirmationDialogFragment frag = new TransactionsDeleteConfirmationDialogFragment(); @@ -76,7 +74,7 @@ public void onClick(DialogInterface dialog, int whichButton) { transactionsDbAdapter.deleteAllRecords(); if (preserveOpeningBalances) { - transactionsDbAdapter.bulkAddTransactions(openingBalances); + transactionsDbAdapter.bulkAddRecords(openingBalances); } } else { transactionsDbAdapter.deleteRecord(rowId); 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 new file mode 100644 index 000000000..2a40a34f4 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/transaction/dialog/TransferFundsDialogFragment.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.ui.transaction.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.design.widget.TextInputLayout; +import android.support.v4.app.DialogFragment; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.RadioButton; +import android.widget.TextView; + +import org.gnucash.android.R; +import org.gnucash.android.db.CommoditiesDbAdapter; +import org.gnucash.android.db.PricesDbAdapter; +import org.gnucash.android.model.Money; +import org.gnucash.android.model.Price; +import org.gnucash.android.ui.transaction.TransactionFormFragment; +import org.gnucash.android.ui.transaction.TransactionsActivity; +import org.gnucash.android.ui.util.AmountInputFormatter; +import org.gnucash.android.ui.util.OnTransferFundsListener; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.Currency; + +import butterknife.Bind; +import butterknife.ButterKnife; + +/** + * Dialog fragment for handling currency conversions when inputting transactions. + *

This is used whenever a multi-currency transaction is being created.

+ */ +public class TransferFundsDialogFragment extends DialogFragment { + + @Bind(R.id.from_currency) TextView mFromCurrencyLabel; + @Bind(R.id.to_currency) TextView mToCurrencyLabel; + @Bind(R.id.target_currency) TextView mConvertedAmountCurrencyLabel; + @Bind(R.id.amount_to_convert) TextView mStartAmountLabel; + @Bind(R.id.input_exchange_rate) EditText mExchangeRateInput; + @Bind(R.id.input_converted_amount) EditText mConvertedAmountInput; + @Bind(R.id.btn_fetch_exchange_rate) Button mFetchExchangeRateButton; + @Bind(R.id.radio_exchange_rate) RadioButton mExchangeRateRadioButton; + @Bind(R.id.radio_converted_amount) RadioButton mConvertedAmountRadioButton; + @Bind(R.id.label_exchange_rate_example) + TextView mSampleExchangeRate; + @Bind(R.id.exchange_rate_text_input_layout) + TextInputLayout mExchangeRateInputLayout; + @Bind(R.id.converted_amount_text_input_layout) + TextInputLayout mConvertedAmountInputLayout; + + @Bind(R.id.btn_save) Button mSaveButton; + @Bind(R.id.btn_cancel) Button mCancelButton; + Money mOriginAmount; + Currency mTargetCurrency; + + Money mConvertedAmount; + OnTransferFundsListener mOnTransferFundsListener; + + public static TransferFundsDialogFragment getInstance(Money transactionAmount, String targetCurrencyCode, + OnTransferFundsListener transferFundsListener){ + TransferFundsDialogFragment fragment = new TransferFundsDialogFragment(); + fragment.mOriginAmount = transactionAmount; + fragment.mTargetCurrency = Currency.getInstance(targetCurrencyCode); + fragment.mOnTransferFundsListener = transferFundsListener; + return fragment; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.dialog_transfer_funds, container, false); + ButterKnife.bind(this, view); + + TransactionsActivity.displayBalance(mStartAmountLabel, mOriginAmount); + Currency fromCurrency = mOriginAmount.getCurrency(); + mFromCurrencyLabel.setText(fromCurrency.getCurrencyCode()); + mToCurrencyLabel.setText(mTargetCurrency.getCurrencyCode()); + mConvertedAmountCurrencyLabel.setText(mTargetCurrency.getCurrencyCode()); + + mSampleExchangeRate.setText("e.g. 1 " + fromCurrency.getCurrencyCode() + " = " + " x.xx " + mTargetCurrency.getCurrencyCode()); + final InputWatcher textChangeListener = new InputWatcher(); + + CommoditiesDbAdapter commoditiesDbAdapter = CommoditiesDbAdapter.getInstance(); + String commodityUID = commoditiesDbAdapter.getCommodityUID(fromCurrency.getCurrencyCode()); + String currencyUID = commoditiesDbAdapter.getCommodityUID(mTargetCurrency.getCurrencyCode()); + PricesDbAdapter pricesDbAdapter = PricesDbAdapter.getInstance(); + Pair price = pricesDbAdapter.getPrice(commodityUID, currencyUID); + + if (price.first > 0 && price.second > 0) { + // a valid price exists + BigDecimal num = new BigDecimal(price.first); + BigDecimal denom = new BigDecimal(price.second); + mExchangeRateInput.setText(num.divide(denom, MathContext.DECIMAL32).toString()); + mConvertedAmountInput.setText(mOriginAmount.asBigDecimal().multiply(num).divide(denom, mTargetCurrency.getDefaultFractionDigits(), BigDecimal.ROUND_HALF_EVEN).toString()); + } + + mExchangeRateInput.addTextChangedListener(textChangeListener); + mExchangeRateInput.addTextChangedListener(new AmountInputFormatter(mExchangeRateInput)); + mConvertedAmountInput.addTextChangedListener(textChangeListener); + mConvertedAmountInput.addTextChangedListener(new AmountInputFormatter(mConvertedAmountInput)); + + mConvertedAmountRadioButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + mConvertedAmountInput.setEnabled(isChecked); + mConvertedAmountInputLayout.setErrorEnabled(isChecked); + mExchangeRateRadioButton.setChecked(!isChecked); + if (isChecked) { + mConvertedAmountInput.requestFocus(); + } + } + }); + + mExchangeRateRadioButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + mExchangeRateInput.setEnabled(isChecked); + mExchangeRateInputLayout.setErrorEnabled(isChecked); + mFetchExchangeRateButton.setEnabled(isChecked); + mConvertedAmountRadioButton.setChecked(!isChecked); + if (isChecked) { + mExchangeRateInput.requestFocus(); + } + } + }); + + mFetchExchangeRateButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + //TODO: Pull the exchange rate for the currency here + } + }); + + mCancelButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + + mSaveButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + transferFunds(); + } + }); + return view; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.setTitle(R.string.title_transfer_funds); + return dialog; + } + + /** + * Converts the currency amount with the given exchange rate and saves the price to the db + */ + private void transferFunds(){ + if (mExchangeRateRadioButton.isChecked()){ + String exchangeRateString = mExchangeRateInput.getText().toString(); + if (exchangeRateString.isEmpty()){ + mExchangeRateInputLayout.setError(getString(R.string.error_exchange_rate_required)); + return; + } + + BigDecimal rate = TransactionFormFragment.parseInputToDecimal(exchangeRateString); + mConvertedAmount = mOriginAmount.multiply(rate); + } + + if (mConvertedAmountRadioButton.isChecked()){ + String convertedAmount = mConvertedAmountInput.getText().toString(); + if (convertedAmount.isEmpty()){ + mConvertedAmountInputLayout.setError(getString(R.string.error_converted_amount_required)); + return; + } + + BigDecimal amount = TransactionFormFragment.parseInputToDecimal(convertedAmount); + mConvertedAmount = new Money(amount, mTargetCurrency); + } + + if (mOnTransferFundsListener != null) { + PricesDbAdapter pricesDbAdapter = PricesDbAdapter.getInstance(); + CommoditiesDbAdapter commoditiesDbAdapter = CommoditiesDbAdapter.getInstance(); + Price price = new Price(commoditiesDbAdapter.getCommodityUID(mOriginAmount.getCurrency().getCurrencyCode()), + commoditiesDbAdapter.getCommodityUID(mTargetCurrency.getCurrencyCode())); + price.setSource(Price.SOURCE_USER); + // fractions cannot be exacted represented by BigDecimal. + price.setValueNum(mConvertedAmount.getNumerator() * mOriginAmount.getDenominator()); + price.setValueDenom(mOriginAmount.getNumerator() * mConvertedAmount.getDenominator()); + price.reduce(); + pricesDbAdapter.addRecord(price); + + mOnTransferFundsListener.transferComplete(mConvertedAmount); + } + dismiss(); + } + + private class InputWatcher implements TextWatcher { + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + mConvertedAmountInputLayout.setErrorEnabled(false); + mExchangeRateInputLayout.setErrorEnabled(false); + } + } +} diff --git a/app/src/main/java/org/gnucash/android/ui/util/AccountBalanceTask.java b/app/src/main/java/org/gnucash/android/ui/util/AccountBalanceTask.java index 0e3c2705c..9dbcb6a5a 100644 --- a/app/src/main/java/org/gnucash/android/ui/util/AccountBalanceTask.java +++ b/app/src/main/java/org/gnucash/android/ui/util/AccountBalanceTask.java @@ -28,6 +28,7 @@ import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.db.AccountsDbAdapter; import org.gnucash.android.model.Money; +import org.gnucash.android.ui.transaction.TransactionsActivity; import java.lang.ref.WeakReference; @@ -59,7 +60,7 @@ protected Money doInBackground(String... params) { try { balance = accountsDbAdapter.getAccountBalance(params[0], -1, System.currentTimeMillis()); } catch (Exception ex) { - Log.e(LOG_TAG, "Error computing account balance: " + ex); + Log.e(LOG_TAG, "Error computing account balance ", ex); Crashlytics.logException(ex); } return balance; @@ -71,10 +72,7 @@ protected void onPostExecute(Money balance) { final Context context = GnuCashApplication.getAppContext(); final TextView balanceTextView = accountBalanceTextViewReference.get(); if (balanceTextView != null){ - balanceTextView.setText(balance.formattedString()); - int fontColor = balance.isNegative() ? context.getResources().getColor(R.color.debit_red) : - context.getResources().getColor(R.color.credit_green); - balanceTextView.setTextColor(fontColor); + TransactionsActivity.displayBalance(balanceTextView, balance); } } } diff --git a/app/src/main/java/org/gnucash/android/ui/util/AmountInputFormatter.java b/app/src/main/java/org/gnucash/android/ui/util/AmountInputFormatter.java index 8b7111504..1e9af5c2c 100644 --- a/app/src/main/java/org/gnucash/android/ui/util/AmountInputFormatter.java +++ b/app/src/main/java/org/gnucash/android/ui/util/AmountInputFormatter.java @@ -33,7 +33,9 @@ * of 2.45 * * @author Ngewi Fet + * @deprecated Use {@link org.gnucash.android.ui.util.widget.CalculatorEditText} for getting input amounts from the user */ +@Deprecated public class AmountInputFormatter implements TextWatcher { private String current = "0"; private EditText amountEditText; diff --git a/app/src/main/java/org/gnucash/android/ui/util/CursorRecyclerAdapter.java b/app/src/main/java/org/gnucash/android/ui/util/CursorRecyclerAdapter.java new file mode 100644 index 000000000..d71a7f792 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/util/CursorRecyclerAdapter.java @@ -0,0 +1,368 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014 Matthieu Harlé + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.gnucash.android.ui.util; + +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.os.Handler; +import android.support.v7.widget.RecyclerView; +import android.widget.Filter; +import android.widget.FilterQueryProvider; +import android.widget.Filterable; + +/** + * Provide a {@link android.support.v7.widget.RecyclerView.Adapter} implementation with cursor + * support. + * + * Child classes only need to implement {@link #onCreateViewHolder(android.view.ViewGroup, int)} and + * {@link #onBindViewHolderCursor(android.support.v7.widget.RecyclerView.ViewHolder, android.database.Cursor)}. + * + * This class does not implement deprecated fields and methods from CursorAdapter! Incidentally, + * only {@link android.widget.CursorAdapter#FLAG_REGISTER_CONTENT_OBSERVER} is available, so the + * flag is implied, and only the Adapter behavior using this flag has been ported. + * + * @param {@inheritDoc} + * + * @see android.support.v7.widget.RecyclerView.Adapter + * @see android.widget.CursorAdapter + * @see android.widget.Filterable + * @see CursorFilter.CursorFilterClient + */ +public abstract class CursorRecyclerAdapter extends RecyclerView.Adapter + implements Filterable, CursorFilter.CursorFilterClient { + private boolean mDataValid; + private int mRowIDColumn; + private Cursor mCursor; + private ChangeObserver mChangeObserver; + private DataSetObserver mDataSetObserver; + private CursorFilter mCursorFilter; + private FilterQueryProvider mFilterQueryProvider; + + public CursorRecyclerAdapter( Cursor cursor) { + init(cursor); + } + + void init(Cursor c) { + boolean cursorPresent = c != null; + mCursor = c; + mDataValid = cursorPresent; + mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1; + + mChangeObserver = new ChangeObserver(); + mDataSetObserver = new MyDataSetObserver(); + + if (cursorPresent) { + if (mChangeObserver != null) c.registerContentObserver(mChangeObserver); + if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver); + } + } + + /** + * This method will move the Cursor to the correct position and call + * {@link #onBindViewHolderCursor(android.support.v7.widget.RecyclerView.ViewHolder, + * android.database.Cursor)}. + * + * @param holder {@inheritDoc} + * @param i {@inheritDoc} + */ + @Override + public void onBindViewHolder(VH holder, int i){ + if (!mDataValid) { + throw new IllegalStateException("this should only be called when the cursor is valid"); + } + if (!mCursor.moveToPosition(i)) { + throw new IllegalStateException("couldn't move cursor to position " + i); + } + onBindViewHolderCursor(holder, mCursor); + } + + /** + * See {@link android.widget.CursorAdapter#bindView(android.view.View, android.content.Context, + * android.database.Cursor)}, + * {@link #onBindViewHolder(android.support.v7.widget.RecyclerView.ViewHolder, int)} + * + * @param holder View holder. + * @param cursor The cursor from which to get the data. The cursor is already + * moved to the correct position. + */ + public abstract void onBindViewHolderCursor(VH holder, Cursor cursor); + + @Override + public int getItemCount() { + if (mDataValid && mCursor != null) { + return mCursor.getCount(); + } else { + return 0; + } + } + + /** + * @see android.widget.ListAdapter#getItemId(int) + */ + @Override + public long getItemId(int position) { + if (mDataValid && mCursor != null) { + if (mCursor.moveToPosition(position)) { + return mCursor.getLong(mRowIDColumn); + } else { + return 0; + } + } else { + return 0; + } + } + + public Cursor getCursor(){ + return mCursor; + } + + /** + * Change the underlying cursor to a new cursor. If there is an existing cursor it will be + * closed. + * + * @param cursor The new cursor to be used + */ + public void changeCursor(Cursor cursor) { + Cursor old = swapCursor(cursor); + if (old != null) { + old.close(); + } + } + + /** + * Swap in a new Cursor, returning the old Cursor. Unlike + * {@link #changeCursor(Cursor)}, the returned old Cursor is not + * closed. + * + * @param newCursor The new cursor to be used. + * @return Returns the previously set Cursor, or null if there wasa not one. + * If the given new Cursor is the same instance is the previously set + * Cursor, null is also returned. + */ + public Cursor swapCursor(Cursor newCursor) { + if (newCursor == mCursor) { + return null; + } + Cursor oldCursor = mCursor; + if (oldCursor != null) { + if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver); + if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver); + } + mCursor = newCursor; + if (newCursor != null) { + if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver); + if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver); + mRowIDColumn = newCursor.getColumnIndexOrThrow("_id"); + mDataValid = true; + // notify the observers about the new cursor + notifyDataSetChanged(); + } else { + mRowIDColumn = -1; + mDataValid = false; + // notify the observers about the lack of a data set + // notifyDataSetInvalidated(); + notifyItemRangeRemoved(0, getItemCount()); + } + return oldCursor; + } + + /** + *

Converts the cursor into a CharSequence. Subclasses should override this + * method to convert their results. The default implementation returns an + * empty String for null values or the default String representation of + * the value.

+ * + * @param cursor the cursor to convert to a CharSequence + * @return a CharSequence representing the value + */ + public CharSequence convertToString(Cursor cursor) { + return cursor == null ? "" : cursor.toString(); + } + + /** + * Runs a query with the specified constraint. This query is requested + * by the filter attached to this adapter. + * + * The query is provided by a + * {@link android.widget.FilterQueryProvider}. + * If no provider is specified, the current cursor is not filtered and returned. + * + * After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)} + * and the previous cursor is closed. + * + * This method is always executed on a background thread, not on the + * application's main thread (or UI thread.) + * + * Contract: when constraint is null or empty, the original results, + * prior to any filtering, must be returned. + * + * @param constraint the constraint with which the query must be filtered + * + * @return a Cursor representing the results of the new query + * + * @see #getFilter() + * @see #getFilterQueryProvider() + * @see #setFilterQueryProvider(android.widget.FilterQueryProvider) + */ + public Cursor runQueryOnBackgroundThread(CharSequence constraint) { + if (mFilterQueryProvider != null) { + return mFilterQueryProvider.runQuery(constraint); + } + + return mCursor; + } + + public Filter getFilter() { + if (mCursorFilter == null) { + mCursorFilter = new CursorFilter(this); + } + return mCursorFilter; + } + + /** + * Returns the query filter provider used for filtering. When the + * provider is null, no filtering occurs. + * + * @return the current filter query provider or null if it does not exist + * + * @see #setFilterQueryProvider(android.widget.FilterQueryProvider) + * @see #runQueryOnBackgroundThread(CharSequence) + */ + public FilterQueryProvider getFilterQueryProvider() { + return mFilterQueryProvider; + } + + /** + * Sets the query filter provider used to filter the current Cursor. + * The provider's + * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)} + * method is invoked when filtering is requested by a client of + * this adapter. + * + * @param filterQueryProvider the filter query provider or null to remove it + * + * @see #getFilterQueryProvider() + * @see #runQueryOnBackgroundThread(CharSequence) + */ + public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) { + mFilterQueryProvider = filterQueryProvider; + } + + /** + * Called when the {@link ContentObserver} on the cursor receives a change notification. + * Can be implemented by sub-class. + * + * @see ContentObserver#onChange(boolean) + */ + protected void onContentChanged() { + + } + + private class ChangeObserver extends ContentObserver { + public ChangeObserver() { + super(new Handler()); + } + + @Override + public boolean deliverSelfNotifications() { + return true; + } + + @Override + public void onChange(boolean selfChange) { + onContentChanged(); + } + } + + private class MyDataSetObserver extends DataSetObserver { + @Override + public void onChanged() { + mDataValid = true; + notifyDataSetChanged(); + } + + @Override + public void onInvalidated() { + mDataValid = false; + // notifyDataSetInvalidated(); + notifyItemRangeRemoved(0, getItemCount()); + } + } + + /** + *

The CursorFilter delegates most of the work to the CursorAdapter. + * Subclasses should override these delegate methods to run the queries + * and convert the results into String that can be used by auto-completion + * widgets.

+ */ + +} + +class CursorFilter extends Filter { + + CursorFilterClient mClient; + + interface CursorFilterClient { + CharSequence convertToString(Cursor cursor); + Cursor runQueryOnBackgroundThread(CharSequence constraint); + Cursor getCursor(); + void changeCursor(Cursor cursor); + } + + CursorFilter(CursorFilterClient client) { + mClient = client; + } + + @Override + public CharSequence convertResultToString(Object resultValue) { + return mClient.convertToString((Cursor) resultValue); + } + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + Cursor cursor = mClient.runQueryOnBackgroundThread(constraint); + + FilterResults results = new FilterResults(); + if (cursor != null) { + results.count = cursor.getCount(); + results.values = cursor; + } else { + results.count = 0; + results.values = null; + } + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + Cursor oldCursor = mClient.getCursor(); + + if (results.values != null && results.values != oldCursor) { + mClient.changeCursor((Cursor) results.values); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/gnucash/android/ui/util/OnTransferFundsListener.java b/app/src/main/java/org/gnucash/android/ui/util/OnTransferFundsListener.java new file mode 100644 index 000000000..fb1c93d85 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/util/OnTransferFundsListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.ui.util; + +import org.gnucash.android.model.Money; + +/** + * Interface to be implemented by classes which start the transfer funds fragment + */ +public interface OnTransferFundsListener { + + /** + * Method called after the funds have been converted to the desired currency + * @param amount Funds in new currency + */ + void transferComplete(Money amount); +} 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 81a031417..660419344 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 @@ -18,7 +18,7 @@ import android.text.format.Time; -import com.doomonafireball.betterpickers.recurrencepicker.EventRecurrence; +import com.codetroopers.betterpickers.recurrencepicker.EventRecurrence; import org.gnucash.android.model.ScheduledAction; @@ -27,7 +27,7 @@ import java.util.List; /** - * Parses {@link com.doomonafireball.betterpickers.recurrencepicker.EventRecurrence}s to generate + * Parses {@link EventRecurrence}s to generate * {@link org.gnucash.android.model.ScheduledAction}s * * @author Ngewi Fet 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 new file mode 100644 index 000000000..6a71f62a3 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/util/ScrollingFABBehavior.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.ui.util; + +import android.content.Context; +import android.os.Build; +import android.support.design.widget.AppBarLayout; +import android.support.design.widget.CoordinatorLayout; +import android.support.design.widget.FloatingActionButton; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; + +import org.gnucash.android.R; + +/** + * Behavior for floating action button when list is scrolled + * Courtesy: https://mzgreen.github.io/2015/06/23/How-to-hideshow-Toolbar-when-list-is-scrolling(part3)/ + */ +public class ScrollingFABBehavior extends CoordinatorLayout.Behavior { + private int toolbarHeight; + + public ScrollingFABBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + this.toolbarHeight = getToolbarHeight(context); + } + + @Override + public boolean layoutDependsOn(CoordinatorLayout parent, FloatingActionButton fab, View dependency) { + return dependency instanceof AppBarLayout; + } + + @Override + public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton fab, View dependency) { + if (dependency instanceof AppBarLayout) { + 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); + } + } + return true; + } + + private int getToolbarHeight(Context context){ + TypedValue tv = new TypedValue(); + int actionBarHeight = android.support.v7.appcompat.R.attr.actionBarSize; + if (context.getTheme().resolveAttribute(R.attr.actionBarSize, tv, true)) + { + actionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data, + context.getResources().getDisplayMetrics()); + } + + return actionBarHeight; + } +} 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 new file mode 100644 index 000000000..9805df25c --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/util/widget/CalculatorEditText.java @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.gnucash.android.ui.util.widget; + +import android.app.Activity; +import android.content.Context; +import android.content.res.TypedArray; +import android.inputmethodservice.KeyboardView; +import android.support.annotation.XmlRes; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; + +import com.crashlytics.android.Crashlytics; + +import net.objecthunter.exp4j.Expression; +import net.objecthunter.exp4j.ExpressionBuilder; + +import org.gnucash.android.R; +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.ui.common.FormActivity; + +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.Currency; +import java.util.Locale; + +/** + * A custom EditText which supports computations and uses a custom calculator keyboard. + *

Afer the view is inflated, make sure to call {@link #bindListeners(KeyboardView)} + * with the view from your layout where the calculator keyboard should be displayed:

+ * @author Ngewi Fet + */ +public class CalculatorEditText extends EditText { + CalculatorKeyboard mCalculatorKeyboard; + private Currency mCurrency = Currency.getInstance(GnuCashApplication.getDefaultCurrencyCode()); + private Context mContext; + + /** + * Flag which is set if the contents of this view have been modified + */ + private boolean isContentModified = false; + + private int mCalculatorKeysLayout; + private KeyboardView mCalculatorKeyboardView; + + public CalculatorEditText(Context context) { + super(context); + this.mContext = context; + } + + public CalculatorEditText(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + public CalculatorEditText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + } + + /** + * Overloaded constructor + * Reads any attributes which are specified in XML and applies them + * @param context Activity context + * @param attrs View attributes + */ + private void init(Context context, AttributeSet attrs){ + this.mContext = context; + TypedArray a = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.CalculatorEditText, + 0, 0); + + try { + mCalculatorKeysLayout = a.getResourceId(R.styleable.CalculatorEditText_keyboardKeysLayout, R.xml.calculator_keyboard); + } finally { + a.recycle(); + } + + addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + isContentModified = true; + } + }); + } + + public void bindListeners(CalculatorKeyboard calculatorKeyboard){ + mCalculatorKeyboard = calculatorKeyboard; + mContext = calculatorKeyboard.getContext(); + setOnFocusChangeListener(new OnFocusChangeListener() { + // NOTE By setting the on focus listener, we can show the custom keyboard when the edit box gets focus, but also hide it when the edit box loses focus + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + setSelection(getText().length()); + mCalculatorKeyboard.showCustomKeyboard(v); + } else { + mCalculatorKeyboard.hideCustomKeyboard(); + evaluate(); + } + } + }); + + setOnClickListener(new OnClickListener() { + // NOTE By setting the on click listener we can show the custom keyboard again, + // by tapping on an edit box that already had focus (but that had the keyboard hidden). + @Override + public void onClick(View v) { + mCalculatorKeyboard.showCustomKeyboard(v); + } + }); + + // Disable spell check (hex strings look like words to Android) + setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + + // FIXME: for some reason, this prevents the text selection from working + setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + if (v != null) + ((InputMethodManager) GnuCashApplication.getAppContext() + .getSystemService(Activity.INPUT_METHOD_SERVICE)) + .hideSoftInputFromWindow(v.getWindowToken(), 0); + + return false; + } + }); + + // Although it looks redundant having both onClickListener and OnTouchListener, removing + // one of them makes the standard keyboard show up in addition to the calculator one. + setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (!mCalculatorKeyboard.isCustomKeyboardVisible()) + mCalculatorKeyboard.showCustomKeyboard(v); + + // XXX: Use dispatchTouchEvent()? + onTouchEvent(event); + return false; + } + }); + + ((FormActivity)mContext).setOnBackListener(mCalculatorKeyboard); + } + + /** + * Initializes listeners on the edittext + */ + public void bindListeners(KeyboardView keyboardView){ + bindListeners(new CalculatorKeyboard(mContext, keyboardView, mCalculatorKeysLayout)); + } + + /** + * Returns the calculator keyboard instantiated by this edittext + * @return CalculatorKeyboard + */ + public CalculatorKeyboard getCalculatorKeyboard(){ + return mCalculatorKeyboard; + } + + /** + * Returns the view Id of the keyboard view + * @return Keyboard view + */ + public KeyboardView getCalculatorKeyboardView() { + return mCalculatorKeyboardView; + } + + /** + * Set the keyboard view used for displaying the keyboard + * @param calculatorKeyboardView Calculator keyboard view + */ + public void setCalculatorKeyboardView(KeyboardView calculatorKeyboardView) { + this.mCalculatorKeyboardView = calculatorKeyboardView; + bindListeners(calculatorKeyboardView); + } + + /** + * Returns the XML resource ID describing the calculator keys layout + * @return XML resource ID + */ + public int getCalculatorKeysLayout() { + return mCalculatorKeysLayout; + } + + /** + * Sets the XML resource describing the layout of the calculator keys + * @param mCalculatorKeysLayout XML resource ID + */ + public void setCalculatorKeysLayout(@XmlRes int mCalculatorKeysLayout) { + this.mCalculatorKeysLayout = mCalculatorKeysLayout; + bindListeners(mCalculatorKeyboardView); + } + + /** + * Sets the calculator keyboard to use for this EditText + * @param keyboard Properly intialized calculator keyobard + */ + public void setCalculatorKeyboard(CalculatorKeyboard keyboard){ + this.mCalculatorKeyboard = keyboard; + } + + /** + * Returns the currency used for computations + * @return ISO 4217 currency + */ + public Currency getCurrency() { + return mCurrency; + } + + /** + * Sets the currency to use for calculations + * The currency determines the number of decimal places used + * @param currency ISO 4217 currency + */ + public void setCurrency(Currency currency) { + this.mCurrency = currency; + } + + /** + * Evaluates the arithmetic expression in the editText and sets the text property + * @return Result of arithmetic evaluation which is same as text displayed in edittext + */ + public String evaluate(){ + String amountString = getCleanString(); + if (amountString.isEmpty()) + return amountString; + + ExpressionBuilder expressionBuilder = new ExpressionBuilder(amountString); + Expression expression; + + try { + expression = expressionBuilder.build(); + } catch (RuntimeException e) { + setError(getContext().getString(R.string.label_error_invalid_expression)); + String msg = "Invalid expression: " + amountString; + Log.e(this.getClass().getSimpleName(), msg); + Crashlytics.log(msg); + return ""; + } + + if (expression != null && expression.validate().isValid()) { + BigDecimal result = new BigDecimal(expression.evaluate()); + setValue(result); + } else { + setError(getContext().getString(R.string.label_error_invalid_expression)); + Log.w(VIEW_LOG_TAG, "Expression is null or invalid: " + expression); + } + return getText().toString(); + } + + /** + * Evaluates the expression in the text and returns true if the result is valid + * @return @{code true} if the input is valid, {@code false} otherwise + */ + public boolean isInputValid(){ + evaluate(); + return getText().length() > 0 && getError() == null; + } + + /** + * Returns the amount string formatted as a decimal in Locale.US and trimmed. + * This also converts decimal operators from other locales into a period (.) + * @return String with the amount in the EditText or empty string if there is no input + */ + public String getCleanString(){ + return getText().toString().replaceAll(",", ".").trim(); + } + + /** + * Returns true if the content of this view has been modified + * @return {@code true} if content has changed, {@code false} otherwise + */ + public boolean isInputModified(){ + return this.isContentModified; + } + + /** + * Returns the value of the amount in the edit text or null if the field is empty. + * Performs an evaluation of the expression first + * @return BigDecimal value + */ + public BigDecimal getValue(){ + evaluate(); + String amountString = getCleanString(); + if (amountString.isEmpty()) + return null; + return new BigDecimal(amountString); + } + + /** + * Set the text to the value of {@code amount} formatted according to the locale + *

The number of decimal places are determined by the currency set to the view, and the + * decimal separator is determined by the device locale. There are no thousandths separators.

+ * @param amount BigDecimal amount + */ + public void setValue(BigDecimal amount){ + BigDecimal newAmount = amount.setScale(mCurrency.getDefaultFractionDigits(), BigDecimal.ROUND_HALF_EVEN); + + DecimalFormat formatter = (DecimalFormat) NumberFormat.getInstance(Locale.getDefault()); + formatter.setMinimumFractionDigits(0); + formatter.setMaximumFractionDigits(mCurrency.getDefaultFractionDigits()); + formatter.setGroupingUsed(false); + String resultString = formatter.format(newAmount.doubleValue()); + + setText(resultString); + setSelection(resultString.length()); + } +} diff --git a/app/src/main/java/org/gnucash/android/ui/util/widget/CalculatorKeyboard.java b/app/src/main/java/org/gnucash/android/ui/util/widget/CalculatorKeyboard.java new file mode 100644 index 000000000..bb9f5d9f8 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/util/widget/CalculatorKeyboard.java @@ -0,0 +1,213 @@ +/** + * Copyright 2013 Maarten Pennings extended by SimplicityApks + * + * Modified by: + * Copyright 2015 Àlex Magaz Graça + * Copyright 2015 Ngewi Fet + * + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *

+ * If you use this software in a product, an acknowledgment in the product + * documentation would be appreciated but is not required. + */ + +package org.gnucash.android.ui.util.widget; + +import android.app.Activity; +import android.content.Context; +import android.inputmethodservice.Keyboard; +import android.inputmethodservice.KeyboardView; +import android.inputmethodservice.KeyboardView.OnKeyboardActionListener; +import android.support.annotation.XmlRes; +import android.text.Editable; +import android.view.HapticFeedbackConstants; +import android.view.View; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; + +import java.text.DecimalFormatSymbols; + + +/** + * When an activity hosts a keyboardView, this class allows several EditText's to register for it. + * + * Known issues: + * - It's not possible to select text. + * - When in landscape, the EditText is covered by the keyboard. + * - No i18n. + * + * @author Maarten Pennings, extended by SimplicityApks + * @date 2012 December 23 + * + * @author Àlex Magaz Graça + * @author Ngewi Fet + * + */ +public class CalculatorKeyboard { + + public static final int KEY_CODE_DECIMAL_SEPARATOR = 46; + /** A link to the KeyboardView that is used to render this CalculatorKeyboard. */ + private KeyboardView mKeyboardView; + + private Context mContext; + private boolean hapticFeedback; + + public static final String LOCALE_DECIMAL_SEPARATOR = Character.toString(DecimalFormatSymbols.getInstance().getDecimalSeparator()); + + private OnKeyboardActionListener mOnKeyboardActionListener = new OnKeyboardActionListener() { + @Override + public void onKey(int primaryCode, int[] keyCodes) { + View focusCurrent = ((Activity)mContext).getWindow().getCurrentFocus(); + + /* + if (focusCurrent == null || focusCurrent.getClass() != EditText.class) + return; + */ + + CalculatorEditText calculatorEditText = (CalculatorEditText) focusCurrent; + Editable editable = calculatorEditText.getText(); + int start = calculatorEditText.getSelectionStart(); + int end = calculatorEditText.getSelectionEnd(); + + // FIXME: use replace() down + // delete the selection, if chars are selected: + if (end > start) + editable.delete(start, end); + + switch (primaryCode) { + case KEY_CODE_DECIMAL_SEPARATOR: + editable.insert(start, LOCALE_DECIMAL_SEPARATOR); + break; + case 42: + case 43: + case 45: + case 47: + case 48: + case 49: + case 50: + case 51: + case 52: + case 53: + case 54: + case 55: + case 56: + case 57: + //editable.replace(start, end, Character.toString((char) primaryCode)); + // XXX: could be android:keyOutputText attribute used instead of this? + editable.insert(start, Character.toString((char) primaryCode)); + break; + case -5: + int deleteStart = start > 0 ? start - 1: 0; + editable.delete(deleteStart, end); + break; + case 1003: // C[lear] + editable.clear(); + break; + case 1001: + calculatorEditText.evaluate(); + break; + case 1002: + calculatorEditText.focusSearch(View.FOCUS_DOWN).requestFocus(); + hideCustomKeyboard(); + break; + } + } + + @Override + public void onPress(int arg0) { + // vibrate if haptic feedback is enabled: + if (hapticFeedback && arg0 != 0) + mKeyboardView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + } + + @Override public void onRelease(int primaryCode) { } + @Override public void onText(CharSequence text) { } + @Override public void swipeLeft() { } + @Override public void swipeRight() { } + @Override public void swipeDown() { } + @Override public void swipeUp() { } + }; + + /** + * Create a custom keyboard, that uses the KeyboardView (with resource id viewid) of the host activity, + * and load the keyboard layout from xml file layoutid (see {@link Keyboard} for description). + * Note that the host activity must have a KeyboardView in its layout (typically aligned with the bottom of the activity). + * Note that the keyboard layout xml file may include key codes for navigation; see the constants in this class for their values. + * + * @param context Context within with the calculator is created + * @param keyboardView KeyboardView in the layout + * @param keyboardLayoutResId The id of the xml file containing the keyboard layout. + */ + public CalculatorKeyboard(Context context, KeyboardView keyboardView, @XmlRes int keyboardLayoutResId) { + mContext = context; + mKeyboardView = keyboardView; + Keyboard keyboard = new Keyboard(mContext, keyboardLayoutResId); + for (Keyboard.Key key : keyboard.getKeys()) { + if (key.codes[0] == KEY_CODE_DECIMAL_SEPARATOR){ + key.label = LOCALE_DECIMAL_SEPARATOR; + break; + } + } + mKeyboardView.setKeyboard(keyboard); + mKeyboardView.setPreviewEnabled(false); // NOTE Do not show the preview balloons + mKeyboardView.setOnKeyboardActionListener(mOnKeyboardActionListener); + // Hide the standard keyboard initially + ((Activity)mContext).getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + } + + /** Returns whether the CalculatorKeyboard is visible. */ + public boolean isCustomKeyboardVisible() { + return mKeyboardView.getVisibility() == View.VISIBLE; + } + + /** Make the CalculatorKeyboard visible, and hide the system keyboard for view v. */ + public void showCustomKeyboard(View v) { + if (v != null) + ((InputMethodManager) mContext.getSystemService(Activity.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(v.getWindowToken(), 0); + + mKeyboardView.setVisibility(View.VISIBLE); + mKeyboardView.setEnabled(true); + } + + /** Make the CalculatorKeyboard invisible. */ + public void hideCustomKeyboard() { + mKeyboardView.setVisibility(View.GONE); + mKeyboardView.setEnabled(false); + } + + /** + * Enables or disables the Haptic feedback on keyboard touches + * @param goEnabled true if you want haptic feedback, falso otherwise + */ + public void enableHapticFeedback(boolean goEnabled) { + mKeyboardView.setHapticFeedbackEnabled(goEnabled); + hapticFeedback = goEnabled; + } + + public boolean onBackPressed() { + if (isCustomKeyboardVisible()) { + hideCustomKeyboard(); + return true; + } else + return false; + } + + /** + * Returns the context of this keyboard + * @return Context + */ + public Context getContext(){ + return mContext; + } +} diff --git a/app/src/main/java/org/gnucash/android/ui/util/CheckableLinearLayout.java b/app/src/main/java/org/gnucash/android/ui/util/widget/CheckableLinearLayout.java similarity index 98% rename from app/src/main/java/org/gnucash/android/ui/util/CheckableLinearLayout.java rename to app/src/main/java/org/gnucash/android/ui/util/widget/CheckableLinearLayout.java index 109163655..5230e8be3 100644 --- a/app/src/main/java/org/gnucash/android/ui/util/CheckableLinearLayout.java +++ b/app/src/main/java/org/gnucash/android/ui/util/widget/CheckableLinearLayout.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.gnucash.android.ui.util; +package org.gnucash.android.ui.util.widget; import android.annotation.TargetApi; import android.content.Context; diff --git a/app/src/main/java/org/gnucash/android/ui/util/widget/EmptyRecyclerView.java b/app/src/main/java/org/gnucash/android/ui/util/widget/EmptyRecyclerView.java new file mode 100644 index 000000000..427e15b5b --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/util/widget/EmptyRecyclerView.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.gnucash.android.ui.util.widget; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.View; + + +/** + * Code from https://gist.github.com/AnirudhaAgashe/61e523dadbaaf064b9a0 + * @author Anirudha Agashe + */ +public class EmptyRecyclerView extends RecyclerView { + @Nullable + View emptyView; + + public EmptyRecyclerView(Context context) { super(context); } + + public EmptyRecyclerView(Context context, AttributeSet attrs) { super(context, attrs); } + + public EmptyRecyclerView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + void checkIfEmpty() { + if (emptyView != null && getAdapter() != null) { + emptyView.setVisibility(getAdapter().getItemCount() > 0 ? GONE : VISIBLE); + } + } + + final @NonNull AdapterDataObserver observer = new AdapterDataObserver() { + + @Override public void onChanged() { + super.onChanged(); + checkIfEmpty(); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + super.onItemRangeInserted(positionStart, itemCount); + checkIfEmpty(); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + super.onItemRangeRemoved(positionStart, itemCount); + checkIfEmpty(); + } + }; + + @Override public void setAdapter(@Nullable Adapter adapter) { + final Adapter oldAdapter = getAdapter(); + if (oldAdapter != null) { + oldAdapter.unregisterAdapterDataObserver(observer); + } + super.setAdapter(adapter); + if (adapter != null) { + adapter.registerAdapterDataObserver(observer); + } + } + + public void setEmptyView(@Nullable View emptyView) { + this.emptyView = emptyView; + checkIfEmpty(); + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..dc32ef8b5 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/util/widget/ReselectSpinner.java @@ -0,0 +1,34 @@ +package org.gnucash.android.ui.util.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Spinner; + +/** + * 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

+ */ +public class ReselectSpinner extends Spinner { + public ReselectSpinner(Context context) { + super(context); + } + + public ReselectSpinner(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ReselectSpinner(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public void setSelection(int position) { + boolean sameSelected = getSelectedItemPosition() == position; + super.setSelection(position); + if (position == 5 && sameSelected){ + getOnItemSelectedListener().onItemSelected(this, getSelectedView(), position, getSelectedItemId()); + } + super.setSelection(position); + } +} diff --git a/app/src/main/java/org/gnucash/android/ui/util/TransactionTypeToggleButton.java b/app/src/main/java/org/gnucash/android/ui/util/widget/TransactionTypeSwitch.java similarity index 83% rename from app/src/main/java/org/gnucash/android/ui/util/TransactionTypeToggleButton.java rename to app/src/main/java/org/gnucash/android/ui/util/widget/TransactionTypeSwitch.java index 5db2ee273..bb6537090 100644 --- a/app/src/main/java/org/gnucash/android/ui/util/TransactionTypeToggleButton.java +++ b/app/src/main/java/org/gnucash/android/ui/util/widget/TransactionTypeSwitch.java @@ -14,38 +14,39 @@ * limitations under the License. */ -package org.gnucash.android.ui.util; +package org.gnucash.android.ui.util.widget; import android.content.Context; +import android.support.v7.widget.SwitchCompat; import android.util.AttributeSet; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.TextView; -import android.widget.ToggleButton; import org.gnucash.android.R; import org.gnucash.android.model.AccountType; import org.gnucash.android.model.Transaction; import org.gnucash.android.model.TransactionType; -import org.gnucash.android.ui.transaction.TransactionFormFragment; + +import java.math.BigDecimal; /** * A special type of {@link android.widget.ToggleButton} which displays the appropriate CREDIT/DEBIT labels for the * different account types. * @author Ngewi Fet */ -public class TransactionTypeToggleButton extends ToggleButton { +public class TransactionTypeSwitch extends SwitchCompat { private AccountType mAccountType = AccountType.EXPENSE; - public TransactionTypeToggleButton(Context context, AttributeSet attrs, int defStyle) { + public TransactionTypeSwitch(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } - public TransactionTypeToggleButton(Context context, AttributeSet attrs) { + public TransactionTypeSwitch(Context context, AttributeSet attrs) { super(context, attrs); } - public TransactionTypeToggleButton(Context context) { + public TransactionTypeSwitch(Context context) { super(context); } @@ -108,7 +109,7 @@ public void setAccountType(AccountType accountType){ * @param amoutView Amount string {@link android.widget.EditText} * @param currencyTextView Currency symbol text view */ - public void setAmountFormattingListener(EditText amoutView, TextView currencyTextView){ + public void setAmountFormattingListener(CalculatorEditText amoutView, TextView currencyTextView){ setOnCheckedChangeListener(new OnTypeChangedListener(amoutView, currencyTextView)); } @@ -137,36 +138,40 @@ public TransactionType getTransactionType(){ } private class OnTypeChangedListener implements OnCheckedChangeListener{ - private EditText mAmountEditText; + private CalculatorEditText mAmountEditText; private TextView mCurrencyTextView; /** * Constructor with the amount view * @param amountEditText EditText displaying the amount value * @param currencyTextView Currency symbol text view */ - public OnTypeChangedListener(EditText amountEditText, TextView currencyTextView){ + public OnTypeChangedListener(CalculatorEditText amountEditText, TextView currencyTextView){ this.mAmountEditText = amountEditText; this.mCurrencyTextView = currencyTextView; } @Override public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { + setText(isChecked ? getTextOn() : getTextOff()); if (isChecked){ int red = getResources().getColor(R.color.debit_red); - TransactionTypeToggleButton.this.setTextColor(red); + TransactionTypeSwitch.this.setTextColor(red); mAmountEditText.setTextColor(red); mCurrencyTextView.setTextColor(red); } else { int green = getResources().getColor(R.color.credit_green); - TransactionTypeToggleButton.this.setTextColor(green); + TransactionTypeSwitch.this.setTextColor(green); mAmountEditText.setTextColor(green); mCurrencyTextView.setTextColor(green); } - String amountText = mAmountEditText.getText().toString(); - if (amountText.length() > 0){ - String changedSignText = TransactionFormFragment.parseInputToDecimal(amountText).negate().toPlainString(); - mAmountEditText.setText(changedSignText); //trigger an edit to update the number sign + BigDecimal amount = mAmountEditText.getValue(); + if (amount != null){ + if ((isChecked && amount.signum() > 0) //we switched to debit but the amount is +ve + || (!isChecked && amount.signum() < 0)){ //credit but amount is -ve + mAmountEditText.setValue(amount.negate()); + } + } } } diff --git a/app/src/main/java/org/gnucash/android/ui/wizard/CurrencySelectFragment.java b/app/src/main/java/org/gnucash/android/ui/wizard/CurrencySelectFragment.java new file mode 100644 index 000000000..00cfe08fe --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/wizard/CurrencySelectFragment.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.ui.wizard; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v4.app.ListFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; + +import com.tech.freak.wizardpager.ui.PageFragmentCallbacks; + +import org.gnucash.android.R; +import org.gnucash.android.db.CommoditiesDbAdapter; +import org.gnucash.android.util.CommoditiesCursorAdapter; + +import butterknife.ButterKnife; + +/** + * Displays a list of all currencies in the database and allows selection of one + *

This fragment is intended for use with the first run wizard

+ * @author Ngewi Fet + * @see CurrencySelectPage + * @see FirstRunWizardActivity + * @see FirstRunWizardModel + */ +public class CurrencySelectFragment extends ListFragment { + + private CurrencySelectPage mPage; + private PageFragmentCallbacks mCallbacks; + + private CommoditiesDbAdapter mCommoditiesDbAdapter; + + String mPageKey; + + public static CurrencySelectFragment newInstance(String key){ + CurrencySelectFragment fragment = new CurrencySelectFragment(); + fragment.mPageKey = key; + return fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_wizard_currency_select_page, container, false); + ButterKnife.bind(this, view); + return view; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mPage = (CurrencySelectPage) mCallbacks.onGetPage(mPageKey); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + CommoditiesCursorAdapter commoditiesCursorAdapter = new CommoditiesCursorAdapter(getActivity(), R.layout.list_item_commodity); + setListAdapter(commoditiesCursorAdapter); + + getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE); + + mCommoditiesDbAdapter = CommoditiesDbAdapter.getInstance(); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + if (!(activity instanceof PageFragmentCallbacks)) { + throw new ClassCastException("Activity must implement PageFragmentCallbacks"); + } + + mCallbacks = (PageFragmentCallbacks) activity; + } + + @Override + public void onDetach() { + super.onDetach(); + mCallbacks = null; + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + super.onListItemClick(l, v, position, id); + + String currencyCode = mCommoditiesDbAdapter.getCurrencyCode(mCommoditiesDbAdapter.getUID(id)); + mPage.getData().putString(CurrencySelectPage.CURRENCY_CODE_DATA_KEY, currencyCode); + } + +} diff --git a/app/src/main/java/org/gnucash/android/ui/wizard/CurrencySelectPage.java b/app/src/main/java/org/gnucash/android/ui/wizard/CurrencySelectPage.java new file mode 100644 index 000000000..3687a8da7 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/wizard/CurrencySelectPage.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.ui.wizard; + +import android.support.v4.app.Fragment; +import android.text.TextUtils; + +import com.tech.freak.wizardpager.model.ModelCallbacks; +import com.tech.freak.wizardpager.model.Page; +import com.tech.freak.wizardpager.model.ReviewItem; + +import java.util.ArrayList; + +/** + * Page displaying all the commodities in the database + */ +public class CurrencySelectPage extends Page{ + + public static final String CURRENCY_CODE_DATA_KEY = "currency_code_data_key"; + + protected CurrencySelectPage(ModelCallbacks callbacks, String title) { + super(callbacks, title); + } + + @Override + public Fragment createFragment() { + return CurrencySelectFragment.newInstance(getKey()); + } + + @Override + public void getReviewItems(ArrayList arrayList) { + arrayList.add(new ReviewItem(getTitle(), mData.getString(CURRENCY_CODE_DATA_KEY), getKey())); + } + + @Override + public boolean isCompleted() { + return !TextUtils.isEmpty(mData.getString(CURRENCY_CODE_DATA_KEY)); + } +} 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 new file mode 100644 index 000000000..83c274002 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/wizard/FirstRunWizardActivity.java @@ -0,0 +1,396 @@ +/* + * Copyright 2012 Roman Nurik + * Copyright 2012 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.ui.wizard; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.drawable.ColorDrawable; +import android.os.Build; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.view.ViewPager; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.AppCompatButton; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.Toast; + +import com.crashlytics.android.Crashlytics; +import com.tech.freak.wizardpager.model.AbstractWizardModel; +import com.tech.freak.wizardpager.model.ModelCallbacks; +import com.tech.freak.wizardpager.model.Page; +import com.tech.freak.wizardpager.model.ReviewItem; +import com.tech.freak.wizardpager.ui.PageFragmentCallbacks; +import com.tech.freak.wizardpager.ui.ReviewFragment; +import com.tech.freak.wizardpager.ui.StepPagerStrip; + +import org.gnucash.android.R; +import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.importer.ImportAsyncTask; +import org.gnucash.android.ui.account.AccountsActivity; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import butterknife.Bind; +import butterknife.ButterKnife; + +/** + * Activity for managing the wizard displayed upon first run of the application + */ +public class FirstRunWizardActivity extends AppCompatActivity implements + PageFragmentCallbacks, ReviewFragment.Callbacks, ModelCallbacks { + + @Bind(R.id.pager) ViewPager mPager; + private MyPagerAdapter mPagerAdapter; + + private boolean mEditingAfterReview; + + private AbstractWizardModel mWizardModel; + + private boolean mConsumePageSelectedEvent; + + @Bind(R.id.btn_save) AppCompatButton mNextButton; + @Bind(R.id.btn_cancel) Button mPrevButton; + @Bind(R.id.strip) StepPagerStrip mStepPagerStrip; + + private List mCurrentPageSequence; + private String mAccountOptions; + private String mCurrencyCode; + + + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_first_run_wizard); + ButterKnife.bind(this); + + setTitle(getString(R.string.title_setup_gnucash)); + + mWizardModel = new FirstRunWizardModel(this); + if (savedInstanceState != null) { + mWizardModel.load(savedInstanceState.getBundle("model")); + } + + mWizardModel.registerListener(this); + + mPagerAdapter = new MyPagerAdapter(getSupportFragmentManager()); + mPager.setAdapter(mPagerAdapter); + mStepPagerStrip + .setOnPageSelectedListener(new StepPagerStrip.OnPageSelectedListener() { + @Override + public void onPageStripSelected(int position) { + position = Math.min(mPagerAdapter.getCount() - 1, + position); + if (mPager.getCurrentItem() != position) { + mPager.setCurrentItem(position); + } + } + }); + + + mPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageSelected(int position) { + mStepPagerStrip.setCurrentPage(position); + + if (mConsumePageSelectedEvent) { + mConsumePageSelectedEvent = false; + return; + } + + mEditingAfterReview = false; + updateBottomBar(); + } + }); + + mNextButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (mPager.getCurrentItem() == mCurrentPageSequence.size()) { + ArrayList reviewItems = new ArrayList<>(); + for (Page page : mCurrentPageSequence) { + page.getReviewItems(reviewItems); + } + + mCurrencyCode = GnuCashApplication.getDefaultCurrencyCode(); + mAccountOptions = getString(R.string.wizard_option_let_me_handle_it); //default value, do nothing + String feedbackOption = getString(R.string.wizard_option_disable_crash_reports); + for (ReviewItem reviewItem : reviewItems) { + String title = reviewItem.getTitle(); + if (title.equals(getString(R.string.wizard_title_default_currency))){ + mCurrencyCode = reviewItem.getDisplayValue(); + } else if (title.equals(getString(R.string.wizard_title_select_currency))){ + mCurrencyCode = reviewItem.getDisplayValue(); + } else if (title.equals(getString(R.string.wizard_title_account_setup))){ + mAccountOptions = reviewItem.getDisplayValue(); + } else if (title.equals(getString(R.string.wizard_title_feedback_options))){ + feedbackOption = reviewItem.getDisplayValue(); + } + } + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(FirstRunWizardActivity.this); + SharedPreferences.Editor preferenceEditor = preferences.edit(); + preferenceEditor.putString(getString(R.string.key_default_currency), mCurrencyCode); + + + if (feedbackOption.equals(getString(R.string.wizard_option_auto_send_crash_reports))){ + preferenceEditor.putBoolean(getString(R.string.key_enable_crashlytics), true); + } else { + preferenceEditor.putBoolean(getString(R.string.key_enable_crashlytics), false); + } + 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(); + } + } else { + if (mEditingAfterReview) { + mPager.setCurrentItem(mPagerAdapter.getCount() - 1); + } else { + mPager.setCurrentItem(mPager.getCurrentItem() + 1); + } + } + } + }); + + mPrevButton.setText(R.string.wizard_btn_back); + TypedValue v = new TypedValue(); + getTheme().resolveAttribute(android.R.attr.textAppearanceMedium, v, + true); + mPrevButton.setTextAppearance(this, v.resourceId); + mNextButton.setTextAppearance(this, v.resourceId); + + mPrevButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + mPager.setCurrentItem(mPager.getCurrentItem() - 1); + } + }); + + onPageTreeChanged(); + updateBottomBar(); + } + + /** + * Create accounts depending on the user preference (import or default set) and finish this activity + *

This method also removes the first run flag from the application

+ */ + private void createAccountsAndFinish() { + AccountsActivity.removeFirstRunFlag(); + + if (mAccountOptions.equals(getString(R.string.wizard_option_create_default_accounts))){ + AccountsActivity.createDefaultAccounts(mCurrencyCode, FirstRunWizardActivity.this); + finish(); + } else if (mAccountOptions.equals(getString(R.string.wizard_option_import_my_accounts))){ + AccountsActivity.startXmlFileChooser(this); + } + } + + @Override + public void onPageTreeChanged() { + mCurrentPageSequence = mWizardModel.getCurrentPageSequence(); + recalculateCutOffPage(); + mStepPagerStrip.setPageCount(mCurrentPageSequence.size() + 1); // + 1 = + // review + // step + mPagerAdapter.notifyDataSetChanged(); + updateBottomBar(); + } + + private void updateBottomBar() { + int position = mPager.getCurrentItem(); + final Resources res = getResources(); + if (position == mCurrentPageSequence.size()) { + mNextButton.setText(R.string.btn_wizard_finish); + + mNextButton.setBackgroundDrawable(new ColorDrawable(res.getColor(R.color.theme_accent))); + mNextButton.setTextColor(res.getColor(android.R.color.white)); + } else { + mNextButton.setText(mEditingAfterReview ? R.string.review + : R.string.btn_wizard_next); + mNextButton + .setBackgroundDrawable(new ColorDrawable(res.getColor(android.R.color.transparent))); + mNextButton.setTextColor(res.getColor(R.color.theme_accent)); + mNextButton.setEnabled(position != mPagerAdapter.getCutOffPage()); + } + + mPrevButton + .setVisibility(position <= 0 ? View.INVISIBLE : View.VISIBLE); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode){ + case AccountsActivity.REQUEST_PICK_ACCOUNTS_FILE: + if (resultCode == Activity.RESULT_OK && data != null) { + AccountsActivity.importXmlFileFromIntent(this, data); + } + finish(); + break; + } + } + + @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(); + mWizardModel.unregisterListener(this); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBundle("model", mWizardModel.save()); + } + + @Override + public AbstractWizardModel onGetModel() { + return mWizardModel; + } + + @Override + public void onEditScreenAfterReview(String key) { + for (int i = mCurrentPageSequence.size() - 1; i >= 0; i--) { + if (mCurrentPageSequence.get(i).getKey().equals(key)) { + mConsumePageSelectedEvent = true; + mEditingAfterReview = true; + mPager.setCurrentItem(i); + updateBottomBar(); + break; + } + } + } + + @Override + public void onPageDataChanged(Page page) { + if (page.isRequired()) { + if (recalculateCutOffPage()) { + mPagerAdapter.notifyDataSetChanged(); + updateBottomBar(); + } + } + } + + @Override + public Page onGetPage(String key) { + return mWizardModel.findByKey(key); + } + + private boolean recalculateCutOffPage() { + // Cut off the pager adapter at first required page that isn't completed + int cutOffPage = mCurrentPageSequence.size() + 1; + for (int i = 0; i < mCurrentPageSequence.size(); i++) { + Page page = mCurrentPageSequence.get(i); + if (page.isRequired() && !page.isCompleted()) { + cutOffPage = i; + break; + } + } + + if (mPagerAdapter.getCutOffPage() != cutOffPage) { + mPagerAdapter.setCutOffPage(cutOffPage); + return true; + } + + return false; + } + + public class MyPagerAdapter extends FragmentStatePagerAdapter { + private int mCutOffPage; + private Fragment mPrimaryItem; + + public MyPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Fragment getItem(int i) { + if (i >= mCurrentPageSequence.size()) { + return new ReviewFragment(); + } + + return mCurrentPageSequence.get(i).createFragment(); + } + + @Override + public int getItemPosition(Object object) { + // TODO: be smarter about this + if (object == mPrimaryItem) { + // Re-use the current fragment (its position never changes) + return POSITION_UNCHANGED; + } + + return POSITION_NONE; + } + + @Override + public void setPrimaryItem(ViewGroup container, int position, + Object object) { + super.setPrimaryItem(container, position, object); + mPrimaryItem = (Fragment) object; + } + + @Override + public int getCount() { + return Math.min(mCutOffPage + 1, mCurrentPageSequence == null ? 1 + : mCurrentPageSequence.size() + 1); + } + + public void setCutOffPage(int cutOffPage) { + if (cutOffPage < 0) { + cutOffPage = Integer.MAX_VALUE; + } + mCutOffPage = cutOffPage; + } + + public int getCutOffPage() { + return mCutOffPage; + } + } +} diff --git a/app/src/main/java/org/gnucash/android/ui/wizard/FirstRunWizardModel.java b/app/src/main/java/org/gnucash/android/ui/wizard/FirstRunWizardModel.java new file mode 100644 index 000000000..1e67d7cd4 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/wizard/FirstRunWizardModel.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.gnucash.android.ui.wizard; + +import android.content.Context; + +import com.tech.freak.wizardpager.model.AbstractWizardModel; +import com.tech.freak.wizardpager.model.BranchPage; +import com.tech.freak.wizardpager.model.Page; +import com.tech.freak.wizardpager.model.PageList; +import com.tech.freak.wizardpager.model.SingleFixedChoicePage; + +import org.gnucash.android.R; +import org.gnucash.android.app.GnuCashApplication; + +import java.util.Arrays; +import java.util.Set; +import java.util.TreeSet; + +/** + * Wizard displayed upon first run of the application for setup + */ +public class FirstRunWizardModel extends AbstractWizardModel { + + public FirstRunWizardModel(Context context) { + super(context); + } + + @Override + protected PageList onNewRootPageList() { + String defaultCurrencyCode = GnuCashApplication.getDefaultCurrencyCode(); + BranchPage defaultCurrencyPage = new BranchPage(this, mContext.getString(R.string.wizard_title_default_currency)); + + String[] currencies = new String[]{defaultCurrencyCode, "CHF", "EUR", "GBP", "USD"}; + Set currencySet = new TreeSet<>(Arrays.asList(currencies)); + + + defaultCurrencyPage.setChoices(currencySet.toArray(new String[currencySet.size()])); + defaultCurrencyPage.setRequired(true); + defaultCurrencyPage.setValue(defaultCurrencyCode); + + Page defaultAccountsPage = new SingleFixedChoicePage(this, mContext.getString(R.string.wizard_title_account_setup)) + .setChoices(mContext.getString(R.string.wizard_option_create_default_accounts), + mContext.getString(R.string.wizard_option_import_my_accounts), + mContext.getString(R.string.wizard_option_let_me_handle_it)) + .setValue(mContext.getString(R.string.wizard_option_create_default_accounts)) + .setRequired(true); + for (String currency : currencySet) { + defaultCurrencyPage.addBranch(currency, defaultAccountsPage); + } + + defaultCurrencyPage.addBranch(mContext.getString(R.string.wizard_option_currency_other), + new CurrencySelectPage(this, mContext.getString(R.string.wizard_title_select_currency)), defaultAccountsPage).setRequired(true); + return new PageList( + new WelcomePage(this, mContext.getString(R.string.wizard_title_welcome_to_gnucash)), + defaultCurrencyPage, + new SingleFixedChoicePage(this, mContext.getString(R.string.wizard_title_feedback_options)) + .setChoices(mContext.getString(R.string.wizard_option_auto_send_crash_reports), + mContext.getString(R.string.wizard_option_disable_crash_reports)) + .setRequired(true) + ); + } +} diff --git a/app/src/main/java/org/gnucash/android/ui/wizard/WelcomePage.java b/app/src/main/java/org/gnucash/android/ui/wizard/WelcomePage.java new file mode 100644 index 000000000..3d5965a30 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/wizard/WelcomePage.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.ui.wizard; + +import android.support.v4.app.Fragment; + +import com.tech.freak.wizardpager.model.ModelCallbacks; +import com.tech.freak.wizardpager.model.Page; +import com.tech.freak.wizardpager.model.ReviewItem; + +import java.util.ArrayList; + +/** + * Welcome page for the first run wizard + * @author Ngewi Fet + */ +public class WelcomePage extends Page { + + protected WelcomePage(ModelCallbacks callbacks, String title) { + super(callbacks, title); + } + + @Override + public Fragment createFragment() { + return new WelcomePageFragment(); + } + + @Override + public void getReviewItems(ArrayList arrayList) { + //nothing to see here, move along + } +} diff --git a/app/src/main/java/org/gnucash/android/ui/wizard/WelcomePageFragment.java b/app/src/main/java/org/gnucash/android/ui/wizard/WelcomePageFragment.java new file mode 100644 index 000000000..fc1323e86 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/wizard/WelcomePageFragment.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.ui.wizard; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.gnucash.android.R; + +/** + * Welcome page fragment is the first fragment that will be displayed to the user + */ +public class WelcomePageFragment extends Fragment { + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_wizard_welcome_page, container, false); + } +} diff --git a/app/src/main/java/org/gnucash/android/util/CommoditiesCursorAdapter.java b/app/src/main/java/org/gnucash/android/util/CommoditiesCursorAdapter.java new file mode 100644 index 000000000..0a5e246cd --- /dev/null +++ b/app/src/main/java/org/gnucash/android/util/CommoditiesCursorAdapter.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2015 Ngewi Fet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gnucash.android.util; + +import android.content.Context; +import android.database.Cursor; +import android.support.annotation.LayoutRes; +import android.support.v4.widget.SimpleCursorAdapter; +import android.text.TextUtils; +import android.view.ContextMenu; +import android.view.View; +import android.widget.TextView; + +import org.gnucash.android.db.CommoditiesDbAdapter; +import org.gnucash.android.db.DatabaseSchema; + +import java.util.Currency; + +/** + * Cursor adapter for displaying list of commodities. + *

You should provide the layout and the layout should contain a view with the id {@code android:id/text1}, + * which is where the name of the commodity will be displayed

+ *

The list is sorted by the currency code (which is also displayed first before the full name)

+ */ +public class CommoditiesCursorAdapter extends SimpleCursorAdapter { + + public CommoditiesCursorAdapter(Context context, @LayoutRes int itemLayoutResource) { + super(context, itemLayoutResource, + CommoditiesDbAdapter.getInstance().fetchAllRecords(DatabaseSchema.CommodityEntry.COLUMN_MNEMONIC + " ASC"), + new String[]{DatabaseSchema.CommodityEntry.COLUMN_FULLNAME}, + new int[] {android.R.id.text1}, 0); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + TextView textView = (TextView) view.findViewById(android.R.id.text1); + textView.setEllipsize(TextUtils.TruncateAt.MIDDLE); + + String currencyName = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.CommodityEntry.COLUMN_FULLNAME)); + String currencyCode = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.CommodityEntry.COLUMN_MNEMONIC)); + + textView.setText(currencyCode + " - " + currencyName); + } +} 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 dbd779b1c..c2183a25c 100644 --- a/app/src/main/java/org/gnucash/android/util/QualifiedAccountNameCursorAdapter.java +++ b/app/src/main/java/org/gnucash/android/util/QualifiedAccountNameCursorAdapter.java @@ -33,10 +33,11 @@ */ public class QualifiedAccountNameCursorAdapter extends SimpleCursorAdapter { - public QualifiedAccountNameCursorAdapter(Context context, int layout, Cursor c) { - super(context, layout, c, - new String[] {DatabaseSchema.AccountEntry.COLUMN_FULL_NAME}, - new int[] {android.R.id.text1}, 0); + public QualifiedAccountNameCursorAdapter(Context context, Cursor cursor) { + super(context, android.R.layout.simple_spinner_item, cursor, + new String[]{DatabaseSchema.AccountEntry.COLUMN_FULL_NAME}, + new int[]{android.R.id.text1}, 0); + setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); } @Override diff --git a/app/src/main/res/drawable-hdpi/action_search.png b/app/src/main/res/drawable-hdpi/action_search.png deleted file mode 100644 index e6b704518..000000000 Binary files a/app/src/main/res/drawable-hdpi/action_search.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/action_settings.png b/app/src/main/res/drawable-hdpi/action_settings.png deleted file mode 100644 index cc32e2d1d..000000000 Binary files a/app/src/main/res/drawable-hdpi/action_settings.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/action_settings_holo_light.png b/app/src/main/res/drawable-hdpi/action_settings_holo_light.png deleted file mode 100644 index cc32e2d1d..000000000 Binary files a/app/src/main/res/drawable-hdpi/action_settings_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/chart_icon.png b/app/src/main/res/drawable-hdpi/chart_icon.png deleted file mode 100644 index 9fb62722a..000000000 Binary files a/app/src/main/res/drawable-hdpi/chart_icon.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/content_copy_holo_dark.png b/app/src/main/res/drawable-hdpi/content_copy_holo_dark.png deleted file mode 100644 index 72c6bc6e2..000000000 Binary files a/app/src/main/res/drawable-hdpi/content_copy_holo_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/content_copy_holo_light.png b/app/src/main/res/drawable-hdpi/content_copy_holo_light.png deleted file mode 100644 index 623b71504..000000000 Binary files a/app/src/main/res/drawable-hdpi/content_copy_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/content_discard_holo_light.png b/app/src/main/res/drawable-hdpi/content_discard_holo_light.png deleted file mode 100644 index e9ce89e04..000000000 Binary files a/app/src/main/res/drawable-hdpi/content_discard_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/content_edit_holo_light.png b/app/src/main/res/drawable-hdpi/content_edit_holo_light.png deleted file mode 100644 index 4a939679e..000000000 Binary files a/app/src/main/res/drawable-hdpi/content_edit_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/content_event_holo_dark.png b/app/src/main/res/drawable-hdpi/content_event_holo_dark.png deleted file mode 100644 index 33e13284f..000000000 Binary files a/app/src/main/res/drawable-hdpi/content_event_holo_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/content_event_holo_light.png b/app/src/main/res/drawable-hdpi/content_event_holo_light.png deleted file mode 100644 index 5a591c23b..000000000 Binary files a/app/src/main/res/drawable-hdpi/content_event_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/content_import_export_holo_dark.png b/app/src/main/res/drawable-hdpi/content_import_export_holo_dark.png deleted file mode 100644 index 160a2b8fe..000000000 Binary files a/app/src/main/res/drawable-hdpi/content_import_export_holo_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/content_import_export_holo_light.png b/app/src/main/res/drawable-hdpi/content_import_export_holo_light.png deleted file mode 100644 index cf7e761ba..000000000 Binary files a/app/src/main/res/drawable-hdpi/content_import_export_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_forward.png b/app/src/main/res/drawable-hdpi/ic_action_forward.png new file mode 100644 index 000000000..5feffbd79 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_forward.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_rotate_right.png b/app/src/main/res/drawable-hdpi/ic_action_rotate_right.png new file mode 100644 index 000000000..6cf48d150 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_rotate_right.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_sort_by_size.png b/app/src/main/res/drawable-hdpi/ic_action_sort_by_size.png new file mode 100644 index 000000000..fae9ae7b9 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_sort_by_size.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_time.png b/app/src/main/res/drawable-hdpi/ic_action_time.png new file mode 100644 index 000000000..7e4e75ec8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_time.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_add_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_add_black_24dp.png new file mode 100644 index 000000000..c04b523c4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_add_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_add_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_add_white_24dp.png new file mode 100644 index 000000000..694179bd4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_add_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_add_white_48dp.png b/app/src/main/res/drawable-hdpi/ic_add_white_48dp.png new file mode 100644 index 000000000..0fdced8fc Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_add_white_48dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_backup_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_backup_black_24dp.png new file mode 100644 index 000000000..e0938f1dc Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_backup_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_chevron_right_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_chevron_right_white_24dp.png new file mode 100644 index 000000000..1f10ee461 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_chevron_right_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_close_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_close_black_24dp.png new file mode 100644 index 000000000..1a9cd75a0 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_close_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png new file mode 100644 index 000000000..ceb1a1eeb Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_data_usage_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_data_usage_white_24dp.png new file mode 100755 index 000000000..3a617247d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_data_usage_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_drawer.png b/app/src/main/res/drawable-hdpi/ic_drawer.png deleted file mode 100644 index 6614ea4f4..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_drawer.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_equalizer_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_equalizer_black_24dp.png new file mode 100644 index 000000000..1f7561b44 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_equalizer_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_equalizer_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_equalizer_white_24dp.png new file mode 100755 index 000000000..aa343b5b0 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_equalizer_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_filter_list_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_filter_list_white_24dp.png new file mode 100644 index 000000000..7e8a6b536 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_filter_list_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_folder_open_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_folder_open_black_24dp.png new file mode 100644 index 000000000..4f90c6310 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_folder_open_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_help_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_help_black_24dp.png new file mode 100644 index 000000000..374fafd7f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_help_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_mode_edit_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_mode_edit_black_24dp.png new file mode 100644 index 000000000..e531d72a5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_mode_edit_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_mode_edit_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_mode_edit_white_24dp.png new file mode 100644 index 000000000..595ff10ac Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_mode_edit_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_more_vert_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_more_vert_black_24dp.png new file mode 100644 index 000000000..22acc5500 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_more_vert_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_settings_backup_restore_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_settings_backup_restore_black_24dp.png new file mode 100644 index 000000000..eecb6f59a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_settings_backup_restore_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_settings_black_24dp.png new file mode 100644 index 000000000..acf1ddf85 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_settings_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_star_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_star_black_24dp.png new file mode 100644 index 000000000..92a0f5862 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_star_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_star_border_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_star_border_black_24dp.png new file mode 100644 index 000000000..cb31ce2f8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_star_border_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_star_border_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_star_border_white_24dp.png new file mode 100644 index 000000000..e302ef6fa Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_star_border_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_star_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_star_white_24dp.png new file mode 100644 index 000000000..86eecdd4a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_star_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_subject_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_subject_white_24dp.png new file mode 100755 index 000000000..1c190264a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_subject_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_trending_up_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_trending_up_white_24dp.png new file mode 100755 index 000000000..a9864a8fc Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_trending_up_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_warning_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_warning_black_24dp.png new file mode 100644 index 000000000..4c3d9a497 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_warning_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/navigation_accept_holo_dark.png b/app/src/main/res/drawable-hdpi/navigation_accept_holo_dark.png deleted file mode 100644 index 53cf6877e..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_accept_holo_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_accept_holo_light.png b/app/src/main/res/drawable-hdpi/navigation_accept_holo_light.png deleted file mode 100644 index 58bf97217..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_accept_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_cancel_holo_dark.png b/app/src/main/res/drawable-hdpi/navigation_cancel_holo_dark.png deleted file mode 100644 index 094eea589..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_cancel_holo_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_cancel_holo_light.png b/app/src/main/res/drawable-hdpi/navigation_cancel_holo_light.png deleted file mode 100644 index cde36e1fa..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_cancel_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_next_item.png b/app/src/main/res/drawable-hdpi/navigation_next_item.png deleted file mode 100644 index 67f148215..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_next_item.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/navigation_previous_item.png b/app/src/main/res/drawable-hdpi/navigation_previous_item.png deleted file mode 100644 index e861ecce9..000000000 Binary files a/app/src/main/res/drawable-hdpi/navigation_previous_item.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/action_search.png b/app/src/main/res/drawable-mdpi/action_search.png deleted file mode 100644 index 3aa644048..000000000 Binary files a/app/src/main/res/drawable-mdpi/action_search.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/action_settings.png b/app/src/main/res/drawable-mdpi/action_settings.png deleted file mode 100644 index dc66d914e..000000000 Binary files a/app/src/main/res/drawable-mdpi/action_settings.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/action_settings_holo_light.png b/app/src/main/res/drawable-mdpi/action_settings_holo_light.png deleted file mode 100644 index dc66d914e..000000000 Binary files a/app/src/main/res/drawable-mdpi/action_settings_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/chart_icon.png b/app/src/main/res/drawable-mdpi/chart_icon.png deleted file mode 100644 index 7bc698cec..000000000 Binary files a/app/src/main/res/drawable-mdpi/chart_icon.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/content_copy_holo_dark.png b/app/src/main/res/drawable-mdpi/content_copy_holo_dark.png deleted file mode 100644 index d93968e5b..000000000 Binary files a/app/src/main/res/drawable-mdpi/content_copy_holo_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/content_copy_holo_light.png b/app/src/main/res/drawable-mdpi/content_copy_holo_light.png deleted file mode 100644 index efb2445f0..000000000 Binary files a/app/src/main/res/drawable-mdpi/content_copy_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/content_discard_holo_light.png b/app/src/main/res/drawable-mdpi/content_discard_holo_light.png deleted file mode 100644 index cedb1085b..000000000 Binary files a/app/src/main/res/drawable-mdpi/content_discard_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/content_edit_holo_light.png b/app/src/main/res/drawable-mdpi/content_edit_holo_light.png deleted file mode 100644 index f09b2e4c2..000000000 Binary files a/app/src/main/res/drawable-mdpi/content_edit_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/content_event_holo_dark.png b/app/src/main/res/drawable-mdpi/content_event_holo_dark.png deleted file mode 100644 index c76f04924..000000000 Binary files a/app/src/main/res/drawable-mdpi/content_event_holo_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/content_event_holo_light.png b/app/src/main/res/drawable-mdpi/content_event_holo_light.png deleted file mode 100644 index ca94b6a6c..000000000 Binary files a/app/src/main/res/drawable-mdpi/content_event_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/content_import_export_holo_dark.png b/app/src/main/res/drawable-mdpi/content_import_export_holo_dark.png deleted file mode 100644 index a74fe6794..000000000 Binary files a/app/src/main/res/drawable-mdpi/content_import_export_holo_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/content_import_export_holo_light.png b/app/src/main/res/drawable-mdpi/content_import_export_holo_light.png deleted file mode 100644 index f481a04aa..000000000 Binary files a/app/src/main/res/drawable-mdpi/content_import_export_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_forward.png b/app/src/main/res/drawable-mdpi/ic_action_forward.png new file mode 100644 index 000000000..2ecf7f99c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_forward.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_rotate_right.png b/app/src/main/res/drawable-mdpi/ic_action_rotate_right.png new file mode 100644 index 000000000..419d26296 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_rotate_right.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_sort_by_size.png b/app/src/main/res/drawable-mdpi/ic_action_sort_by_size.png new file mode 100644 index 000000000..ea1d35975 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_sort_by_size.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_time.png b/app/src/main/res/drawable-mdpi/ic_action_time.png new file mode 100644 index 000000000..c8fda52e4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_time.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_add_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_add_black_24dp.png new file mode 100644 index 000000000..23bf11921 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_add_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_add_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_add_white_24dp.png new file mode 100644 index 000000000..3856041d7 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_add_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_add_white_48dp.png b/app/src/main/res/drawable-mdpi/ic_add_white_48dp.png new file mode 100644 index 000000000..67bb598e5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_add_white_48dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_backup_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_backup_black_24dp.png new file mode 100644 index 000000000..4cd6741c0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_backup_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_chevron_right_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_chevron_right_white_24dp.png new file mode 100644 index 000000000..b4f3c6d4c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_chevron_right_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_close_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_close_black_24dp.png new file mode 100644 index 000000000..40a1a84e3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_close_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png new file mode 100644 index 000000000..af7f8288d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_data_usage_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_data_usage_white_24dp.png new file mode 100755 index 000000000..051ce8410 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_data_usage_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_drawer.png b/app/src/main/res/drawable-mdpi/ic_drawer.png deleted file mode 100644 index b05c026c1..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_drawer.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_equalizer_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_equalizer_black_24dp.png new file mode 100644 index 000000000..cc612de36 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_equalizer_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_equalizer_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_equalizer_white_24dp.png new file mode 100755 index 000000000..bc600d35d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_equalizer_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_filter_list_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_filter_list_white_24dp.png new file mode 100644 index 000000000..59a2ec755 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_filter_list_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_folder_open_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_folder_open_black_24dp.png new file mode 100644 index 000000000..d7f357142 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_folder_open_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_help_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_help_black_24dp.png new file mode 100644 index 000000000..f6e789ba1 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_help_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_mode_edit_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_mode_edit_black_24dp.png new file mode 100644 index 000000000..9efbaae28 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_mode_edit_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_mode_edit_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_mode_edit_white_24dp.png new file mode 100644 index 000000000..12b09f1d9 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_mode_edit_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_more_vert_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_more_vert_black_24dp.png new file mode 100644 index 000000000..0e4f2f6ea Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_more_vert_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_settings_backup_restore_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_settings_backup_restore_black_24dp.png new file mode 100644 index 000000000..51f5ecab1 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_settings_backup_restore_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_settings_black_24dp.png new file mode 100644 index 000000000..c59419c02 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_settings_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_star_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_star_black_24dp.png new file mode 100644 index 000000000..a728afe60 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_star_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_star_border_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_star_border_black_24dp.png new file mode 100644 index 000000000..b75384809 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_star_border_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_star_border_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_star_border_white_24dp.png new file mode 100644 index 000000000..88142bf78 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_star_border_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_star_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_star_white_24dp.png new file mode 100644 index 000000000..d2cbe4c92 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_star_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_subject_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_subject_white_24dp.png new file mode 100755 index 000000000..3f17b3b4a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_subject_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_trending_up_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_trending_up_white_24dp.png new file mode 100755 index 000000000..fb04031d4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_trending_up_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_warning_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_warning_black_24dp.png new file mode 100644 index 000000000..e768d1125 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_warning_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/navigation_accept_holo_dark.png b/app/src/main/res/drawable-mdpi/navigation_accept_holo_dark.png deleted file mode 100644 index 35cda8e11..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_accept_holo_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_accept_holo_light.png b/app/src/main/res/drawable-mdpi/navigation_accept_holo_light.png deleted file mode 100644 index cf5fab3ad..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_accept_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_back_holo_dark.png b/app/src/main/res/drawable-mdpi/navigation_back_holo_dark.png deleted file mode 100644 index e0b79763f..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_back_holo_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_cancel_holo_light.png b/app/src/main/res/drawable-mdpi/navigation_cancel_holo_light.png deleted file mode 100644 index 9f4c3d6a2..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_cancel_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_next_item.png b/app/src/main/res/drawable-mdpi/navigation_next_item.png deleted file mode 100644 index 47365a300..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_next_item.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/navigation_previous_item.png b/app/src/main/res/drawable-mdpi/navigation_previous_item.png deleted file mode 100644 index 4ad2df427..000000000 Binary files a/app/src/main/res/drawable-mdpi/navigation_previous_item.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/widget_preview.png.jpg b/app/src/main/res/drawable-nodpi/widget_preview.jpg similarity index 100% rename from app/src/main/res/drawable-nodpi/widget_preview.png.jpg rename to app/src/main/res/drawable-nodpi/widget_preview.jpg diff --git a/app/src/main/res/drawable-xhdpi/action_settings_holo_light.png b/app/src/main/res/drawable-xhdpi/action_settings_holo_light.png deleted file mode 100644 index 04b65dc34..000000000 Binary files a/app/src/main/res/drawable-xhdpi/action_settings_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/chart_bar_button.png b/app/src/main/res/drawable-xhdpi/chart_bar_button.png deleted file mode 100644 index f75cc0d3a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/chart_bar_button.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/chart_line_button.png b/app/src/main/res/drawable-xhdpi/chart_line_button.png deleted file mode 100644 index 3658a14cd..000000000 Binary files a/app/src/main/res/drawable-xhdpi/chart_line_button.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/chart_pie_button.png b/app/src/main/res/drawable-xhdpi/chart_pie_button.png deleted file mode 100644 index 97fa0dc35..000000000 Binary files a/app/src/main/res/drawable-xhdpi/chart_pie_button.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/content_copy_holo_dark.png b/app/src/main/res/drawable-xhdpi/content_copy_holo_dark.png deleted file mode 100644 index 04e290d8b..000000000 Binary files a/app/src/main/res/drawable-xhdpi/content_copy_holo_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/content_copy_holo_light.png b/app/src/main/res/drawable-xhdpi/content_copy_holo_light.png deleted file mode 100644 index 00bff33c7..000000000 Binary files a/app/src/main/res/drawable-xhdpi/content_copy_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/content_discard_holo_light.png b/app/src/main/res/drawable-xhdpi/content_discard_holo_light.png deleted file mode 100644 index 98c73da1f..000000000 Binary files a/app/src/main/res/drawable-xhdpi/content_discard_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/content_edit_holo_light.png b/app/src/main/res/drawable-xhdpi/content_edit_holo_light.png deleted file mode 100644 index 1ead8fc33..000000000 Binary files a/app/src/main/res/drawable-xhdpi/content_edit_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/content_event_holo_dark.png b/app/src/main/res/drawable-xhdpi/content_event_holo_dark.png deleted file mode 100644 index 6111cabf1..000000000 Binary files a/app/src/main/res/drawable-xhdpi/content_event_holo_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/content_event_holo_light.png b/app/src/main/res/drawable-xhdpi/content_event_holo_light.png deleted file mode 100644 index c284f3572..000000000 Binary files a/app/src/main/res/drawable-xhdpi/content_event_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/content_import_export_holo_dark.png b/app/src/main/res/drawable-xhdpi/content_import_export_holo_dark.png deleted file mode 100644 index 6161b58e6..000000000 Binary files a/app/src/main/res/drawable-xhdpi/content_import_export_holo_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/content_import_export_holo_light.png b/app/src/main/res/drawable-xhdpi/content_import_export_holo_light.png deleted file mode 100644 index 7c1c1d718..000000000 Binary files a/app/src/main/res/drawable-xhdpi/content_import_export_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_forward.png b/app/src/main/res/drawable-xhdpi/ic_action_forward.png new file mode 100644 index 000000000..8cd5f0f0a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_forward.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rotate_right.png b/app/src/main/res/drawable-xhdpi/ic_action_rotate_right.png new file mode 100644 index 000000000..b23069517 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_rotate_right.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_sort_by_size.png b/app/src/main/res/drawable-xhdpi/ic_action_sort_by_size.png new file mode 100644 index 000000000..0dac7de6d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_sort_by_size.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_time.png b/app/src/main/res/drawable-xhdpi/ic_action_time.png new file mode 100644 index 000000000..772025b1f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_time.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_add_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_add_black_24dp.png new file mode 100644 index 000000000..3191d5283 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_add_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_add_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_add_white_24dp.png new file mode 100644 index 000000000..67bb598e5 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_add_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_add_white_48dp.png b/app/src/main/res/drawable-xhdpi/ic_add_white_48dp.png new file mode 100644 index 000000000..d64c22e9e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_add_white_48dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_backup_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_backup_black_24dp.png new file mode 100644 index 000000000..81155da52 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_backup_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_chevron_right_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_chevron_right_white_24dp.png new file mode 100644 index 000000000..93dec392d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_chevron_right_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_close_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_close_black_24dp.png new file mode 100644 index 000000000..6bc437298 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_close_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png new file mode 100644 index 000000000..b7c7ffd0e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_data_usage_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_data_usage_white_24dp.png new file mode 100755 index 000000000..73402866e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_data_usage_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_drawer.png b/app/src/main/res/drawable-xhdpi/ic_drawer.png deleted file mode 100644 index bcf49dd73..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_drawer.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_equalizer_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_equalizer_black_24dp.png new file mode 100644 index 000000000..4fb11f2d9 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_equalizer_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_equalizer_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_equalizer_white_24dp.png new file mode 100755 index 000000000..40c572c30 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_equalizer_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_filter_list_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_filter_list_white_24dp.png new file mode 100644 index 000000000..9416c70ec Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_filter_list_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_folder_open_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_folder_open_black_24dp.png new file mode 100644 index 000000000..f55b92e9b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_folder_open_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_help_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_help_black_24dp.png new file mode 100644 index 000000000..d3542c6bc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_help_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_mode_edit_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_mode_edit_black_24dp.png new file mode 100644 index 000000000..87f8de1ca Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_mode_edit_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_mode_edit_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_mode_edit_white_24dp.png new file mode 100644 index 000000000..5a06bff5a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_mode_edit_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_more_vert_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_more_vert_black_24dp.png new file mode 100644 index 000000000..9f10aa275 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_more_vert_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_settings_backup_restore_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_settings_backup_restore_black_24dp.png new file mode 100644 index 000000000..8f97526dc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_settings_backup_restore_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png new file mode 100644 index 000000000..e84e188a1 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_star_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_star_black_24dp.png new file mode 100644 index 000000000..c636ce8e8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_star_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_star_border_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_star_border_black_24dp.png new file mode 100644 index 000000000..4f978e739 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_star_border_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_star_border_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_star_border_white_24dp.png new file mode 100644 index 000000000..c7a5388ef Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_star_border_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_star_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_star_white_24dp.png new file mode 100644 index 000000000..914340683 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_star_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_subject_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_subject_white_24dp.png new file mode 100755 index 000000000..2c041de65 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_subject_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_trending_up_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_trending_up_white_24dp.png new file mode 100755 index 000000000..ba6fbe9af Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_trending_up_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_warning_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_warning_black_24dp.png new file mode 100644 index 000000000..2ea616491 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_warning_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_accept_holo_dark.png b/app/src/main/res/drawable-xhdpi/navigation_accept_holo_dark.png deleted file mode 100644 index b52dc3701..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_accept_holo_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_accept_holo_light.png b/app/src/main/res/drawable-xhdpi/navigation_accept_holo_light.png deleted file mode 100644 index b8915716e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_accept_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_back_holo_dark.png b/app/src/main/res/drawable-xhdpi/navigation_back_holo_dark.png deleted file mode 100644 index 3bdda98c3..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_back_holo_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_back_holo_light.png b/app/src/main/res/drawable-xhdpi/navigation_back_holo_light.png deleted file mode 100644 index f420e43f1..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_back_holo_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_next_item.png b/app/src/main/res/drawable-xhdpi/navigation_next_item.png deleted file mode 100644 index 5f304742f..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_next_item.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_previous_item.png b/app/src/main/res/drawable-xhdpi/navigation_previous_item.png deleted file mode 100644 index ed8ac91de..000000000 Binary files a/app/src/main/res/drawable-xhdpi/navigation_previous_item.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_forward.png b/app/src/main/res/drawable-xxhdpi/ic_action_forward.png new file mode 100644 index 000000000..ef3d2bfd5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_forward.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rotate_right.png b/app/src/main/res/drawable-xxhdpi/ic_action_rotate_right.png new file mode 100644 index 000000000..22baca143 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_rotate_right.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_sort_by_size.png b/app/src/main/res/drawable-xxhdpi/ic_action_sort_by_size.png new file mode 100644 index 000000000..2bab185be Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_sort_by_size.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_time.png b/app/src/main/res/drawable-xxhdpi/ic_action_time.png new file mode 100644 index 000000000..66a149eed Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_time.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_add_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_add_black_24dp.png new file mode 100644 index 000000000..a84106b01 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_add_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png new file mode 100644 index 000000000..0fdced8fc Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_add_white_48dp.png b/app/src/main/res/drawable-xxhdpi/ic_add_white_48dp.png new file mode 100644 index 000000000..7e6991372 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_add_white_48dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_backup_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_backup_black_24dp.png new file mode 100644 index 000000000..6506c7236 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_backup_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_chevron_right_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_chevron_right_white_24dp.png new file mode 100644 index 000000000..7920aa3d2 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_chevron_right_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_close_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_close_black_24dp.png new file mode 100644 index 000000000..51b4401ca Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_close_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png new file mode 100644 index 000000000..6b717e0dd Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_data_usage_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_data_usage_white_24dp.png new file mode 100755 index 000000000..18e9060e1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_data_usage_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_drawer.png b/app/src/main/res/drawable-xxhdpi/ic_drawer.png deleted file mode 100644 index f7e3b3079..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_drawer.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_equalizer_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_equalizer_black_24dp.png new file mode 100644 index 000000000..f7ef5fa4e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_equalizer_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_equalizer_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_equalizer_white_24dp.png new file mode 100755 index 000000000..d603c4f53 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_equalizer_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_filter_list_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_filter_list_white_24dp.png new file mode 100644 index 000000000..1263ae82e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_filter_list_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_folder_open_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_folder_open_black_24dp.png new file mode 100644 index 000000000..ac4de2caf Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_folder_open_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_help_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_help_black_24dp.png new file mode 100644 index 000000000..645822e83 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_help_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_mode_edit_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_mode_edit_black_24dp.png new file mode 100644 index 000000000..4af4ae634 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_mode_edit_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_mode_edit_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_mode_edit_white_24dp.png new file mode 100644 index 000000000..02e19d045 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_mode_edit_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_more_vert_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_more_vert_black_24dp.png new file mode 100644 index 000000000..94d5ab98c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_more_vert_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings_backup_restore_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_settings_backup_restore_black_24dp.png new file mode 100644 index 000000000..1f9d09ec5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_settings_backup_restore_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png new file mode 100644 index 000000000..3023ff8da Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_star_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_star_black_24dp.png new file mode 100644 index 000000000..54d306599 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_star_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_star_border_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_star_border_black_24dp.png new file mode 100644 index 000000000..f10d4274d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_star_border_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_star_border_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_star_border_white_24dp.png new file mode 100644 index 000000000..7e41906c5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_star_border_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_star_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_star_white_24dp.png new file mode 100644 index 000000000..aa5879215 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_star_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_subject_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_subject_white_24dp.png new file mode 100755 index 000000000..2713b24a0 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_subject_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_trending_up_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_trending_up_white_24dp.png new file mode 100755 index 000000000..78119528c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_trending_up_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_warning_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_warning_black_24dp.png new file mode 100644 index 000000000..ed36f700a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_warning_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_add_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_add_white_24dp.png new file mode 100644 index 000000000..d64c22e9e Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_add_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_backup_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_backup_black_24dp.png new file mode 100644 index 000000000..248289e97 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_backup_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_chevron_right_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_chevron_right_white_24dp.png new file mode 100644 index 000000000..6858f02b1 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_chevron_right_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_folder_open_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_folder_open_black_24dp.png new file mode 100644 index 000000000..e6faa991f Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_folder_open_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_help_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_help_black_24dp.png new file mode 100644 index 000000000..7c4823055 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_help_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_settings_backup_restore_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_settings_backup_restore_black_24dp.png new file mode 100644 index 000000000..fa0323229 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_settings_backup_restore_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png new file mode 100644 index 000000000..476d5c978 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_warning_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_warning_black_24dp.png new file mode 100644 index 000000000..3f4d539a4 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_warning_black_24dp.png differ diff --git a/app/src/main/res/drawable/numeric_button.xml b/app/src/main/res/drawable/numeric_button.xml index 610ad3499..696f061cb 100644 --- a/app/src/main/res/drawable/numeric_button.xml +++ b/app/src/main/res/drawable/numeric_button.xml @@ -25,18 +25,18 @@ android:radius="200dp" /> + android:color="@color/vpi__bright_foreground_disabled_holo_light" /> + android:color="@color/abc_primary_text_disable_only_material_dark" /> + android:color="@color/vpi__bright_foreground_disabled_holo_light" /> @@ -46,7 +46,7 @@ android:radius="200dp" /> + android:color="@color/vpi__bright_foreground_disabled_holo_light" /> diff --git a/app/src/main/res/drawable/selected_background.xml b/app/src/main/res/drawable/selected_background.xml index dc2f9cc35..9c4e8722b 100644 --- a/app/src/main/res/drawable/selected_background.xml +++ b/app/src/main/res/drawable/selected_background.xml @@ -1,8 +1,8 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_report_summary.xml b/app/src/main/res/layout-land/fragment_report_summary.xml new file mode 100644 index 000000000..678f9ba70 --- /dev/null +++ b/app/src/main/res/layout-land/fragment_report_summary.xml @@ -0,0 +1,136 @@ + + + + + +