diff --git a/app/build.gradle b/app/build.gradle index 28a494651..a59dddabf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,7 @@ apply plugin: 'crashlytics' def versionMajor = 1 def versionMinor = 6 def versionPatch = 0 -def versionBuild = 4 +def versionBuild = 5 def buildTime() { def df = new SimpleDateFormat("yyyyMMdd") diff --git a/app/src/main/java/org/gnucash/android/db/DatabaseAdapter.java b/app/src/main/java/org/gnucash/android/db/DatabaseAdapter.java index f10c315da..e63ccb95d 100644 --- a/app/src/main/java/org/gnucash/android/db/DatabaseAdapter.java +++ b/app/src/main/java/org/gnucash/android/db/DatabaseAdapter.java @@ -42,7 +42,7 @@ public abstract class DatabaseAdapter { /** * Tag for logging */ - protected static String LOG_TAG = "DatabaseAdapter"; + protected String LOG_TAG = "DatabaseAdapter"; /** * SQLite database diff --git a/app/src/main/java/org/gnucash/android/db/DatabaseHelper.java b/app/src/main/java/org/gnucash/android/db/DatabaseHelper.java index 412f04866..a46486730 100644 --- a/app/src/main/java/org/gnucash/android/db/DatabaseHelper.java +++ b/app/src/main/java/org/gnucash/android/db/DatabaseHelper.java @@ -26,16 +26,15 @@ import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; +import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.export.Exporter; -import org.gnucash.android.model.Account; import org.gnucash.android.model.AccountType; import org.gnucash.android.model.Money; -import org.gnucash.android.model.ScheduledAction; -import org.gnucash.android.model.Split; import org.gnucash.android.model.Transaction; import java.io.File; +import java.math.BigDecimal; import java.sql.Timestamp; import static org.gnucash.android.db.DatabaseSchema.AccountEntry; @@ -439,36 +438,84 @@ private int upgradeDbToVersion8(SQLiteDatabase db) { //================================ END TABLE MIGRATIONS ================================ + // String timestamp to be used for all new created entities in migration + String timestamp = (new Timestamp(System.currentTimeMillis())).toString(); - - ScheduledActionDbAdapter scheduledActionDbAdapter = new ScheduledActionDbAdapter(db); - SplitsDbAdapter splitsDbAdapter = new SplitsDbAdapter(db); - TransactionsDbAdapter transactionsDbAdapter = new TransactionsDbAdapter(db, splitsDbAdapter); - AccountsDbAdapter accountsDbAdapter = new AccountsDbAdapter(db,transactionsDbAdapter); + //ScheduledActionDbAdapter scheduledActionDbAdapter = new ScheduledActionDbAdapter(db); + //SplitsDbAdapter splitsDbAdapter = new SplitsDbAdapter(db); + //TransactionsDbAdapter transactionsDbAdapter = new TransactionsDbAdapter(db, splitsDbAdapter); + //AccountsDbAdapter accountsDbAdapter = new AccountsDbAdapter(db,transactionsDbAdapter); Log.i(LOG_TAG, "Creating default root account if none exists"); ContentValues contentValues = new ContentValues(); - //assign a root account to all accounts which had null as parent (top-level accounts) - String rootAccountUID = accountsDbAdapter.getOrCreateGnuCashRootAccountUID(); + //assign a root account to all accounts which had null as parent except ROOT (top-level accounts) + String rootAccountUID; + Cursor cursor = db.query(AccountEntry.TABLE_NAME, + new String[]{AccountEntry.COLUMN_UID}, + AccountEntry.COLUMN_TYPE + "= ?", + new String[]{AccountType.ROOT.name()}, null, null, null); + try { + if (cursor.moveToFirst()) { + rootAccountUID = cursor.getString(cursor.getColumnIndexOrThrow(AccountEntry.COLUMN_UID)); + } + else + { + rootAccountUID = MigrationHelper.generateUUID(); + contentValues.clear(); + contentValues.put(DatabaseSchema.CommonColumns.COLUMN_UID, rootAccountUID); + contentValues.put(DatabaseSchema.CommonColumns.COLUMN_CREATED_AT, timestamp); + contentValues.put(AccountEntry.COLUMN_NAME, "ROOT"); + contentValues.put(AccountEntry.COLUMN_TYPE, "ROOT"); + contentValues.put(AccountEntry.COLUMN_CURRENCY, Money.DEFAULT_CURRENCY_CODE); + contentValues.put(AccountEntry.COLUMN_PLACEHOLDER, 0); + contentValues.put(AccountEntry.COLUMN_HIDDEN, 1); + contentValues.putNull(AccountEntry.COLUMN_COLOR_CODE); + contentValues.put(AccountEntry.COLUMN_FAVORITE, 0); + contentValues.put(AccountEntry.COLUMN_FULL_NAME, " "); + contentValues.putNull(AccountEntry.COLUMN_PARENT_ACCOUNT_UID); + contentValues.putNull(AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID); + db.insert(AccountEntry.TABLE_NAME, null, contentValues); + } + } finally { + cursor.close(); + } + //String rootAccountUID = accountsDbAdapter.getOrCreateGnuCashRootAccountUID(); + contentValues.clear(); contentValues.put(AccountEntry.COLUMN_PARENT_ACCOUNT_UID, rootAccountUID); - db.update(AccountEntry.TABLE_NAME, contentValues, AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " IS NULL", null); + db.update(AccountEntry.TABLE_NAME, contentValues, AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " IS NULL AND " + AccountEntry.COLUMN_TYPE + " != ?", new String[]{"ROOT"}); Log.i(LOG_TAG, "Migrating existing recurring transactions"); - Cursor cursor = db.query(TransactionEntry.TABLE_NAME + "_bak", null, "recurrence_period > 0", null, null, null, null); + cursor = db.query(TransactionEntry.TABLE_NAME + "_bak", null, "recurrence_period > 0", null, null, null, null); + long lastRun = System.currentTimeMillis(); while (cursor.moveToNext()){ contentValues.clear(); - Timestamp timestamp = new Timestamp(cursor.getLong(cursor.getColumnIndexOrThrow(TransactionEntry.COLUMN_TIMESTAMP))); - contentValues.put(TransactionEntry.COLUMN_CREATED_AT, timestamp.toString()); + Timestamp timestampT = new Timestamp(cursor.getLong(cursor.getColumnIndexOrThrow(TransactionEntry.COLUMN_TIMESTAMP))); + contentValues.put(TransactionEntry.COLUMN_CREATED_AT, timestampT.toString()); long transactionId = cursor.getLong(cursor.getColumnIndexOrThrow(TransactionEntry._ID)); db.update(TransactionEntry.TABLE_NAME, contentValues, TransactionEntry._ID + "=" + transactionId, null); - ScheduledAction scheduledAction = new ScheduledAction(ScheduledAction.ActionType.TRANSACTION); - scheduledAction.setActionUID(cursor.getString(cursor.getColumnIndexOrThrow(TransactionEntry.COLUMN_UID))); - long period = cursor.getLong(cursor.getColumnIndexOrThrow("recurrence_period")); - scheduledAction.setPeriod(period); - scheduledAction.setStartTime(timestamp.getTime()); //the start time is when the transaction was created - scheduledAction.setLastRun(System.currentTimeMillis()); //prevent this from being executed at the end of migration - scheduledActionDbAdapter.addScheduledAction(scheduledAction); + //ScheduledAction scheduledAction = new ScheduledAction(ScheduledAction.ActionType.TRANSACTION); + //scheduledAction.setActionUID(cursor.getString(cursor.getColumnIndexOrThrow(TransactionEntry.COLUMN_UID))); + //long period = cursor.getLong(cursor.getColumnIndexOrThrow("recurrence_period")); + //scheduledAction.setPeriod(period); + //scheduledAction.setStartTime(timestampT.getTime()); //the start time is when the transaction was created + //scheduledAction.setLastRun(System.currentTimeMillis()); //prevent this from being executed at the end of migration + + contentValues.clear(); + contentValues.put(DatabaseSchema.CommonColumns.COLUMN_UID, MigrationHelper.generateUUID()); + contentValues.put(DatabaseSchema.CommonColumns.COLUMN_CREATED_AT, timestamp); + contentValues.put(ScheduledActionEntry.COLUMN_ACTION_UID, cursor.getString(cursor.getColumnIndexOrThrow(TransactionEntry.COLUMN_UID))); + contentValues.put(ScheduledActionEntry.COLUMN_PERIOD, cursor.getLong(cursor.getColumnIndexOrThrow("recurrence_period"))); + contentValues.put(ScheduledActionEntry.COLUMN_START_TIME, timestampT.getTime()); + contentValues.put(ScheduledActionEntry.COLUMN_END_TIME, 0); + contentValues.put(ScheduledActionEntry.COLUMN_LAST_RUN, lastRun); + contentValues.put(ScheduledActionEntry.COLUMN_TYPE, "TRANSACTION"); + contentValues.put(ScheduledActionEntry.COLUMN_TAG, ""); + contentValues.put(ScheduledActionEntry.COLUMN_ENABLED, 1); + contentValues.put(ScheduledActionEntry.COLUMN_TOTAL_FREQUENCY, 0); + contentValues.put(ScheduledActionEntry.COLUMN_EXECUTION_COUNT, 0); + //scheduledActionDbAdapter.addScheduledAction(scheduledAction); + db.insert(ScheduledActionEntry.TABLE_NAME, null, contentValues); //build intent for recurring transactions in the database Intent intent = new Intent(Intent.ACTION_INSERT); @@ -485,20 +532,95 @@ private int upgradeDbToVersion8(SQLiteDatabase db) { //auto-balance existing splits Log.i(LOG_TAG, "Auto-balancing existing transaction splits"); - cursor = transactionsDbAdapter.fetchAllRecords(); - while (cursor.moveToNext()){ - Transaction transaction = transactionsDbAdapter.buildTransactionInstance(cursor); - if (transaction.isTemplate()) - continue; - Money imbalance = transaction.getImbalance(); - if (!imbalance.isAmountZero()){ - Split split = new Split(imbalance.negate(), - accountsDbAdapter.getOrCreateImbalanceAccountUID(imbalance.getCurrency())); - split.setTransactionUID(transaction.getUID()); - splitsDbAdapter.addSplit(split); +// cursor = transactionsDbAdapter.fetchAllRecords(); +// while (cursor.moveToNext()){ +// Transaction transaction = transactionsDbAdapter.buildTransactionInstance(cursor); +// if (transaction.isTemplate()) +// continue; +// Money imbalance = transaction.getImbalance(); +// if (!imbalance.isAmountZero()){ +// Split split = new Split(imbalance.negate(), +// accountsDbAdapter.getOrCreateImbalanceAccountUID(imbalance.getCurrency())); +// split.setTransactionUID(transaction.getUID()); +// splitsDbAdapter.addSplit(split); +// } +// } +// cursor.close(); + cursor = db.query( + TransactionEntry.TABLE_NAME + " , " + SplitEntry.TABLE_NAME + " ON " + + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_UID + "=" + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_TRANSACTION_UID + + " , " + AccountEntry.TABLE_NAME + " ON " + + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_ACCOUNT_UID + "=" + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_UID, + new String[]{ + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_UID + " AS trans_uid", + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_CURRENCY + " AS trans_currency", + "TOTAL ( CASE WHEN " + + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_TYPE + " = 'DEBIT' THEN " + + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_AMOUNT + " ELSE - " + + SplitEntry.TABLE_NAME + "." + SplitEntry.COLUMN_AMOUNT + " END ) AS trans_acct_balance", + "COUNT ( DISTINCT " + + AccountEntry.TABLE_NAME + "." + AccountEntry.COLUMN_CURRENCY + + " ) AS trans_currency_count" + }, + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_TEMPLATE + " == 0", + null, + TransactionEntry.TABLE_NAME + "." + TransactionEntry.COLUMN_UID, + "trans_acct_balance != 0 AND trans_currency_count = 1", + null); + try { + while (cursor.moveToNext()){ + double imbalance = cursor.getDouble(cursor.getColumnIndexOrThrow("trans_acct_balance")); + BigDecimal decimalImbalance = BigDecimal.valueOf(imbalance).setScale(2, BigDecimal.ROUND_HALF_UP); + if (decimalImbalance.compareTo(BigDecimal.ZERO) != 0) { + String currencyCode = cursor.getString(cursor.getColumnIndexOrThrow("trans_currency")); + String imbalanceAccountName = GnuCashApplication.getAppContext().getString(R.string.imbalance_account_name) + "-" + currencyCode; + String imbalanceAccountUID; + Cursor c = db.query(AccountEntry.TABLE_NAME, new String[]{AccountEntry.COLUMN_UID}, + AccountEntry.COLUMN_FULL_NAME + "= ?", new String[]{imbalanceAccountName}, + null, null, null); + try { + if (c.moveToFirst()) { + imbalanceAccountUID = c.getString(c.getColumnIndexOrThrow(AccountEntry.COLUMN_UID)); + } + else { + imbalanceAccountUID = MigrationHelper.generateUUID(); + contentValues.clear(); + contentValues.put(DatabaseSchema.CommonColumns.COLUMN_UID, imbalanceAccountUID); + contentValues.put(DatabaseSchema.CommonColumns.COLUMN_CREATED_AT, timestamp); + contentValues.put(AccountEntry.COLUMN_NAME, imbalanceAccountName); + contentValues.put(AccountEntry.COLUMN_TYPE, "BANK"); + contentValues.put(AccountEntry.COLUMN_CURRENCY, currencyCode); + contentValues.put(AccountEntry.COLUMN_PLACEHOLDER, 0); + contentValues.put(AccountEntry.COLUMN_HIDDEN, GnuCashApplication.isDoubleEntryEnabled() ? 0 : 1); + contentValues.putNull(AccountEntry.COLUMN_COLOR_CODE); + contentValues.put(AccountEntry.COLUMN_FAVORITE, 0); + contentValues.put(AccountEntry.COLUMN_FULL_NAME, imbalanceAccountName); + contentValues.put(AccountEntry.COLUMN_PARENT_ACCOUNT_UID, rootAccountUID); + contentValues.putNull(AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID); + db.insert(AccountEntry.TABLE_NAME, null, contentValues); + } + } finally { + c.close(); + } + String TransactionUID = cursor.getString(cursor.getColumnIndexOrThrow("trans_uid")); + contentValues.clear(); + contentValues.put(DatabaseSchema.CommonColumns.COLUMN_UID, MigrationHelper.generateUUID()); + contentValues.put(DatabaseSchema.CommonColumns.COLUMN_CREATED_AT, timestamp); + contentValues.put(SplitEntry.COLUMN_AMOUNT, decimalImbalance.abs().toPlainString()); + contentValues.put(SplitEntry.COLUMN_TYPE, decimalImbalance.compareTo(BigDecimal.ZERO) < 0 ? "DEBIT" : "CREDIT"); + contentValues.put(SplitEntry.COLUMN_MEMO, ""); + contentValues.put(SplitEntry.COLUMN_ACCOUNT_UID, imbalanceAccountUID); + contentValues.put(SplitEntry.COLUMN_TRANSACTION_UID, TransactionUID); + db.insert(SplitEntry.TABLE_NAME, null, contentValues); + contentValues.clear(); + contentValues.put(TransactionEntry.COLUMN_MODIFIED_AT, timestamp); + db.update(TransactionEntry.TABLE_NAME, contentValues, TransactionEntry.COLUMN_UID + " == ?", + new String[]{TransactionUID}); + } } + } finally { + cursor.close(); } - cursor.close(); Log.i(LOG_TAG, "Dropping temporary migration tables"); db.execSQL("DROP TABLE " + SplitEntry.TABLE_NAME + "_bak"); diff --git a/app/src/main/java/org/gnucash/android/db/MigrationHelper.java b/app/src/main/java/org/gnucash/android/db/MigrationHelper.java index 60cabf63b..5b90ebca2 100644 --- a/app/src/main/java/org/gnucash/android/db/MigrationHelper.java +++ b/app/src/main/java/org/gnucash/android/db/MigrationHelper.java @@ -33,6 +33,7 @@ import java.io.IOError; import java.io.IOException; import java.nio.channels.FileChannel; +import java.util.UUID; import static org.gnucash.android.db.DatabaseSchema.AccountEntry; @@ -187,4 +188,9 @@ public void run() { oldExportFolder.delete(); } }; + + public static String generateUUID() + { + return UUID.randomUUID().toString().replaceAll("-", ""); + } } diff --git a/app/src/main/java/org/gnucash/android/ui/chart/BarChartActivity.java b/app/src/main/java/org/gnucash/android/ui/chart/BarChartActivity.java index 3a7cefa0e..b66478969 100644 --- a/app/src/main/java/org/gnucash/android/ui/chart/BarChartActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/chart/BarChartActivity.java @@ -20,7 +20,11 @@ import android.os.Bundle; import android.preference.PreferenceManager; import android.util.Log; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; import android.widget.LinearLayout; +import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; @@ -43,6 +47,7 @@ import org.gnucash.android.model.AccountType; import org.gnucash.android.model.Money; import org.gnucash.android.ui.passcode.PassLockActivity; +import org.joda.time.LocalDate; import org.joda.time.LocalDateTime; import org.joda.time.Months; @@ -50,8 +55,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.Currency; -import java.util.HashMap; -import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -70,173 +75,223 @@ public class BarChartActivity extends PassLockActivity implements OnChartValueSe private static final int NO_DATA_COLOR = Color.LTGRAY; private static final int NO_DATA_BAR_COUNTS = 3; private static final int[] COLORS = { - Color.parseColor("#68F1AF"), Color.parseColor("#CC1f09"), Color.parseColor("#EE8600"), - Color.parseColor("#1469EB"), Color.parseColor("#B304AD"), + Color.parseColor("#17ee4e"), Color.parseColor("#cc1f09"), Color.parseColor("#3940f7"), + Color.parseColor("#f9cd04"), Color.parseColor("#5f33a8"), Color.parseColor("#e005b6"), + Color.parseColor("#17d6ed"), Color.parseColor("#e4a9a2"), Color.parseColor("#8fe6cd"), + Color.parseColor("#8b48fb"), Color.parseColor("#343a36"), Color.parseColor("#6decb1"), + Color.parseColor("#a6dcfd"), Color.parseColor("#5c3378"), Color.parseColor("#a6dcfd"), + Color.parseColor("#ba037c"), Color.parseColor("#708809"), Color.parseColor("#32072c"), + Color.parseColor("#fddef8"), Color.parseColor("#fa0e6e"), Color.parseColor("#d9e7b5") }; - private BarChart mChart; private AccountsDbAdapter mAccountsDbAdapter = AccountsDbAdapter.getInstance(); - private Map mEarliestTimestampsMap = new HashMap<>(); - private Map mLatestTimestampsMap = new HashMap<>(); - private long mEarliestTransactionTimestamp; - private long mLatestTransactionTimestamp; + + private TextView selectedValueTextView; + + private BarChart mChart; + + private Currency mCurrency; + + private boolean mUseAccountColor = true; private boolean mTotalPercentageMode = true; private boolean mChartDataPresent = true; - private Currency mCurrency; @Override protected void onCreate(Bundle savedInstanceState) { //it is necessary to set the view first before calling super because of the nav drawer in BaseDrawerActivity - setContentView(R.layout.activity_line_chart); + setContentView(R.layout.activity_bar_chart); super.onCreate(savedInstanceState); getSupportActionBar().setTitle(R.string.title_bar_chart); + selectedValueTextView = (TextView) findViewById(R.id.selected_chart_slice); + + mUseAccountColor = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()) + .getBoolean(getString(R.string.key_use_account_color), false); + mCurrency = Currency.getInstance(PreferenceManager.getDefaultSharedPreferences(this) .getString(getString(R.string.key_report_currency), Money.DEFAULT_CURRENCY_CODE)); mChart = new BarChart(this); - ((LinearLayout) findViewById(R.id.chart)).addView(mChart); + ((LinearLayout) findViewById(R.id.bar_chart)).addView(mChart); mChart.setOnChartValueSelectedListener(this); mChart.setDescription(""); + mChart.setDrawValuesForWholeStack(false); mChart.getXAxis().setDrawGridLines(false); mChart.getAxisRight().setEnabled(false); mChart.getAxisLeft().enableGridDashedLine(4.0f, 4.0f, 0); - mChart.setDrawValuesForWholeStack(false); mChart.getAxisLeft().setValueFormatter(new LargeValueFormatter(mCurrency.getSymbol(Locale.getDefault()))); - mChart.getAxisRight().setEnabled(false); - - // below we can add/remove displayed account's types - mChart.setData(getData(new ArrayList<>(Arrays.asList(AccountType.INCOME, AccountType.EXPENSE)))); - - Legend legend = mChart.getLegend(); - legend.setForm(Legend.LegendForm.CIRCLE); - legend.setPosition(Legend.LegendPosition.RIGHT_OF_CHART_INSIDE); + mChart.getLegend().setForm(Legend.LegendForm.CIRCLE); + mChart.getLegend().setPosition(Legend.LegendPosition.RIGHT_OF_CHART_INSIDE); - if (!mChartDataPresent) { - mChart.getAxisLeft().setAxisMaxValue(10); - mChart.getAxisLeft().setDrawLabels(false); - mChart.getXAxis().setDrawLabels(false); - mChart.setTouchEnabled(false); - ((TextView) findViewById(R.id.selected_chart_slice)).setText(getResources().getString(R.string.label_chart_no_data)); - } else { - mChart.animateY(ANIMATION_DURATION); - } - mChart.invalidate(); + setUpSpinner(); } /** * Returns a data object that represents a user data of the specified account types - * @param accountTypeList account's types which will be displayed + * @param accountType account's type which will be displayed * @return a {@code BarData} instance that represents a user data */ - private BarData getData(List accountTypeList) { - calculateEarliestAndLatestTimestamps(accountTypeList); - - LocalDateTime startDate = new LocalDateTime(mEarliestTransactionTimestamp).withDayOfMonth(1).withMillisOfDay(0); - LocalDateTime endDate = new LocalDateTime(mLatestTransactionTimestamp).withDayOfMonth(1).withMillisOfDay(0); - Log.d(TAG, "X-axis star date: " + startDate.toString("dd MM yyyy")); - Log.d(TAG, "X-axis end date: " + endDate.toString("dd MM yyyy")); - int months = Months.monthsBetween(startDate, endDate).getMonths(); - - List dataSets = new ArrayList<>(); + private BarData getData(AccountType accountType) { List values = new ArrayList<>(); + List labels = new ArrayList<>(); + List colors = new ArrayList<>(); + Map accountToColorMap = new LinkedHashMap<>(); List xValues = new ArrayList<>(); - for (int i = 0; i <= months; i++) { - xValues.add(startDate.toString(X_AXIS_PATTERN)); - - long start = startDate.dayOfMonth().withMinimumValue().millisOfDay().withMinimumValue().toDate().getTime(); - long end = startDate.dayOfMonth().withMaximumValue().millisOfDay().withMaximumValue().toDate().getTime(); - float stack[] = new float[accountTypeList.size()]; - int j = 0; - for (Map.Entry> entry : getAccountTypeToAccountUidMap(accountTypeList).entrySet()) { - stack[j++] = (float) mAccountsDbAdapter.getAccountsBalance(entry.getValue(), start, end).absolute().asDouble(); - Log.d(TAG, entry.getKey() + startDate.toString(" MMMM yyyy") + ", balance = " + stack[j - 1]); + LocalDateTime tmpDate = new LocalDateTime(getStartDate(accountType).toDate().getTime()); + for (int i = 0; i <= Months.monthsBetween(getStartDate(accountType), getEndDate(accountType)).getMonths(); i++) { + long start = tmpDate.dayOfMonth().withMinimumValue().millisOfDay().withMinimumValue().toDate().getTime(); + long end = tmpDate.dayOfMonth().withMaximumValue().millisOfDay().withMaximumValue().toDate().getTime(); + List stack = new ArrayList<>(); + for (Account account : mAccountsDbAdapter.getSimpleAccountList()) { + if (account.getAccountType() == accountType + && !account.isPlaceholderAccount() + && account.getCurrency() == mCurrency) { + + double balance = mAccountsDbAdapter.getAccountsBalance( + Collections.singletonList(account.getUID()), start, end).asDouble(); + if (balance != 0) { + if (!accountToColorMap.containsKey(account.getUID())) { + Integer color; + if (mUseAccountColor) { + color = (account.getColorHexCode() != null) + ? Color.parseColor(account.getColorHexCode()) + : COLORS[accountToColorMap.size() % COLORS.length]; + } else { + color = COLORS[accountToColorMap.size() % COLORS.length]; + } + accountToColorMap.put(account.getUID(), color); + } + + stack.add((float) balance); + labels.add(account.getName()); + colors.add(accountToColorMap.get(account.getUID())); + Log.d(TAG, accountType + tmpDate.toString(" MMMM yyyy ") + account.getName() + " = " + stack.get(stack.size() - 1)); + } + } } - values.add(new BarEntry(stack, i)); - startDate = startDate.plusMonths(1); + String stackLabels = labels.subList(labels.size() - stack.size(), labels.size()).toString(); + values.add(new BarEntry(floatListToArray(stack), i, stackLabels)); + + xValues.add(tmpDate.toString(X_AXIS_PATTERN)); + + tmpDate = tmpDate.plusMonths(1); } BarDataSet set = new BarDataSet(values, ""); - // conversion an enum list to a string array - set.setStackLabels(accountTypeList.toString().substring(1, accountTypeList.toString().length() - 1).split(", ")); - set.setColors(Arrays.copyOfRange(COLORS, 0, accountTypeList.size())); - dataSets.add(set); + set.setStackLabels(labels.toArray(new String[labels.size()])); + set.setColors(colors); if (set.getYValueSum() == 0) { mChartDataPresent = false; return getEmptyData(); } - return new BarData(xValues, dataSets); + mChartDataPresent = true; + return new BarData(xValues, set); } /** - * Calculates the earliest and latest transaction's timestamps of the specified account types - * @param accountTypeList account's types which will be processed + * Returns a data object that represents situation when no user data available + * @return a {@code BarData} instance for situation when no user data available */ - private void calculateEarliestAndLatestTimestamps(List accountTypeList) { - TransactionsDbAdapter dbAdapter = TransactionsDbAdapter.getInstance(); - for (Iterator iter = accountTypeList.iterator(); iter.hasNext();) { - AccountType type = iter.next(); - long earliest = dbAdapter.getTimestampOfEarliestTransaction(type, mCurrency.getCurrencyCode()); - long latest = dbAdapter.getTimestampOfLatestTransaction(type, mCurrency.getCurrencyCode()); - if (earliest > 0 && latest > 0) { - mEarliestTimestampsMap.put(type, earliest); - mLatestTimestampsMap.put(type, latest); - } else { - iter.remove(); - } + private BarData getEmptyData() { + List xValues = new ArrayList<>(); + List yValues = new ArrayList<>(); + for (int i = 0; i < NO_DATA_BAR_COUNTS; i++) { + xValues.add(""); + yValues.add(new BarEntry(i + 1, i)); } + BarDataSet set = new BarDataSet(yValues, getResources().getString(R.string.label_chart_no_data)); + set.setDrawValues(false); + set.setColor(NO_DATA_COLOR); - if (mEarliestTimestampsMap.isEmpty() || mLatestTimestampsMap.isEmpty()) { - return; - } + return new BarData(xValues, set); + } - List timestamps = new ArrayList<>(mEarliestTimestampsMap.values()); - timestamps.addAll(mLatestTimestampsMap.values()); - Collections.sort(timestamps); - mEarliestTransactionTimestamp = timestamps.get(0); - mLatestTransactionTimestamp = timestamps.get(timestamps.size() - 1); + /** + * Returns the start data of x-axis for the specified account type + * @param accountType account type + * @return the start data + */ + private LocalDate getStartDate(AccountType accountType) { + TransactionsDbAdapter adapter = TransactionsDbAdapter.getInstance(); + String code = mCurrency.getCurrencyCode(); + LocalDate startDate = new LocalDate(adapter.getTimestampOfEarliestTransaction(accountType, code)).withDayOfMonth(1); + Log.d(TAG, accountType + " X-axis star date: " + startDate.toString("dd MM yyyy")); + return startDate; } /** - * Returns a map with an account type as key and correspond accounts UIDs as value - * from a specified list of account types - * @param accountTypeList account's types which will be used as keys - * @return a map with an account type as key and correspond accounts UIDs as value + * Returns the end data of x-axis for the specified account type + * @param accountType account type + * @return the end data */ - private Map> getAccountTypeToAccountUidMap(List accountTypeList) { - Map> accountMap = new HashMap<>(); - for (AccountType accountType : accountTypeList) { - List accountUIDList = new ArrayList<>(); - for (Account account : mAccountsDbAdapter.getSimpleAccountList()) { - if (account.getAccountType() == accountType - && !account.isPlaceholderAccount() - && account.getCurrency() == mCurrency) { - accountUIDList.add(account.getUID()); - } - accountMap.put(accountType, accountUIDList); - } + private LocalDate getEndDate(AccountType accountType) { + TransactionsDbAdapter adapter = TransactionsDbAdapter.getInstance(); + String code = mCurrency.getCurrencyCode(); + LocalDate endDate = new LocalDate(adapter.getTimestampOfLatestTransaction(accountType, code)).withDayOfMonth(1); + Log.d(TAG, accountType + " X-axis end date: " + endDate.toString("dd MM yyyy")); + return endDate; + } + + /** + * Converts the specified list of floats to an array + * @param list a list of floats + * @return a float array + */ + private float[] floatListToArray(List list) { + float array[] = new float[list.size()]; + for (int i = 0; i < list.size(); i++) { + array[i] = list.get(i); } - return accountMap; + return array; } /** - * Returns a data object that represents situation when no user data available - * @return a {@code BarData} instance for situation when no user data available + * Sets up settings and data for the account type spinner. Currently used only {@code EXPENSE} and {@code INCOME} + * account types. */ - private BarData getEmptyData() { - List xValues = new ArrayList<>(); - List yValues = new ArrayList<>(); - for (int i = 0; i < NO_DATA_BAR_COUNTS; i++) { - xValues.add(""); - yValues.add(new BarEntry(i % 2 == 0 ? 5f : 4.5f, i)); + private void setUpSpinner() { + final Spinner spinner = (Spinner) findViewById(R.id.chart_data_spinner); + ArrayAdapter dataAdapter = new ArrayAdapter<>(this, + android.R.layout.simple_spinner_item, + Arrays.asList(AccountType.EXPENSE, AccountType.INCOME)); + dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(dataAdapter); + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView adapterView, View view, int i, long l) { + mChart.setData(getData((AccountType) spinner.getSelectedItem())); + displayChart(); + } + + @Override + public void onNothingSelected(AdapterView adapterView) { + } + }); + } + + /** + * Displays the stacked bar chart + */ + private void displayChart() { + mChart.highlightValues(null); + mChart.getLegend().setEnabled(false); + + mChart.getAxisLeft().setDrawLabels(mChartDataPresent); + mChart.getXAxis().setDrawLabels(mChartDataPresent); + mChart.setTouchEnabled(mChartDataPresent); + + selectedValueTextView.setText(""); + + if (mChartDataPresent) { + mChart.animateY(ANIMATION_DURATION); + } else { + mChart.clearAnimation(); + selectedValueTextView.setText(getResources().getString(R.string.label_chart_no_data)); } - BarDataSet set = new BarDataSet(yValues, getResources().getString(R.string.label_chart_no_data)); - set.setDrawValues(false); - set.setColor(NO_DATA_COLOR); - return new BarData(xValues, Collections.singletonList(set)); + mChart.invalidate(); } @Override @@ -260,7 +315,14 @@ public boolean onPrepareOptionsMenu(Menu menu) { public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_toggle_legend: - mChart.getLegend().setEnabled(!mChart.getLegend().isEnabled()); + // workaround for buggy legend + Legend legend = mChart.getLegend(); + legend.setEnabled(!mChart.getLegend().isEnabled()); + BarDataSet dataSet = mChart.getData().getDataSetByIndex(0); + LinkedHashSet labels = new LinkedHashSet<>(Arrays.asList(dataSet.getStackLabels())); + legend.setLabels(labels.toArray(new String[labels.size()])); + LinkedHashSet colors = new LinkedHashSet<>(dataSet.getColors()); + legend.setColors(Arrays.asList(colors.toArray(new Integer[colors.size()]))); mChart.invalidate(); break; @@ -280,17 +342,19 @@ public boolean onOptionsItemSelected(MenuItem item) { @Override public void onValueSelected(Entry e, int dataSetIndex, Highlight h) { - if (e == null) return; + if (e == null || ((BarEntry) e).getVals().length == 0) return; BarEntry entry = (BarEntry) e; - String label = mChart.getData().getXVals().get(entry.getXIndex()); - double value = entry.getVals()[ h.getStackIndex() == -1 ? 0 : h.getStackIndex() ]; + int index = h.getStackIndex() == -1 ? 0 : h.getStackIndex(); + String stackLabels = entry.getData().toString(); + String label = mChart.getData().getXVals().get(entry.getXIndex()) + ", " + + stackLabels.substring(1, stackLabels.length() - 1).split(",")[index]; + double value = entry.getVals()[index]; double sum = mTotalPercentageMode ? mChart.getData().getDataSetByIndex(dataSetIndex).getYValueSum() : entry.getVal(); - ((TextView) findViewById(R.id.selected_chart_slice)) - .setText(String.format(SELECTED_VALUE_PATTERN, label, value, value / sum * 100)); + selectedValueTextView.setText(String.format(SELECTED_VALUE_PATTERN, label, value, value / sum * 100)); } @Override public void onNothingSelected() { - ((TextView) findViewById(R.id.selected_chart_slice)).setText(""); + selectedValueTextView.setText(""); } } diff --git a/app/src/main/java/org/gnucash/android/ui/settings/SettingsActivity.java b/app/src/main/java/org/gnucash/android/ui/settings/SettingsActivity.java index 5f351e854..a4ef751e5 100644 --- a/app/src/main/java/org/gnucash/android/ui/settings/SettingsActivity.java +++ b/app/src/main/java/org/gnucash/android/ui/settings/SettingsActivity.java @@ -573,12 +573,14 @@ public void onClick(DialogInterface dialog, int which) { public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case AccountsActivity.REQUEST_PICK_ACCOUNTS_FILE: - try { - InputStream accountInputStream = getContentResolver().openInputStream(data.getData()); - new ImportAsyncTask(this).execute(accountInputStream); - } catch (FileNotFoundException e) { - Crashlytics.logException(e); - Toast.makeText(this, R.string.toast_error_importing_accounts, Toast.LENGTH_SHORT).show(); + if (resultCode == Activity.RESULT_OK && data != null) { + try { + InputStream accountInputStream = getContentResolver().openInputStream(data.getData()); + new ImportAsyncTask(this).execute(accountInputStream); + } catch (FileNotFoundException e) { + Crashlytics.logException(e); + Toast.makeText(this, R.string.toast_error_importing_accounts, Toast.LENGTH_SHORT).show(); + } } break; case PasscodePreferenceFragment.PASSCODE_REQUEST_CODE: diff --git a/app/src/main/res/layout/activity_bar_chart.xml b/app/src/main/res/layout/activity_bar_chart.xml new file mode 100644 index 000000000..288124eb4 --- /dev/null +++ b/app/src/main/res/layout/activity_bar_chart.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 7b21687a6..c9a6b399b 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -490,7 +490,7 @@ No user-identifiable information will be collected as part of this process!Reports Select currency Account color in reports - Use account color in the pie chart + Use account color in the bar/pie chart Please confirm your passcode Enter your new passcode Enter your old passcode diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 05aaf65e1..e503637a1 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -509,7 +509,7 @@ No user-identifiable information will be collected as part of this process! Reports Select currency Account color in reports - Use account color in the pie chart + Use account color in the bar/pie chart Please confirm your passcode Enter your new passcode Enter your old passcode diff --git a/app/src/main/res/values-es-rMX/strings.xml b/app/src/main/res/values-es-rMX/strings.xml index 92d04e0f5..67aa4efad 100644 --- a/app/src/main/res/values-es-rMX/strings.xml +++ b/app/src/main/res/values-es-rMX/strings.xml @@ -494,7 +494,7 @@ No user-identifiable information will be collected as part of this process! Reports Select currency Account color in reports - Use account color in the pie chart + Use account color in the bar/pie chart Please confirm your passcode Enter your new passcode Enter your old passcode diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 961d8d9c9..2ff2c2a05 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -491,7 +491,7 @@ Este proceso solo recoge información que no permite identificar al usuario< Reports Select currency Account color in reports - Use account color in the pie chart + Use account color in the bar/pie chart Confirme su contraseña Enter your new passcode Enter your old passcode diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 38c9ca45d..1eed10dcb 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -491,7 +491,7 @@ Aucune information permettant d\'identifier l\'utilisateur ne sera recueillis da Rapports Sélectionnez la monnaie Couleur du compte dans les rapports - utiliser la couleur du compte dans le diagramme circulaire + utiliser la couleur du compte dans le diagramme bandes/circulaire Veuillez confirmer vorte code Entrez votre nouveau code Entrez votre ancien mot de passe diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 7f732dbf2..eba5056a0 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -495,7 +495,7 @@ No user-identifiable information will be collected as part of this process! Reports Select currency Account color in reports - Use account color in the pie chart + Use account color in the bar/pie chart Please confirm your passcode Enter your new passcode Enter your old passcode diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index a2529151a..7cb3f3aa4 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -495,7 +495,7 @@ No user-identifiable information will be collected as part of this process! Reports Select currency Account color in reports - Use account color in the pie chart + Use account color in the bar/pie chart Please confirm your passcode Enter your new passcode Enter your old passcode diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 532626776..ac4f421a3 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -496,7 +496,7 @@ No user-identifiable information will be collected as part of this process! Reports Select currency Account color in reports - Use account color in the pie chart + Use account color in the bar/pie chart Please confirm your passcode Enter your new passcode Enter your old passcode diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index afb38b01f..8714b7c22 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -497,7 +497,7 @@ Nenhuma informação de identificação do usuário será coletada neste proces Relatórios Selecionar moeda Cor de contas nos relatórios - Utilizar cor de contas no gráfico de pizza + Utilizar cor de contas no gráfico de barras/pizza Backups agendados Exportações agendadas Sem exportações agendadas para mostrar diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index f627ad525..5aac50b60 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -491,7 +491,8 @@ 启用崩溃日志 Enable to send information about malfunctions to the developers for improvement (recommended). -No user-identifiable information will be collected as part of this process! +No user-identifiable information will be collected as part of this process! + 导出格式 找不到备份目录,请确认SD卡正常。 先输入现在的密码 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3545d22db..a6391dc3e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -437,7 +437,7 @@ Report Preferences Select currency Account color in reports - Use account color in the pie chart + Use account color in the bar/pie chart Reports Order by size Toggle legend