diff --git a/.gitignore b/.gitignore index bd87d9b4f..ee987b848 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ out/ /captures gradle.properties +crowdin.properties #Crashlytics diff --git a/.travis.yml b/.travis.yml index 9dcd1ce95..94dc52c0a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,17 +5,17 @@ android: - platform-tools - tools - tools #not a typo. Needed for SDK update - - build-tools-27.0.3 + - build-tools-29.0.0 # The SDK version used to compile your project - - android-27 + - android-29 # Additional components - extra-android-support - extra-google-google_play_services - extra-google-m2repository - extra-android-m2repository - - addon-google_apis-google-26 + - addon-google_apis-google-29 # Specify at least one system image, # if you need to run emulator(s) during your tests @@ -23,7 +23,7 @@ android: # XXX: Temporary workaround. Remove once fixed before_install: - - yes | sdkmanager "platforms;android-27" + - yes | sdkmanager "platforms;android-29" # Emulator Management: Create, Start and Wait # Re-enable this when we figure out how to reliably build on Travis diff --git a/CHANGELOG.md b/CHANGELOG.md index 17a7f8258..97d30e73f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ Change Log =============================================================================== +Version 2.4.1 *(2019-11-30)* +---------------------------- +* Fixes #809: Crash when exporting CSV +* Fixes #811: Add button in schedule action disappears after rotation +* Fixes #790: Missing transaction description margin +* Fixes crash if during database upgrade triggered by scheduled action +* Improve performance of CSV export +* Improved: Add book name to accounts listing view +* Improved: Update translations + Version 2.4.0 *(2018-06-15)* ---------------------------- * Feature #665: Adds CSV export format diff --git a/app/build.gradle b/app/build.gradle index 674ca8088..b4483fbfe 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,8 +5,8 @@ apply plugin: 'io.fabric' def versionMajor = 2 def versionMinor = 4 -def versionPatch = 0 -def versionBuild = 3 +def versionPatch = 1 +def versionBuild = 4 static def buildTime() { def df = new SimpleDateFormat("yyyyMMdd HH:mm 'UTC'") @@ -20,13 +20,13 @@ static def gitSha() { android { - compileSdkVersion 27 - buildToolsVersion '27.0.3' + compileSdkVersion 29 + buildToolsVersion '29.0.0' defaultConfig { applicationId "org.gnucash.android" testApplicationId 'org.gnucash.android.test' minSdkVersion 19 - targetSdkVersion 27 + targetSdkVersion 29 versionCode versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild versionName "${versionMajor}.${versionMinor}.${versionPatch}" resValue "string", "app_version_name", "${versionName}" @@ -142,11 +142,12 @@ android { compileOptions { //we want switch with strings during xml parsing encoding "UTF-8" - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } testOptions { + animationsDisabled true unitTests { includeAndroidResources = true } @@ -179,7 +180,7 @@ afterEvaluate { } -def androidSupportVersion = "27.0.2" +def androidSupportVersion = "28.0.0" def androidEspressoVersion = "3.0.0" def androidSupportTestVersion = "1.0.0" @@ -223,19 +224,19 @@ dependencies { exclude module: 'httpclient' } - implementation('com.crashlytics.sdk.android:crashlytics:2.6.7@aar') { - transitive = true; + implementation('com.crashlytics.sdk.android:crashlytics:2.10.1@aar') { + transitive = true } - testImplementation 'org.robolectric:robolectric:3.5.1' + testImplementation 'org.robolectric:robolectric:4.3.1' testImplementation( 'junit:junit:4.12', 'joda-time:joda-time:2.9.4', - 'org.assertj:assertj-core:1.7.1' + 'org.assertj:assertj-core:3.14.0' ) - testImplementation 'org.robolectric:shadows-multidex:3.0' + testImplementation 'org.robolectric:shadows-multidex:4.3.1' androidTestImplementation ( 'com.android.support:support-annotations:' + androidSupportVersion, 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 eb7f43c13..54afddccd 100644 --- a/app/src/androidTest/java/org/gnucash/android/test/ui/AccountsActivityTest.java +++ b/app/src/androidTest/java/org/gnucash/android/test/ui/AccountsActivityTest.java @@ -17,7 +17,6 @@ package org.gnucash.android.test.ui; import android.Manifest; -import android.content.Context; import android.content.Intent; import android.content.SharedPreferences.Editor; import android.database.SQLException; @@ -32,8 +31,6 @@ import android.util.Log; import android.view.View; -import com.kobakei.ratethisapp.RateThisApp; - import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.db.DatabaseHelper; @@ -51,6 +48,7 @@ import org.gnucash.android.model.Transaction; import org.gnucash.android.receivers.AccountCreator; import org.gnucash.android.test.ui.util.DisableAnimationsRule; +import org.gnucash.android.test.ui.util.GnucashAndroidTestRunner; import org.gnucash.android.ui.account.AccountsActivity; import org.gnucash.android.ui.account.AccountsListFragment; import org.hamcrest.Description; @@ -131,7 +129,7 @@ public AccountsActivityTest() { @BeforeClass public static void prepTest(){ - preventFirstRunDialogs(GnuCashApplication.getAppContext()); + GnucashAndroidTestRunner.preventFirstRunDialogs(GnuCashApplication.getAppContext()); String activeBookUID = BooksDbAdapter.getInstance().getActiveBookUID(); mDbHelper = new DatabaseHelper(GnuCashApplication.getAppContext(), activeBookUID); @@ -163,26 +161,6 @@ public void setUp() throws Exception { } - /** - * 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) { - AccountsActivity.rateAppConfig = new RateThisApp.Config(10000, 10000); - Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); - - //do not show first run dialog - editor.putBoolean(context.getString(R.string.key_first_run), false); - editor.putInt(AccountsActivity.LAST_OPEN_TAB_INDEX, AccountsActivity.INDEX_TOP_LEVEL_ACCOUNTS_FRAGMENT); - - //do not show "What's new" dialog - String minorVersion = context.getString(R.string.app_minor_version); - int currentMinor = Integer.parseInt(minorVersion); - editor.putInt(context.getString(R.string.key_previous_minor_version), currentMinor); - editor.commit(); - } - - public void testDisplayAccountsList(){ AccountsActivity.createDefaultAccounts("EUR", mAccountsActivity); mAccountsActivity.recreate(); diff --git a/app/src/androidTest/java/org/gnucash/android/test/ui/CalculatorEditTextTest.java b/app/src/androidTest/java/org/gnucash/android/test/ui/CalculatorEditTextTest.java index f1932b732..6e8b807c4 100644 --- a/app/src/androidTest/java/org/gnucash/android/test/ui/CalculatorEditTextTest.java +++ b/app/src/androidTest/java/org/gnucash/android/test/ui/CalculatorEditTextTest.java @@ -36,6 +36,7 @@ import org.gnucash.android.model.Account; import org.gnucash.android.model.Commodity; import org.gnucash.android.test.ui.util.DisableAnimationsRule; +import org.gnucash.android.test.ui.util.GnucashAndroidTestRunner; import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.transaction.TransactionsActivity; import org.junit.After; @@ -105,7 +106,7 @@ public static void prepTestCase(){ mTransactionsDbAdapter = TransactionsDbAdapter.getInstance(); mAccountsDbAdapter = AccountsDbAdapter.getInstance(); - AccountsActivityTest.preventFirstRunDialogs(GnuCashApplication.getAppContext()); + GnucashAndroidTestRunner.preventFirstRunDialogs(GnuCashApplication.getAppContext()); } @Before 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 119e4f033..2e8d511b0 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 @@ -17,23 +17,13 @@ package org.gnucash.android.test.ui; import android.Manifest; -import android.app.AlertDialog; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.os.Build; -import android.support.test.InstrumentationRegistry; -import android.support.test.espresso.Espresso; import android.support.test.espresso.contrib.DrawerActions; -import android.support.test.espresso.matcher.ViewMatchers; +import android.support.test.rule.ActivityTestRule; import android.support.test.rule.GrantPermissionRule; import android.support.test.runner.AndroidJUnit4; -import android.support.v7.preference.PreferenceManager; -import android.test.ActivityInstrumentationTestCase2; import android.util.Log; -import android.widget.CompoundButton; import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; @@ -42,52 +32,38 @@ import org.gnucash.android.db.adapter.BooksDbAdapter; import org.gnucash.android.db.adapter.CommoditiesDbAdapter; import org.gnucash.android.db.adapter.DatabaseAdapter; -import org.gnucash.android.db.adapter.ScheduledActionDbAdapter; import org.gnucash.android.db.adapter.SplitsDbAdapter; import org.gnucash.android.db.adapter.TransactionsDbAdapter; -import org.gnucash.android.export.ExportFormat; -import org.gnucash.android.export.Exporter; import org.gnucash.android.model.Account; import org.gnucash.android.model.Commodity; import org.gnucash.android.model.Money; -import org.gnucash.android.model.PeriodType; -import org.gnucash.android.model.ScheduledAction; import org.gnucash.android.model.Split; import org.gnucash.android.model.Transaction; +import org.gnucash.android.test.ui.util.GnucashAndroidTestRunner; import org.gnucash.android.ui.account.AccountsActivity; -import org.gnucash.android.ui.settings.PreferenceActivity; -import org.gnucash.android.util.BookUtils; import org.junit.After; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.FixMethodOrder; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.MethodSorters; -import java.io.File; -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.action.ViewActions.swipeUp; 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.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; -import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @RunWith(AndroidJUnit4.class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class ExportTransactionsTest extends - ActivityInstrumentationTestCase2 { +public class ExportTransactionsTest { private DatabaseHelper mDbHelper; private SQLiteDatabase mDb; @@ -99,20 +75,23 @@ public class ExportTransactionsTest extends @Rule public GrantPermissionRule animationPermissionsRule = GrantPermissionRule.grant(Manifest.permission.SET_ANIMATION_SCALE); + @Rule + public ActivityTestRule mActivityRule = new ActivityTestRule<>(AccountsActivity.class); + public ExportTransactionsTest() { - super(AccountsActivity.class); } - @Override + @BeforeClass + public static void setupClass(){ + GnucashAndroidTestRunner.preventFirstRunDialogs(GnuCashApplication.getAppContext()); + } + @Before - public void setUp() throws Exception { - super.setUp(); - injectInstrumentation(InstrumentationRegistry.getInstrumentation()); - AccountsActivityTest.preventFirstRunDialogs(getInstrumentation().getTargetContext()); - mAcccountsActivity = getActivity(); + public void setUp() { + mAcccountsActivity = mActivityRule.getActivity(); String activeBookUID = BooksDbAdapter.getInstance().getActiveBookUID(); - mDbHelper = new DatabaseHelper(getActivity(), activeBookUID); + mDbHelper = new DatabaseHelper(mAcccountsActivity.getApplicationContext(), activeBookUID); try { mDb = mDbHelper.getWritableDatabase(); } catch (SQLException e) { @@ -164,16 +143,14 @@ public void testCreateBackup(){ */ private void assertToastDisplayed(int toastString) { onView(withText(toastString)) - .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView())))) + .inRoot(withDecorView(not(is(mAcccountsActivity.getWindow().getDecorView())))) .check(matches(isDisplayed())); } //todo: add testing of export flag to unit test //todo: add test of ignore exported transactions to unit tests - @Override - @After public void tearDown() throws Exception { + @After public void tearDown(){ mDbHelper.close(); mDb.close(); - super.tearDown(); } } 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 index 7c4ab6d39..ce335de06 100644 --- a/app/src/androidTest/java/org/gnucash/android/test/ui/FirstRunWizardActivityTest.java +++ b/app/src/androidTest/java/org/gnucash/android/test/ui/FirstRunWizardActivityTest.java @@ -18,10 +18,9 @@ import android.Manifest; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; -import android.support.test.InstrumentationRegistry; +import android.support.test.rule.ActivityTestRule; import android.support.test.rule.GrantPermissionRule; import android.support.test.runner.AndroidJUnit4; -import android.test.ActivityInstrumentationTestCase2; import android.util.Log; import org.gnucash.android.R; @@ -50,7 +49,7 @@ * @author Ngewi Fet */ @RunWith(AndroidJUnit4.class) -public class FirstRunWizardActivityTest extends ActivityInstrumentationTestCase2{ +public class FirstRunWizardActivityTest { private DatabaseHelper mDbHelper; private SQLiteDatabase mDb; @@ -62,16 +61,12 @@ public class FirstRunWizardActivityTest extends ActivityInstrumentationTestCase2 @Rule public GrantPermissionRule animationPermissionsRule = GrantPermissionRule.grant(Manifest.permission.SET_ANIMATION_SCALE); - public FirstRunWizardActivityTest() { - super(FirstRunWizardActivity.class); - } + @Rule + public ActivityTestRule mActivityRule = new ActivityTestRule<>(FirstRunWizardActivity.class); @Before - public void setUp() throws Exception { - super.setUp(); - injectInstrumentation(InstrumentationRegistry.getInstrumentation()); - - mActivity = getActivity(); + public void setUp() { + mActivity = mActivityRule.getActivity(); mDbHelper = new DatabaseHelper(mActivity, BaseModel.generateUID()); try { mDb = mDbHelper.getWritableDatabase(); diff --git a/app/src/androidTest/java/org/gnucash/android/test/ui/OwnCloudExportTest.java b/app/src/androidTest/java/org/gnucash/android/test/ui/OwnCloudExportTest.java index b3e512037..008909b11 100644 --- a/app/src/androidTest/java/org/gnucash/android/test/ui/OwnCloudExportTest.java +++ b/app/src/androidTest/java/org/gnucash/android/test/ui/OwnCloudExportTest.java @@ -28,7 +28,6 @@ import android.support.test.rule.ActivityTestRule; import android.support.test.rule.GrantPermissionRule; import android.support.test.runner.AndroidJUnit4; -import android.test.ActivityInstrumentationTestCase2; import android.util.Log; import org.gnucash.android.R; @@ -66,7 +65,7 @@ import static android.support.test.espresso.matcher.ViewMatchers.withText; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; -import static org.gnucash.android.test.ui.AccountsActivityTest.preventFirstRunDialogs; +import static org.gnucash.android.test.ui.util.GnucashAndroidTestRunner.preventFirstRunDialogs; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @@ -84,8 +83,7 @@ public class OwnCloudExportTest { private String OC_DIR = "gc_test"; /** - * A JUnit {@link Rule @Rule} to launch your activity under test. This is a replacement - * for {@link ActivityInstrumentationTestCase2}. + * A JUnit {@link Rule @Rule} to launch your activity under test. *

