diff --git a/app/build.gradle b/app/build.gradle index 60160b8ac..a59dddabf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,7 @@ apply plugin: 'crashlytics' def versionMajor = 1 def versionMinor = 6 def versionPatch = 0 -def versionBuild = 4 +def versionBuild = 5 def buildTime() { def df = new SimpleDateFormat("yyyyMMdd") @@ -41,6 +41,17 @@ android { resValue "string", "dropbox_app_secret", "h2t9fphj3nr4wkw" resValue "string", "manifest_dropbox_app_key", "db-dhjh8ke9wf05948" } + testInstrumentationRunner "org.gnucash.android.test.ui.GnucashAndroidTestRunner" + + } + + packagingOptions { + exclude 'META-INF/DEPENDENCIES' + exclude 'META-INF/NOTICE' + exclude 'META-INF/LICENSE' + exclude 'META-INF/LICENSE.txt' + exclude 'LICENSE.txt' + exclude 'META-INF/NOTICE.txt' } applicationVariants.all { variant -> @@ -114,6 +125,30 @@ android { } } +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(' ') + } + + task grantAnimationPermissionProduction(type: Exec, dependsOn: 'installProductionDebug'){ + commandLine "$adb 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 + tasks.each { task -> + if (task.name.startsWith('assembleDevelopmentDebugAndroidTest')){ + task.dependsOn grantAnimationPermissionDevel + } else if (task.name.startsWith('assembleBetaDebugAndroidTest')){ + task.dependsOn grantAnimationPermissionProduction + } else if (task.name.startsWith('assembleProductionDebugAndroidTest')){ + task.dependsOn grantAnimationPermissionProduction + } + } +} + + dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile('com.android.support:support-v4:22.1.1', @@ -131,9 +166,11 @@ dependencies { 'junit:junit:4.12', 'org.assertj:assertj-core:1.7.1' ) - androidTestCompile('com.jayway.android.robotium:robotium-solo:5.3.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'){ 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/src/androidTest/java/org/gnucash/android/test/ui/AccountsActivityTest.java b/app/src/androidTest/java/org/gnucash/android/test/ui/AccountsActivityTest.java index 81bd8da1d..13d2cfcfb 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012 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. @@ -22,13 +22,12 @@ import android.database.SQLException; 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.support.v4.app.Fragment; import android.test.ActivityInstrumentationTestCase2; import android.util.Log; -import android.view.View; -import android.widget.EditText; - -import com.robotium.solo.Solo; import org.gnucash.android.R; import org.gnucash.android.db.AccountsDbAdapter; @@ -39,35 +38,57 @@ import org.gnucash.android.model.Money; import org.gnucash.android.model.Split; import org.gnucash.android.model.Transaction; +import org.gnucash.android.receivers.AccountCreator; import org.gnucash.android.ui.account.AccountsActivity; import org.gnucash.android.ui.account.AccountsListFragment; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; import java.util.Currency; import java.util.List; +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.scrollTo; +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.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.isNotChecked; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.assertj.android.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.not; +@RunWith(AndroidJUnit4.class) public class AccountsActivityTest extends ActivityInstrumentationTestCase2 { private static final String DUMMY_ACCOUNT_CURRENCY_CODE = "USD"; private static final String DUMMY_ACCOUNT_NAME = "Dummy account"; public static final String DUMMY_ACCOUNT_UID = "dummy-account"; - private Solo mSolo; private DatabaseHelper mDbHelper; private SQLiteDatabase mDb; private AccountsDbAdapter mAccountsDbAdapter; private TransactionsDbAdapter mTransactionsDbAdapter; private SplitsDbAdapter mSplitsDbAdapter; + private AccountsActivity mAcccountsActivity; public AccountsActivityTest() { super(AccountsActivity.class); } - protected void setUp() throws Exception { - Context context = getInstrumentation().getTargetContext(); - preventFirstRunDialogs(context); + @Before + public void setUp() throws Exception { + super.setUp(); + injectInstrumentation(InstrumentationRegistry.getInstrumentation()); + preventFirstRunDialogs(getInstrumentation().getTargetContext()); + mAcccountsActivity = getActivity(); - mDbHelper = new DatabaseHelper(context); + mDbHelper = new DatabaseHelper(mAcccountsActivity); try { mDb = mDbHelper.getWritableDatabase(); } catch (SQLException e) { @@ -78,14 +99,17 @@ protected void setUp() throws Exception { mTransactionsDbAdapter = new TransactionsDbAdapter(mDb, mSplitsDbAdapter); mAccountsDbAdapter = new AccountsDbAdapter(mDb, mTransactionsDbAdapter); - mSolo = new Solo(getInstrumentation(), getActivity()); - Account account = new Account(DUMMY_ACCOUNT_NAME); account.setUID(DUMMY_ACCOUNT_UID); account.setCurrency(Currency.getInstance(DUMMY_ACCOUNT_CURRENCY_CODE)); mAccountsDbAdapter.addAccount(account); + refreshAccountsList(); } + /** + * Prevents the first-run dialogs (Whats new, Create accounts etc) from being displayed when testing + * @param context Application context + */ public static void preventFirstRunDialogs(Context context) { Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); @@ -118,6 +142,7 @@ public void testDisplayAccountsList(){ assertEquals(NUMBER_OF_ACCOUNTS + 1, accountsListView.getCount()); } */ + @Test public void testSearchAccounts(){ String SEARCH_ACCOUNT_NAME = "Search Account"; @@ -125,45 +150,35 @@ public void testSearchAccounts(){ account.setParentUID(DUMMY_ACCOUNT_UID); mAccountsDbAdapter.addAccount(account); - refreshAccountsList(); - //enter search query // ActionBarUtils.clickSherlockActionBarItem(mSolo, R.id.menu_search); - mSolo.clickOnActionBarItem(R.id.menu_search); - mSolo.sleep(2000); - mSolo.enterText(0, "Se"); - mSolo.sleep(3000); - boolean accountFound = mSolo.waitForText(SEARCH_ACCOUNT_NAME, 1, 2000); - assertTrue(accountFound); - - mSolo.clearEditText(0); - - mSolo.sleep(2000); - //the child account should be hidden again - accountFound = mSolo.waitForText(SEARCH_ACCOUNT_NAME, 1, 2000); - assertFalse(accountFound); + onView(withId(R.id.menu_search)).perform(click()); + onView(withId(R.id.abs__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.primary_text)).check(matches(not(withText(SEARCH_ACCOUNT_NAME)))); } /** * Tests that an account can be created successfully and that the account list is sorted alphabetically. */ + @Test public void testCreateAccount(){ - mSolo.clickOnActionBarItem(R.id.menu_add_account); - mSolo.waitForText(getActivity().getString(R.string.title_add_account)); - - View checkbox = mSolo.getCurrentActivity().findViewById(R.id.checkbox_parent_account); - //there already exists one eligible parent account in the system - assertThat(checkbox).isVisible(); - - mSolo.clickOnCheckBox(0); + onView(withId(R.id.menu_add_account)).check(matches(isDisplayed())).perform(click()); - EditText inputAccountName = (EditText) mSolo.getCurrentActivity().findViewById(R.id.edit_text_account_name); String NEW_ACCOUNT_NAME = "A New Account"; - mSolo.enterText(inputAccountName, NEW_ACCOUNT_NAME); - mSolo.clickOnActionBarItem(R.id.menu_save); + onView(withId(R.id.input_account_name)).perform(typeText(NEW_ACCOUNT_NAME)); + Espresso.closeSoftKeyboard(); + onView(withId(R.id.checkbox_placeholder_account)) + .check(matches(isNotChecked())) + .perform(click()); - mSolo.waitForText(NEW_ACCOUNT_NAME); - mSolo.sleep(3000); + onView(withId(R.id.checkbox_parent_account)).perform(scrollTo()) + .check(matches(allOf(isDisplayed(), isNotChecked()))) + .perform(click()); + + onView(withId(R.id.menu_save)).perform(click()); List accounts = mAccountsDbAdapter.getAllAccounts(); assertThat(accounts).isNotNull(); @@ -175,24 +190,24 @@ public void testCreateAccount(){ assertThat(newestAccount.isPlaceholderAccount()).isTrue(); } - public void testChangeParentAccount(){ + @Test + public void testChangeParentAccount() { final String accountName = "Euro Account"; Account account = new Account(accountName, Currency.getInstance("EUR")); mAccountsDbAdapter.addAccount(account); refreshAccountsList(); - mSolo.waitForText(accountName); - mSolo.clickLongOnText(accountName); - mSolo.clickOnView(mSolo.getView(R.id.context_menu_edit_accounts)); - mSolo.waitForView(EditText.class); + onView(withText(accountName)).perform(longClick()); + onView(withId(R.id.context_menu_edit_accounts)).perform(click()); + onView(withId(R.id.fragment_account_form)).check(matches(isDisplayed())); + Espresso.closeSoftKeyboard(); + onView(withId(R.id.checkbox_parent_account)).perform(scrollTo()) + .check(matches(isNotChecked())) + .perform(click()); - mSolo.clickOnCheckBox(1); - mSolo.sleep(2000); + onView(withId(R.id.menu_save)).perform(click()); - mSolo.clickOnActionBarItem(R.id.menu_save); - mSolo.sleep(1000); - mSolo.waitForText(getActivity().getString(R.string.title_accounts)); Account editedAccount = mAccountsDbAdapter.getAccount(account.getUID()); String parentUID = editedAccount.getParentUID(); @@ -200,126 +215,92 @@ public void testChangeParentAccount(){ assertThat(DUMMY_ACCOUNT_UID).isEqualTo(parentUID); } + @Test public void testEditAccount(){ - refreshAccountsList(); - mSolo.sleep(2000); - mSolo.waitForText(DUMMY_ACCOUNT_NAME); - String editedAccountName = "Edited Account"; - - mSolo.clickLongOnText(DUMMY_ACCOUNT_NAME); - - clickSherlockActionBarItem(R.id.context_menu_edit_accounts); - - mSolo.waitForView(EditText.class); +// onView(withText(DUMMY_ACCOUNT_NAME)).perform(longClick()); + onView(withId(R.id.primary_text)).perform(longClick()); + onView(withId(R.id.context_menu_edit_accounts)).perform(click()); - mSolo.clearEditText(0); - mSolo.enterText(0, editedAccountName); + onView(withId(R.id.fragment_account_form)).check(matches(isDisplayed())); - clickSherlockActionBarItem(R.id.menu_save); + onView(withId(R.id.input_account_name)).perform(clearText()).perform(typeText(editedAccountName)); - mSolo.waitForDialogToClose(); - mSolo.waitForText("Accounts"); + onView(withId(R.id.menu_save)).perform(click()); List accounts = mAccountsDbAdapter.getAllAccounts(); Account latest = accounts.get(0); //will be the first due to alphabetical sorting - - assertEquals("Edited Account", latest.getName()); - assertEquals(DUMMY_ACCOUNT_CURRENCY_CODE, latest.getCurrency().getCurrencyCode()); + + assertThat(latest.getName()).isEqualTo(editedAccountName); + assertThat(latest.getCurrency().getCurrencyCode()).isEqualTo(DUMMY_ACCOUNT_CURRENCY_CODE); } //TODO: Add test for moving content of accounts before deleting it - public void testDeleteAccount(){ - final String accountNameToDelete = "TO BE DELETED"; - final String accountUidToDelete = "to-be-deleted"; - - Account acc = new Account(accountNameToDelete); - acc.setUID(accountUidToDelete); - + @Test(expected = IllegalArgumentException.class) + public void testDeleteAccount() { Transaction transaction = new Transaction("hats"); - transaction.addSplit(new Split(Money.getZeroInstance(), accountUidToDelete)); - acc.addTransaction(transaction); - mAccountsDbAdapter.addAccount(acc); - - Fragment fragment = getActivity().getCurrentAccountListFragment(); - assertNotNull(fragment); + transaction.addSplit(new Split(Money.getZeroInstance(), DUMMY_ACCOUNT_UID)); + mTransactionsDbAdapter.addTransaction(transaction); - ((AccountsListFragment) fragment).refresh(); + onView(withText(DUMMY_ACCOUNT_NAME)).perform(longClick()); + onView(withId(R.id.context_menu_delete)).perform(click()); - mSolo.clickLongOnText(accountNameToDelete); + //the account has no sub-accounts + onView(withId(R.id.accounts_options)).check(matches(not(isDisplayed()))); + onView(withId(R.id.transactions_options)).check(matches(isDisplayed())); - clickSherlockActionBarItem(R.id.context_menu_delete); + onView(withText(R.string.label_delete_transactions)).perform(click()); + onView(withId(R.id.btn_save)).perform(click()); - mSolo.waitForDialogToOpen(); - mSolo.clickOnRadioButton(0); - mSolo.clickOnView(mSolo.getView(R.id.btn_save)); + //should throw expected exception + mAccountsDbAdapter.getID(DUMMY_ACCOUNT_UID); - mSolo.waitForDialogToClose(); - mSolo.waitForText("Accounts"); - - Exception expectedException = null; - try { - mAccountsDbAdapter.getID(accountUidToDelete); - } catch (IllegalArgumentException e){ - expectedException = e; - } - assertNotNull(expectedException); - - List transactions = mTransactionsDbAdapter.getAllTransactionsForAccount(accountUidToDelete); - assertEquals(0, transactions.size()); + List transactions = mTransactionsDbAdapter.getAllTransactionsForAccount(DUMMY_ACCOUNT_UID); + assertThat(transactions).isEmpty(); } //TODO: Test import of account file //TODO: test settings activity - + @Test public void testIntentAccountCreation(){ Intent intent = new Intent(Intent.ACTION_INSERT); - intent.putExtra(Intent.EXTRA_TITLE, "Intent Account"); - intent.putExtra(Intent.EXTRA_UID, "intent-account"); - intent.putExtra(Account.EXTRA_CURRENCY_CODE, "EUR"); - intent.setType(Account.MIME_TYPE); - getActivity().sendBroadcast(intent); - - //give time for the account to be created - synchronized (mSolo) { - try { - mSolo.wait(2000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - + intent.putExtra(Intent.EXTRA_TITLE, "Intent Account"); + intent.putExtra(Intent.EXTRA_UID, "intent-account"); + intent.putExtra(Account.EXTRA_CURRENCY_CODE, "EUR"); + intent.setType(Account.MIME_TYPE); + + new AccountCreator().onReceive(mAcccountsActivity, intent); + Account account = mAccountsDbAdapter.getAccount("intent-account"); - assertNotNull(account); - assertEquals("Intent Account", account.getName()); - assertEquals("intent-account", account.getUID()); - assertEquals("EUR", account.getCurrency().getCurrencyCode()); + assertThat(account).isNotNull(); + assertThat(account.getName()).isEqualTo("Intent Account"); + assertThat(account.getUID()).isEqualTo("intent-account"); + assertThat(account.getCurrency().getCurrencyCode()).isEqualTo("EUR"); } - - protected void tearDown() throws Exception { - mSolo.finishOpenedActivities(); - mSolo.waitForEmptyActivityStack(20000); - mSolo.sleep(5000); - mAccountsDbAdapter.deleteAllRecords(); - + @After + public void tearDown() throws Exception { + mAcccountsActivity.finish(); + Thread.sleep(1000); + mAccountsDbAdapter.deleteAllRecords(); //clear the data super.tearDown(); } - /** - * Finds a view in the action bar and clicks it, since the native methods are not supported by ActionBarSherlock - * @param id - */ - private void clickSherlockActionBarItem(int id){ - View view = mSolo.getView(id); - mSolo.clickOnView(view); - } - /** * Refresh the account list fragment */ private void refreshAccountsList(){ - Fragment fragment = getActivity().getCurrentAccountListFragment(); - ((AccountsListFragment)fragment).refresh(); + try { + runTestOnUiThread(new Runnable() { + @Override + public void run() { + Fragment fragment = mAcccountsActivity.getCurrentAccountListFragment(); + ((AccountsListFragment) fragment).refresh(); + } + }); + } catch (Throwable throwable) { + System.err.println("Failed to refresh fragment"); + } + } } 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 953e9e0e4..a176ecbc8 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012 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. @@ -19,10 +19,11 @@ import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.preference.PreferenceManager; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; import android.test.ActivityInstrumentationTestCase2; import android.util.Log; - -import com.robotium.solo.Solo; +import android.widget.CompoundButton; import org.gnucash.android.R; import org.gnucash.android.db.AccountsDbAdapter; @@ -39,31 +40,48 @@ import org.gnucash.android.model.Split; import org.gnucash.android.model.Transaction; import org.gnucash.android.ui.account.AccountsActivity; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; import java.io.File; import java.util.Currency; import java.util.List; +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +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.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.allOf; +@RunWith(AndroidJUnit4.class) public class ExportTransactionsTest extends ActivityInstrumentationTestCase2 { - private Solo mSolo; private DatabaseHelper mDbHelper; private SQLiteDatabase mDb; private AccountsDbAdapter mAccountsDbAdapter; private TransactionsDbAdapter mTransactionsDbAdapter; private SplitsDbAdapter mSplitsDbAdapter; + private AccountsActivity mAcccountsActivity; + public ExportTransactionsTest() { super(AccountsActivity.class); } @Override - protected void setUp() throws Exception { + @Before + public void setUp() throws Exception { + super.setUp(); + injectInstrumentation(InstrumentationRegistry.getInstrumentation()); AccountsActivityTest.preventFirstRunDialogs(getInstrumentation().getTargetContext()); - mSolo = new Solo(getInstrumentation(), getActivity()); + mAcccountsActivity = getActivity(); mDbHelper = new DatabaseHelper(getActivity()); try { @@ -75,6 +93,7 @@ protected void setUp() throws Exception { mSplitsDbAdapter = new SplitsDbAdapter(mDb); mTransactionsDbAdapter = new TransactionsDbAdapter(mDb, mSplitsDbAdapter); mAccountsDbAdapter = new AccountsDbAdapter(mDb, mTransactionsDbAdapter); + mAccountsDbAdapter.deleteAllRecords(); Account account = new Account("Exportable"); Transaction transaction = new Transaction("Pizza"); @@ -82,11 +101,12 @@ protected void setUp() throws Exception { transaction.setTime(System.currentTimeMillis()); Split split = new Split(new Money("8.99", "USD"), account.getUID()); split.setMemo("Hawaii is the best!"); - transaction.addSplit(split); + transaction.addSplit(split); transaction.addSplit(split.createPair(mAccountsDbAdapter.getOrCreateImbalanceAccountUID(Currency.getInstance("USD")))); account.addTransaction(transaction); - + mAccountsDbAdapter.addAccount(account); + } /** @@ -95,6 +115,7 @@ protected void setUp() throws Exception { * If this test fails, it may be due to the file being created and tested in different minutes of the clock * Just try rerunning it again. */ + @Test public void testOfxExport(){ testExport(ExportFormat.OFX); } @@ -102,10 +123,12 @@ public void testOfxExport(){ /** * Test the export of transactions in the QIF format */ + @Test public void testQifExport(){ testExport(ExportFormat.QIF); } + @Test public void testXmlExport(){ testExport(ExportFormat.XML); } @@ -117,67 +140,55 @@ public void testXmlExport(){ public void testExport(ExportFormat format){ File folder = new File(Exporter.EXPORT_FOLDER_PATH); folder.mkdirs(); - mSolo.sleep(5000); assertThat(folder).exists(); 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()); + onView(withText(R.string.nav_menu_export)).perform(click()); + onView(withText(format.name())).perform(click()); - mSolo.clickOnActionBarItem(R.id.menu_export); - mSolo.waitForDialogToOpen(5000); - - mSolo.waitForText(getActivity().getString(R.string.title_export_dialog)); - - mSolo.clickOnText(format.name()); - mSolo.clickOnView(mSolo.getView(R.id.btn_save)); - - mSolo.waitForDialogToClose(10000); - mSolo.sleep(5000); //sleep so that emulators can save the file + onView(withId(R.id.btn_save)).perform(click()); assertThat(folder.listFiles().length).isEqualTo(1); File exportFile = folder.listFiles()[0]; assertThat(exportFile.getName()).endsWith(format.getExtension()); } + @Test public void testDeleteTransactionsAfterExport(){ assertThat(mTransactionsDbAdapter.getAllTransactionsCount()).isGreaterThan(0); PreferenceManager.getDefaultSharedPreferences(getActivity()).edit() - .putBoolean(mSolo.getString(R.string.key_delete_transactions_after_export), true).commit(); + .putBoolean(mAcccountsActivity.getString(R.string.key_delete_transactions_after_export), true).commit(); testExport(ExportFormat.QIF); assertThat(mTransactionsDbAdapter.getAllTransactionsCount()).isEqualTo(0); PreferenceManager.getDefaultSharedPreferences(getActivity()).edit() - .putBoolean(mSolo.getString(R.string.key_delete_transactions_after_export), false).commit(); + .putBoolean(mAcccountsActivity.getString(R.string.key_delete_transactions_after_export), false).commit(); } /** * Test creating a scheduled export * Does not work on Travis yet */ - public void atestCreateExportSchedule(){ -// mSolo.setNavigationDrawer(Solo.OPENED); -// mSolo.clickOnText(mSolo.getString(R.string.nav_menu_export)); - mSolo.clickOnActionBarItem(R.id.menu_export); - mSolo.waitForDialogToOpen(5000); - - mSolo.clickOnText(ExportFormat.XML.name()); - mSolo.clickOnView(mSolo.getView(R.id.input_recurrence)); - mSolo.waitForDialogToOpen(); - mSolo.sleep(3000); - mSolo.clickOnButton(0); //switch on the recurrence dialog - mSolo.sleep(2000); - mSolo.pressSpinnerItem(0, -1); - mSolo.sleep(2000); - mSolo.clickOnButton(1); - mSolo.sleep(3000); - mSolo.clickOnButton(5); //the export button is the second - mSolo.waitForDialogToClose(); - - mSolo.sleep(5000); //wait for database save + @Test + public void shouldCreateExportSchedule(){ + onView(withId(android.R.id.home)).perform(click()); + onView(withText(R.string.nav_menu_export)).perform(click()); + + onView(withText(ExportFormat.XML.name())).perform(click()); + onView(withId(R.id.input_recurrence)).perform(click()); + //switch on recurrence dialog + onView(allOf(isAssignableFrom(CompoundButton.class), isDisplayed(), isEnabled())).perform(click()); + onView(withText("Done")).perform(click()); + + onView(withId(R.id.btn_save)).perform(click()); ScheduledActionDbAdapter scheduledactionDbAdapter = new ScheduledActionDbAdapter(mDb); List scheduledActions = scheduledactionDbAdapter.getAllEnabledScheduledActions(); assertThat(scheduledActions) @@ -185,18 +196,14 @@ public void atestCreateExportSchedule(){ .extracting("mActionType").contains(ScheduledAction.ActionType.BACKUP); ScheduledAction action = scheduledActions.get(0); - assertThat(action.getPeriodType()).isEqualTo(PeriodType.DAY); + assertThat(action.getPeriodType()).isEqualTo(PeriodType.WEEK); assertThat(action.getEndTime()).isEqualTo(0); } //todo: add testing of export flag to unit test //todo: add test of ignore exported transactions to unit tests @Override - protected void tearDown() throws Exception { - mSolo.finishOpenedActivities(); - mSolo.waitForEmptyActivityStack(20000); - mSolo.sleep(5000); - mAccountsDbAdapter.deleteAllRecords(); + @After public void tearDown() throws Exception { mDbHelper.close(); mDb.close(); super.tearDown(); diff --git a/app/src/androidTest/java/org/gnucash/android/test/ui/GnucashAndroidTestRunner.java b/app/src/androidTest/java/org/gnucash/android/test/ui/GnucashAndroidTestRunner.java new file mode 100644 index 000000000..3418f8796 --- /dev/null +++ b/app/src/androidTest/java/org/gnucash/android/test/ui/GnucashAndroidTestRunner.java @@ -0,0 +1,111 @@ +/* + * 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.content.pm.PackageManager; +import android.os.Bundle; +import android.os.IBinder; +import android.support.test.runner.AndroidJUnitRunner; +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Custom test runner + */ +public class GnucashAndroidTestRunner extends AndroidJUnitRunner{ + private static final String TAG = "GncAndroidTestRunner"; + private static final String ANIMATION_PERMISSION = "android.permission.SET_ANIMATION_SCALE"; + private static final float DISABLED = 0.0f; + private static final float DEFAULT = 1.0f; + + @Override + public void onCreate(Bundle args) { + super.onCreate(args); + // as time goes on we may actually need to process our arguments. + disableAnimation(); + + } + + @Override + public void onDestroy() { + enableAnimation(); + super.onDestroy(); + } + + private void disableAnimation() { + int permStatus = getContext().checkCallingOrSelfPermission(ANIMATION_PERMISSION); + if (permStatus == PackageManager.PERMISSION_GRANTED) { + if (reflectivelyDisableAnimation(DISABLED)) { + Log.i(TAG, "All animations disabled."); + } else { + Log.i(TAG, "Could not disable animations."); + } + } else { + Log.i(TAG, "Cannot disable animations due to lack of permission."); + } + } + + private void enableAnimation(){ + int permStatus = getContext().checkCallingOrSelfPermission(ANIMATION_PERMISSION); + if (permStatus == PackageManager.PERMISSION_GRANTED) { + if (reflectivelyDisableAnimation(DEFAULT)) { + Log.i(TAG, "All animations enabled."); + } else { + Log.i(TAG, "Could not enable animations."); + } + } else { + Log.i(TAG, "Cannot disable animations due to lack of permission."); + } + } + + private boolean reflectivelyDisableAnimation(float animationScale) { + try { + Class windowManagerStubClazz = Class.forName("android.view.IWindowManager$Stub"); + Method asInterface = windowManagerStubClazz.getDeclaredMethod("asInterface", IBinder.class); + Class serviceManagerClazz = Class.forName("android.os.ServiceManager"); + Method getService = serviceManagerClazz.getDeclaredMethod("getService", String.class); + Class windowManagerClazz = Class.forName("android.view.IWindowManager"); + Method setAnimationScales = windowManagerClazz.getDeclaredMethod("setAnimationScales", + float[].class); + Method getAnimationScales = windowManagerClazz.getDeclaredMethod("getAnimationScales"); + + IBinder windowManagerBinder = (IBinder) getService.invoke(null, "window"); + Object windowManagerObj = asInterface.invoke(null, windowManagerBinder); + float[] currentScales = (float[]) getAnimationScales.invoke(windowManagerObj); + for (int i = 0; i < currentScales.length; i++) { + currentScales[i] = animationScale; + } + setAnimationScales.invoke(windowManagerObj, currentScales); + return true; + } catch (ClassNotFoundException cnfe) { + Log.w(TAG, "Cannot disable animations reflectively.", cnfe); + } catch (NoSuchMethodException mnfe) { + Log.w(TAG, "Cannot disable animations reflectively.", mnfe); + } catch (SecurityException se) { + Log.w(TAG, "Cannot disable animations reflectively.", se); + } catch (InvocationTargetException ite) { + Log.w(TAG, "Cannot disable animations reflectively.", ite); + } catch (IllegalAccessException iae) { + Log.w(TAG, "Cannot disable animations reflectively.", iae); + } catch (RuntimeException re) { + Log.w(TAG, "Cannot disable animations reflectively.", re); + } + return false; + } +} 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 3754382f1..b33684368 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012 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,6 +16,7 @@ package org.gnucash.android.test.ui; +import android.app.Fragment; import android.content.ContentValues; import android.content.Intent; import android.content.SharedPreferences; @@ -23,40 +24,64 @@ import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.preference.PreferenceManager; -import android.support.v4.app.Fragment; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; import android.test.ActivityInstrumentationTestCase2; import android.util.Log; -import android.view.View; -import android.widget.EditText; import android.widget.LinearLayout; import android.widget.Spinner; -import android.widget.TextView; -import android.widget.ToggleButton; -import com.robotium.solo.Solo; + import org.gnucash.android.R; import org.gnucash.android.db.AccountsDbAdapter; import org.gnucash.android.db.DatabaseHelper; import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.db.SplitsDbAdapter; import org.gnucash.android.db.TransactionsDbAdapter; -import org.gnucash.android.model.*; +import org.gnucash.android.model.Account; +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.receivers.TransactionRecorder; 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.TransactionTypeToggleButton; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; import java.math.BigDecimal; import java.text.NumberFormat; -import java.text.ParseException; import java.util.Currency; import java.util.Date; import java.util.List; import java.util.Locale; +import static android.support.test.espresso.Espresso.onData; +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; +import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant; +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.withChild; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withSpinnerText; +import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.assertj.android.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; - +@RunWith(AndroidJUnit4.class) public class TransactionsActivityTest extends ActivityInstrumentationTestCase2 { private static final String TRANSACTION_AMOUNT = "9.99"; @@ -68,7 +93,6 @@ public class TransactionsActivityTest extends private static final String TRANSFER_ACCOUNT_UID = "transfer_account"; public static final String CURRENCY_CODE = "USD"; - private Solo mSolo; private Transaction mTransaction; private long mTransactionTimeMillis; @@ -77,13 +101,20 @@ public class TransactionsActivityTest extends private AccountsDbAdapter mAccountsDbAdapter; private TransactionsDbAdapter mTransactionsDbAdapter; private SplitsDbAdapter mSplitsDbAdapter; - + private TransactionsActivity mTransactionsActivity; + public TransactionsActivityTest() { super(TransactionsActivity.class); } @Override - protected void setUp() throws Exception { + @Before + public void setUp() throws Exception { + super.setUp(); + injectInstrumentation(InstrumentationRegistry.getInstrumentation()); + AccountsActivityTest.preventFirstRunDialogs(getInstrumentation().getTargetContext()); + + mDbHelper = new DatabaseHelper(getInstrumentation().getTargetContext()); try { mDb = mDbHelper.getWritableDatabase(); @@ -95,7 +126,7 @@ protected void setUp() throws Exception { mTransactionsDbAdapter = new TransactionsDbAdapter(mDb, mSplitsDbAdapter); mAccountsDbAdapter = new AccountsDbAdapter(mDb, mTransactionsDbAdapter); - mTransactionTimeMillis = System.currentTimeMillis(); + mTransactionTimeMillis = System.currentTimeMillis(); Account account = new Account(DUMMY_ACCOUNT_NAME); account.setUID(DUMMY_ACCOUNT_UID); account.setCurrency(Currency.getInstance(CURRENCY_CODE)); @@ -124,187 +155,142 @@ protected void setUp() throws Exception { Intent intent = new Intent(Intent.ACTION_VIEW); intent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, DUMMY_ACCOUNT_UID); setActivityIntent(intent); - - mSolo = new Solo(getInstrumentation(), getActivity()); + mTransactionsActivity = getActivity(); } - /** - * Finds a view in the action bar and clicks it, since the native methods are not supported by ActionBarSherlock - * @param id - */ - private void clickSherlockActionBarItem(int id){ - View view = mSolo.getView(id); - mSolo.clickOnView(view); - } private void validateTransactionListDisplayed(){ - Fragment fragment = getActivity().getCurrentPagerFragment(); - assertNotNull(fragment); + onView(withId(R.id.fragment_transaction_list)).check(matches(isDisplayed())); +// Fragment fragment = getActivity().getCurrentPagerFragment(); +// assertThat(fragment).isNotNull(); +// assertThat(fragment).isInstanceOf(TransactionsListFragment.class); } private int getTransactionCount(){ return mTransactionsDbAdapter.getAllTransactionsForAccount(DUMMY_ACCOUNT_UID).size(); } - private void validateNewTransactionFields(){ - String expectedValue = TransactionFormFragment.DATE_FORMATTER.format(new Date(mTransactionTimeMillis)); - TextView dateView = (TextView) mSolo.getView(R.id.input_date); - String actualValue = dateView.getText().toString(); - assertEquals(expectedValue, actualValue); - - expectedValue = TransactionFormFragment.TIME_FORMATTER.format(new Date(mTransactionTimeMillis)); - TextView timeView = (TextView) mSolo.getView(R.id.input_time); - actualValue = timeView.getText().toString(); - assertEquals(expectedValue, actualValue); + private void validateTimeInput(long timeMillis){ + String expectedValue = TransactionFormFragment.DATE_FORMATTER.format(new Date(timeMillis)); + onView(withId(R.id.input_date)).check(matches(withText(expectedValue))); + expectedValue = TransactionFormFragment.TIME_FORMATTER.format(new Date(timeMillis)); + onView(withId(R.id.input_time)).check(matches(withText(expectedValue))); } - + + @Test public void testAddTransactionShouldRequireAmount(){ - mSolo.waitForText(TRANSACTION_NAME); validateTransactionListDisplayed(); int beforeCount = mTransactionsDbAdapter.getTransactionsCount(DUMMY_ACCOUNT_UID); - clickSherlockActionBarItem(R.id.menu_add_transaction); - mSolo.waitForText("Description"); - mSolo.enterText(0, "Lunch"); + onView(withId(R.id.menu_add_transaction)).perform(click()); + + onView(withId(R.id.input_transaction_name)) + .check(matches(isDisplayed())) + .perform(typeText("Lunch")); + + onView(withId(R.id.menu_save)).perform(click()); - clickSherlockActionBarItem(R.id.menu_save); - String toastAmountRequired = getActivity().getString(R.string.toast_transanction_amount_required); - boolean toastFound = mSolo.waitForText(toastAmountRequired); - assertTrue(toastFound); + assertToastDisplayed(R.string.toast_transanction_amount_required); int afterCount = mTransactionsDbAdapter.getTransactionsCount(DUMMY_ACCOUNT_UID); - assertEquals(beforeCount, afterCount); + assertThat(afterCount).isEqualTo(beforeCount); - mSolo.goBack(); } - + + /** + * Checks that a specific toast message is displayed + * @param toastString + */ + private void assertToastDisplayed(int toastString) { + onView(withText(toastString)) + .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView())))) + .check(matches(isDisplayed())); + } + + private void validateEditTransactionFields(Transaction transaction){ - - String name = ((EditText)mSolo.getView(R.id.input_transaction_name)).getText().toString(); - assertEquals(transaction.getDescription(), name); - - EditText amountEdittext = (EditText) mSolo.getView(R.id.input_transaction_amount); - String amountString = amountEdittext.getText().toString(); - NumberFormat formatter = NumberFormat.getInstance(); - try { - amountString = formatter.parse(amountString).toString(); - } catch (ParseException e) { - e.printStackTrace(); - } - Money amount = new Money(amountString, Currency.getInstance(Locale.getDefault()).getCurrencyCode()); - assertEquals(transaction.getBalance(DUMMY_ACCOUNT_UID), amount); - EditText notesEditText = (EditText) mSolo.getView(R.id.input_description); - String transactionNotes = notesEditText.getText().toString(); - assertEquals(transaction.getNote(), transactionNotes); - - String expectedValue = TransactionFormFragment.DATE_FORMATTER.format(transaction.getTimeMillis()); - TextView dateView = (TextView) mSolo.getView(R.id.input_date); - String actualValue = dateView.getText().toString(); //mSolo.getText(6).getText().toString(); - assertEquals(expectedValue, actualValue); - - expectedValue = TransactionFormFragment.TIME_FORMATTER.format(transaction.getTimeMillis()); - TextView timeView = (TextView) mSolo.getView(R.id.input_time); - actualValue = timeView.getText().toString();// mSolo.getText(7).getText().toString(); - assertEquals(expectedValue, actualValue); + onView(withId(R.id.input_transaction_name)).check(matches(withText(transaction.getDescription()))); + + Money balance = transaction.getBalance(DUMMY_ACCOUNT_UID); + NumberFormat formatter = NumberFormat.getInstance(Locale.getDefault()); + formatter.setMinimumFractionDigits(2); + formatter.setMaximumFractionDigits(2); + onView(withId(R.id.input_transaction_amount)).check(matches(withText(formatter.format(balance.asDouble())))); + + onView(withId(R.id.input_description)).check(matches(withText(transaction.getNote()))); + + validateTimeInput(transaction.getTimeMillis()); } //TODO: Add test for only one account but with double-entry enabled - + @Test public void testAddTransaction(){ setDoubleEntryEnabled(true); - mSolo.waitForText(TRANSACTION_NAME); - + setDefaultTransactionType(TransactionType.DEBIT); validateTransactionListDisplayed(); - clickSherlockActionBarItem(R.id.menu_add_transaction); - - mSolo.waitForText("New transaction"); - - //validate creation of transaction - mSolo.enterText(0, "Lunch"); - mSolo.enterText(1, "899"); - mSolo.sleep(2000); - TransactionTypeToggleButton typeToggleButton = (TransactionTypeToggleButton) mSolo.getView(R.id.input_transaction_type); - assertThat(typeToggleButton).isVisible(); - if (!typeToggleButton.isChecked()){ - mSolo.clickOnButton(0); - } - mSolo.sleep(1000); - //check that the amount is correctly converted in the input field - String value = mSolo.getEditText(1).getText().toString(); - String expectedValue = NumberFormat.getInstance().format(-8.99); - assertThat(value).isEqualTo(expectedValue); - int transactionsCount = getTransactionCount(); + onView(withId(R.id.menu_add_transaction)).perform(click()); - mSolo.clickOnActionBarItem(R.id.menu_save); + onView(withId(R.id.input_transaction_name)).perform(typeText("Lunch")); + onView(withId(R.id.input_transaction_amount)).perform(typeText("899")); + onView(withId(R.id.input_transaction_type)) + .check(matches(allOf(isDisplayed(), withText(R.string.label_receive)))) + .perform(click()) + .check(matches(withText(R.string.label_spend))); - mSolo.waitForText(DUMMY_ACCOUNT_NAME); - validateTransactionListDisplayed(); + String expectedValue = NumberFormat.getInstance().format(-8.99); + onView(withId(R.id.input_transaction_amount)).check(matches(withText(expectedValue))); - mSolo.sleep(1000); + int transactionsCount = getTransactionCount(); + onView(withId(R.id.menu_save)).perform(click()); + + validateTransactionListDisplayed(); List transactions = mTransactionsDbAdapter.getAllTransactionsForAccount(DUMMY_ACCOUNT_UID); assertThat(transactions).hasSize(2); Transaction transaction = transactions.get(0); assertThat(transaction.getSplits()).hasSize(2); - Split split = transaction.getSplits(TRANSFER_ACCOUNT_UID).get(0); - //the main account is a CASH account which has debit normal type, so a negative value means actually CREDIT - //so the other side of the split has to be a debit - assertEquals(TransactionType.DEBIT, split.getType()); - assertEquals(transactionsCount + 1, getTransactionCount()); - + assertThat(getTransactionCount()).isEqualTo(transactionsCount + 1); } - public void testEditTransaction(){ - //open transactions - mSolo.waitForText(DUMMY_ACCOUNT_NAME); - + @Test + public void testEditTransaction(){ validateTransactionListDisplayed(); - - mSolo.clickOnText(TRANSACTION_NAME); - mSolo.waitForText("Note"); + + onView(withText(TRANSACTION_NAME)).perform(click()); validateEditTransactionFields(mTransaction); - - mSolo.enterText(0, "Pasta"); - clickSherlockActionBarItem(R.id.menu_save); - //if we see the text, then it was successfully created - mSolo.waitForText("Pasta"); + onView(withId(R.id.input_transaction_name)).perform(clearText(), typeText("Pasta")); + onView(withId(R.id.menu_save)).perform(click()); } /** * Tests that transactions splits are automatically balanced and an imbalance account will be created * This test case assumes that single entry is used */ + @Test public void testAutoBalanceTransactions(){ setDoubleEntryEnabled(false); mTransactionsDbAdapter.deleteAllRecords(); - mSolo.sleep(1000); + assertThat(mTransactionsDbAdapter.getTotalTransactionsCount()).isEqualTo(0); String imbalanceAcctUID = mAccountsDbAdapter.getImbalanceAccountUID(Currency.getInstance(CURRENCY_CODE)); assertThat(imbalanceAcctUID).isNull(); - mSolo.waitForText(TRANSACTION_NAME); - validateTransactionListDisplayed(); - clickSherlockActionBarItem(R.id.menu_add_transaction); - - mSolo.waitForText("New transaction"); - - //validate creation of transaction - mSolo.enterText(0, "Autobalance"); - mSolo.enterText(1, "499"); - - View typeToogleButton = mSolo.getView(R.id.btn_open_splits); - assertThat(typeToogleButton).isNotVisible(); //no double entry so no split editor + onView(withId(R.id.menu_add_transaction)).perform(click()); + onView(withId(R.id.fragment_transaction_form)).check(matches(isDisplayed())); - mSolo.clickOnActionBarItem(R.id.menu_save); + onView(withId(R.id.input_transaction_name)).perform(typeText("Autobalance")); + onView(withId(R.id.input_transaction_amount)).perform(typeText("499")); - mSolo.sleep(2000); + //no double entry so no split editor + onView(withId(R.id.btn_open_splits)).check(matches(not(isDisplayed()))); + onView(withId(R.id.menu_save)).perform(click()); assertThat(mTransactionsDbAdapter.getTotalTransactionsCount()).isEqualTo(1); Transaction transaction = mTransactionsDbAdapter.getAllTransactions().get(0); @@ -322,52 +308,38 @@ 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 */ + @Test public void testSplitEditor(){ setDoubleEntryEnabled(true); + setDefaultTransactionType(TransactionType.DEBIT); mTransactionsDbAdapter.deleteAllRecords(); - mSolo.sleep(1000); + //when we start there should be no imbalance account in the system String imbalanceAcctUID = mAccountsDbAdapter.getImbalanceAccountUID(Currency.getInstance(CURRENCY_CODE)); assertThat(imbalanceAcctUID).isNull(); - mSolo.waitForText(TRANSACTION_NAME); - validateTransactionListDisplayed(); - clickSherlockActionBarItem(R.id.menu_add_transaction); - - mSolo.waitForText("New transaction"); + onView(withId(R.id.menu_add_transaction)).perform(click()); - //validate creation of transaction - mSolo.enterText(0, "Autobalance"); - mSolo.enterText(1, "4499"); + onView(withId(R.id.input_transaction_name)).perform(typeText("Autobalance")); + onView(withId(R.id.input_transaction_amount)).perform(typeText("499")); - mSolo.clickOnButton(1); - mSolo.waitForDialogToOpen(); + onView(withId(R.id.btn_open_splits)).perform(click()); - LinearLayout splitListView = (LinearLayout) mSolo.getView(R.id.split_list_layout); - assertThat(splitListView).hasChildCount(1); + 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 - //assertThat(mSolo.getEditText(0).getText().toString()).isEqualTo("44.99"); - View addSplit = mSolo.getView(R.id.btn_add_split); - mSolo.clickOnView(addSplit); - mSolo.sleep(5000); - assertThat(splitListView).hasChildCount(2); - mSolo.enterText(0, "4000"); + onView(withId(R.id.btn_add_split)).perform(click()); - TextView imbalanceTextView = (TextView) mSolo.getView(R.id.imbalance_textview); - assertThat(imbalanceTextView).hasText("-4.99 $"); + onView(allOf(withId(R.id.input_split_amount), withText(""))).perform(typeText("400")); + onView(withId(R.id.imbalance_textview)).check(matches(withText("-0.99 $"))); - mSolo.clickOnView(mSolo.getView(R.id.btn_save)); - mSolo.waitForDialogToClose(); - mSolo.sleep(3000); + onView(withId(R.id.btn_save)).perform(click()); //after we use split editor, we should not be able to toggle the transaction type - assertThat(mSolo.getView(R.id.input_transaction_type)).isNotVisible(); + onView(withId(R.id.input_transaction_type)).check(matches(not(isDisplayed()))); - mSolo.clickOnActionBarItem(R.id.menu_save); - - mSolo.sleep(3000); + onView(withId(R.id.menu_save)).perform(click()); List transactions = mTransactionsDbAdapter.getAllTransactions(); assertThat(transactions).hasSize(1); @@ -387,9 +359,8 @@ public void testSplitEditor(){ assertThat(imbalanceSplits).hasSize(1); Split split = imbalanceSplits.get(0); - assertThat(split.getAmount().toPlainString()).isEqualTo("4.99"); + assertThat(split.getAmount().toPlainString()).isEqualTo("0.99"); assertThat(split.getType()).isEqualTo(TransactionType.CREDIT); - } @@ -400,37 +371,35 @@ private void setDoubleEntryEnabled(boolean enabled){ editor.commit(); } + @Test public void testDefaultTransactionType(){ - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); - Editor editor = prefs.edit(); - editor.putString(getActivity().getString(R.string.key_default_transaction_type), "CREDIT"); - editor.commit(); + setDefaultTransactionType(TransactionType.CREDIT); - clickSherlockActionBarItem(R.id.menu_add_transaction); - mSolo.waitForText(getActivity().getString(R.string.label_transaction_name)); - - ToggleButton transactionTypeButton = (ToggleButton) mSolo.getButton(0); - assertThat(transactionTypeButton).isChecked(); + onView(withId(R.id.menu_add_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()); + + //now validate the other case - clickSherlockActionBarItem(R.id.menu_cancel); + setDefaultTransactionType(TransactionType.DEBIT); - //now validate the other case - editor = prefs.edit(); - editor.putString(getActivity().getString(R.string.key_default_transaction_type), "DEBIT"); + onView(withId(R.id.menu_add_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()); + } + + private void setDefaultTransactionType(TransactionType type) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); + Editor editor = prefs.edit(); + editor.putString(getActivity().getString(R.string.key_default_transaction_type), type.name()); editor.commit(); - - clickSherlockActionBarItem(R.id.menu_add_transaction); - mSolo.waitForText(getActivity().getString(R.string.label_transaction_name)); - - transactionTypeButton = (ToggleButton) mSolo.getButton(0); - assertThat(transactionTypeButton).isNotChecked(); - clickSherlockActionBarItem(R.id.menu_cancel); - mSolo.goBack(); } - public void testChildAccountsShouldUseParentTransferAccountSetting(){ + //FIXME: Improve on this test + public void childAccountsShouldUseParentTransferAccountSetting(){ Account transferAccount = new Account("New Transfer Acct"); mAccountsDbAdapter.addAccount(transferAccount); + mAccountsDbAdapter.addAccount(new Account("Higher account")); Account childAccount = new Account("Child Account"); childAccount.setParentUID(DUMMY_ACCOUNT_UID); @@ -439,43 +408,35 @@ public void testChildAccountsShouldUseParentTransferAccountSetting(){ contentValues.put(DatabaseSchema.AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID, transferAccount.getUID()); mAccountsDbAdapter.updateRecord(DUMMY_ACCOUNT_UID, contentValues); - - Intent intent = new Intent(mSolo.getCurrentActivity(), TransactionsActivity.class); + Intent intent = new Intent(mTransactionsActivity, TransactionsActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setAction(Intent.ACTION_INSERT_OR_EDIT); intent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, childAccount.getUID()); - getActivity().startActivity(intent); - mSolo.waitForActivity(TransactionsActivity.class); - mSolo.sleep(3000); - Spinner spinner = (Spinner) mSolo.getView(R.id.input_double_entry_accounts_spinner); - long transferAccountID = mAccountsDbAdapter.getID(transferAccount.getUID()); - assertThat(transferAccountID).isEqualTo(spinner.getSelectedItemId()); + + mTransactionsActivity.startActivity(intent); + + onView(withId(R.id.input_transaction_amount)).perform(typeText("1299")); + clickOnView(R.id.menu_save); + + //if our transfer account has a transaction then the right transfer account was used + List transactions = mTransactionsDbAdapter.getAllTransactionsForAccount(transferAccount.getUID()); + assertThat(transactions).hasSize(1); } + @Test public void testToggleTransactionType(){ - mSolo.waitForText(DUMMY_ACCOUNT_NAME); - validateTransactionListDisplayed(); - mSolo.clickOnText(TRANSACTION_NAME); - mSolo.waitForText(getActivity().getString(R.string.title_edit_transaction)); - - validateEditTransactionFields(mTransaction); + onView(withText(TRANSACTION_NAME)).perform(click()); - TransactionTypeToggleButton toggleButton = (TransactionTypeToggleButton) mSolo.getView(R.id.input_transaction_type); - assertThat(toggleButton).isNotNull(); - assertThat(toggleButton).isVisible(); - assertThat(toggleButton).hasText(R.string.label_receive); + validateEditTransactionFields(mTransaction); - mSolo.clickOnView(toggleButton); - mSolo.sleep(2000); + onView(withId(R.id.input_transaction_type)).check(matches( + allOf(isDisplayed(), withText(R.string.label_receive)) + )).perform(click()).check(matches(withText(R.string.label_spend))); - assertThat(toggleButton).hasText(R.string.label_spend); - EditText amountView = (EditText) mSolo.getView(R.id.input_transaction_amount); - String amountString = amountView.getText().toString(); - assertThat(amountString).startsWith("-"); - assertThat("-9.99").isEqualTo(amountString); + onView(withId(R.id.input_transaction_amount)).check(matches(withText("-9.99"))); - mSolo.clickOnActionBarItem(R.id.menu_save); - mSolo.waitForText(DUMMY_ACCOUNT_NAME); + onView(withId(R.id.menu_save)).perform(click()); List transactions = mTransactionsDbAdapter.getAllTransactionsForAccount(DUMMY_ACCOUNT_UID); assertThat(transactions).hasSize(1); @@ -483,46 +444,40 @@ public void testToggleTransactionType(){ assertThat(trx.getSplits()).hasSize(2); //auto-balancing of splits assertTrue(trx.getBalance(DUMMY_ACCOUNT_UID).isNegative()); } - + + @Test public void testOpenTransactionEditShouldNotModifyTransaction(){ - mSolo.waitForText(DUMMY_ACCOUNT_NAME); - - validateTransactionListDisplayed(); - - mSolo.clickOnText(TRANSACTION_NAME); - mSolo.waitForText("Edit transaction"); - - validateNewTransactionFields(); - - clickSherlockActionBarItem(R.id.menu_save); - - mSolo.waitForText(DUMMY_ACCOUNT_NAME); - - List transactions = mTransactionsDbAdapter.getAllTransactionsForAccount(DUMMY_ACCOUNT_UID); - - assertEquals(1, transactions.size()); - Transaction trx = transactions.get(0); - assertEquals(TRANSACTION_NAME, trx.getDescription()); - Date expectedDate = new Date(mTransactionTimeMillis); - Date trxDate = new Date(trx.getTimeMillis()); - assertEquals(TransactionFormFragment.DATE_FORMATTER.format(expectedDate), - TransactionFormFragment.DATE_FORMATTER.format(trxDate)); - assertEquals(TransactionFormFragment.TIME_FORMATTER.format(expectedDate), - TransactionFormFragment.TIME_FORMATTER.format(trxDate)); - } + validateTransactionListDisplayed(); - public void testDeleteTransaction(){ - mSolo.waitForText(DUMMY_ACCOUNT_NAME); - - mSolo.clickOnCheckBox(0); - clickSherlockActionBarItem(R.id.context_menu_delete); + onView(withText(TRANSACTION_NAME)).perform(click()); - mSolo.sleep(500); + validateTimeInput(mTransactionTimeMillis); + + clickOnView(R.id.menu_save); + + List transactions = mTransactionsDbAdapter.getAllTransactionsForAccount(DUMMY_ACCOUNT_UID); + + assertThat(transactions).hasSize(1); + Transaction trx = transactions.get(0); + assertEquals(TRANSACTION_NAME, trx.getDescription()); + Date expectedDate = new Date(mTransactionTimeMillis); + Date trxDate = new Date(trx.getTimeMillis()); + assertEquals(TransactionFormFragment.DATE_FORMATTER.format(expectedDate), + TransactionFormFragment.DATE_FORMATTER.format(trxDate)); + assertEquals(TransactionFormFragment.TIME_FORMATTER.format(expectedDate), + TransactionFormFragment.TIME_FORMATTER.format(trxDate)); + } + + @Test + public void testDeleteTransaction(){ + onView(withId(R.id.primary_text)).perform(longClick()); + clickOnView(R.id.context_menu_delete); long id = mAccountsDbAdapter.getID(DUMMY_ACCOUNT_UID); assertEquals(0, mTransactionsDbAdapter.getTransactionsCount(id)); } - + + @Test public void testBulkMoveTransactions(){ String targetAccountName = "Target"; Account account = new Account(targetAccountName); @@ -531,34 +486,22 @@ public void testBulkMoveTransactions(){ int beforeOriginCount = mAccountsDbAdapter.getAccount(DUMMY_ACCOUNT_UID).getTransactionCount(); - mSolo.waitForText(DUMMY_ACCOUNT_NAME); - validateTransactionListDisplayed(); - - mSolo.clickOnCheckBox(0); - mSolo.waitForText(getActivity().getString(R.string.title_selected, 1)); - //initiate bulk move - clickSherlockActionBarItem(R.id.context_menu_move_transactions); - - mSolo.waitForDialogToClose(); - - Spinner spinner = mSolo.getCurrentViews(Spinner.class).get(0); - mSolo.clickOnView(spinner); - mSolo.sleep(500); - mSolo.clickOnText(targetAccountName); - mSolo.clickOnButton(1); -// mSolo.clickOnText(getActivity().getString(R.string.btn_move)); - - mSolo.waitForDialogToClose(); - + + clickOnView(R.id.checkbox_transaction); + clickOnView(R.id.context_menu_move_transactions); + + clickOnView(R.id.btn_save); + int targetCount = mAccountsDbAdapter.getAccount(account.getUID()).getTransactionCount(); - assertEquals(1, targetCount); + assertThat(targetCount).isEqualTo(1); int afterOriginCount = mAccountsDbAdapter.getAccount(DUMMY_ACCOUNT_UID).getTransactionCount(); - assertEquals(beforeOriginCount-1, afterOriginCount); + assertThat(afterOriginCount).isEqualTo(beforeOriginCount-1); } //TODO: add normal transaction recording + @Test public void testLegacyIntentTransactionRecording(){ int beforeCount = mTransactionsDbAdapter.getTransactionsCount(DUMMY_ACCOUNT_UID); Intent transactionIntent = new Intent(Intent.ACTION_INSERT); @@ -569,9 +512,7 @@ public void testLegacyIntentTransactionRecording(){ transactionIntent.putExtra(Transaction.EXTRA_ACCOUNT_UID, DUMMY_ACCOUNT_UID); transactionIntent.putExtra(Transaction.EXTRA_TRANSACTION_TYPE, TransactionType.DEBIT.name()); - getActivity().sendBroadcast(transactionIntent); - - mSolo.sleep(2000); + new TransactionRecorder().onReceive(mTransactionsActivity, transactionIntent); int afterCount = mTransactionsDbAdapter.getTransactionsCount(DUMMY_ACCOUNT_UID); @@ -587,11 +528,19 @@ public void testLegacyIntentTransactionRecording(){ } } + /** + * Simple wrapper for clicking on views with espresso + * @param viewId View resource ID + */ + private void clickOnView(int viewId){ + onView(withId(viewId)).perform(click()); + } + @Override - protected void tearDown() throws Exception { - mSolo.finishOpenedActivities(); - mSolo.waitForEmptyActivityStack(20000); - mSolo.sleep(5000); + @After + public void tearDown() throws Exception { + mTransactionsActivity.finish(); + Thread.sleep(1000); mAccountsDbAdapter.deleteAllRecords(); super.tearDown(); } diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..33c406435 --- /dev/null +++ b/app/src/debug/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file 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 f10c315da..e63ccb95d 100644 --- a/app/src/main/java/org/gnucash/android/db/DatabaseAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/DatabaseAdapter.java @@ -42,7 +42,7 @@ public abstract class DatabaseAdapter { /** * Tag for logging */ - protected static String LOG_TAG = "DatabaseAdapter"; + protected String LOG_TAG = "DatabaseAdapter"; /** * SQLite database 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 412f04866..a46486730 100644 --- a/app/src/main/java/org/gnucash/android/db/DatabaseHelper.java +++ b/app/src/main/java/org/gnucash/android/db/DatabaseHelper.java @@ -26,16 +26,15 @@ import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; +import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.export.Exporter; -import org.gnucash.android.model.Account; import org.gnucash.android.model.AccountType; 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 java.io.File; +import java.math.BigDecimal; import java.sql.Timestamp; import static org.gnucash.android.db.DatabaseSchema.AccountEntry; @@ -439,36 +438,84 @@ private int upgradeDbToVersion8(SQLiteDatabase db) { //================================ 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); + //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 (top-level accounts) - String rootAccountUID = accountsDbAdapter.getOrCreateGnuCashRootAccountUID(); + //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", null); + 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 cursor = db.query(TransactionEntry.TABLE_NAME + "_bak", null, "recurrence_period > 0", null, null, null, null); + 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 timestamp = new Timestamp(cursor.getLong(cursor.getColumnIndexOrThrow(TransactionEntry.COLUMN_TIMESTAMP))); - contentValues.put(TransactionEntry.COLUMN_CREATED_AT, timestamp.toString()); + 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(timestamp.getTime()); //the start time is when the transaction was created - scheduledAction.setLastRun(System.currentTimeMillis()); //prevent this from being executed at the end of migration - scheduledActionDbAdapter.addScheduledAction(scheduledAction); + //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); @@ -485,20 +532,95 @@ private int upgradeDbToVersion8(SQLiteDatabase db) { //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 = 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); + 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(); } - cursor.close(); Log.i(LOG_TAG, "Dropping temporary migration tables"); db.execSQL("DROP TABLE " + SplitEntry.TABLE_NAME + "_bak"); 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 60cabf63b..5b90ebca2 100644 --- a/app/src/main/java/org/gnucash/android/db/MigrationHelper.java +++ b/app/src/main/java/org/gnucash/android/db/MigrationHelper.java @@ -33,6 +33,7 @@ import java.io.IOError; import java.io.IOException; import java.nio.channels.FileChannel; +import java.util.UUID; import static org.gnucash.android.db.DatabaseSchema.AccountEntry; @@ -187,4 +188,9 @@ public void run() { oldExportFolder.delete(); } }; + + public static String generateUUID() + { + return UUID.randomUUID().toString().replaceAll("-", ""); + } } 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 81aedaede..af7381a8d 100644 --- a/app/src/main/java/org/gnucash/android/db/TransactionsDbAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/TransactionsDbAdapter.java @@ -301,7 +301,7 @@ public Cursor fetchAllTransactionsForAccount(long accountID){ */ public List getAllTransactionsForAccount(String accountUID){ Cursor c = fetchAllTransactionsForAccount(accountUID); - ArrayList transactionsList = new ArrayList(); + ArrayList transactionsList = new ArrayList<>(); try { while (c.moveToNext()) { transactionsList.add(buildTransactionInstance(c)); 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 466e6d75b..31494c5fe 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 @@ -241,7 +241,7 @@ public void onCreate(Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_new_account, container, false); getSherlockActivity().getSupportActionBar().setTitle(R.string.title_add_account); mCurrencySpinner = (Spinner) view.findViewById(R.id.input_currency_spinner); - mNameEditText = (EditText) view.findViewById(R.id.edit_text_account_name); + mNameEditText = (EditText) view.findViewById(R.id.input_account_name); //mNameEditText.requestFocus(); mAccountTypeSpinner = (Spinner) view.findViewById(R.id.input_account_type_spinner); 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 3f36d318b..015cdaa5b 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 @@ -29,7 +29,6 @@ import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; import android.support.v4.widget.SimpleCursorAdapter; -import android.text.format.DateFormat; import android.text.format.DateUtils; import android.util.Log; import android.util.SparseBooleanArray; @@ -218,7 +217,7 @@ public void onResume() { 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_parent_account); + CheckBox checkbox = (CheckBox) v.findViewById(R.id.checkbox_transaction); checkbox.setChecked(!checkbox.isChecked()); return; } @@ -228,7 +227,7 @@ public void onListItemClick(ListView l, View v, int position, long 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_parent_account); + CheckBox checkbox = (CheckBox) view.findViewById(R.id.checkbox_transaction); checkbox.setChecked(true); startActionMode(); return true; @@ -365,7 +364,7 @@ public TransactionsCursorAdapter(Context context, int layout, Cursor c, 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_parent_account); + 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 diff --git a/app/src/main/res/layout/fragment_accounts_list.xml b/app/src/main/res/layout/fragment_accounts_list.xml index 6fe184ec4..05c73ab92 100644 --- a/app/src/main/res/layout/fragment_accounts_list.xml +++ b/app/src/main/res/layout/fragment_accounts_list.xml @@ -16,6 +16,7 @@ --> diff --git a/app/src/main/res/layout/fragment_new_account.xml b/app/src/main/res/layout/fragment_new_account.xml index ca13889fc..9de2f9239 100644 --- a/app/src/main/res/layout/fragment_new_account.xml +++ b/app/src/main/res/layout/fragment_new_account.xml @@ -18,14 +18,14 @@ - - diff --git a/app/src/main/res/layout/list_item_transaction.xml b/app/src/main/res/layout/list_item_transaction.xml index 0c9446948..d21cbc6cf 100644 --- a/app/src/main/res/layout/list_item_transaction.xml +++ b/app/src/main/res/layout/list_item_transaction.xml @@ -37,7 +37,7 @@ android:orientation="horizontal"> + 创建科目 修改科目 @@ -28,6 +29,7 @@ 输入密码 密码错误,请再试一次 密码已设置 + 请再输入一次 密码不一致,请重试 描述 金额 @@ -79,8 +81,8 @@ 允许创建科目 GnuCash的数据 读取并修改GnuCash数据 - Record transactions in GnuCash - Create accounts in GnuCash + 记录交易 + 创建科目 显示科目 创建科目 选择要创建的科目 @@ -228,8 +230,8 @@ Sucre Sudanese Pound Surinam Dollar - Swedish Krona - Swiss Franc + 瑞典克朗 + 瑞士法郎 Syrian Pound Taka Tala @@ -265,7 +267,7 @@ 版本号 授权许可 Apache License v2.0,点击查看明细(将打开网页)。 - 常规 + 通用 选择科目 没有需要导出的交易 密码 @@ -306,15 +308,16 @@ 当导出数据到GnuCash桌面版以外的程序时需要开启这个选项。 新功能 - - Visual reports (Bar/Pie/line charts) \n - - Scheduled backups of (QIF, OFX, and XML)\n - - Backup/Export to DropBox and Google Drive\n - - Better recurrence options for scheduled transactions\n - - Navigation drawer for easier access to options\n - - Multiple bug fixes and improvements\n - + - 图表报表 (饼图、柱状图、折线图) \n + - 定时备份 (QIF, OFX, and XML)\n + - 导出到DropBox或者Google Drive\n + - 定期交易的设置调整\n + - 右划的导航菜单\n + - 一些性能上的调整和错误修复\n + 知道了 输入金额才能保存交易 + 不支持修改多币种的交易 导入GnuCash科目 导入科目 导入 GnuCash 科目中发生错误。 @@ -384,7 +387,7 @@ 所有 创建通用的科目结构 创建默认科目 - All existing accounts and transactions on the device will be deleted.\n\nAre you sure you want to proceed? + 将会删除所有科目和交易信息。\n\n确定继续? 计划的交易 欢迎使用GnuCash Android! \n你可以选择:1)创建常用的科目结构,2)导入自定义的科目结构。\或者稍后再决定,两种选择也能在设置中找到。 @@ -413,92 +416,90 @@ 当删除所有交易后,还保持曾经的账户余额作为新的期初余额。 保存账户的期初余额 OFX 格式不支持复式簿记 - Generates separate QIF files per currency + 每种货币都会生成一个QIF文件 拆分交易 - Imbalance: + 不平衡的: 添加一行 收藏 Navigation drawer opened Navigation drawer closed 报表 饼图 - Line Chart - Bar Chart + 折线图 + 柱状图 + 报表设置 + 报表使用的货币 + 用不同颜色区分科目 + 在饼图中使用科目的颜色 + 报表 按数量排序 显示图例 显示标签 - Toggle percentage - Toggle average lines - Group Smaller Slices + 显示为百分比 + 显示平均值 + 合并太小的数据 没有数据可显示 - 概要 + 全部 总计 - Other + 其他 The percentage of selected value calculated from the total amount The percentage of selected value calculated from the current stacked bar amount 保存成模板 科目中包含交易信息。 \n如何处理交易信息 还存在子科目。 \n如何处理这些子科目 删除交易 - Create and specify a transfer account OR disable double-entry in settings to save the transaction + 请选择一个转账科目,否则在“设置”中关闭双航模式(复式簿记) 点击然后创建计划交易 - Restore Backup… - Backup & export + 从备份恢复… + 备份和导出 DropBox Sync - Backup - Enable to sync to DropBox + 备份 + 同步到DropBox Select GnuCash XML file - Backup Preferences - Create Backup - By default backups are saved to the SDCARD - Select a specific backup to restore - Backup successful - Backup failed - Exports all accounts and transactions + 备份设置 + 创建备份 + 备份会保存到SD卡上 + 选择恢复到哪一个备份 + 备份成功 + 备份失败 + 导出所有科目和交易 Google Drive Sync - Enable to sync to Google Drive - Install a file manager to select files - Select backup to restore - Report Preferences - Favorites - Open... - Reports - Scheduled Transactions - Export... - Settings + 同步到Google Drive + 你需要安装一个文件管理器 + 选择备份 + 收藏 + 打开... + 报表 + 计划交易 + 导出... + 设置 - Daily - Every %d days + 每天 + 每 %d 天 - Weekly - Every %d weeks + 每周 + 每 %d 周 - Monthly - Every %d months + 每月 + 每 %d 月 - Yearly - Every %d years + 每年 + 每 %d 年 - Multi-currency transactions cannot be modified - Enable Crash Logging + 启用崩溃日志 Enable to send information about malfunctions to the developers for improvement (recommended). No user-identifiable information will be collected as part of this process! - Export Format - Backup folder cannot be found. Make sure the SD Card is mounted! - Reports - Select currency - Account color in reports - Use account color in the bar/pie chart - 请再输入一次 - Enter your new passcode - Enter your old passcode - Scheduled Exports - Scheduled Exports - No scheduled exports to display - Create export schedule - Exported to: %1$s + 导出格式 + 找不到备份目录,请确认SD卡正常。 + 先输入现在的密码 + 请输入新密码 + 定时备份 + 定时备份 + 没有备份任务 + 创建定时备份 + 导出到:%1$s