* Rules are interceptors which are executed for each test method and will run before * any of your setup code in the {@link Before @Before} method. 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 6821e4ca2..4395e7386 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 @@ -43,6 +43,7 @@ import org.gnucash.android.model.TransactionType; import org.gnucash.android.receivers.TransactionRecorder; import org.gnucash.android.test.ui.util.DisableAnimationsRule; +import org.gnucash.android.test.ui.util.GnucashAndroidTestRunner; import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.settings.PreferenceActivity; import org.gnucash.android.ui.transaction.TransactionFormFragment; @@ -57,7 +58,6 @@ import java.math.BigDecimal; import java.text.NumberFormat; -import java.util.Currency; import java.util.Date; import java.util.List; import java.util.Locale; @@ -76,7 +76,6 @@ import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @RunWith(AndroidJUnit4.class) @@ -135,7 +134,7 @@ public TransactionsActivityTest() { @BeforeClass public static void prepareTestCase(){ Context context = GnuCashApplication.getAppContext(); - AccountsActivityTest.preventFirstRunDialogs(context); + GnucashAndroidTestRunner.preventFirstRunDialogs(context); mSplitsDbAdapter = SplitsDbAdapter.getInstance(); mTransactionsDbAdapter = TransactionsDbAdapter.getInstance(); diff --git a/app/src/androidTest/java/org/gnucash/android/test/ui/util/GnucashAndroidTestRunner.java b/app/src/androidTest/java/org/gnucash/android/test/ui/util/GnucashAndroidTestRunner.java index 40d9691bb..393e6b0c5 100644 --- a/app/src/androidTest/java/org/gnucash/android/test/ui/util/GnucashAndroidTestRunner.java +++ b/app/src/androidTest/java/org/gnucash/android/test/ui/util/GnucashAndroidTestRunner.java @@ -16,13 +16,20 @@ package org.gnucash.android.test.ui.util; +import android.content.Context; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.IBinder; -import android.support.multidex.MultiDex; +import android.preference.PreferenceManager; import android.support.test.runner.AndroidJUnitRunner; import android.util.Log; +import com.kobakei.ratethisapp.RateThisApp; + +import org.gnucash.android.R; +import org.gnucash.android.ui.account.AccountsActivity; + import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -35,6 +42,25 @@ public class GnucashAndroidTestRunner extends AndroidJUnitRunner{ private static final float DISABLED = 0.0f; private static final float DEFAULT = 1.0f; + /** + * 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) { + AccountsActivity.rateAppConfig = new RateThisApp.Config(10000, 10000); + SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); + + //do not show first run dialog + editor.putBoolean(context.getString(R.string.key_first_run), false); + editor.putInt(AccountsActivity.LAST_OPEN_TAB_INDEX, AccountsActivity.INDEX_TOP_LEVEL_ACCOUNTS_FRAGMENT); + + //do not show "What's new" dialog + String minorVersion = context.getString(R.string.app_minor_version); + int currentMinor = Integer.parseInt(minorVersion); + editor.putInt(context.getString(R.string.key_previous_minor_version), currentMinor); + editor.commit(); + } + @Override public void onCreate(Bundle args) { super.onCreate(args); 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 c079a7eb6..9d4762ef3 100644 --- a/app/src/main/java/org/gnucash/android/db/DatabaseHelper.java +++ b/app/src/main/java/org/gnucash/android/db/DatabaseHelper.java @@ -20,11 +20,9 @@ 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.app.GnuCashApplication; import org.gnucash.android.model.Commodity; import org.xml.sax.SAXException; @@ -264,7 +262,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){ Log.i(LOG_TAG, "Upgrading database from version " + oldVersion + " to " + newVersion); - Toast.makeText(GnuCashApplication.getAppContext(), "Upgrading GnuCash database", Toast.LENGTH_SHORT).show(); + //TODO: Find way to show a progress dialog for long running db upgrades /* * 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. @@ -278,9 +276,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){ while(oldVersion < newVersion){ try { Method method = MigrationHelper.class.getDeclaredMethod("upgradeDbToVersion" + (oldVersion+1), SQLiteDatabase.class); - Object result = method.invoke(null, db); - oldVersion = Integer.parseInt(result.toString()); - + oldVersion = (int) method.invoke(null, db); } catch (NoSuchMethodException e) { String msg = String.format("Database upgrade method upgradeToVersion%d(SQLiteDatabase) definition not found ", newVersion); Log.e(LOG_TAG, msg, e); @@ -356,7 +352,7 @@ private void createDatabaseTables(SQLiteDatabase db) { db.execSQL(createBudgetAmountUidIndex); try { - MigrationHelper.importCommodities(db); + MigrationHelper.importCommodities(db, true); } catch (SAXException | ParserConfigurationException | IOException e) { Log.e(LOG_TAG, "Error loading currencies into the database"); e.printStackTrace(); 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 f3dcbeaf8..eb689affe 100644 --- a/app/src/main/java/org/gnucash/android/db/DatabaseSchema.java +++ b/app/src/main/java/org/gnucash/android/db/DatabaseSchema.java @@ -39,7 +39,7 @@ public class DatabaseSchema { * Version number of database containing accounts and transactions info. * With any change to the database schema, this number must increase */ - public static final int DATABASE_VERSION = 15; + public static final int DATABASE_VERSION = 17; /** * Name of the database diff --git a/app/src/main/java/org/gnucash/android/db/MigrationHelper.java b/app/src/main/java/org/gnucash/android/db/MigrationHelper.java index 5acc1d74a..f56b72eb8 100644 --- a/app/src/main/java/org/gnucash/android/db/MigrationHelper.java +++ b/app/src/main/java/org/gnucash/android/db/MigrationHelper.java @@ -235,7 +235,7 @@ public void run() { /** * Imports commodities into the database from XML resource file */ - static void importCommodities(SQLiteDatabase db) throws SAXException, ParserConfigurationException, IOException { + static void importCommodities(SQLiteDatabase db, boolean deleteExisting) throws SAXException, ParserConfigurationException, IOException { SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser sp = spf.newSAXParser(); XMLReader xr = sp.getXMLReader(); @@ -246,7 +246,7 @@ static void importCommodities(SQLiteDatabase db) throws SAXException, ParserConf /** Create handler to handle XML Tags ( extends DefaultHandler ) */ - CommoditiesXmlHandler handler = new CommoditiesXmlHandler(db); + CommoditiesXmlHandler handler = new CommoditiesXmlHandler(db, deleteExisting); xr.setContentHandler(handler); xr.parse(new InputSource(bos)); @@ -264,7 +264,7 @@ public static int upgradeDbToVersion2(SQLiteDatabase db) { " ADD COLUMN double_account_uid varchar(255)"; //introducing sub accounts - Log.i(DatabaseHelper.LOG_TAG, "Adding column for parent 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)"; @@ -273,7 +273,7 @@ public static int upgradeDbToVersion2(SQLiteDatabase db) { //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"); + 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); @@ -495,7 +495,7 @@ static int upgradeDbToVersion7(SQLiteDatabase db) { * @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"); + Log.i(LOG_TAG, "Upgrading database to version 8"); int oldVersion = 7; new File(Exporter.LEGACY_BASE_FOLDER_PATH + "/backups/").mkdirs(); new File(Exporter.LEGACY_BASE_FOLDER_PATH + "/exports/").mkdirs(); @@ -505,7 +505,7 @@ static int upgradeDbToVersion8(SQLiteDatabase db) { db.beginTransaction(); try { - Log.i(DatabaseHelper.LOG_TAG, "Creating scheduled actions table"); + 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, " @@ -525,7 +525,7 @@ static int upgradeDbToVersion8(SQLiteDatabase db) { //==============================BEGIN TABLE MIGRATIONS ======================================== - Log.i(DatabaseHelper.LOG_TAG, "Migrating accounts table"); + 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 @@ -577,7 +577,7 @@ static int upgradeDbToVersion8(SQLiteDatabase db) { + " FROM " + AccountEntry.TABLE_NAME + "_bak;" ); - Log.i(DatabaseHelper.LOG_TAG, "Migrating transactions table"); + 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 @@ -618,7 +618,7 @@ static int upgradeDbToVersion8(SQLiteDatabase db) { + " FROM " + TransactionEntry.TABLE_NAME + "_bak;" ); - Log.i(DatabaseHelper.LOG_TAG, "Migrating splits table"); + Log.i(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 @@ -668,7 +668,7 @@ static int upgradeDbToVersion8(SQLiteDatabase 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"); + 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; @@ -706,7 +706,7 @@ static int upgradeDbToVersion8(SQLiteDatabase db) { 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"); + 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()){ @@ -753,7 +753,7 @@ static int upgradeDbToVersion8(SQLiteDatabase db) { cursor.close(); //auto-balance existing splits - Log.i(DatabaseHelper.LOG_TAG, "Auto-balancing existing transaction splits"); + Log.i(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 @@ -830,7 +830,7 @@ static int upgradeDbToVersion8(SQLiteDatabase db) { cursor.close(); } - Log.i(DatabaseHelper.LOG_TAG, "Dropping temporary migration tables"); + 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"); @@ -861,7 +861,7 @@ static int upgradeDbToVersion8(SQLiteDatabase db) { * @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"); + Log.i(LOG_TAG, "Upgrading database to version 9"); int oldVersion = 8; db.beginTransaction(); @@ -883,9 +883,9 @@ static int upgradeDbToVersion9(SQLiteDatabase db){ + "' ON " + CommodityEntry.TABLE_NAME + "(" + CommodityEntry.COLUMN_UID + ")"); try { - importCommodities(db); + importCommodities(db, true); } catch (SAXException | ParserConfigurationException | IOException e) { - Log.e(DatabaseHelper.LOG_TAG, "Error loading currencies into the database", e); + Log.e(LOG_TAG, "Error loading currencies into the database", e); Crashlytics.logException(e); throw new RuntimeException(e); } @@ -1092,7 +1092,7 @@ static int upgradeDbToVersion9(SQLiteDatabase db){ * @return 10 if upgrade was successful, 9 otherwise */ static int upgradeDbToVersion10(SQLiteDatabase db){ - Log.i(DatabaseHelper.LOG_TAG, "Upgrading database to version 9"); + Log.i(LOG_TAG, "Upgrading database to version 9"); int oldVersion = 9; db.beginTransaction(); @@ -1145,7 +1145,7 @@ static int upgradeDbToVersion10(SQLiteDatabase db){ * @return 11 if upgrade was successful, 10 otherwise */ static int upgradeDbToVersion11(SQLiteDatabase db){ - Log.i(DatabaseHelper.LOG_TAG, "Upgrading database to version 9"); + Log.i(LOG_TAG, "Upgrading database to version 9"); int oldVersion = 10; db.beginTransaction(); @@ -1241,7 +1241,7 @@ static int upgradeDbToVersion12(SQLiteDatabase db){ * @return New database version, 13 if migration succeeds, 11 otherwise */ static int upgradeDbToVersion13(SQLiteDatabase db){ - Log.i(DatabaseHelper.LOG_TAG, "Upgrading database to version 13"); + Log.i(LOG_TAG, "Upgrading database to version 13"); int oldVersion = 12; db.beginTransaction(); @@ -1552,7 +1552,7 @@ private static void moveDirectory(File srcDir, File dstDir) throws IOException { * @return New database version */ public static int upgradeDbToVersion14(SQLiteDatabase db){ - Log.i(DatabaseHelper.LOG_TAG, "Upgrading database to version 14"); + Log.i(LOG_TAG, "Upgrading database to version 14"); int oldDbVersion = 13; File backupFolder = new File(Exporter.BASE_FOLDER_PATH); backupFolder.mkdir(); @@ -1585,7 +1585,7 @@ public void run() { } /** - * Upgrades the database to version 14. + * Upgrades the database to version 15. *

This migration makes the following changes to the database: *

    *
  • Fixes accounts referencing a default transfer account that no longer @@ -1593,10 +1593,10 @@ public void run() { *
*

* @param db SQLite database to be upgraded - * @return New database version, 14 if migration succeeds, 13 otherwise + * @return New database version, 15 if migration succeeds, 14 otherwise */ static int upgradeDbToVersion15(SQLiteDatabase db) { - Log.i(DatabaseHelper.LOG_TAG, "Upgrading database to version 15"); + Log.i(LOG_TAG, "Upgrading database to version 15"); int dbVersion = 14; db.beginTransaction(); @@ -1628,4 +1628,51 @@ static int upgradeDbToVersion15(SQLiteDatabase db) { rescheduleServiceAlarm(); return dbVersion; } + + /** + * Upgrades the database to version 16. + *

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

    + *
  • Update the commodities table (see #731)
  • + *
+ *

+ * @param db SQLite database to be upgraded + * @return New database version, 16 if migration succeeds, 15 otherwise + */ + static int upgradeDbToVersion16(SQLiteDatabase db) { + Log.i(LOG_TAG, "Upgrading database to version 16"); + int dbVersion = 15; + + try { + importCommodities(db, false); + dbVersion = 16; + } catch (SAXException | ParserConfigurationException | IOException e) { + Log.e(LOG_TAG, "Error loading currencies into the database", e); + Crashlytics.logException(e); + } + + return dbVersion; + } + + /** + * Upgrades the database to version 17. + * This migration updates the foreign keys to the commodities in the splits table + * after a previous migration which overwrote the db table for commodities, rendering + * all foreign keys to commodity GUIDs invalid. + * + * @param db SQLite database to be upgraded + * @return New database version, 16 if migration succeeds, 15 otherwise + */ + static int upgradeDbToVersion17(SQLiteDatabase db) { + Log.i(LOG_TAG, "Upgrading database to version 17"); + int dbVersion = 16; + + db.beginTransaction(); + db.execSQL("UPDATE accounts SET commodity_uid = (SELECT uid FROM commodities WHERE mnemonic = accounts.currency_code)"); + db.execSQL("UPDATE transactions SET commodity_uid = (SELECT uid FROM commodities WHERE commodities.mnemonic = transactions.currency_code)"); + + db.endTransaction(); + + return dbVersion + 1; + } } diff --git a/app/src/main/java/org/gnucash/android/db/adapter/AccountsDbAdapter.java b/app/src/main/java/org/gnucash/android/db/adapter/AccountsDbAdapter.java index e19138306..1b73586f2 100644 --- a/app/src/main/java/org/gnucash/android/db/adapter/AccountsDbAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/adapter/AccountsDbAdapter.java @@ -59,6 +59,7 @@ * @author Oleksandr Tyshkovets */ public class AccountsDbAdapter extends DatabaseAdapter { + /** * Separator used for account name hierarchies between parent and child accounts */ @@ -71,7 +72,14 @@ public class AccountsDbAdapter extends DatabaseAdapter { */ public static final String ROOT_ACCOUNT_FULL_NAME = " "; - /** + /** + * Where clause to get non hidden nor root account + */ + public static final String WHERE_NOT_HIDDEN_AND_NOT_ROOT_ACCOUNT = + AccountEntry.COLUMN_HIDDEN + " = 0 AND " + AccountEntry.COLUMN_TYPE + " != ?"; + + + /** * Transactions database adapter for manipulating transactions associated with accounts */ private final TransactionsDbAdapter mTransactionsAdapter; @@ -702,15 +710,20 @@ public Cursor fetchAllRecords(){ * GnuCash ROOT accounts and hidden accounts will not be included in the result set. * @return {@link Cursor} to all account records */ - public Cursor fetchAllRecordsOrderedByFullName(){ - Log.v(LOG_TAG, "Fetching all accounts from db"); - String selection = AccountEntry.COLUMN_HIDDEN + " = 0 AND " + AccountEntry.COLUMN_TYPE + " != ?" ; + public Cursor fetchAllRecordsOrderedByFullName() { + + Log.v(LOG_TAG, + "Fetching all accounts from db"); + + String selection = AccountEntry.COLUMN_HIDDEN + " = 0 AND " + AccountEntry.COLUMN_TYPE + " != ?"; + return mDb.query(AccountEntry.TABLE_NAME, - null, - selection, - new String[]{AccountType.ROOT.name()}, - null, null, - AccountEntry.COLUMN_FULL_NAME + " ASC"); + null, + selection, + new String[]{AccountType.ROOT.name()}, + null, + null, + AccountEntry.COLUMN_FULL_NAME + " ASC"); } /** @@ -728,8 +741,12 @@ public Cursor fetchAccounts(@Nullable String where, @Nullable String[] whereArgs Log.v(LOG_TAG, "Fetching all accounts from db where " + where + " order by " + orderBy); return mDb.query(AccountEntry.TABLE_NAME, - null, where, whereArgs, null, null, - orderBy); + null, + where, + whereArgs, + null, + null, + orderBy); } /** @@ -740,10 +757,16 @@ public Cursor fetchAccounts(@Nullable String where, @Nullable String[] whereArgs * @return Cursor set of accounts which fulfill where */ public Cursor fetchAccountsOrderedByFullName(String where, String[] whereArgs) { + Log.v(LOG_TAG, "Fetching all accounts from db where " + where); + return mDb.query(AccountEntry.TABLE_NAME, - null, where, whereArgs, null, null, - AccountEntry.COLUMN_FULL_NAME + " ASC"); + null, + where, + whereArgs, + null, + null, + AccountEntry.COLUMN_FULL_NAME + " ASC"); } /** @@ -754,13 +777,37 @@ public Cursor fetchAccountsOrderedByFullName(String where, String[] whereArgs) { * @param whereArgs where args * @return Cursor set of accounts which fulfill where */ - public Cursor fetchAccountsOrderedByFavoriteAndFullName(String where, String[] whereArgs) { - Log.v(LOG_TAG, "Fetching all accounts from db where " + where + " order by Favorite then Name"); + public Cursor fetchAccountsOrderedByFavoriteAndFullName(String where, + String[] whereArgs) { + + Log.v(LOG_TAG, + "Fetching all accounts from db where " + where + " order by Favorite then Name"); + return mDb.query(AccountEntry.TABLE_NAME, - null, where, whereArgs, null, null, - AccountEntry.COLUMN_FAVORITE + " DESC, " + AccountEntry.COLUMN_FULL_NAME + " ASC"); + null, + where, + whereArgs, + null, + null, + AccountEntry.COLUMN_FAVORITE + " DESC, " + AccountEntry.COLUMN_FULL_NAME + " ASC"); } + /** + * Returns a Cursor set of all Accounts + * + *

This method returns the favorite accounts first, sorted by name, and then the other accounts, + * sorted by name.

+ * + * @return Cursor set of all accounts + */ + public Cursor fetchAccountsOrderedByFavoriteAndFullName() { + + return fetchAccountsOrderedByFavoriteAndFullName(WHERE_NOT_HIDDEN_AND_NOT_ROOT_ACCOUNT, + new String[]{AccountType.ROOT.name()}); + } + + + /** * Returns the balance of an account while taking sub-accounts into consideration * @return Account Balance of an account including sub-accounts @@ -929,12 +976,18 @@ public Cursor fetchSubAccounts(String accountUID) { */ public Cursor fetchTopLevelAccounts() { //condition which selects accounts with no parent, whose UID is not ROOT and whose type is not ROOT - return fetchAccounts("(" + AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " IS NULL OR " - + AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " = ?) AND " - + AccountEntry.COLUMN_HIDDEN + " = 0 AND " - + AccountEntry.COLUMN_TYPE + " != ?", - new String[]{getOrCreateGnuCashRootAccountUID(), AccountType.ROOT.name()}, - AccountEntry.COLUMN_NAME + " ASC"); + return fetchAccounts("(" + + AccountEntry.COLUMN_PARENT_ACCOUNT_UID + + " IS NULL OR " + + AccountEntry.COLUMN_PARENT_ACCOUNT_UID + + " = ?) AND " + + AccountEntry.COLUMN_HIDDEN + + " = 0 AND " + + AccountEntry.COLUMN_TYPE + + " != ?", + new String[]{getOrCreateGnuCashRootAccountUID(), + AccountType.ROOT.name()}, + AccountEntry.COLUMN_NAME + " ASC"); } /** @@ -1215,22 +1268,33 @@ public static String getOpeningBalanceAccountFullName(){ * @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 colorCode = null; + int iColor = -1; + String parentAccountUID = accountUID; - while (parentAccountUID != null ) { + while (parentAccountUID != null) { + colorCode = accountsDbAdapter.getAccountColorCode(accountsDbAdapter.getID(parentAccountUID)); + if (colorCode != null) { iColor = Color.parseColor(colorCode); break; } + + // Climb to parent account parentAccountUID = accountsDbAdapter.getParentAccountUID(parentAccountUID); } if (colorCode == null) { - iColor = GnuCashApplication.getAppContext().getResources().getColor(R.color.theme_primary); + // No color has been found defined in any ancestor + + // Use default theme color + iColor = GnuCashApplication.getAppContext() + .getResources() + .getColor(R.color.theme_primary); } return iColor; diff --git a/app/src/main/java/org/gnucash/android/db/adapter/DatabaseAdapter.java b/app/src/main/java/org/gnucash/android/db/adapter/DatabaseAdapter.java index ccaa4e18c..d146e6b0c 100644 --- a/app/src/main/java/org/gnucash/android/db/adapter/DatabaseAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/adapter/DatabaseAdapter.java @@ -556,10 +556,14 @@ public long getID(@NonNull String uid){ * @throws IllegalArgumentException if the record ID does not exist in the database */ public String getUID(long id){ + Cursor cursor = mDb.query(mTableName, - new String[]{DatabaseSchema.CommonColumns.COLUMN_UID}, - DatabaseSchema.CommonColumns._ID + " = " + id, - null, null, null, null); + new String[]{DatabaseSchema.CommonColumns.COLUMN_UID}, + DatabaseSchema.CommonColumns._ID + " = " + id, + null, + null, + null, + null); String uid = null; try { diff --git a/app/src/main/java/org/gnucash/android/export/csv/CsvTransactionsExporter.java b/app/src/main/java/org/gnucash/android/export/csv/CsvTransactionsExporter.java index f0d082e6e..f298cd201 100644 --- a/app/src/main/java/org/gnucash/android/export/csv/CsvTransactionsExporter.java +++ b/app/src/main/java/org/gnucash/android/export/csv/CsvTransactionsExporter.java @@ -96,11 +96,10 @@ public List generateExport() throws ExporterException { * Write splits to CSV format * @param splits Splits to be written */ - private void writeSplitsToCsv(@NonNull List splits, @NonNull CsvWriter writer) throws IOException { + private void writeSplitsToCsv(@NonNull List splits, @NonNull CsvWriter writer, + Map accountNames, Map accountFullNames) throws IOException { int index = 0; - Map uidAccountMap = new HashMap<>(); - for (Split split : splits) { if (index++ > 0){ // the first split is on the same line as the transactions. But after that, we writer.write("" + mCsvSeparator + mCsvSeparator + mCsvSeparator + mCsvSeparator @@ -108,18 +107,22 @@ private void writeSplitsToCsv(@NonNull List splits, @NonNull CsvWriter wr } writer.writeToken(split.getMemo()); - //cache accounts so that we do not have to go to the DB each time String accountUID = split.getAccountUID(); - Account account; - if (uidAccountMap.containsKey(accountUID)) { - account = uidAccountMap.get(accountUID); + + // Cache account names + String fullName, name; + if (accountNames.containsKey(accountUID)) { + fullName = accountFullNames.get(accountUID); + name = accountNames.get(accountUID); } else { - account = mAccountsDbAdapter.getRecord(accountUID); - uidAccountMap.put(accountUID, account); + fullName = mAccountsDbAdapter.getAccountFullName(accountUID); + name = mAccountsDbAdapter.getAccountName(accountUID); + accountFullNames.put(accountUID, fullName); + accountNames.put(accountUID, name); } - writer.writeToken(account.getFullName()); - writer.writeToken(account.getName()); + writer.writeToken(fullName); + writer.writeToken(name); String sign = split.getType() == TransactionType.CREDIT ? "-" : ""; writer.writeToken(sign + split.getQuantity().formattedString()); @@ -143,6 +146,8 @@ private void generateExport(final CsvWriter csvWriter) throws ExporterException } csvWriter.newLine(); + Map nameCache = new HashMap<>(); + Map fullNameCache = new HashMap<>(); Cursor cursor = mTransactionsDbAdapter.fetchTransactionsModifiedSince(mExportParams.getExportStartTime()); Log.d(LOG_TAG, String.format("Exporting %d transactions to CSV", cursor.getCount())); @@ -159,7 +164,7 @@ private void generateExport(final CsvWriter csvWriter) throws ExporterException csvWriter.writeToken("CURRENCY::" + transaction.getCurrencyCode()); csvWriter.writeToken(null); // Void Reason csvWriter.writeToken(null); // Action - writeSplitsToCsv(transaction.getSplits(), csvWriter); + writeSplitsToCsv(transaction.getSplits(), csvWriter, nameCache, fullNameCache); } PreferencesHelper.setLastExportTime(TimestampHelper.getTimestampFromNow()); diff --git a/app/src/main/java/org/gnucash/android/importer/CommoditiesXmlHandler.java b/app/src/main/java/org/gnucash/android/importer/CommoditiesXmlHandler.java index 977328fa6..0cc9b84b4 100644 --- a/app/src/main/java/org/gnucash/android/importer/CommoditiesXmlHandler.java +++ b/app/src/main/java/org/gnucash/android/importer/CommoditiesXmlHandler.java @@ -15,9 +15,11 @@ */ package org.gnucash.android.importer; +import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import org.gnucash.android.app.GnuCashApplication; +import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.db.adapter.CommoditiesDbAdapter; import org.gnucash.android.db.adapter.DatabaseAdapter; import org.gnucash.android.model.Commodity; @@ -44,17 +46,23 @@ public class CommoditiesXmlHandler extends DefaultHandler { * 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 List mCommodities = new ArrayList<>(); + + private boolean deleteExisting; private CommoditiesDbAdapter mCommoditiesDbAdapter; - public CommoditiesXmlHandler(SQLiteDatabase db){ + public CommoditiesXmlHandler(SQLiteDatabase db, boolean deleteExisting){ + initAdapter(db); + this.deleteExisting = deleteExisting; + } + + private void initAdapter(SQLiteDatabase db) { if (db == null){ mCommoditiesDbAdapter = GnuCashApplication.getCommoditiesDbAdapter(); } else { mCommoditiesDbAdapter = new CommoditiesDbAdapter(db); } - mCommodities = new ArrayList<>(); } @Override @@ -82,6 +90,25 @@ public void startElement(String uri, String localName, String qName, Attributes @Override public void endDocument() throws SAXException { - mCommoditiesDbAdapter.bulkAddRecords(mCommodities, DatabaseAdapter.UpdateMethod.insert); + if (this.deleteExisting){ + mCommoditiesDbAdapter.deleteAllRecords(); + mCommoditiesDbAdapter.bulkAddRecords(mCommodities, DatabaseAdapter.UpdateMethod.insert); + } else { + List existingCurrencyCodes = new ArrayList<>(); + + try(Cursor cursor = mCommoditiesDbAdapter.fetchAllRecords()) { + while (cursor.moveToNext()) { + String code = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.CommodityEntry.COLUMN_MNEMONIC)); + existingCurrencyCodes.add(code); + } + } + for (Commodity commodity : mCommodities) { + if (existingCurrencyCodes.contains(commodity.getCurrencyCode())){ + mCommoditiesDbAdapter.addRecord(commodity, DatabaseAdapter.UpdateMethod.update); + } else { + mCommoditiesDbAdapter.addRecord(commodity, DatabaseAdapter.UpdateMethod.insert); + } + } + } } } 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 35d5f3e7b..b027e78f4 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 @@ -67,6 +67,8 @@ import org.gnucash.android.ui.colorpicker.ColorSquare; import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.settings.PreferenceActivity; +import org.gnucash.android.ui.util.AccountUtils; +import org.gnucash.android.ui.util.widget.searchablespinner.SearchableSpinnerView; import org.gnucash.android.util.CommoditiesCursorAdapter; import org.gnucash.android.util.QualifiedAccountNameCursorAdapter; @@ -135,6 +137,14 @@ public class AccountFormFragment extends Fragment { */ private String mAccountUID = null; + /** + * Spinner for the account type + * + * @see org.gnucash.android.model.AccountType + */ + @BindView(R.id.input_account_type_spinner) + Spinner mAccountTypeSpinner; + /** * Cursor which will hold set of eligible parent accounts */ @@ -155,7 +165,8 @@ public class AccountFormFragment extends Fragment { /** * Spinner for parent account list */ - @BindView(R.id.input_parent_account) Spinner mParentAccountSpinner; + @BindView(R.id.input_parent_account) + SearchableSpinnerView mParentAccountSpinner; /** * Checkbox which activates the parent account spinner when selected @@ -163,12 +174,6 @@ public class AccountFormFragment extends Fragment { */ @BindView(R.id.checkbox_parent_account) CheckBox mParentCheckBox; - /** - * Spinner for the account type - * @see org.gnucash.android.model.AccountType - */ - @BindView(R.id.input_account_type_spinner) Spinner mAccountTypeSpinner; - /** * Checkbox for activating the default transfer account spinner */ @@ -177,7 +182,8 @@ public class AccountFormFragment extends Fragment { /** * Spinner for selecting the default transfer account */ - @BindView(R.id.input_default_transfer_account) Spinner mDefaultTransferAccountSpinner; + @BindView(R.id.input_default_transfer_account) + SearchableSpinnerView mDefaultTransferAccountSpinner; /** * Account description input text view @@ -286,7 +292,9 @@ public void onNothingSelected(AdapterView adapterView) { }); - mParentAccountSpinner.setEnabled(false); + mParentAccountSpinner.setTitle(getString(R.string.select_account)); + + mParentAccountSpinner.setEnabled(false); mParentCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() { @@ -296,6 +304,8 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { } }); + mDefaultTransferAccountSpinner.setTitle(getString(R.string.select_account)); + mDefaultTransferAccountSpinner.setEnabled(false); mDefaultTransferAccountCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override @@ -322,8 +332,8 @@ public void onClick(View view) { public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - CommoditiesCursorAdapter commoditiesAdapter = new CommoditiesCursorAdapter( - getActivity(), android.R.layout.simple_spinner_item); + CommoditiesCursorAdapter commoditiesAdapter = new CommoditiesCursorAdapter(getActivity(), + android.R.layout.simple_spinner_item); commoditiesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); mCurrencySpinner.setAdapter(commoditiesAdapter); @@ -569,21 +579,24 @@ public boolean onOptionsItemSelected(MenuItem item) { /** * Initializes the default transfer account spinner with eligible accounts */ - private void loadDefaultTransferAccountList(){ - String condition = DatabaseSchema.AccountEntry.COLUMN_UID + " != '" + mAccountUID + "' " //when creating a new account mAccountUID is null, so don't use whereArgs - + " AND " + DatabaseSchema.AccountEntry.COLUMN_PLACEHOLDER + "=0" - + " AND " + DatabaseSchema.AccountEntry.COLUMN_HIDDEN + "=0" - + " AND " + DatabaseSchema.AccountEntry.COLUMN_TYPE + " != ?"; + private void loadDefaultTransferAccountList() { + + // Get Accounts that are not hidden, nor Placeholder, nor root Account, nor the edited Account itself + String where = AccountUtils.getTransfertAccountWhereClause(mAccountUID); - Cursor defaultTransferAccountCursor = mAccountsDbAdapter.fetchAccountsOrderedByFullName(condition, - new String[]{AccountType.ROOT.name()}); + Cursor defaultTransferAccountCursor = mAccountsDbAdapter.fetchAccountsOrderedByFavoriteAndFullName(where, + null); if (mDefaultTransferAccountSpinner.getCount() <= 0) { setDefaultTransferAccountInputsVisible(false); } mDefaultTransferAccountCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), - defaultTransferAccountCursor); + defaultTransferAccountCursor, + where, + null, + R.layout.account_spinner_dropdown_item); + mDefaultTransferAccountSpinner.setAdapter(mDefaultTransferAccountCursorAdapter); } @@ -592,41 +605,102 @@ private void loadDefaultTransferAccountList(){ * The allowed parent accounts depends on the account type * @param accountType AccountType of account whose allowed parent list is to be loaded */ - private void loadParentAccountList(AccountType accountType){ - String condition = DatabaseSchema.SplitEntry.COLUMN_TYPE + " IN (" - + getAllowedParentAccountTypes(accountType) + ") AND " + DatabaseSchema.AccountEntry.COLUMN_HIDDEN + "!=1 "; - - if (mAccount != null){ //if editing an account - mDescendantAccountUIDs = mAccountsDbAdapter.getDescendantAccountUIDs(mAccount.getUID(), null, null); - String rootAccountUID = mAccountsDbAdapter.getOrCreateGnuCashRootAccountUID(); - List descendantAccountUIDs = new ArrayList<>(mDescendantAccountUIDs); - if (rootAccountUID != null) - descendantAccountUIDs.add(rootAccountUID); - // limit cyclic account hierarchies. - condition += " AND (" + DatabaseSchema.AccountEntry.COLUMN_UID + " NOT IN ( '" - + TextUtils.join("','", descendantAccountUIDs) + "','" + mAccountUID + "' ) )"; + private void loadParentAccountList(AccountType accountType) { + + // + // Build SQL request + // + + String where = DatabaseSchema.SplitEntry.COLUMN_TYPE + + " IN (" + + getAllowedParentAccountTypes(accountType) + + ") AND " + + DatabaseSchema.AccountEntry.COLUMN_HIDDEN + + "!=1 "; + + if (mAccount != null) { + // An Account is defined + + // + // Get descendant Accounts + // + + // Get descendant Accounts UIDs + mDescendantAccountUIDs = mAccountsDbAdapter.getDescendantAccountUIDs(mAccount.getUID(), + null, + null); + + // Clone descendant Account UIDs + List accountUIDsToExclude = new ArrayList<>(mDescendantAccountUIDs); + + // Get root Account UID + String rootAccountUID = mAccountsDbAdapter.getOrCreateGnuCashRootAccountUID(); + + if (rootAccountUID != null) { + + // Add root account to descendants + accountUIDsToExclude.add(rootAccountUID); + } + + // Exclude Accounts to Exclude and edited Account itself + where += " AND (" + + DatabaseSchema.AccountEntry.COLUMN_UID + + " NOT IN ( '" + + TextUtils.join("','", + accountUIDsToExclude) + + "','" + + mAccountUID + + "' ) )"; } //if we are reloading the list, close the previous cursor first - if (mParentAccountCursor != null) + if (mParentAccountCursor != null) { mParentAccountCursor.close(); + } + + mParentAccountCursor = mAccountsDbAdapter.fetchAccountsOrderedByFavoriteAndFullName(where, + null); + + // + // ? + // - mParentAccountCursor = mAccountsDbAdapter.fetchAccountsOrderedByFullName(condition, null); final View view = getView(); assert view != null; - if (mParentAccountCursor.getCount() <= 0){ + + if (mParentAccountCursor.getCount() <= 0) { + // No parent account + mParentCheckBox.setChecked(false); //disable before hiding, else we can still read it when saving - view.findViewById(R.id.layout_parent_account).setVisibility(View.GONE); - view.findViewById(R.id.label_parent_account).setVisibility(View.GONE); + + view.findViewById(R.id.layout_parent_account) + .setVisibility(View.GONE); + + view.findViewById(R.id.label_parent_account) + .setVisibility(View.GONE); + } else { - view.findViewById(R.id.layout_parent_account).setVisibility(View.VISIBLE); - view.findViewById(R.id.label_parent_account).setVisibility(View.VISIBLE); + // There are potential parent accounts + + view.findViewById(R.id.layout_parent_account) + .setVisibility(View.VISIBLE); + + view.findViewById(R.id.label_parent_account) + .setVisibility(View.VISIBLE); } - mParentAccountCursorAdapter = new QualifiedAccountNameCursorAdapter( - getActivity(), mParentAccountCursor); - mParentAccountSpinner.setAdapter(mParentAccountCursorAdapter); - } + // + // Build CursorAdapter + // + + mParentAccountCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), + mParentAccountCursor, + where, + null, + R.layout.account_spinner_dropdown_item); + + mParentAccountSpinner.setAdapter(mParentAccountCursorAdapter); + } /** * Returns a comma separated list of account types which can be parent accounts for the specified type. 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 846330af8..629f350c4 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 @@ -37,6 +37,7 @@ import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; +import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.support.v7.preference.PreferenceManager; import android.util.Log; @@ -268,6 +269,15 @@ public void onClick(View v) { }); } + @Override + protected void onResume() { + super.onResume(); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setSubtitle(BooksDbAdapter.getInstance().getActiveBookDisplayName()); + } + } + @Override protected void onStart() { super.onStart(); @@ -323,18 +333,33 @@ public void setCurrentTab(){ *

Also handles displaying the What's New dialog

*/ private void init() { - PreferenceManager.setDefaultValues(this, BooksDbAdapter.getInstance().getActiveBookUID(), - Context.MODE_PRIVATE, R.xml.fragment_transaction_preferences, true); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - boolean firstRun = prefs.getBoolean(getString(R.string.key_first_run), true); + PreferenceManager.setDefaultValues(this, + BooksDbAdapter.getInstance() + .getActiveBookUID(), + Context.MODE_PRIVATE, + R.xml.fragment_transaction_preferences, + true); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean firstRun = prefs.getBoolean(getString(R.string.key_first_run), + true); if (firstRun){ startActivity(new Intent(GnuCashApplication.getAppContext(), FirstRunWizardActivity.class)); - //default to using double entry and save the preference explicitly + // Default Preference to using double entry and save the preference explicitly prefs.edit().putBoolean(getString(R.string.key_use_double_entry), true).apply(); + + // Default preference to open keyboard in account searchable spinners + prefs.edit().putBoolean(getString(R.string.key_shall_open_keyboard_in_account_searchable_spinner), false).apply(); + + // Default preference to use colors in account lists + prefs.edit().putBoolean(getString(R.string.key_use_color_in_account_list),true).apply(); + + // Finish Activity finish(); + return; } 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 a83ab10bc..0d3d47c54 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 @@ -25,7 +25,6 @@ import android.database.Cursor; import android.graphics.Color; 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; @@ -433,19 +432,24 @@ public AccountsCursorLoader(Context context, String filter){ @Override public Cursor loadInBackground() { + mDatabaseAdapter = AccountsDbAdapter.getInstance(); Cursor cursor; - if (mFilter != null){ - cursor = ((AccountsDbAdapter)mDatabaseAdapter) - .fetchAccounts(DatabaseSchema.AccountEntry.COLUMN_HIDDEN + "= 0 AND " - + DatabaseSchema.AccountEntry.COLUMN_NAME + " LIKE '%" + mFilter + "%'", - null, null); + if (mFilter != null) { + cursor = ((AccountsDbAdapter) mDatabaseAdapter).fetchAccounts(DatabaseSchema.AccountEntry.COLUMN_HIDDEN + + "= 0 AND " + + DatabaseSchema.AccountEntry.COLUMN_NAME + + " LIKE '%" + + mFilter + + "%'", + null, + null); } else { - if (mParentAccountUID != null && mParentAccountUID.length() > 0) + if (mParentAccountUID != null && mParentAccountUID.length() > 0) { cursor = ((AccountsDbAdapter) mDatabaseAdapter).fetchSubAccounts(mParentAccountUID); - else { - switch (this.mDisplayMode){ + } else { + switch (this.mDisplayMode) { case RECENT: cursor = ((AccountsDbAdapter) mDatabaseAdapter).fetchRecentAccounts(10); break; @@ -461,8 +465,9 @@ public Cursor loadInBackground() { } - if (cursor != null) + if (cursor != null) { registerContentObserver(cursor); + } return cursor; } } diff --git a/app/src/main/java/org/gnucash/android/ui/report/ReportType.java b/app/src/main/java/org/gnucash/android/ui/report/ReportType.java index d9887e1db..f88cf45b6 100644 --- a/app/src/main/java/org/gnucash/android/ui/report/ReportType.java +++ b/app/src/main/java/org/gnucash/android/ui/report/ReportType.java @@ -39,28 +39,36 @@ public enum ReportType { PIE_CHART(0), BAR_CHART(1), LINE_CHART(2), TEXT(3), NONE(4); + // #872 Use a list to be sure of the sort order which is not guaranted with a hashmap keys + List mReportNames = null; + Map mReportTypeMap = new HashMap<>(); + int mValue = 4; + /** + * Constructor + * + * @param index + */ ReportType(int index){ + mValue = index; + Context context = GnuCashApplication.getAppContext(); - switch (index){ - case 0: - mReportTypeMap.put(context.getString(R.string.title_pie_chart), PieChartFragment.class); - break; - case 1: - mReportTypeMap.put(context.getString(R.string.title_bar_chart), StackedBarChartFragment.class); - break; - case 2: - mReportTypeMap.put(context.getString(R.string.title_cash_flow_report), CashFlowLineChartFragment.class); - break; - case 3: - mReportTypeMap.put(context.getString(R.string.title_balance_sheet_report), BalanceSheetFragment.class); - break; - case 4: - break; - } + + // #872 Fill the map with all the items, in order to fill the Report Toolbar Spinner + mReportTypeMap.put(context.getString(R.string.title_pie_chart), + PieChartFragment.class); + + mReportTypeMap.put(context.getString(R.string.title_bar_chart), + StackedBarChartFragment.class); + + mReportTypeMap.put(context.getString(R.string.title_cash_flow_report), + CashFlowLineChartFragment.class); + + mReportTypeMap.put(context.getString(R.string.title_balance_sheet_report), + BalanceSheetFragment.class); } /** @@ -83,8 +91,54 @@ public enum ReportType { } } + public static ReportType getReportType(final String name) { + + Context context = GnuCashApplication.getAppContext(); + + if (name.equals(context.getString(R.string.title_pie_chart))) { + + return PIE_CHART; + + } else if (name.equals(context.getString(R.string.title_bar_chart))) { + + return BAR_CHART; + + } else if (name.equals(context.getString(R.string.title_cash_flow_report))) { + + return LINE_CHART; + + } else if (name.equals(context.getString(R.string.title_balance_sheet_report))) { + + return TEXT; + + } else { + + return NONE; + } + } + public List getReportNames(){ - return new ArrayList<>(mReportTypeMap.keySet()); + + Context context = GnuCashApplication.getAppContext(); + + if (mReportNames == null) { + // + + // + mReportNames = new ArrayList(); + + mReportNames.add(context.getString(R.string.title_pie_chart)); + mReportNames.add(context.getString(R.string.title_bar_chart)); + mReportNames.add(context.getString(R.string.title_cash_flow_report)); + mReportNames.add(context.getString(R.string.title_balance_sheet_report)); + + } else { + // n' pas + + // RAF + } + + return mReportNames; } public BaseReportFragment getFragment(String name){ 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 index d8750a9a8..e2c478218 100644 --- a/app/src/main/java/org/gnucash/android/ui/report/ReportsActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/report/ReportsActivity.java @@ -32,10 +32,12 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.DatePicker; import android.widget.Spinner; +import android.widget.TextView; import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; @@ -76,9 +78,12 @@ public class ReportsActivity extends BaseDrawerActivity implements AdapterView.O }; private static final String STATE_REPORT_TYPE = "STATE_REPORT_TYPE"; - @BindView(R.id.time_range_spinner) Spinner mTimeRangeSpinner; - @BindView(R.id.report_account_type_spinner) Spinner mAccountTypeSpinner; - @BindView(R.id.toolbar_spinner) Spinner mReportTypeSpinner; + @BindView(R.id.time_range_spinner) + Spinner mTimeRangeSpinner; + @BindView(R.id.report_account_type_spinner) + Spinner mAccountTypeSpinner; + @BindView(R.id.toolbar_spinner) + Spinner mReportsToolbarSpinner; private TransactionsDbAdapter mTransactionsDbAdapter; private AccountType mAccountType = AccountType.EXPENSE; @@ -202,14 +207,64 @@ public void updateReportTypeSpinner(ReportType reportType, String reportName) { mReportType = reportType; ActionBar actionBar = getSupportActionBar(); assert actionBar != null; - ArrayAdapter arrayAdapter = new ArrayAdapter<>(actionBar.getThemedContext(), - android.R.layout.simple_list_item_1, - mReportType.getReportNames()); + + ArrayAdapter arrayAdapter = new ArrayAdapter(actionBar.getThemedContext(), + R.layout.toolbar_spinner_selected_item, + mReportType.getReportNames()) { + /** + * @param position + * @param convertView + * @param parent + * + * @return + */ + @Override + public View getView(final int position, + final View convertView, + final ViewGroup parent) { + + View view = super.getView(position, + convertView, + parent); + + if (parent.getId() != R.id.toolbar_spinner) { + // Parent view is not the Toolbar Spinner + + // + // Set item text color according to Report's type + // + + TextView reportTextView = (TextView) view.findViewById(android.R.id.text1); + + if (reportTextView != null) { + // + + String reportName = (String) getItem(position); + + final ReportType reportType = ReportType.getReportType(reportName); + + reportTextView.setTextColor(getResources().getColor(reportType.getTitleColor())); + + } else { + // n' pas + + // RAF + } + + } else { + // Parent view is the Toolbar + + // NTD (White by default) + } + + return view; + } + }; mSkipNextReportTypeSelectedRun = true; //selection event will be fired again - mReportTypeSpinner.setAdapter(arrayAdapter); - mReportTypeSpinner.setSelection(arrayAdapter.getPosition(reportName)); - mReportTypeSpinner.setOnItemSelectedListener(mReportTypeSelectedListener); + mReportsToolbarSpinner.setAdapter(arrayAdapter); + mReportsToolbarSpinner.setSelection(arrayAdapter.getPosition(reportName)); + mReportsToolbarSpinner.setOnItemSelectedListener(mReportTypeSelectedListener); toggleToolbarTitleVisibility(); @@ -220,9 +275,9 @@ public void toggleToolbarTitleVisibility() { assert actionBar != null; if (mReportType == ReportType.NONE){ - mReportTypeSpinner.setVisibility(View.GONE); + mReportsToolbarSpinner.setVisibility(View.GONE); } else { - mReportTypeSpinner.setVisibility(View.VISIBLE); + mReportsToolbarSpinner.setVisibility(View.VISIBLE); } actionBar.setDisplayShowTitleEnabled(mReportType == ReportType.NONE); } diff --git a/app/src/main/java/org/gnucash/android/ui/report/barchart/StackedBarChartFragment.java b/app/src/main/java/org/gnucash/android/ui/report/barchart/StackedBarChartFragment.java index 5556c7896..ef501ea3f 100644 --- a/app/src/main/java/org/gnucash/android/ui/report/barchart/StackedBarChartFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/report/barchart/StackedBarChartFragment.java @@ -81,7 +81,7 @@ public class StackedBarChartFragment extends BaseReportFragment { @Override public int getTitle() { - return R.string.title_cash_flow_report; + return R.string.title_bar_chart; } @Override diff --git a/app/src/main/java/org/gnucash/android/ui/settings/GeneralPreferenceFragment.java b/app/src/main/java/org/gnucash/android/ui/settings/GeneralPreferenceFragment.java index db07e2753..027a1e731 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/GeneralPreferenceFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/GeneralPreferenceFragment.java @@ -57,6 +57,7 @@ public class GeneralPreferenceFragment extends PreferenceFragmentCompat implemen @Override public void onCreatePreferences(Bundle bundle, String s) { + addPreferencesFromResource(R.xml.fragment_general_preferences); } @@ -107,24 +108,77 @@ public boolean onPreferenceClick(Preference preference) { } @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - if (preference.getKey().equals(getString(R.string.key_enable_passcode))) { + public boolean onPreferenceChange(Preference preference, + Object newValue) { + + // + // Set Preference : enable_passcode + // + + if (preference.getKey() + .equals(getString(R.string.key_enable_passcode))) { + if ((Boolean) newValue) { - startActivityForResult(new Intent(getActivity(), PasscodePreferenceActivity.class), - GeneralPreferenceFragment.PASSCODE_REQUEST_CODE); + + startActivityForResult(new Intent(getActivity(), + PasscodePreferenceActivity.class), + GeneralPreferenceFragment.PASSCODE_REQUEST_CODE); + } else { - Intent passIntent = new Intent(getActivity(), PasscodeLockScreenActivity.class); - passIntent.putExtra(UxArgument.DISABLE_PASSCODE, UxArgument.DISABLE_PASSCODE); - startActivityForResult(passIntent, GeneralPreferenceFragment.REQUEST_DISABLE_PASSCODE); + + Intent passIntent = new Intent(getActivity(), + PasscodeLockScreenActivity.class); + passIntent.putExtra(UxArgument.DISABLE_PASSCODE, + UxArgument.DISABLE_PASSCODE); + startActivityForResult(passIntent, + GeneralPreferenceFragment.REQUEST_DISABLE_PASSCODE); } } - if (preference.getKey().equals(getString(R.string.key_use_account_color))) { + // + // Set Preference : use_color_in_reports + // + + if (preference.getKey() + .equals(getString(R.string.key_use_account_color))) { + + getPreferenceManager().getSharedPreferences() + .edit() + .putBoolean(getString(R.string.key_use_account_color), + Boolean.valueOf(newValue.toString())) + .commit(); + } + + // + // Set Preference : key_account_searchable_spinner_openkeyboard + // + + if (preference.getKey() + .equals(getString(R.string.key_shall_open_keyboard_in_account_searchable_spinner))) { + + // Store the new value of the Preference + getPreferenceManager().getSharedPreferences() + .edit() + .putBoolean(getString(R.string.key_shall_open_keyboard_in_account_searchable_spinner), + Boolean.valueOf(newValue.toString())) + .commit(); + } + + // + // Set Preference : use_color_in_account_list + // + + if (preference.getKey() + .equals(getString(R.string.key_use_color_in_account_list))) { + + // Store the new value of the Preference getPreferenceManager().getSharedPreferences() - .edit() - .putBoolean(getString(R.string.key_use_account_color), Boolean.valueOf(newValue.toString())) - .commit(); + .edit() + .putBoolean(getString(R.string.key_use_color_in_account_list), + Boolean.valueOf(newValue.toString())) + .commit(); } + return true; } diff --git a/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledActionsListFragment.java b/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledActionsListFragment.java index 01f25340e..abf75287d 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledActionsListFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/ScheduledActionsListFragment.java @@ -25,6 +25,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import android.support.v4.app.ListFragment; import android.support.v4.app.LoaderManager; @@ -78,6 +79,7 @@ public class ScheduledActionsListFragment extends ListFragment implements * Logging tag */ protected static final String TAG = "ScheduledActionFragment"; + private static final String FRAGMENT_ACTION_KEY = "action_key"; private TransactionsDbAdapter mTransactionsDbAdapter; private SimpleCursorAdapter mCursorAdapter; @@ -175,6 +177,10 @@ public static Fragment getInstance(ScheduledAction.ActionType actionType){ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + if(savedInstanceState != null && savedInstanceState.containsKey(FRAGMENT_ACTION_KEY)) { + mActionType = (ScheduledAction.ActionType) savedInstanceState.getSerializable(FRAGMENT_ACTION_KEY); + } + mTransactionsDbAdapter = TransactionsDbAdapter.getInstance(); switch (mActionType){ case TRANSACTION: @@ -641,5 +647,10 @@ public Cursor loadInBackground() { } } + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putSerializable(FRAGMENT_ACTION_KEY, mActionType); + super.onSaveInstanceState(outState); + } } 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 index 1494b16c7..d2fc1b86f 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/SplitEditorFragment.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/SplitEditorFragment.java @@ -60,6 +60,7 @@ 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.AccountUtils; import org.gnucash.android.ui.util.widget.CalculatorEditText; import org.gnucash.android.ui.util.widget.CalculatorKeyboard; import org.gnucash.android.ui.util.widget.TransactionTypeSwitch; @@ -197,13 +198,23 @@ public boolean onOptionsItemSelected(MenuItem item) { * @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; + + View splitEntryView = layoutInflater.inflate(R.layout.item_split_entry, + mSplitsLinearLayout, + false); + + // Respect sort list order + mSplitsLinearLayout.addView(splitEntryView); + + SplitViewHolder viewHolder = new SplitViewHolder(splitEntryView, + split); + splitEntryView.setTag(viewHolder); + + mSplitItemViewList.add(splitEntryView); + + return splitEntryView; } /** @@ -216,11 +227,12 @@ private void initArgs() { mAccountUID = ((FormActivity) getActivity()).getCurrentAccountUID(); mBaseAmount = new BigDecimal(args.getString(UxArgument.AMOUNT_STRING)); - String conditions = "(" + String where = "(" + DatabaseSchema.AccountEntry.COLUMN_HIDDEN + " = 0 AND " + DatabaseSchema.AccountEntry.COLUMN_PLACEHOLDER + " = 0" + ")"; - mCursor = mAccountsDbAdapter.fetchAccountsOrderedByFullName(conditions, null); + mCursor = mAccountsDbAdapter.fetchAccountsOrderedByFavoriteAndFullName(where, null); + mCommodity = CommoditiesDbAdapter.getInstance().getCommodity(mAccountsDbAdapter.getCurrencyCode(mAccountUID)); } @@ -228,13 +240,20 @@ private void initArgs() { * Holds a split item view and binds the items in it */ class SplitViewHolder implements OnTransferFundsListener{ - @BindView(R.id.input_split_memo) EditText splitMemoEditText; - @BindView(R.id.input_split_amount) CalculatorEditText splitAmountEditText; - @BindView(R.id.btn_remove_split) ImageView removeSplitButton; - @BindView(R.id.input_accounts_spinner) Spinner accountsSpinner; - @BindView(R.id.split_currency_symbol) TextView splitCurrencyTextView; - @BindView(R.id.split_uid) TextView splitUidTextView; - @BindView(R.id.btn_split_type) TransactionTypeSwitch splitTypeSwitch; + @BindView(R.id.split_currency_symbol) + TextView splitCurrencyTextView; + @BindView(R.id.input_split_amount) + CalculatorEditText splitAmountEditText; + @BindView(R.id.btn_split_type) + TransactionTypeSwitch splitTypeSwitch; + @BindView(R.id.btn_remove_split) + ImageView removeSplitButton; + @BindView(R.id.input_accounts_spinner) + Spinner accountsSpinner; + @BindView(R.id.input_split_memo) + EditText splitMemoEditText; + @BindView(R.id.split_uid) + TextView splitUidTextView; View splitView; Money quantity; @@ -267,8 +286,13 @@ public void onClick(View view) { updateTransferAccountsList(accountsSpinner); splitCurrencyTextView.setText(mCommodity.getSymbol()); + + // Set an amount formatting listener splitTypeSwitch.setAmountFormattingListener(splitAmountEditText, splitCurrencyTextView); + + // Switch on/off according to amount signum splitTypeSwitch.setChecked(mBaseAmount.signum() > 0); + splitUidTextView.setText(BaseModel.generateUID()); if (split != null) { @@ -340,7 +364,15 @@ private void setSelectedTransferAccount(long accountId, final Spinner accountsSp * Only accounts with the same currency can be transferred to */ private void updateTransferAccountsList(Spinner transferAccountSpinner){ - mCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), mCursor); + + // In Splits, an account is allowed to appear many times, therefore there is no restriction on uid + String where = AccountUtils.getTransfertAccountWhereClause(null); + + mCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), + mCursor, + where, + null); + transferAccountSpinner.setAdapter(mCursorAdapter); } @@ -422,12 +454,17 @@ public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { @Override public void afterTextChanged(Editable editable) { + + BigDecimal imbalance = BigDecimal.ZERO; for (View splitItem : mSplitItemViewList) { SplitViewHolder viewHolder = (SplitViewHolder) splitItem.getTag(); + BigDecimal amount = viewHolder.getAmountValue().abs(); + long accountId = viewHolder.accountsSpinner.getSelectedItemId(); + boolean hasDebitNormalBalance = AccountsDbAdapter.getInstance() .getAccountType(accountId).hasDebitNormalBalance(); @@ -436,16 +473,20 @@ public void afterTextChanged(Editable editable) { imbalance = imbalance.add(amount); else imbalance = imbalance.subtract(amount); + } else { if (hasDebitNormalBalance) imbalance = imbalance.subtract(amount); else imbalance = imbalance.add(amount); + } } - TransactionsActivity.displayBalance(mImbalanceTextView, new Money(imbalance, mCommodity)); + TransactionsActivity.displayBalance(mImbalanceTextView, + new Money(imbalance, + mCommodity)); } } 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 index 44771f872..e498ae4cd 100644 --- a/app/src/main/java/org/gnucash/android/ui/transaction/TransactionDetailActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/transaction/TransactionDetailActivity.java @@ -25,6 +25,7 @@ import org.gnucash.android.ui.common.FormActivity; import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.passcode.PasscodeLockActivity; +import org.gnucash.android.ui.util.AccountUtils; import java.text.DateFormat; import java.util.Date; @@ -99,15 +100,30 @@ class SplitAmountViewHolder { View itemView; - public SplitAmountViewHolder(View view, Split split){ + public SplitAmountViewHolder(View view, + Split split) { + itemView = view; - ButterKnife.bind(this, 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); + + // Set color according to Account + AccountUtils.setAccountTextColor(accountName, + split.getAccountUID()); + + + Money quantity = split.getFormattedQuantity(); + TextView balanceView = quantity.isNegative() + ? splitDebit + : splitCredit; + + TransactionsActivity.displayBalance(balanceView, + quantity); } } 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 ad9f36e03..1203d43fc 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 @@ -18,6 +18,7 @@ import android.app.Activity; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; @@ -46,7 +47,6 @@ import android.widget.EditText; import android.widget.FilterQueryProvider; import android.widget.ImageView; -import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; @@ -78,10 +78,13 @@ import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; import org.gnucash.android.ui.settings.PreferenceActivity; import org.gnucash.android.ui.transaction.dialog.TransferFundsDialogFragment; +import org.gnucash.android.ui.util.AccountUtils; import org.gnucash.android.ui.util.RecurrenceParser; import org.gnucash.android.ui.util.RecurrenceViewClickListener; import org.gnucash.android.ui.util.widget.CalculatorEditText; import org.gnucash.android.ui.util.widget.TransactionTypeSwitch; +import org.gnucash.android.ui.util.widget.searchablespinner.SearchableSpinnerView; +import org.gnucash.android.util.KeyboardUtils; import org.gnucash.android.util.QualifiedAccountNameCursorAdapter; import java.math.BigDecimal; @@ -96,6 +99,9 @@ import butterknife.BindView; import butterknife.ButterKnife; +import static org.gnucash.android.R.id.secondary_text; +import static org.gnucash.android.util.QualifiedAccountNameCursorAdapter.hideFavoriteAccountStarIcon; + /** * Fragment for creating or editing transactions * @author Ngewi Fet @@ -180,7 +186,8 @@ public class TransactionFormFragment extends Fragment implements /** * Spinner for selecting the transfer account */ - @BindView(R.id.input_transfer_account_spinner) Spinner mTransferAccountSpinner; + @BindView(R.id.input_transfer_account_spinner) + SearchableSpinnerView mTransferAccountSearchableSpinnerView; /** * Checkbox indicating if this transaction should be saved as a template or not @@ -251,26 +258,35 @@ public class TransactionFormFragment extends Fragment implements * 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_transaction_form, container, false); - ButterKnife.bind(this, v); + public View onCreateView(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) { + + 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; - } + } /** * Starts the transfer of funds from one currency to another */ private void startTransferFunds() { Commodity fromCommodity = Commodity.getInstance((mTransactionsDbAdapter.getAccountCurrencyCode(mAccountUID))); - long id = mTransferAccountSpinner.getSelectedItemId(); + long id = mTransferAccountSearchableSpinnerView.getSelectedItemId(); String targetCurrencyCode = mAccountsDbAdapter.getCurrencyCode(mAccountsDbAdapter.getUID(id)); if (fromCommodity.equals(Commodity.getInstance(targetCurrencyCode)) @@ -296,7 +312,9 @@ public void onConfigurationChanged(Configuration newConfig) { @Override public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setHasOptionsMenu(true); SharedPreferences sharedPrefs = PreferenceActivity.getActiveBookSharedPreferences(); @@ -318,9 +336,17 @@ public void onActivityCreated(Bundle savedInstanceState) { } setListeners(); + + // + // mTransferAccountSearchableSpinnerView + // + //updateTransferAccountsList must only be called after initializing mAccountsDbAdapter updateTransferAccountsList(); - mTransferAccountSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + + mTransferAccountSearchableSpinnerView.setTitle(getString(R.string.select_account)); + + mTransferAccountSearchableSpinnerView.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 @@ -328,29 +354,43 @@ public void onActivityCreated(Bundle savedInstanceState) { boolean userInteraction = false; @Override - public void onItemSelected(AdapterView adapterView, View view, int position, long id) { - removeFavoriteIconFromSelectedView((TextView) view); + public void onItemSelected(AdapterView adapterView, + View spinnerSelectedItemView, + int position, + long id) { + + hideFavoriteAccountStarIcon(spinnerSelectedItemView); + + 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)) { - split.setAccountUID(mAccountsDbAdapter.getUID(id)); + + final String accountUID = mAccountsDbAdapter.getUID(id); + + split.setAccountUID(accountUID); + + // + // Set Account Color + // + + TextView accountFullNameTextView = (TextView) spinnerSelectedItemView.findViewById(android.R.id.text1); + + AccountUtils.setAccountTextColor(accountFullNameTextView, + accountUID); } // else case is handled when saving the transactions } } + if (!userInteraction) { userInteraction = true; return; } - startTransferFunds(); - } - // Removes the icon from view to avoid visual clutter - private void removeFavoriteIconFromSelectedView(TextView view) { - if (view != null) { - view.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); - } + startTransferFunds(); } @Override @@ -367,11 +407,18 @@ public void onNothingSelected(AdapterView adapterView) { actionBar.setTitle(R.string.title_add_transaction); initalizeViews(); initTransactionNameAutocomplete(); + } else { actionBar.setTitle(R.string.title_edit_transaction); - initializeViewsWithTransaction(); + initializeViewsWithTransaction(); mEditMode = true; - } + } + + // Set Focus onto Amount at first +// mDescriptionEditText.clearFocus(); + KeyboardUtils.hideKeyboard(mDescriptionEditText, + 200); + mAmountEditText.requestFocus(); getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); } @@ -387,16 +434,28 @@ public DropDownCursorAdapter(Context context, int layout, Cursor c, String[] fro } @Override - public void bindView(View view, Context context, Cursor cursor) { - super.bindView(view, context, cursor); + 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 balance = TransactionsDbAdapter.getInstance().getBalance(transactionUID, mAccountUID); + + Money balance = TransactionsDbAdapter.getInstance() + .getBalance(transactionUID, + mAccountUID); long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(DatabaseSchema.TransactionEntry.COLUMN_TIMESTAMP)); - String dateString = DateUtils.formatDateTime(getActivity(), timestamp, - DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR); + String dateString = DateUtils.formatDateTime(getActivity(), + timestamp, + DateUtils.FORMAT_SHOW_WEEKDAY + | DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_SHOW_YEAR); - TextView secondaryTextView = (TextView) view.findViewById(R.id.secondary_text); + TextView secondaryTextView = (TextView) view.findViewById(secondary_text); secondaryTextView.setText(balance.formattedString() + " on " + dateString); //TODO: Extract string } } @@ -585,18 +644,24 @@ private void initalizeViews() { * Only accounts with the same currency can be transferred to */ private void updateTransferAccountsList(){ - String conditions = "(" + DatabaseSchema.AccountEntry.COLUMN_UID + " != ?" - + " AND " + DatabaseSchema.AccountEntry.COLUMN_TYPE + " != ?" - + " AND " + DatabaseSchema.AccountEntry.COLUMN_PLACEHOLDER + " = 0" - + ")"; + + // Get Accounts that are not hidden, nor Placeholder, nor root Account, nor the Account itself + String where = AccountUtils.getTransfertAccountWhereClause(mAccountUID); if (mCursor != null) { mCursor.close(); } - mCursor = mAccountsDbAdapter.fetchAccountsOrderedByFavoriteAndFullName(conditions, new String[]{mAccountUID, AccountType.ROOT.name()}); - mAccountCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), mCursor); - mTransferAccountSpinner.setAdapter(mAccountCursorAdapter); + mCursor = mAccountsDbAdapter.fetchAccountsOrderedByFavoriteAndFullName(where, + null); + + mAccountCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), + mCursor, + where, + null, + R.layout.account_spinner_dropdown_item); + + mTransferAccountSearchableSpinnerView.setAdapter(mAccountCursorAdapter); } /** @@ -692,10 +757,13 @@ public void onClick(View v) { * @param accountId Database ID of the transfer account */ private void setSelectedTransferAccount(long accountId){ - int position = mAccountCursorAdapter.getPosition(mAccountsDbAdapter.getUID(accountId)); + + final String accountUID = mAccountsDbAdapter.getUID(accountId); + + int position = mAccountCursorAdapter.getPosition(accountUID); if (position >= 0) - mTransferAccountSpinner.setSelection(position); - } + mTransferAccountSearchableSpinnerView.setSelection(position); + } /** * Returns a list of splits based on the input in the transaction form. @@ -766,7 +834,7 @@ private List extractSplitsFromView(){ private @NonNull String getTransferAccountUID() { String transferAcctUID; if (mUseDoubleEntry) { - long transferAcctId = mTransferAccountSpinner.getSelectedItemId(); + long transferAcctId = mTransferAccountSearchableSpinnerView.getSelectedItemId(); transferAcctUID = mAccountsDbAdapter.getUID(transferAcctId); } else { Commodity baseCommodity = mAccountsDbAdapter.getRecord(mAccountUID).getCommodity(); @@ -823,7 +891,7 @@ private boolean isMultiCurrencyTransaction(){ if (!mUseDoubleEntry) return false; - String transferAcctUID = mAccountsDbAdapter.getUID(mTransferAccountSpinner.getSelectedItemId()); + String transferAcctUID = mAccountsDbAdapter.getUID(mTransferAccountSearchableSpinnerView.getSelectedItemId()); String currencyCode = mAccountsDbAdapter.getAccountCurrencyCode(mAccountUID); String transferCurrencyCode = mAccountsDbAdapter.getCurrencyCode(transferAcctUID); @@ -949,7 +1017,7 @@ public boolean onOptionsItemSelected(MenuItem item) { if (mAmountEditText.getValue() == null) { Toast.makeText(getActivity(), R.string.toast_transanction_amount_required, Toast.LENGTH_SHORT).show(); } - if (mUseDoubleEntry && mTransferAccountSpinner.getCount() == 0){ + if (mUseDoubleEntry && mTransferAccountSearchableSpinnerView.getCount() == 0){ Toast.makeText(getActivity(), R.string.toast_disable_double_entry_to_save_transaction, Toast.LENGTH_LONG).show(); @@ -969,7 +1037,7 @@ public boolean onOptionsItemSelected(MenuItem item) { */ private boolean canSave(){ return (mUseDoubleEntry && mAmountEditText.isInputValid() - && mTransferAccountSpinner.getCount() > 0) + && mTransferAccountSearchableSpinnerView.getCount() > 0) || (!mUseDoubleEntry && mAmountEditText.isInputValid()); } 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 fb4ba91ef..4475f7203 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 @@ -18,8 +18,10 @@ package org.gnucash.android.ui.transaction; import android.content.Context; +import android.content.DialogInterface; 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; @@ -40,7 +42,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; -import android.widget.Spinner; import android.widget.SpinnerAdapter; import android.widget.TextView; @@ -50,6 +51,7 @@ import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.db.adapter.TransactionsDbAdapter; import org.gnucash.android.model.Account; +import org.gnucash.android.model.AccountType; import org.gnucash.android.model.Money; import org.gnucash.android.ui.account.AccountsActivity; import org.gnucash.android.ui.account.AccountsListFragment; @@ -59,6 +61,7 @@ import org.gnucash.android.ui.common.Refreshable; import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.util.AccountBalanceTask; +import org.gnucash.android.ui.util.widget.searchablespinner.SearchableSpinnerView; import org.gnucash.android.util.QualifiedAccountNameCursorAdapter; import org.joda.time.LocalDate; @@ -68,6 +71,8 @@ import butterknife.BindView; +import static org.gnucash.android.util.QualifiedAccountNameCursorAdapter.hideFavoriteAccountStarIcon; + /** * Activity for displaying, creating and editing transactions * @author Ngewi Fet @@ -78,7 +83,7 @@ public class TransactionsActivity extends BaseDrawerActivity implements /** * Logging tag */ - protected static final String TAG = "TransactionsActivity"; + protected static final String LOG_TAG = "TransactionsActivity"; /** * ViewPager index for sub-accounts fragment @@ -111,16 +116,17 @@ public class TransactionsActivity extends BaseDrawerActivity implements */ private Cursor mAccountsCursor = null; - @BindView(R.id.pager) ViewPager mViewPager; - @BindView(R.id.toolbar_spinner) Spinner mToolbarSpinner; - @BindView(R.id.tab_layout) TabLayout mTabLayout; - @BindView(R.id.transactions_sum) TextView mSumTextView; - @BindView(R.id.fab_create_transaction) FloatingActionButton mCreateFloatingButton; + @BindView(R.id.pager) ViewPager mViewPager; + @BindView(R.id.toolbar_spinner) + SearchableSpinnerView mToolbarSpinner; + @BindView(R.id.tab_layout) TabLayout mTabLayout; + @BindView(R.id.transactions_sum) TextView mSumTextView; + @BindView(R.id.fab_create_transaction)FloatingActionButton mCreateFloatingButton; private SparseArray mFragmentPageReferenceMap = new SparseArray<>(); /** - * Flag for determining is the currently displayed account is a placeholder account or not. + * Flag for determining if the currently displayed account is a placeholder account or not. * This will determine if the transactions tab is displayed or not */ private boolean mIsPlaceholderAccount; @@ -128,10 +134,18 @@ public class TransactionsActivity extends BaseDrawerActivity implements private AdapterView.OnItemSelectedListener mTransactionListNavigationListener = new AdapterView.OnItemSelectedListener() { @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { + public void onItemSelected(AdapterView parent, View spinnerSelectedItemView, 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); + getIntent().putExtra(UxArgument.SELECTED_ACCOUNT_UID, + getCurrentAccountUID()); //update the intent in case the account gets rotated + + // + // Show Transaction Page if not a PlaceHolder, hide otherwise + // + + mIsPlaceholderAccount = mAccountsDbAdapter.isPlaceholderAccount(getCurrentAccountUID()); + if (mIsPlaceholderAccount){ if (mTabLayout.getTabCount() > 1) { mPagerAdapter.notifyDataSetChanged(); @@ -143,10 +157,11 @@ public void onItemSelected(AdapterView parent, View view, int position, long mTabLayout.addTab(mTabLayout.newTab().setText(R.string.section_header_transactions)); } } - if (view != null) { - // Hide the favorite icon of the selected account to avoid clutter - ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); - } + + // Hide star in ToolBar Spinner +// TextView text1 = (TextView) selectedItemView.findViewById(android.R.id.text1); + hideFavoriteAccountStarIcon(spinnerSelectedItemView ); + //refresh any fragments in the tab with the new account UID refresh(); } @@ -157,6 +172,16 @@ public void onNothingSelected(AdapterView parent) { } }; + private DialogInterface.OnClickListener mSearchableSpinnerPositiveBtnOnClickListener = new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + + // NTD + } + }; + private PagerAdapter mPagerAdapter; @@ -229,7 +254,8 @@ public int getCount() { private AccountsListFragment prepareSubAccountsListFragment(){ AccountsListFragment subAccountsListFragment = new AccountsListFragment(); Bundle args = new Bundle(); - args.putString(UxArgument.PARENT_ACCOUNT_UID, mAccountUID); + args.putString(UxArgument.PARENT_ACCOUNT_UID, + getCurrentAccountUID()); subAccountsListFragment.setArguments(args); return subAccountsListFragment; } @@ -238,12 +264,17 @@ private AccountsListFragment prepareSubAccountsListFragment(){ * Creates and initializes fragment for displaying transactions * @return {@link TransactionsListFragment} initialized with the current account transactions */ - private TransactionsListFragment prepareTransactionsListFragment(){ + private TransactionsListFragment prepareTransactionsListFragment() { + TransactionsListFragment transactionsListFragment = new TransactionsListFragment(); - Bundle args = new Bundle(); - args.putString(UxArgument.SELECTED_ACCOUNT_UID, mAccountUID); + Bundle args = new Bundle(); + args.putString(UxArgument.SELECTED_ACCOUNT_UID, + getCurrentAccountUID()); transactionsListFragment.setArguments(args); - Log.i(TAG, "Opening transactions for account: " + mAccountUID); + + Log.i(LOG_TAG, + "Opening transactions for account: " + getCurrentAccountUID()); + return transactionsListFragment; } } @@ -260,13 +291,15 @@ public void refresh(String accountUID) { if (mPagerAdapter != null) mPagerAdapter.notifyDataSetChanged(); - new AccountBalanceTask(mSumTextView).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mAccountUID); + new AccountBalanceTask(mSumTextView).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, + getCurrentAccountUID()); } @Override public void refresh(){ - refresh(mAccountUID); + + refresh(getCurrentAccountUID()); setTitleIndicatorColor(); } @@ -282,6 +315,7 @@ public int getTitleRes() { @Override protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); getSupportActionBar().setDisplayShowTitleEnabled(false); @@ -289,7 +323,11 @@ protected void onCreate(Bundle savedInstanceState) { mAccountUID = getIntent().getStringExtra(UxArgument.SELECTED_ACCOUNT_UID); mAccountsDbAdapter = AccountsDbAdapter.getInstance(); - mIsPlaceholderAccount = mAccountsDbAdapter.isPlaceholderAccount(mAccountUID); + // + // Add Tranbsaction Page + // + + mIsPlaceholderAccount = mAccountsDbAdapter.isPlaceholderAccount(getCurrentAccountUID()); mTabLayout.addTab(mTabLayout.newTab().setText(R.string.section_header_subaccounts)); if (!mIsPlaceholderAccount) { @@ -320,8 +358,9 @@ public void onTabReselected(TabLayout.Tab tab) { }); //if there are no transactions, and there are sub-accounts, show the sub-accounts - if (TransactionsDbAdapter.getInstance().getTransactionsCount(mAccountUID) == 0 - && mAccountsDbAdapter.getSubAccountCount(mAccountUID) > 0){ + if (TransactionsDbAdapter.getInstance() + .getTransactionsCount(getCurrentAccountUID()) == 0 + && mAccountsDbAdapter.getSubAccountCount(getCurrentAccountUID()) > 0) { mViewPager.setCurrentItem(INDEX_SUB_ACCOUNTS_FRAGMENT); } else { mViewPager.setCurrentItem(INDEX_TRANSACTIONS_FRAGMENT); @@ -335,13 +374,14 @@ public void onClick(View v) { 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); + addAccountIntent.putExtra(UxArgument.PARENT_ACCOUNT_UID, + getCurrentAccountUID()); startActivityForResult(addAccountIntent, AccountsActivity.REQUEST_EDIT_ACCOUNT); ; break; case INDEX_TRANSACTIONS_FRAGMENT: - createNewTransaction(mAccountUID); + createNewTransaction(getCurrentAccountUID()); break; } @@ -359,7 +399,8 @@ protected void onResume() { * Sets the color for the ViewPager title indicator to match the account color */ private void setTitleIndicatorColor() { - int iColor = AccountsDbAdapter.getActiveAccountColorResource(mAccountUID); + + int iColor = AccountsDbAdapter.getActiveAccountColorResource(getCurrentAccountUID()); mTabLayout.setBackgroundColor(iColor); @@ -374,40 +415,51 @@ private void setTitleIndicatorColor() { * Set up action bar navigation list and listener callbacks */ private void setupActionBarNavigation() { + + // // set up spinner adapter for navigation list + // + if (mAccountsCursor != null) { mAccountsCursor.close(); } - mAccountsCursor = mAccountsDbAdapter.fetchAllRecordsOrderedByFullName(); + mAccountsCursor = mAccountsDbAdapter.fetchAccountsOrderedByFavoriteAndFullName(); + + SpinnerAdapter qualifiedAccountNameCursorAdapter = new QualifiedAccountNameCursorAdapter(getSupportActionBar().getThemedContext(), + getAccountsCursor(), + AccountsDbAdapter.WHERE_NOT_HIDDEN_AND_NOT_ROOT_ACCOUNT, + new String[]{AccountType.ROOT.name()}, + R.layout.toolbar_spinner_selected_item); - SpinnerAdapter mSpinnerAdapter = new QualifiedAccountNameCursorAdapter( - getSupportActionBar().getThemedContext(), mAccountsCursor, R.layout.account_spinner_item); + mToolbarSpinner.setAdapter(qualifiedAccountNameCursorAdapter); - mToolbarSpinner.setAdapter(mSpinnerAdapter); mToolbarSpinner.setOnItemSelectedListener(mTransactionListNavigationListener); + + mToolbarSpinner.setTitle(getString(R.string.select_account)); + + // The "positive" button act as a Cancel button + mToolbarSpinner.setPositiveButton(getString(R.string.alert_dialog_cancel), + mSearchableSpinnerPositiveBtnOnClickListener); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); - updateNavigationSelection(); + selectCurrentAccountInToolbarSpinner(); } /** * Updates the action bar navigation list selection to that of the current account * whose transactions are being displayed/manipulated */ - public void updateNavigationSelection() { - // set the selected item in the spinner - int i = 0; - Cursor accountsCursor = mAccountsDbAdapter.fetchAllRecordsOrderedByFullName(); - while (accountsCursor.moveToNext()) { - String uid = accountsCursor.getString(accountsCursor.getColumnIndexOrThrow(DatabaseSchema.AccountEntry.COLUMN_UID)); - if (mAccountUID.equals(uid)) { - mToolbarSpinner.setSelection(i); - break; - } - ++i; - } + public void selectCurrentAccountInToolbarSpinner() { + + Cursor accountsCursor = mAccountsDbAdapter.fetchAccountsOrderedByFavoriteAndFullName(); + + SearchableSpinnerView.selectSpinnerAccount(accountsCursor, + getCurrentAccountUID(), + mToolbarSpinner); + accountsCursor.close(); - } + } @Override public boolean onPrepareOptionsMenu(Menu menu) { @@ -416,7 +468,8 @@ public boolean onPrepareOptionsMenu(Menu menu) { if (favoriteAccountMenuItem == null) //when the activity is used to edit a transaction return super.onPrepareOptionsMenu(menu); - boolean isFavoriteAccount = AccountsDbAdapter.getInstance().isFavoriteAccount(mAccountUID); + boolean isFavoriteAccount = AccountsDbAdapter.getInstance() + .isFavoriteAccount(getCurrentAccountUID()); int favoriteIcon = isFavoriteAccount ? R.drawable.ic_star_white_24dp : R.drawable.ic_star_border_white_24dp; favoriteAccountMenuItem.setIcon(favoriteIcon); @@ -432,8 +485,8 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.menu_favorite_account: AccountsDbAdapter accountsDbAdapter = AccountsDbAdapter.getInstance(); - long accountId = accountsDbAdapter.getID(mAccountUID); - boolean isFavorite = accountsDbAdapter.isFavoriteAccount(mAccountUID); + long accountId = accountsDbAdapter.getID(getCurrentAccountUID()); + boolean isFavorite = accountsDbAdapter.isFavoriteAccount(getCurrentAccountUID()); //toggle favorite preference accountsDbAdapter.updateAccount(accountId, DatabaseSchema.AccountEntry.COLUMN_FAVORITE, isFavorite ? "0" : "1"); supportInvalidateOptionsMenu(); @@ -442,7 +495,8 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.menu_edit_account: Intent editAccountIntent = new Intent(this, FormActivity.class); editAccountIntent.setAction(Intent.ACTION_INSERT_OR_EDIT); - editAccountIntent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, mAccountUID); + editAccountIntent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, + getCurrentAccountUID()); editAccountIntent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.ACCOUNT.name()); startActivityForResult(editAccountIntent, AccountsActivity.REQUEST_EDIT_ACCOUNT); return true; @@ -465,7 +519,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { @Override protected void onDestroy() { super.onDestroy(); - mAccountsCursor.close(); + getAccountsCursor().close(); } /** @@ -476,6 +530,11 @@ public String getCurrentAccountUID(){ return mAccountUID; } + public Cursor getAccountsCursor() { + + return mAccountsCursor; + } + /** * 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 @@ -528,7 +587,8 @@ public void createNewTransaction(String accountUID) { public void editTransaction(String transactionUID){ 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_ACCOUNT_UID, + getCurrentAccountUID()); 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 9ea0847e9..3f718f69e 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 @@ -58,6 +58,7 @@ import org.gnucash.android.ui.homescreen.WidgetConfigurationActivity; import org.gnucash.android.ui.settings.PreferenceActivity; import org.gnucash.android.ui.transaction.dialog.BulkMoveDialogFragment; +import org.gnucash.android.ui.util.AccountUtils; import org.gnucash.android.ui.util.CursorRecyclerAdapter; import org.gnucash.android.ui.util.widget.EmptyRecyclerView; import org.gnucash.android.util.BackupManager; @@ -164,8 +165,13 @@ public void refresh(){ @Override public void onResume() { + super.onResume(); - ((TransactionsActivity)getActivity()).updateNavigationSelection(); + + // Select Current Account in Toolbar + ((TransactionsActivity)getActivity()).selectCurrentAccountInToolbarSpinner(); + + // Refresh Transaction List according to currently selected Account in Toolbar Spinner refresh(); } @@ -299,6 +305,11 @@ public void onClick(View v) { for (Split split : splits) { if (!split.getAccountUID().equals(mAccountUID)) { text = AccountsDbAdapter.getInstance().getFullyQualifiedAccountName(split.getAccountUID()); + + // Set color according to Account + AccountUtils.setAccountTextColor(holder.secondaryText, + split.getAccountUID()); + break; } } @@ -308,6 +319,7 @@ public void onClick(View v) { text = splits.size() + " splits"; } holder.secondaryText.setText(text); + holder.transactionDate.setText(dateText); holder.editTransaction.setOnClickListener(new View.OnClickListener() { diff --git a/app/src/main/java/org/gnucash/android/ui/util/AccountUtils.java b/app/src/main/java/org/gnucash/android/ui/util/AccountUtils.java new file mode 100644 index 000000000..b2e8c48a9 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/util/AccountUtils.java @@ -0,0 +1,93 @@ +package org.gnucash.android.ui.util; + +import android.support.v7.preference.PreferenceManager; +import android.widget.TextView; + +import org.gnucash.android.R; +import org.gnucash.android.db.DatabaseSchema; +import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.model.AccountType; + +/** + * Utilities for Accounts UI + * + * @author JeanGarf + */ +public class AccountUtils { + + /** + * Set text color according to account one + * if preference about using colors in account list is true + * + * @param accountTextView + * View containing text field to colorize + * + * @param accountUID + * Account UID + */ + public static void setAccountTextColor(final TextView accountTextView, + final String accountUID) { + + if (accountTextView != null) { + // accountTextView is not null + + // Get Preference about using colors in account list + boolean prefShallUseColorInAccountList = PreferenceManager.getDefaultSharedPreferences(accountTextView.getContext()) + .getBoolean(accountTextView.getContext() + .getString(R.string.key_use_color_in_account_list), + true); + + if (prefShallUseColorInAccountList) { + // Want to use colors for Accounts + + // Get Account color + int iColor = AccountsDbAdapter.getActiveAccountColorResource(accountUID); + + // Override color + accountTextView.setTextColor(iColor); + + } else { + // Do not want to use colors for Accounts + + // NTD + } + + } else { + // accountTextView is null + + // RAF + } + } + + /** + * Build the where clause to select Accounts allowed for Transfer + * for the given accountUID + * + * @param accountUID + * The account UID for which we want to collect account allowed for transfer + * May be null (to allow all non special accounts) + * + * @return + * the where clause + * + * @author JeanGarf + */ + public static String getTransfertAccountWhereClause(final String accountUID) { + + return "(" + + DatabaseSchema.AccountEntry.COLUMN_UID + + " != '" + + ((accountUID != null) ? accountUID : "") + + "' AND " + + DatabaseSchema.AccountEntry.COLUMN_TYPE + + " != '" + + AccountType.ROOT.name() + + "' AND " + + DatabaseSchema.AccountEntry.COLUMN_PLACEHOLDER + + " = 0" + + " AND " + + DatabaseSchema.AccountEntry.COLUMN_HIDDEN + + " = 0" + + ")"; + } +} diff --git a/app/src/main/java/org/gnucash/android/ui/util/widget/searchablespinner/ItemContainingTextFilter.java b/app/src/main/java/org/gnucash/android/ui/util/widget/searchablespinner/ItemContainingTextFilter.java new file mode 100644 index 000000000..c927e4c97 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/util/widget/searchablespinner/ItemContainingTextFilter.java @@ -0,0 +1,171 @@ +package org.gnucash.android.ui.util.widget.searchablespinner; + +import android.widget.Filter; + +import java.util.ArrayList; +import java.util.List; + +/** + * Generic filter that filters (it modifies it) the given list + * to retain items whose item contains a text to search + * + * The isFoundInItem() method can be overridden to change the search criteria + * The default search criteria consist to find the lower case text to search in + * the lower case of the item.toString() + * + * @author JeanGarf + */ +public class ItemContainingTextFilter + extends Filter { + + /** + * Copy of original all items list + */ + private List mOriginalNonFilteredItemsList; + + /** + * Pointer on Adapter's item list (which will be filtered) + */ + private List mAdaptersItemsList; + + /** + * Constructor + */ + public ItemContainingTextFilter(final List adaptersItemsList) { + + // Store a pointer to adapter's item list + setAdaptersItemsList(adaptersItemsList); + + // Create a second list which won't be filtered to store the original non filtered items + setOriginalNonFilteredItemsList(new ArrayList<>(adaptersItemsList)); + } + + + /** + * Build filtered results, which is a structure containing + * filtered items (whose text contains textToSearch) + * count of filtered items + * + * @param textToSearch + * text to search (in item.toString()), to retain item + * + * @return structure containing filtered items and count + */ + @Override + protected FilterResults performFiltering(CharSequence textToSearch) { + + final FilterResults filterResults = new FilterResults(); + + // Create a new list to store filtered items (the list points to the same items as the original's one, but not all the + // items + final List filteredItems = new ArrayList(); + + if (textToSearch == null || textToSearch.length() == 0) { + // Nothing to search + + // Create a new List pointing on the same items as the original one + // in order not to alter the original non filtered items list + filteredItems.addAll(getOriginalNonFilteredItemsList()); + + } else { + // There is something to search + + // + // Filter original items list + // + + final int count = getOriginalNonFilteredItemsList().size(); + + for (int i = 0; i < count; i++) { + + // Get item from original non filtered list + final T_ITEM item = getOriginalNonFilteredItemsList().get(i); + + final boolean isFoundInItem = isFoundInItem(textToSearch, + item); + + if (isFoundInItem) { + // It matches + + // Add it to filtered list + filteredItems.add(item); + + } else { + // It does not match + + // NTD + } + } // for + + } + + filterResults.values = filteredItems; + filterResults.count = filteredItems.size(); + + return filterResults; + } + + @Override + protected void publishResults(CharSequence constraint, + FilterResults filteredResults) { + + // Replace Adapter's items list with the filtered list + getAdaptersItemsList().clear(); + getAdaptersItemsList().addAll((List) filteredResults.values); + } + + // + // Methods to be overridden + // + + /** + * Return true if textToSearch has been found in item + *

+ * In this default implementation, the text is found if the lower case + * textToSearch is found in the lower case of the item.toString() string + * + * @param textToSearch + * @param item + * + * @return + * Return true if textToSearch has been found in item + */ + protected boolean isFoundInItem(final CharSequence textToSearch, + final T_ITEM item) { + + // get the item.toString() + final String itemTextLowerCase = item.toString() + .toLowerCase(); + + final String textToSearchLowerCase = textToSearch.toString() + .toLowerCase(); + + // First match against the whole, non-splitted value + return itemTextLowerCase.contains(textToSearchLowerCase); + } + + // + // Getters/Setters + // + + + protected List getOriginalNonFilteredItemsList() { + + return mOriginalNonFilteredItemsList; + } + + protected void setOriginalNonFilteredItemsList(final List originalNonFilteredItemsList) { + + mOriginalNonFilteredItemsList = originalNonFilteredItemsList; + } + + protected List getAdaptersItemsList() { + + return mAdaptersItemsList; + } + + protected void setAdaptersItemsList(final List adaptersItemsList) { + + mAdaptersItemsList = adaptersItemsList; + } +} diff --git a/app/src/main/java/org/gnucash/android/ui/util/widget/searchablespinner/SearchableListDialogFragment.java b/app/src/main/java/org/gnucash/android/ui/util/widget/searchablespinner/SearchableListDialogFragment.java new file mode 100644 index 000000000..988643811 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/util/widget/searchablespinner/SearchableListDialogFragment.java @@ -0,0 +1,617 @@ +package org.gnucash.android.ui.util.widget.searchablespinner; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.SearchManager; +import android.content.Context; +import android.content.DialogInterface; +import android.database.DataSetObserver; +import android.os.Bundle; +import android.support.v7.preference.PreferenceManager; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.ListView; +import android.widget.SearchView; + +import org.gnucash.android.R; +import org.gnucash.android.util.KeyboardUtils; +import org.gnucash.android.util.QualifiedAccountNameCursorAdapter; + +import java.io.Serializable; + +/** + * Pop-up that display a ListView with a search text field + * + * @author JeanGarf + */ +public class SearchableListDialogFragment + extends DialogFragment + implements SearchView.OnQueryTextListener, + SearchView.OnCloseListener, + Filter.FilterListener { + + /** + * Logging tag + */ + protected static final String LOG_TAG = "SearchLstDlgFragment"; + + public static final String KEY_ACCOUNT_UID = "key_accountUID"; + public static final String KEY_ACCOUNT_SIMPLE_NAME = "key_accountName"; + public static final String KEY_ACCOUNT_FULL_NAME = "key_accountFullName"; + public static final String KEY_PARENT_ACCOUNT_FULL_NAME = "key_parentAccountFullName"; + public static final String KEY_IS_FAVORITE_ACCOUNT = "key_isFavoriteAccount"; + + /** + * Listener to call when user clicks on an item + * + * @param + * item Type + */ + public interface OnSearchableItemClickedListener + extends Serializable { + + void onSearchableListItemClicked(T_ITEM item); + } + + /** + * Listener to call when Search text change + */ + public interface OnSearchTextChangedListener { + void onSearchTextChanged(String strText); + } + + // + // Parent SearchableSpinnerView + // + + private SearchableSpinnerView _parentSearchableSpinnerView; + + // + // Dialog + // + + // Dialog Title + private String mStrTitle; + + // Search Edit text zone + private SearchView mSearchTextEditView; + + // Item list + ListView mListView; + + // Bottom right button to close the pop-up + private String mStrPositiveButtonText; + + // + // ListView Adapter + // + + // Adapter for the mListView + private BaseAdapter mListViewAdapter; + + // + // Listeners + // + + private OnSearchTextChangedListener mOnSearchTextChangedListener; + + private OnSearchableItemClickedListener mOnSearchableListItemClickedListener; + + private DialogInterface.OnClickListener mOnPositiveBtnClickListener; + + private DialogInterface.OnCancelListener mOnCancelListener; + + // true if dismiss dialog has already been requested + private boolean mIsDismissing; + + + /** + * Constructor + */ + public SearchableListDialogFragment() { + + } + + /** + * Factory + * + * @return + */ + public static SearchableListDialogFragment makeInstance(SearchableSpinnerView parentSearchableSpinnerView) { + + SearchableListDialogFragment searchableListDialogFragment = new SearchableListDialogFragment(); + + // Store a link to the Parent SearchableSpinnerView which holds the CursorAdapter + searchableListDialogFragment.setParentSearchableSpinnerView(parentSearchableSpinnerView); + + return searchableListDialogFragment; + } + + // + // Event handlers + // + + @Override + public void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + + } + + @Override + public View onCreateView(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) { + + return super.onCreateView(inflater, + container, + savedInstanceState); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + + // Getting the layout inflater to inflate the view in an alert dialog. + LayoutInflater inflater = LayoutInflater.from(getActivity()); + + // Crash on orientation change #7 + // Change Start + // Description: As the instance was re initializing to null on rotating the device, + // getting the instance from the saved instance + if (null != savedInstanceState) { + setOnSearchableListItemClickListener((OnSearchableItemClickedListener) savedInstanceState.getSerializable("item")); + } + // Change End + + // + // Prepare the searchableListView + // + + // Instantiate the searchableListView from XML + View searchableListRootView = inflater.inflate(R.layout.searchable_list_dialog, + null); + + // Configure the searchableListView + configureView(searchableListRootView); + + // + // Create dialog builder + // + + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); + + // Indicate to put the searchableListView in the alertDialog + alertDialogBuilder.setView(searchableListRootView); + + // Title + + String strTitle = mStrTitle == null + ? "Select Item" + : mStrTitle; + + alertDialogBuilder.setTitle(strTitle); + + // Positive Button + + String strPositiveButton = mStrPositiveButtonText == null + ? "CLOSE" + : mStrPositiveButtonText; + + alertDialogBuilder.setPositiveButton(strPositiveButton, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(final DialogInterface dialog, + final int which) { + + // Dismiss dialog + dismissDialog(); + + if (mOnPositiveBtnClickListener != null) { + // + + // Call listener + mOnPositiveBtnClickListener.onClick(dialog, + which); + + } else { + // n' pas + + // RAF + } + } + }); + + // + // Create searchableListDialog + // + + final AlertDialog searchableListDialog = alertDialogBuilder.create(); + + return searchableListDialog; + } + + @Override + public boolean onQueryTextChange(String s) { + + if (getListView().isTextFilterEnabled()) { + // Filtering is enabled + + // + // + // Start List filtering Thread + // + + final Filterable listViewCursorAdapter = (Filterable) getListView().getAdapter(); + + if (TextUtils.isEmpty(s)) { + + + // Force filtering with null string to get the full account list + + listViewCursorAdapter.getFilter() + .filter(null, + this); + + } else { + + // Perform filtering + + // Do not use this, because it makes a big black square appears when typing text + // getListView().setFilterText(s); + + // instead, use this + listViewCursorAdapter.getFilter() + .filter(s, + this); + } + + } else { + // Filtering is enabled n' pas + + // RAF + } + + // + // Call Search Text Change Listener + // + + if (mOnSearchTextChangedListener != null) { + + // Call Listener + mOnSearchTextChangedListener.onSearchTextChanged(s); + } + + return true; + } + + @Override + public boolean onQueryTextSubmit(String s) { + + mSearchTextEditView.clearFocus(); + + return true; + } + + @Override + public void onFilterComplete(final int count) { + + if (count > 0) { + // There are filtered items + + getListViewAdapter().notifyDataSetChanged(); + + } else { + // There is none filtered items + + getListViewAdapter().notifyDataSetInvalidated(); + } + } + + // Crash on orientation change #7 + // Change Start + // Description: Saving the instance of searchable item instance. + @Override + public void onSaveInstanceState(Bundle outState) { + + outState.putSerializable("item", + getOnSearchableListItemClickedListener()); + super.onSaveInstanceState(outState); + } + // Change End + + + @Override + public void onCancel(DialogInterface dialog) { + + dismissDialog(); + + if (mOnCancelListener != null) { + // There is a listener + + // Call listener + mOnCancelListener.onCancel(dialog); + + } else { + // There is no listener + + // RAF + } + } + + @Override + public boolean onClose() { + + return false; + } + + @Override + public void onPause() { + + super.onPause(); + dismiss(); + } + + // + // local methods + // + + private void configureView(View searchableListRootView) { + + mIsDismissing = false; + + // + // Search Edit text + // + + mSearchTextEditView = (SearchView) searchableListRootView.findViewById(R.id.search); + + SearchManager searchManager = (SearchManager) getActivity().getSystemService(Context.SEARCH_SERVICE); + + mSearchTextEditView.setSearchableInfo(searchManager.getSearchableInfo(getActivity().getComponentName())); + // mSearchTextEditView.setIconifiedByDefault(false); // Already done in xml + mSearchTextEditView.setOnQueryTextListener(this); + mSearchTextEditView.setOnCloseListener(this); + + // Get Preference about opening keyboard, default to false + boolean prefShallOpenKeyboard = PreferenceManager.getDefaultSharedPreferences(getActivity()) + .getBoolean(getString(R.string.key_shall_open_keyboard_in_account_searchable_spinner), + false); + + if (prefShallOpenKeyboard) { + // Want to open keyboard + + // Set Focus on searchTextEditView to open cursor + mSearchTextEditView.setFocusable(true); + mSearchTextEditView.requestFocus(); + + } else { + // Do not want to open keyboard + + // Clear Focus + mSearchTextEditView.clearFocus(); + + // + // Hide keyboard after 500ms to let keyboard appeared before hiding it + // + + KeyboardUtils.hideKeyboard(mSearchTextEditView, + 500); + } + + // + // Items list + // + + setListView((ListView) searchableListRootView.findViewById(R.id.listItems)); + + // Use the parent spinner view adapter for the list view + setListViewAdapter((BaseAdapter) getParentSearchableSpinnerView().getAdapter()); + + if (getListViewAdapter() != null) { + + if (QualifiedAccountNameCursorAdapter.class.isAssignableFrom(getListViewAdapter().getClass())) { + // The parentSpinnerAdapter is a QualifiedAccountNameCursorAdapter + + QualifiedAccountNameCursorAdapter parentCursorAdapter = (QualifiedAccountNameCursorAdapter) getListViewAdapter(); + + // + // Put temporarily DropDownItemLayout in selectedItemView, + // because ListView use only selectedItemView for list item + // + + parentCursorAdapter.setViewResource(parentCursorAdapter.getSpinnerDropDownItemLayout()); + + } else { + // The parentSpinnerAdapter is another kind of Adapter + + // NTD + } + +// // +// // Register a Listener to close dialog if there is only one item remaining in the filtered list, and select it +// // automatically +// // +// +// getListViewAdapter().registerDataSetObserver(new DataSetObserver() { +// +// @Override +// public void onChanged() { +// +// if (getListViewAdapter().getCount() == 1) { +// // only one account +// +// dismissDialog(); +// +// final T_ITEM item = (T_ITEM) getListViewAdapter().getItem(0); +// +// // Simulate a onSearchableListItemClicked +// getOnSearchableListItemClickedListener().onSearchableListItemClicked(item); +// +// } else { +// // only one account n' pas +// +// // RAF +// } +// +// } +// }); + + // On item click listener + getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { + + @Override + public void onItemClick(AdapterView parent, + View view, + int position, + long id) { + + dismissDialog(); + + final T_ITEM item = (T_ITEM) getListViewAdapter().getItem(position); + + // Call Listener + getOnSearchableListItemClickedListener().onSearchableListItemClicked(item); + } + }); + + // + // Attach the adapter to the list + // + + getListView().setAdapter(getListViewAdapter()); + + // Enable filtering based on search text field + getListView().setTextFilterEnabled(true); + } + + // Simulate an empty search text field to build the full accounts list + onQueryTextChange(null); + } + + protected void dismissDialog() { + + if (!mIsDismissing) { + // It is the first time dismissing has been requested + + // Avoid infinite looping + mIsDismissing = true; + + // + // Restore original Spinner Selected Item Layout + // + + if (QualifiedAccountNameCursorAdapter.class.isAssignableFrom(getParentSearchableSpinnerView().getAdapter() + .getClass())) { + // The Adapter is a QualifiedAccountNameCursorAdapter + + QualifiedAccountNameCursorAdapter parentCursorAdapter = (QualifiedAccountNameCursorAdapter) getParentSearchableSpinnerView().getAdapter(); + + parentCursorAdapter.setViewResource(parentCursorAdapter.getSpinnerSelectedItemLayout()); + + // Refresh spinner selected item using spinner selected item layout + parentCursorAdapter.notifyDataSetChanged(); + + } else { + // The Adapter is not a QualifiedAccountNameCursorAdapter + + // NTD + } + + // + // Hide keyboard + // + + KeyboardUtils.hideKeyboard(mSearchTextEditView); + + // + // Close Dialog + // + + getDialog().dismiss(); + + } else { + // Dismissing has already been requested + + // NTD + } + } + + // + // Getters / Setters + // + + protected SearchableSpinnerView getParentSearchableSpinnerView() { + + return _parentSearchableSpinnerView; + } + + protected void setParentSearchableSpinnerView(SearchableSpinnerView parentSearchableSpinnerView) { + + _parentSearchableSpinnerView = parentSearchableSpinnerView; + } + + protected ListView getListView() { + + return mListView; + } + + protected void setListView(final ListView listView) { + + this.mListView = listView; + } + + protected BaseAdapter getListViewAdapter() { + + return mListViewAdapter; + } + + protected void setListViewAdapter(final BaseAdapter listViewAdapter) { + + mListViewAdapter = listViewAdapter; + } + + protected void setTitle(String strTitle) { + + mStrTitle = strTitle; + } + + protected void setPositiveButtonText(String strPositiveButtonText) { + + mStrPositiveButtonText = strPositiveButtonText; + } + + protected void setPositiveButtonClickListener(DialogInterface.OnClickListener onClickListener) { + + mOnPositiveBtnClickListener = onClickListener; + } + + protected void setOnSearchableListItemClickListener(OnSearchableItemClickedListener onSearchableListItemClickedListener) { + + this.mOnSearchableListItemClickedListener = onSearchableListItemClickedListener; + } + + protected OnSearchableItemClickedListener getOnSearchableListItemClickedListener() { + + return mOnSearchableListItemClickedListener; + } + + protected void setOnCancelListener(DialogInterface.OnCancelListener onCancelListener) { + + this.mOnCancelListener = onCancelListener; + } + + protected void setOnSearchTextChangedListener(OnSearchTextChangedListener onSearchTextChangedListener) { + + this.mOnSearchTextChangedListener = onSearchTextChangedListener; + } + + +} diff --git a/app/src/main/java/org/gnucash/android/ui/util/widget/searchablespinner/SearchableSpinnerView.java b/app/src/main/java/org/gnucash/android/ui/util/widget/searchablespinner/SearchableSpinnerView.java new file mode 100644 index 000000000..8d853486b --- /dev/null +++ b/app/src/main/java/org/gnucash/android/ui/util/widget/searchablespinner/SearchableSpinnerView.java @@ -0,0 +1,270 @@ +package org.gnucash.android.ui.util.widget.searchablespinner; + +import android.app.Activity; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.DialogInterface; +import android.database.Cursor; +import android.support.v4.widget.CursorAdapter; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import android.widget.SpinnerAdapter; + +import org.gnucash.android.db.DatabaseSchema; +import org.gnucash.android.util.QualifiedAccountNameCursorAdapter; + +/** + * Spinner that open a dialog box with text search criteria + * to filter the item list + * + * @param + * Type of an item + * + * * @author JeanGarf + */ +public class SearchableSpinnerView + extends android.support.v7.widget.AppCompatSpinner + implements View.OnTouchListener, + SearchableListDialogFragment.OnSearchableItemClickedListener { + + /** + * Logging tag + */ + protected static final String LOG_TAG = "SearchableSpinnerView"; + + // Embedded DialogFragment + private SearchableListDialogFragment mSearchableListDialogFragment; + + public SearchableSpinnerView(Context context) { + + super(context); + + init(); + } + + public SearchableSpinnerView(Context context, + AttributeSet attrs) { + + super(context, + attrs); + + // + // Init + // + + init(); + } + + public SearchableSpinnerView(Context context, + AttributeSet attrs, + int defStyleAttr) { + + super(context, + attrs, + defStyleAttr); + + init(); + } + + private void init() { + + // Create Dialog instance + setSearchableListDialogFragment(SearchableListDialogFragment.makeInstance(this)); + + // S'abonner aux clicks sur un item + getSearchableListDialogFragment().setOnSearchableListItemClickListener(this); + + // S'abonner aux évènements onTouch + setOnTouchListener(this); + } + + // + // Listeners + // + + @Override + public boolean onTouch(View v, + MotionEvent event) { + + boolean handled = false; + + if (getSearchableListDialogFragment() != null) { + // There is a DialogFragment defined + + handled = true; + + if (getSearchableListDialogFragment().isAdded()) { + // dialog is already visible + + // NTD + + } else { + // dialog is not visible + + if (event.getAction() == MotionEvent.ACTION_UP) { + // User has just clicked on the spinner + + // Display SearchableListDialogFragment + getSearchableListDialogFragment().show(scanForActivity(getContext()).getFragmentManager(), + "LOG_TAG"); + } + } + + } else { + // There is no DialogFragment defined + + // NTD + } + + return handled; + } + + private Activity scanForActivity(Context context) { + + if (context == null) { + return null; + + } else if (context instanceof Activity) { + return (Activity) context; + + } else if (context instanceof ContextWrapper) { + return scanForActivity(((ContextWrapper) context).getBaseContext()); + } + + return null; + } + + @Override + public void onSearchableListItemClicked(T_ITEM item) { + + if (CursorAdapter.class.isAssignableFrom(getAdapter().getClass())) { + // The Adapter is a CursorAdapter + + final Cursor cursor = (Cursor) item; + + String accountUID = cursor.getString(cursor.getColumnIndex(DatabaseSchema.AccountEntry.COLUMN_UID)); + + selectSpinnerAccount(cursor, + accountUID, + this); + + } else if (getAdapter() instanceof ArrayAdapter) { + // The Adapter is a ListAdapter + + setSelection(((ArrayAdapter) getAdapter()).getPosition(item)); + + } else { + + throw new IllegalArgumentException("SearchableSpinnerView can only handle ArrayAdapter and CursorAdapter"); + } + } + + /** + * + * @param accountsCursor + * @param accountUID + * @param spinnerView + */ + public static void selectSpinnerAccount(Cursor accountsCursor, + final String accountUID, + final Spinner spinnerView) { + + // + // set the selected item in the spinner + // + + int spinnerSelectedPosition = 0; + boolean found = false; + + for (accountsCursor.moveToFirst(); !accountsCursor.isAfterLast(); accountsCursor.moveToNext()) { + + String uid = accountsCursor.getString(accountsCursor.getColumnIndexOrThrow(DatabaseSchema.AccountEntry.COLUMN_UID)); + String accountFullName = accountsCursor.getString(accountsCursor.getColumnIndexOrThrow(DatabaseSchema.AccountEntry.COLUMN_FULL_NAME)); + + if (accountUID.equals(uid)) { + // Found + + Log.d(LOG_TAG, + "Account found in current Cursor for (" + + accountUID + + ") => (" + + accountFullName + + "), position (" + + spinnerSelectedPosition + + ")"); + + // Set Spinner selection + spinnerView.setSelection(spinnerSelectedPosition); + + found = true; + break; + } + + ++spinnerSelectedPosition; + + } // for + + if (found) { + // Account has found + + // NTD + + } else { + // Account has not been found + + // Log message + Log.e(LOG_TAG, + "No Account found in current Cursor for (" + accountUID + ")"); + } + } + + // + // Getters/Setters + // + + + protected SearchableListDialogFragment getSearchableListDialogFragment() { + + return mSearchableListDialogFragment; + } + + protected void setSearchableListDialogFragment(final SearchableListDialogFragment searchableListDialogFragment) { + + mSearchableListDialogFragment = searchableListDialogFragment; + } + + public void setTitle(String strTitle) { + + getSearchableListDialogFragment().setTitle(strTitle); + } + + public void setPositiveButton(String strPositiveButtonText, + DialogInterface.OnClickListener onPositiveBtnClickListener) { + + getSearchableListDialogFragment().setPositiveButtonText(strPositiveButtonText); + + getSearchableListDialogFragment().setPositiveButtonClickListener(onPositiveBtnClickListener); + } + + + public void setOnSearchTextChangedListener(SearchableListDialogFragment.OnSearchTextChangedListener onSearchTextChangedListener) { + + getSearchableListDialogFragment().setOnSearchTextChangedListener(onSearchTextChangedListener); + } + + /** + * Register a callback to be invoked when an item in this AdapterView has + * been selected. + * + * @param listener The callback that will run + */ + public void setOnCancelListener(DialogInterface.OnCancelListener listener) { + + getSearchableListDialogFragment().setOnCancelListener(listener); + } + +} diff --git a/app/src/main/java/org/gnucash/android/util/CommoditiesCursorAdapter.java b/app/src/main/java/org/gnucash/android/util/CommoditiesCursorAdapter.java index 03f16608c..41b59e6c1 100644 --- a/app/src/main/java/org/gnucash/android/util/CommoditiesCursorAdapter.java +++ b/app/src/main/java/org/gnucash/android/util/CommoditiesCursorAdapter.java @@ -35,11 +35,16 @@ */ 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); + 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 diff --git a/app/src/main/java/org/gnucash/android/util/KeyboardUtils.java b/app/src/main/java/org/gnucash/android/util/KeyboardUtils.java new file mode 100644 index 000000000..2894d5076 --- /dev/null +++ b/app/src/main/java/org/gnucash/android/util/KeyboardUtils.java @@ -0,0 +1,54 @@ +package org.gnucash.android.util; + +import android.content.Context; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +/** + * Created by JeanGarf on 2020-02-01. + */ +public class KeyboardUtils { + + /** + * Hide keyboard + * + * @param editTextView + */ + public static void hideKeyboard(final View editTextView) { + + // + // Hide keyboard + // + + InputMethodManager keyboard = (InputMethodManager) editTextView.getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + + keyboard.hideSoftInputFromWindow(editTextView.getWindowToken(), + 0); + } + + /** + * Hide keyboard after a delay + * + * @param editTextView + * @param delay + */ + public static void hideKeyboard(final View editTextView, + final long delay) { + +// editTextView.requestFocus(); + + // Delay the keyboard hiding + editTextView.postDelayed(new Runnable() { + @Override + public void run() { + + // Hide keyboard + hideKeyboard(editTextView); + } + }, + delay); + } + + +} 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 77bc86674..888027c8e 100644 --- a/app/src/main/java/org/gnucash/android/util/QualifiedAccountNameCursorAdapter.java +++ b/app/src/main/java/org/gnucash/android/util/QualifiedAccountNameCursorAdapter.java @@ -21,13 +21,19 @@ import android.support.annotation.LayoutRes; import android.support.annotation.NonNull; import android.support.v4.widget.SimpleCursorAdapter; -import android.text.TextUtils; import android.view.View; +import android.view.ViewGroup; +import android.widget.FilterQueryProvider; import android.widget.TextView; import org.gnucash.android.R; import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.db.adapter.AccountsDbAdapter; +import org.gnucash.android.ui.util.AccountUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; /** * Cursor adapter which looks up the fully qualified account name and returns that instead of just the simple name. @@ -35,60 +41,436 @@ * * @author Ngewi Fet */ -public class QualifiedAccountNameCursorAdapter extends SimpleCursorAdapter { +public class QualifiedAccountNameCursorAdapter + extends SimpleCursorAdapter { /** - * Initialize the Cursor adapter for account names using default spinner views - * @param context Application context - * @param cursor Cursor to accounts + * Removes the icon from view to avoid visual clutter + * + * @param spinnerView + */ + public static void hideFavoriteAccountStarIcon(View spinnerView) { + + TextView textViewWithStarIcon = (TextView) spinnerView.findViewById(R.id.text2); + + if (textViewWithStarIcon != null) { + + textViewWithStarIcon.setCompoundDrawablesWithIntrinsicBounds(0, + 0, + 0, + 0); + } + } + + // Layout of the selected spinner item (the one with down arrow) + private int mSpinnerSelectedItemLayout; + + // Layout of items in the drop down list + private int mSpinnerDropDownItemLayout; + + // Clause WHERE du Cursor (in order to be replayed by the item filter) + private String mCursorWhere; + private String[] mCursorWhereArgs; + + /** + * Overloaded constructor. Specifies the view to use for displaying selected spinner text + * + * @param context + * Application context + * @param cursor + * Cursor to account data + * @param spinnerSelectedItemLayout + * Layout resource for selected item text */ - 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(R.layout.account_spinner_dropdown_item); + public QualifiedAccountNameCursorAdapter(Context context, + Cursor cursor, + String cursorWhere, + String[] cursorWhereArgs, + @LayoutRes int spinnerSelectedItemLayout, + @LayoutRes int spinnerDropDownItemLayout + ) { + + super(context, + spinnerSelectedItemLayout,// Layout of the selected spinner item + cursor, + new String[]{DatabaseSchema.AccountEntry.COLUMN_FULL_NAME, + DatabaseSchema.AccountEntry.COLUMN_NAME}, + new int[]{android.R.id.text1, + R.id.text2}, + 0); + + // Store layout of each item in the open drop down of the spinner + setSpinnerSelectedItemLayout(spinnerSelectedItemLayout); + + // Store layout of each item in the open drop down of the spinner + setSpinnerDropDownItemLayout(spinnerDropDownItemLayout); + + // Store the WHERE clause associated with the Cursor + setCursorWhere(cursorWhere); + setCursorWhereArgs(cursorWhereArgs); + + // Define filter + setFilterQueryProvider(new FilterQueryProvider() { + + public Cursor runQuery(CharSequence constraint) { + + // + // Add %constraint% at the end of the whereArgs + // + + // Convert WhereArgs into List + final String[] cursorWhereArgs = getCursorWhereArgs(); + final List whereArgsAsList = (cursorWhereArgs != null) + ? new ArrayList(Arrays.asList(cursorWhereArgs)) + : new ArrayList(); + + // Add the %constraint% for the LIKE added in the where clause + whereArgsAsList.add("%" + ((constraint != null) + ? constraint.toString() + : "") + "%"); + + // Convert List into WhereArgs + final String[] whereArgs = whereArgsAsList.toArray(new String[whereArgsAsList.size()]); + + + // + // Run the original query but constrained with full account name containing constraint + // + + final AccountsDbAdapter accountsDbAdapter = AccountsDbAdapter.getInstance(); + + final String where = ((getCursorWhere() != null) + ? getCursorWhere() + " AND " + : "") + + DatabaseSchema.AccountEntry.COLUMN_FULL_NAME + + " LIKE ?"; + + final Cursor accountsCursor = accountsDbAdapter.fetchAccountsOrderedByFavoriteAndFullName(where, + whereArgs); + + return accountsCursor; + } + }); } /** * Overloaded constructor. Specifies the view to use for displaying selected spinner text - * @param context Application context - * @param cursor Cursor to account data - * @param selectedSpinnerItem Layout resource for selected item text + * + * @param context + * Application context + * @param cursor + * Cursor to account data + * @param selectedSpinnerItemLayout + * Layout resource for selected item text */ - public QualifiedAccountNameCursorAdapter(Context context, Cursor cursor, - @LayoutRes int selectedSpinnerItem) { - super(context, selectedSpinnerItem, cursor, - new String[]{DatabaseSchema.AccountEntry.COLUMN_FULL_NAME}, - new int[]{android.R.id.text1}, 0); - setDropDownViewResource(R.layout.account_spinner_dropdown_item); + public QualifiedAccountNameCursorAdapter(Context context, + Cursor cursor, + String cursorWhere, + String[] cursorWhereArgs, + @LayoutRes int selectedSpinnerItemLayout) { + + this(context, + cursor, + cursorWhere, + cursorWhereArgs, + selectedSpinnerItemLayout, // Layout of the closed spinner item + R.layout.account_spinner_dropdown_item_2lines + ); } + /** + * Initialize the Cursor adapter for account names using default spinner views + * + * @param context + * Application context + * @param cursor + * Cursor to accounts + */ + public QualifiedAccountNameCursorAdapter(Context context, + Cursor cursor, + String cursorWhere, + String[] cursorWhereArgs) { + + this(context, + cursor, + cursorWhere, + cursorWhereArgs, + android.R.layout.simple_spinner_item // Layout of the closed spinner item + ); + } + + /** + * Initialize the Cursor adapter for account names using default spinner views + * + * @param context + * Application context + * @param cursor + * Cursor to accounts + */ + public QualifiedAccountNameCursorAdapter(Context context, + Cursor cursor) { + + this(context, + cursor, + null, + null + ); + } + + // + // Overrides + // + + @Override - public void bindView(View view, Context context, Cursor cursor) { - super.bindView(view, context, cursor); - TextView textView = (TextView) view.findViewById(android.R.id.text1); - textView.setEllipsize(TextUtils.TruncateAt.MIDDLE); + public View getView(final int position, + final View convertView, + final ViewGroup parent) { + + View view = super.getView(position, + convertView, + parent); + + Cursor cursor = getCursor(); + + if (parent.getId() != R.id.toolbar_spinner) { + // Parent view is not the Toolbar Spinner + + // + // Set Account Color + // + + String accountUID = cursor.getString(cursor.getColumnIndex(DatabaseSchema.AccountEntry.COLUMN_UID)); + + setTextColorAccordingToAccountUID(view, + accountUID); + + } else { + // Parent view is the Toolbar Spinner + + // NTD (White by default) + } + + // + // Put Parent Account Full Name in text3 + // + + TextView parentAccountFullNameTextView = (TextView) view.findViewById(R.id.text3); + + if (parentAccountFullNameTextView != null) { + // The field exists + + String accountFullName = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseSchema.AccountEntry.COLUMN_FULL_NAME)); + + // Get Parent account Full Name + String parentAccountFullName = getParentAccountFullName(accountFullName); + + // Display Parent Account Full Name + parentAccountFullNameTextView.setText(parentAccountFullName); + + } else { + // The field does not exist + + // NTD + } + + // + // Add or not Favorite Star Icon + // Integer isFavorite = cursor.getInt(cursor.getColumnIndex(DatabaseSchema.AccountEntry.COLUMN_FAVORITE)); - if(isFavorite == 0) { - textView.setCompoundDrawablesWithIntrinsicBounds(0,0,0,0); + + displayFavoriteAccountStarIcon(view, + isFavorite); + + return view; + } + + /** + * To avoid "attempt to re-open an already-closed object" exception + * due to use of runQuery() + * + * See + * https://stackoverflow.com/questions/17458251/android-attempt-to-re-open-an-already-closed-object-sqlitequery-using-loaderm + * + * @param cursor + */ + @Override + public void changeCursor(final Cursor cursor) { + + // Do not close cursor + super.swapCursor(cursor); + } + + // + // Local methods + // + + /** + * Extract parent account full name + * + * @param accountFullName + * Account full name + * + * @return parent account full name + */ + String getParentAccountFullName(final String accountFullName) { + + String parentAccountFullName; + + // Look for last separator + int index = accountFullName.lastIndexOf(AccountsDbAdapter.ACCOUNT_NAME_SEPARATOR); + + if (index > 0) { + // An account separator has been found + + // parent full name is before the account separator + parentAccountFullName = accountFullName.substring(0, + index); + + } else { + // No account separator has been found + + // Do not display anything for parent + parentAccountFullName = ""; + } + return parentAccountFullName; + } + + /** + * Set text color according to account one + * + * @param view + * View containing text field to colorize + * @param accountUID + * Account UID + */ + void setTextColorAccordingToAccountUID(final View view, + final String accountUID) { + + // + // Set color on text1 (selected spinner item) + // + + TextView simpleAccountNameTextView = (TextView) view.findViewById(android.R.id.text1); + + AccountUtils.setAccountTextColor(simpleAccountNameTextView, + accountUID); + + // + // Set color on text2 + // + + simpleAccountNameTextView = (TextView) view.findViewById(R.id.text2); + + AccountUtils.setAccountTextColor(simpleAccountNameTextView, + accountUID); + } + + /** + * Display or hide star icon according to favorite account status + * + * @param spinnerSelectedItemView + * @param isFavoriteAccount + */ + void displayFavoriteAccountStarIcon(View spinnerSelectedItemView, + Integer isFavoriteAccount) { + + TextView simpleAccountNameTextView = (TextView) spinnerSelectedItemView.findViewById(R.id.text2); + + if (simpleAccountNameTextView != null) { + // The field exists + + if (isFavoriteAccount == 0) { + // It is not a Favorite account + + // Hide Favorite Account Star + hideFavoriteAccountStarIcon(spinnerSelectedItemView); + + } else { + // It is a Favorite account + + // Display Favorite Account Star + simpleAccountNameTextView.setCompoundDrawablesWithIntrinsicBounds(0, + 0, + R.drawable.ic_star_black_18dp, + 0); + } + } else { - textView.setCompoundDrawablesWithIntrinsicBounds(0,0,R.drawable.ic_star_black_18dp,0); + // The field does not exists + + // NTD } } /** * Returns the position of a given account in the adapter - * @param accountUID GUID of the account + * + * @param accountUID + * GUID of the account + * * @return Position of the account or -1 if the account is not found */ - public int getPosition(@NonNull String accountUID){ - long accountId = AccountsDbAdapter.getInstance().getID(accountUID); + public int getPosition(@NonNull String accountUID) { + + long accountId = AccountsDbAdapter.getInstance() + .getID(accountUID); + for (int pos = 0; pos < getCount(); pos++) { - if (getItemId(pos) == accountId){ + + if (getItemId(pos) == accountId) { return pos; } } + return -1; } + + // + // Getters/Setters + // + + String getCursorWhere() { + + return mCursorWhere; + } + + protected void setCursorWhere(final String cursorWhere) { + + mCursorWhere = cursorWhere; + } + + String[] getCursorWhereArgs() { + + return mCursorWhereArgs; + } + + protected void setCursorWhereArgs(final String[] cursorWhereArgs) { + + mCursorWhereArgs = cursorWhereArgs; + } + + public int getSpinnerSelectedItemLayout() { + + return mSpinnerSelectedItemLayout; + } + + public void setSpinnerSelectedItemLayout(int spinnerSelectedItemLayout) { + + mSpinnerSelectedItemLayout = spinnerSelectedItemLayout; + } + + public int getSpinnerDropDownItemLayout() { + + return mSpinnerDropDownItemLayout; + } + + public void setSpinnerDropDownItemLayout(int spinnerDropDownItemLayout) { + + mSpinnerDropDownItemLayout = spinnerDropDownItemLayout; + + setDropDownViewResource(getSpinnerDropDownItemLayout()); + } + } diff --git a/app/src/main/res/layout/account_spinner_dropdown_item.xml b/app/src/main/res/layout/account_spinner_dropdown_item.xml index c32bfdc6a..6d53e5d7e 100644 --- a/app/src/main/res/layout/account_spinner_dropdown_item.xml +++ b/app/src/main/res/layout/account_spinner_dropdown_item.xml @@ -15,10 +15,8 @@ limitations under the License. --> \ No newline at end of file + android:id="@android:id/text1" + android:layout_width="match_parent" + android:layout_height="?attr/dropdownListPreferredItemHeight" + style="?android:attr/spinnerDropDownItemStyle" +/> \ No newline at end of file diff --git a/app/src/main/res/layout/account_spinner_dropdown_item_2lines.xml b/app/src/main/res/layout/account_spinner_dropdown_item_2lines.xml new file mode 100644 index 000000000..74e8572e2 --- /dev/null +++ b/app/src/main/res/layout/account_spinner_dropdown_item_2lines.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_transaction_detail.xml b/app/src/main/res/layout/activity_transaction_detail.xml index 66a50ed96..f0a2d3739 100644 --- a/app/src/main/res/layout/activity_transaction_detail.xml +++ b/app/src/main/res/layout/activity_transaction_detail.xml @@ -41,6 +41,8 @@ @@ -132,10 +132,11 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:focusable="false" /> - + android:layout_height="wrap_content" + /> - diff --git a/app/src/main/res/layout/fragment_report_summary.xml b/app/src/main/res/layout/fragment_report_summary.xml index dfb9ab9dd..20c239edf 100644 --- a/app/src/main/res/layout/fragment_report_summary.xml +++ b/app/src/main/res/layout/fragment_report_summary.xml @@ -49,7 +49,7 @@ limitations under the License. android:layout_weight="1" android:drawableLeft="@drawable/ic_trending_up_white_24dp" android:drawableStart="@drawable/ic_trending_up_white_24dp" - android:text="@string/title_line_chart"/> + android:text="@string/title_cash_flow_report"/>